237 lines
7.9 KiB
Python
237 lines
7.9 KiB
Python
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
|