163 lines
5.4 KiB
Python
163 lines
5.4 KiB
Python
"""
|
|
calcdb: Utility to load and manipulate the database in Python
|
|
"""
|
|
|
|
import yaml
|
|
import os
|
|
import os.path
|
|
import re
|
|
|
|
class CalcDB:
|
|
"""The calculator database."""
|
|
def __init__(self, base):
|
|
self.calcs = {}
|
|
self.lang = {}
|
|
|
|
for (path, name) in self._list_folder(f"{base}/calculators"):
|
|
self.calcs.update(self._load_yaml(path))
|
|
|
|
for (path, name) in self._list_folder(f"{base}/lang"):
|
|
self.lang[name] = Lang(self._load_yaml(path))
|
|
self.lang[name]["name"] = name
|
|
|
|
self.model = self._load_yaml(f"{base}/model.yaml")
|
|
|
|
@staticmethod
|
|
def _load_yaml(path):
|
|
"""Safely load a YAML file."""
|
|
with open(path, "r") as fp:
|
|
return yaml.safe_load(fp.read())
|
|
|
|
@staticmethod
|
|
def _list_folder(folder):
|
|
"""List elements of folder, with prefix."""
|
|
for file in os.listdir(folder):
|
|
yield (f"{folder}/{file}", os.path.splitext(file)[0])
|
|
|
|
def all_calcs(self):
|
|
return ((name, self.calcs[name]) for name in self.model["all_calcs"])
|
|
def all_categories(self):
|
|
return self.model["all_categories"]
|
|
def all_fields(self, category):
|
|
return self.model["all_fields"].get(category)
|
|
|
|
class Lang:
|
|
"""A language-data dictionary extended with methods for easy access."""
|
|
def __init__(self, data):
|
|
self.data = data
|
|
def __getitem__(self, key):
|
|
return self.data[key]
|
|
def __setitem__(self, key, value):
|
|
self.data[key] = value
|
|
def get(self, key, default=None):
|
|
return self.data.get(key, default)
|
|
|
|
def category(self, name):
|
|
return self.data["categories"].get(name, None)
|
|
|
|
def field(self, category, field):
|
|
return self.data["fields"].get(category, {}).get(field, None)
|
|
|
|
def unit(self, name):
|
|
return self.data.get("units", {}).get(name, None)
|
|
|
|
def filter(self, text):
|
|
regex = re.compile(r'@([a-z]+){([^}]+)}')
|
|
return regex.sub(
|
|
lambda match: match[2] if match[1] == self.data["name"] else "",
|
|
text)
|
|
|
|
def render(db, lang, category, field, value):
|
|
"""
|
|
Render a CalcDB field into text (Markdown when applicable) in the specified
|
|
language. Returns a pair (text, style) where "text" is the string to
|
|
display, and "style" is a list of named styles (for CSS mainly).
|
|
"""
|
|
|
|
is_numeric = isinstance(value, (int,float)) and value not in [True, False]
|
|
is_base = isinstance(value, (int,float,bool,str))
|
|
is_str = isinstance(value, str)
|
|
is_list = isinstance(value, list)
|
|
is_dict = isinstance(value, dict)
|
|
|
|
f = f"{category}.{field}"
|
|
styles = []
|
|
|
|
# Unknown values
|
|
if value is None:
|
|
return ("?", ["bad"])
|
|
|
|
# Styles for some fixed field/value pairs
|
|
if f == "teaching.exam_mode":
|
|
styles = { False: ["bad"], "exam_mode.bad": ["warn"], True: ["good"] }
|
|
styles = styles.get(value,[])
|
|
if (category == "links" or category == "manuals") and value == False:
|
|
styles = ["warn"]
|
|
|
|
# Apply units for numeric values with units
|
|
if is_numeric:
|
|
unit = db.model["units"].get(category,{}).get(field)
|
|
suffixes = [(1e0, ""), (1e3, "k"), (1e6, "M"), (1e9, "G")]
|
|
|
|
if unit:
|
|
i = 0
|
|
while i+1 < len(suffixes) and value >= suffixes[i+1][0]:
|
|
i += 1
|
|
value /= suffixes[i][0]
|
|
|
|
# Reform an integer if possible
|
|
if int(value) == value:
|
|
value = int(value)
|
|
value = f"{value} {suffixes[i][1]}{unit}"
|
|
|
|
# Use enumerated values
|
|
if is_base:
|
|
if is_str and value.startswith(field + "."):
|
|
v2 = value[len(field)+1:]
|
|
else:
|
|
v2 = value
|
|
|
|
enum = lang["enums"].get(category,{}).get(field,{}).get(v2)
|
|
if enum is not None:
|
|
return (enum, styles)
|
|
|
|
# Use constants
|
|
if is_base and lang["constants"].get(value):
|
|
return (lang["constants"].get(value), styles)
|
|
|
|
# Query the full calculator name of fields holding a model name
|
|
if f in ["general.predecessor", "general.successor"] and is_str:
|
|
if value not in db.calcs:
|
|
return (str(value), styles)
|
|
else:
|
|
return (db.calcs[value]["general"]["full_name"], styles)
|
|
|
|
# Fixed formats for some fields
|
|
if f == "packaging.size":
|
|
return ("{} x {} x {}".format(*value), styles)
|
|
if f == "packaging.power_runtime":
|
|
return (f"~{value} @fr{{heures}}@en{{hours}}", styles)
|
|
if f == "hardware.sd_card":
|
|
return (f"@fr{{Carte SD}}@en{{SD card}} ≤ {value}", styles)
|
|
if f == "devices.display_size_pixels" and len(value) == 3:
|
|
return ("{}x{} ({})".format(*value), styles)
|
|
if f == "devices.display_size_pixels":
|
|
return ("{}x{}".format(*value), styles)
|
|
if f == "devices.display_size_text":
|
|
return ("{}x{}".format(*value), styles)
|
|
if f == "misc.price_range":
|
|
return ("{}-{} €".format(*value), styles)
|
|
if category == "manuals":
|
|
links = [ f"[{name}]({url})" for (name, url) in value.items() ]
|
|
return (", ".join(links), styles)
|
|
|
|
# For lists, use the empty constant or process each item individually
|
|
if value == []:
|
|
return (lang["constants"]["empty_list"], styles)
|
|
elif is_list:
|
|
value, styles = zip(*(render(db, lang, category, field, element)
|
|
for element in value))
|
|
return (", ".join(value), set().union(*styles))
|
|
|
|
return (str(value), styles)
|