comfy-recipes/comfyrecipes/builder.py

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