""" 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)