from typing import Dict, List, Any import os import yaml import jinja2 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) -> List[str]: if key == "": if not isinstance(dct, type): return [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 [] 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 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) def main() -> None: issues = [] units = load_file("recipes/units.yaml") issues += validate_units(units) ingredients = load_file("recipes/ingredients.yaml") issues += validate_ingredients(ingredients) if len(issues) != 0: for issue in issues: print("ERROR", issue) return recipes = get_recipes(units, ingredients) 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) if __name__ == "__main__": main()