CalcDB/tools/calcdb.py

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)