diff --git a/recipes.py b/recipes.py index 55ed303..9984c07 100644 --- a/recipes.py +++ b/recipes.py @@ -1,10 +1,187 @@ -from typing import Dict, List, Any +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) + self.unit = units.get(dct["unit"]) + 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"] + + self.ingredient = ingredients.get(self.name) + + 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) + self.unit = units.get(dct["unit"]) + + 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]: + 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: @@ -27,86 +204,17 @@ def assert_dict( return issues -def assert_type(dct: Dict[str, Any], key: str, type: type) -> List[str]: +def assert_type(dct: Dict[str, Any], key: str, type: type) -> None: if key == "": if not isinstance(dct, type): - return [f"{key} has to be a {type}"] + raise RuntimeError(f"{key} has to be a {type}") elif key in dct and not isinstance(dct[key], type): - return [f"{key} has to be a {type}"] - return [] + raise RuntimeError(f"{key} has to be a {type}") -def validate_units(units: List[Any]) -> List[str]: - if not isinstance(units, list): - raise RuntimeError(f"{units} has to be a list") - issues = [] - for unit in units: - issues += assert_type(unit, "", str) - return issues - - -def validate_ingredients(ingredients: Any) -> List[str]: - if not isinstance(ingredients, list): - raise RuntimeError(f"{ingredients} has to be a list") - issues = [] - for ing in ingredients: - issues += assert_dict(ing, ["name"], ["wdid"]) - issues += assert_type(ing, "name", str) - return issues - - -def validate_recipe(recipe: Any, units: List[Any], ingredients: List[Any]) -> List[str]: - issues = [] - issues += assert_dict(recipe, ["title", "ingredients", "steps"], []) - - issues += assert_type(recipe, "title", str) - if not isinstance(recipe["ingredients"], list): - raise RuntimeError(f'{recipe["ingredients"]} has to be a list') - if not isinstance(recipe["steps"], list): - raise RuntimeError(f'{recipe["steps"]} has to be a list') - - for ingredient in recipe["ingredients"]: - issues += assert_dict(ingredient, ["name"], ["amount", "unit", "note"]) - issues += assert_type(ingredient, "name", str) - if ( - "amount" in ingredient - and not isinstance(ingredient["amount"], int) - and not isinstance(ingredient["amount"], float) - ): - issues.append("each ingredient has to have a int or float amount") - issues += assert_type(ingredient, "unit", str) - issues += assert_type(ingredient, "note", str) - if ingredient["name"] not in [ing["name"] for ing in ingredients]: - issues.append(f"unknown ingredient {ingredient['name']}") - - for step in recipe["steps"]: - if not isinstance(step, str): - issues.append("each step has to be a string") - return issues - - -def get_recipes(units: List[Any], ingredients: List[Any]) -> List[Any]: - recipes = [] - for _, _, files in os.walk("recipes/recipes"): - files.sort() - for file in files: - recipe = load_file("recipes/recipes/" + file) - try: - issues = validate_recipe(recipe, units, ingredients) - except RuntimeError as e: - print("WARNING:", e, "skipping") - continue - if len(issues) != 0: - for issue in issues: - print("WARNING:", file, issue, "skipping") - continue - recipe["srcpath"] = file - if not file.endswith(".yaml"): - print(f"unknown extension of {file}") - continue - recipe["outpath"] = file[:-5] + ".html" - recipes.append(recipe) - return recipes +def assert_list(lst: List[Any]) -> None: + if not isinstance(lst, list): + raise RuntimeError(f"{lst} has to be a {list}") def rendertemplate( @@ -129,20 +237,45 @@ def rendertemplate( f.write(outstr) -def main() -> None: - issues = [] - units = load_file("recipes/units.yaml") - issues += validate_units(units) +units = Units() +ingredients = Ingredients() - ingredients = load_file("recipes/ingredients.yaml") - issues += validate_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 - recipes = get_recipes(units, ingredients) + 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"), @@ -154,7 +287,7 @@ def main() -> None: recipetemplate = env.get_template("recipe.html") for recipe in recipes: - rendertemplate(recipetemplate, "html", recipe["outpath"], recipe) + rendertemplate(recipetemplate, "html", recipe.outpath, {"recipe": recipe}) if __name__ == "__main__": diff --git a/templates/recipe.html b/templates/recipe.html index 5bfcd9e..bda1b65 100644 --- a/templates/recipe.html +++ b/templates/recipe.html @@ -1,19 +1,21 @@ - {{title}} + {{recipe.title}} -

{{title}}

+

{{recipe.title}}

+ {% for part in recipe.parts %}

Ingredients

- {% for ing in ingredients %} -
  • {{ing.amount}} {{ing.unit}} {{ ing.name }}
  • + {% for ing in part.ingredients %} +
  • {{ing.amount}} {{ing.unit.name}} {{ ing.name }}
  • {% endfor %}

    Steps

    - {% for step in steps %} + {% for step in part.steps %}
  • {{ step }}
  • {% endfor %} + {% endfor %} \ No newline at end of file