from typing import Dict, List, Any, Optional from dataclasses import dataclass import os import yaml import jinja2 class Unit: def __init__(self, name: str) -> None: self.name = name class Units: def __init__(self) -> None: self.units: List[Unit] = [] def load(self, lst: List[Any]) -> List[str]: assert_list(lst) for unit in lst: assert_type(unit, "", str) self.units.append(Unit(unit)) return [] def get(self, name: str) -> Optional[Unit]: for unit in self.units: if unit.name == name: return unit raise RuntimeError(f"unit {name} not found") class Ingredient: def load(self, dct: Dict[str, Any]) -> List[str]: issues = [] issues += assert_dict(dct, ["name"], ["wdid", "pricedb"]) assert_type(dct, "name", str) self.name = dct["name"] self.wdid = -1 if "wdid" in dct: assert_type(dct, "wdid", int) self.wdid = dct["wdid"] self.pricedb = None if "pricedb" in dct: assert_list(dct["pricedb"]) self.pricedb = PriceDBs() issues += self.pricedb.load(dct["pricedb"]) return issues class Ingredients: def __init__(self) -> None: self.ingredients: List[Ingredient] = [] def load(self, lst: List[Any]) -> List[str]: issues = [] assert_list(lst) for ingdct in lst: ing = Ingredient() issues += ing.load(ingdct) self.ingredients.append(ing) return issues def get(self, name: str) -> Optional[Ingredient]: for ing in self.ingredients: if ing.name == name: return ing raise RuntimeError(f"ingredient {name} not found") class PriceDBs: def __init__(self) -> None: self.pricedbs: List[PriceDB] = [] def load(self, lst: List[Any]) -> List[str]: issues = [] assert_list(lst) for elem in lst: pricedb = PriceDB() issues += pricedb.load(elem) self.pricedbs.append(pricedb) return issues class PriceDB: def load(self, dct: Dict[str, Any]) -> List[str]: issues = [] issues += assert_dict(dct, ["price"], ["amount", "unit"]) if isinstance(dct["price"], float): self.price = dct["price"] elif isinstance(dct["price"], int): self.price = float(dct["price"]) else: raise RuntimeError(f"{dct['price']} has to be int or float") self.amount = 1.0 if "amount" in dct: if isinstance(dct["amount"], float): self.amount = dct["amount"] elif isinstance(dct["amount"], int): self.amount = float(dct["amount"]) else: raise RuntimeError(f"{dct['amount']} has to be int or float") self.unit = None if "unit" in dct: assert_type(dct, "unit", str) try: self.unit = units.get(dct["unit"]) except RuntimeError as e: issues.append(str(e)) return issues class IngredientInstance: def load(self, dct: Dict[str, Any]) -> List[str]: issues = [] issues += assert_dict(dct, ["name"], ["amount", "unit", "note"]) assert_type(dct, "name", str) self.name = dct["name"] try: self.ingredient = ingredients.get(self.name) except RuntimeError as e: issues.append(str(e)) self.amount = 1.0 if "amount" in dct: if isinstance(dct["amount"], float): self.amount = dct["amount"] elif isinstance(dct["amount"], int): self.amount = float(dct["amount"]) else: raise RuntimeError(f"{dct['amount']} has to be int or float") self.unit = None if "unit" in dct: assert_type(dct, "unit", str) try: self.unit = units.get(dct["unit"]) except RuntimeError as e: issues.append(str(e)) self.note = "" if "note" in dct: assert_type(dct, "note", str) self.note = dct["note"] return issues class RecipePart: def load(self, dct: Dict[str, Any]) -> List[str]: issues = [] issues += assert_dict(dct, ["title", "ingredients", "steps"], []) assert_type(dct, "title", str) self.title = dct["title"] assert_type(dct, "ingredients", list) self.ingredients: List[IngredientInstance] = [] for ing in dct["ingredients"]: ingredient = IngredientInstance() issues += ingredient.load(ing) self.ingredients.append(ingredient) assert_type(dct, "steps", list) for elem in dct["steps"]: assert_type(elem, "", str) self.steps: List[str] = dct["steps"] return issues class Recipe: def __init__(self) -> None: self.parts: List[RecipePart] = [] self.srcpath = "" self.outpath = "" self.title = "" def load(self, dct: Dict[str, Any]) -> List[str]: issues: List[str] = [] if "parts" in dct: assert_dict(dct, ["title"], []) assert_type(dct, "title", str) self.title = dct["title"] assert_list(dct["parts"]) for partdct in dct["parts"]: rp = RecipePart() issues += rp.load(partdct) self.parts.append(rp) else: rp = RecipePart() issues = rp.load(dct) self.parts = [rp] self.title = rp.title return issues def load_file(file: str) -> Any: print(f"loading {file}") with open(file, encoding="utf-8") as f: ymltxt = f.read() return yaml.safe_load(ymltxt) def assert_dict( dct: Dict[str, Any], required_keys: List[str], optional_keys: List[str] ) -> List[str]: issues = [] if not isinstance(dct, dict): raise RuntimeError(f"{dct} has to be a dict") for reqkey in required_keys: if reqkey not in dct: issues.append(f"{reqkey} is required") extraelems = [x for x in dct.keys() if x not in required_keys + optional_keys] if len(extraelems) != 0: issues.append(f"{extraelems} not allowed") return issues def assert_type(dct: Dict[str, Any], key: str, type: type) -> None: if key == "": if not isinstance(dct, type): raise RuntimeError(f"{key} has to be a {type}") elif key in dct and not isinstance(dct[key], type): raise RuntimeError(f"{key} has to be a {type}") def assert_list(lst: List[Any]) -> None: if not isinstance(lst, list): raise RuntimeError(f"{lst} has to be a {list}") def rendertemplate( template: jinja2.Template, format: str, file: str, args: Any ) -> None: print(f"rendering {file}") outstr = template.render(args) try: os.mkdir("out") except FileExistsError: pass try: os.mkdir(f"out/{format}") except FileExistsError: pass with open(f"out/{format}/{file}", "w", encoding="utf-8") as f: f.write(outstr) units = Units() ingredients = Ingredients() def main() -> None: issues: List[str] = [] unitsdct = load_file("recipes/units.yaml") issues += units.load(unitsdct) ingredientsdct = load_file("recipes/ingredients.yaml") issues += ingredients.load(ingredientsdct) if len(issues) != 0: for issue in issues: print("ERROR", issue) return files = [] for _, _, filesx in os.walk("recipes/recipes"): files = filesx files.sort() recipes: List[Recipe] = [] for file in files: if not file.endswith(".yaml"): print(f"unknown extension of {file}") continue recipe = Recipe() recipedct = load_file("recipes/recipes/" + file) issues += recipe.load(recipedct) recipe.srcpath = file recipe.outpath = file[:-5] + ".html" recipes.append(recipe) if len(issues) != 0: for issue in issues: print("ERROR", issue) return env = jinja2.Environment( loader=jinja2.FileSystemLoader("templates"), autoescape=jinja2.select_autoescape(), ) indextemplate = env.get_template("index.html") rendertemplate(indextemplate, "html", "index.html", {"recipes": recipes}) recipetemplate = env.get_template("recipe.html") for recipe in recipes: rendertemplate(recipetemplate, "html", recipe.outpath, {"recipe": recipe}) if __name__ == "__main__": main()