import sys from typing import Any, Callable, Dict, List, Set, TypeVar import os import json import yaml import jinja2 import jsonschema import jsonschema.exceptions import comfyrecipes.parsing as parsing T = TypeVar("T") class Builder: def __init__(self) -> None: self.jinjaenv = jinja2.Environment( loader=jinja2.FileSystemLoader(f"{os.path.dirname(__file__)}/templates"), autoescape=jinja2.select_autoescape(), ) def numprint(input: int) -> str: out = str(input) if out.endswith(".0"): return out.split(".", maxsplit=1)[0] return out def amountprint(input: int) -> str: out = numprint(input) if out == "0.5": return "1/2" if out == "0.25": return "1/4" if out == "0.75": return "3/4" return out self.jinjaenv.filters["numprint"] = numprint self.jinjaenv.filters["amountprint"] = amountprint self.ctx = parsing.Context() # list of output files that will be built self.outfiles: Set[str] = set() def load_file(self, file: str) -> Any: print(f"loading {file}") with open(file, encoding="utf-8") as f: txt = f.read() if file.endswith(".json"): return json.loads(txt) return yaml.safe_load(txt) def load_pkgfile(self, file: str) -> Any: return self.load_file(f"{os.path.dirname(__file__)}/{file}") def rendertemplate( self, templatepath: str, format: str, file: str, outdir: str, args: Any ) -> None: template = self.jinjaenv.get_template(templatepath) print(f"rendering {file}") outstr = template.render(args) os.makedirs(f"{outdir}/{format}", exist_ok=True) with open(f"{outdir}/{format}/{file}", "w", encoding="utf-8") as f: f.write(outstr) self.outfiles.add(file) def load(self, outdir: str) -> int: if os.path.isfile("settings.yaml"): settingsdct = self.load_file("settings.yaml") settingsschema = self.load_pkgfile("schemas/settings.json") self.ctx.load_settings(settingsdct, settingsschema) retcode = self.ctx.issues.check() if retcode != 0: return 1 if os.path.isfile("units.yaml"): unitsdct = self.load_file("units.yaml") unitsschema = self.load_pkgfile("schemas/units.json") self.ctx.load_units(unitsdct, unitsschema) retcode = self.ctx.issues.check() if retcode != 0: return 1 if os.path.isfile("ingredients.yaml"): ingredientsdct = self.load_file("ingredients.yaml") ingredientsschema = self.load_pkgfile("schemas/ingredients.json") self.ctx.load_ingredients(ingredientsdct, ingredientsschema) retcode = self.ctx.issues.check() if retcode != 0: return 1 return 0 def run(self, outdir: str) -> int: files = [] for _, _, filesx in os.walk("recipes"): files = filesx files.sort() recipes: List[parsing.Recipe] = [] recipeschema = self.load_pkgfile("schemas/recipe.json") for file in files: if not file.endswith(".yaml"): print(f"unknown extension of {file}") continue recipedct = self.load_file("recipes/" + file) jsonschema.validate(instance=recipedct, schema=recipeschema) recipe = parsing.Recipe.from_dict(self.ctx, recipedct) recipe.srcpath = file recipe.outpath = file[:-5] + ".html" if self.ctx.issues.check() != 0: continue recipes.append(recipe) retcode = self.ctx.issues.check() if retcode != 0: return 1 self.rendertemplate( templatepath="index.html", format="html", file="index.html", outdir=outdir, args={"recipes": recipes}, ) for recipe in recipes: self.rendertemplate( templatepath="recipe.html", format="html", file=recipe.outpath, outdir=outdir, args={"recipe": recipe}, ) return 0 def generate_units(self) -> None: def collect_unitnames(rec: parsing.Recipe) -> List[str]: results: List[str] = [] for ing in rec.ingredients: if ing.unit is not None: results.append(ing.unit.name) return results unitnamelists = self.foreach_recipe(collect_unitnames) unitnamesset: Set[str] = set() for unitnamelst in unitnamelists: for unitname in unitnamelst: unitnamesset.add(unitname) unitnames = list(unitnamesset) unitnames.sort() units: List[Dict[str, str]] = [] for name in unitnames: units.append({"name": name}) file = "units.yaml" with open(file, "w") as f: f.write(yaml.dump(units)) print("found units written to", file) def generate_ingredients(self) -> None: def collect_ingnames(rec: parsing.Recipe) -> List[str]: results: List[str] = [] for ing in rec.ingredients: results.append(ing.name) return results ingredientnamelists = self.foreach_recipe(collect_ingnames) ingredientnamesset: Set[str] = set() for ingredientnamelst in ingredientnamelists: for ingredientname in ingredientnamelst: ingredientnamesset.add(ingredientname) ingredientnames = list(ingredientnamesset) ingredientnames.sort() ingredients: List[Dict[str, str]] = [] for name in ingredientnames: ingredients.append({"name": name}) file = "ingredients.yaml" with open(file, "w") as f: f.write(yaml.dump(ingredients)) print("found ingredients written to", file) def foreach_recipe(self, func: Callable[[parsing.Recipe], T]) -> List[T]: files = [] for _, _, filesx in os.walk("recipes"): files = filesx files.sort() def foreach_subrecipe(recipe: parsing.Recipe) -> List[T]: results: List[T] = [] for rec in [recipe] + recipe.subrecipes: results.append(func(rec)) for subrec in recipe.subrecipes: for subsubrec in subrec.subrecipes: results += foreach_subrecipe(subsubrec) return results results: List[T] = [] for file in files: if not file.endswith(".yaml"): print(f"unknown extension of {file}") continue recipedct = self.load_file("recipes/" + file) recipe = parsing.Recipe.from_dict(self.ctx, recipedct) if self.ctx.issues.check() != 0: continue results += foreach_subrecipe(recipe) return results def finish(self, outdir: str) -> int: files = set() for _, _, filesx in os.walk(f"{outdir}/html"): files = set(filesx) # files we did not generate, probably left by a previous run, but not valid anymore extra_files = files - self.outfiles for file in extra_files: print(f"removing obsolete {file}") os.remove(f"{outdir}/html/{file}") return 0 def build(self, directory: str, outdir: str) -> int: fcs = [self.load, self.run, self.finish] os.chdir(directory) for func in fcs: try: ret = func(outdir) if ret != 0: return ret except jsonschema.exceptions.ValidationError as e: print("ERROR:", e) return 1 return 0