comfy-recipes/recipes.py
2023-11-06 23:31:05 +01:00

349 lines
11 KiB
Python

from abc import abstractmethod
import sys
from typing import Dict, List, Any, Optional, Set
import os
import json
import yaml
import jinja2
import jsonschema
import jsonschema.exceptions
class Context:
def __init__(self) -> None:
self.units = Units(self)
self.ingredients = Ingredients(self)
self.issues: List[str] = []
class Element:
def __init__(self, ctx: Context, dct: Dict[str, Any]) -> None:
self.ctx = ctx
self.dct = dct
self.load(dct)
for elem in self.dct.values():
if isinstance(elem, dict):
raise RuntimeError("Something wasn't processed properly")
def __contains__(self, item: Any) -> bool:
return item in self.dct
def __getitem__(self, key: str) -> Any:
return self.dct[key]
def __setitem__(self, key: str, val: Any) -> None:
self.dct[key] = val
def __repr__(self) -> str:
return self.dct.__repr__()
@abstractmethod
def load(self, dct: Dict[str, Any]) -> None:
...
class Conversion(Element):
def load(self, dct: Dict[str, Any]) -> None:
def find_unit(name: str) -> Optional[Unit]:
for unit in self.ctx.units.units:
if unit["name"] == name:
return unit
return None
fromunit = find_unit(dct["from"])
if fromunit is None:
raise RuntimeError(f"unit {fromunit} doesn't exist")
self["from"] = fromunit
tounit = find_unit(dct["to"])
if tounit is None:
raise RuntimeError(f"unit {tounit} doesn't exist")
self["to"] = tounit
class Unit(Element):
def load(self, dct: Dict[str, Any]) -> None:
oldunits = self.ctx.units.units[:]
self.ctx.units.units.append(self)
conversions: List[Conversion] = []
if "conversions" in dct:
for convdct in dct["conversions"]:
if "from" in dct["conversions"]:
raise RuntimeError(
f"conversions in units.yaml cannot have a from field, it is automatically assigned from the unit name"
)
convdct["from"] = self["name"]
conversion = Conversion(self.ctx, convdct)
conversions.append(conversion)
self["conversions"] = conversions
aliases: List[str] = []
if "aliases" in dct:
for alias in dct["aliases"]:
aliases.append(alias)
self["aliases"] = aliases
self.ctx.units.units = oldunits
class Units:
def __init__(self, ctx: Context) -> None:
self.ctx = ctx
self.units: List[Unit] = []
def load(self, lst: List[Any]) -> None:
for unitdct in lst:
unit = Unit(self.ctx, unitdct)
self.units.append(unit)
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(Element):
def load(self, dct: Dict[str, Any]) -> None:
if "pricedb" in dct:
pricedb = PriceDBs(self.ctx)
self.ctx.issues += pricedb.load(dct["pricedb"])
self["pricedb"] = pricedb
conversions = []
if "conversions" in dct:
for convdct in dct["conversions"]:
conversion = Conversion(self.ctx, convdct)
conversions.append(conversion)
self["conversions"] = conversions
class Ingredients:
def __init__(self, ctx: Context) -> None:
self.ctx = ctx
self.ingredients: List[Ingredient] = []
def load(self, lst: List[Any]) -> None:
for ingdct in lst:
ing = Ingredient(self.ctx, ingdct)
self.ingredients.append(ing)
def get(self, name: str) -> Optional[Ingredient]:
for ing in self.ingredients:
if ing["name"] == name or "aliases" in ing and name in ing["aliases"]:
return ing
raise RuntimeError(f"ingredient {name} not found")
class PriceDBs:
def __init__(self, ctx: Context) -> None:
self.ctx = ctx
self.pricedbs: List[PriceDB] = []
def load(self, lst: List[Any]) -> List[str]:
for elem in lst:
pricedb = PriceDB(self.ctx, elem)
self.pricedbs.append(pricedb)
return self.ctx.issues
class PriceDB(Element):
def load(self, dct: Dict[str, Any]) -> None:
if "amount" not in dct:
self["amount"] = 1.0
if "unit" in dct:
try:
self["unit"] = self.ctx.units.get(dct["unit"])
except RuntimeError as e:
self.ctx.issues.append(str(e))
else:
self["unit"] = None
class IngredientInstance(Element):
def load(self, dct: Dict[str, Any]) -> None:
try:
self["ingredient"] = self.ctx.ingredients.get(dct["name"])
except RuntimeError as e:
self.ctx.issues.append(str(e))
if "amount" not in dct:
self["amount"] = 1.0
if "unit" in dct:
try:
self["unit"] = self.ctx.units.get(dct["unit"])
except RuntimeError as e:
self.ctx.issues.append(str(e))
else:
self["unit"] = None
if "note" not in dct:
self["note"] = ""
alternatives = []
if "or" in dct:
for ingdct in dct["or"]:
ingredient = IngredientInstance(self.ctx, ingdct)
alternatives.append(ingredient)
self["alternatives"] = alternatives
class Recipe(Element):
def __init__(self, ctx: Context, dct: Dict[str, Any]) -> None:
super().__init__(ctx, dct)
self.srcpath = ""
self.outpath = ""
def load(self, dct: Dict[str, Any]) -> None:
ingredients: List[IngredientInstance] = []
if "ingredients" in dct:
for ing in dct["ingredients"]:
ingredient = IngredientInstance(self.ctx, ing)
ingredients.append(ingredient)
self["ingredients"] = ingredients
subrecipes: List[Recipe] = []
if "subrecipes" in dct:
for partdct in dct["subrecipes"]:
rp = Recipe(self.ctx, partdct)
subrecipes.append(rp)
self["subrecipes"] = subrecipes
class Builder:
def __init__(self) -> None:
self.jinjaenv = jinja2.Environment(
loader=jinja2.FileSystemLoader("templates"),
autoescape=jinja2.select_autoescape(),
)
def numprint(input: int) -> str:
out = str(input)
if out.endswith(".0"):
out = out.split(".")[0]
if out == "0.5":
return "1/2"
elif out == "0.25":
return "1/4"
elif out == "0.75":
return "3/4"
return out
self.jinjaenv.filters["numprint"] = numprint
self.ctx = 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 rendertemplate(
self, templatepath: str, format: str, file: str, dir: str, args: Any
) -> None:
template = self.jinjaenv.get_template(templatepath)
print(f"rendering {file}")
outstr = template.render(args)
os.makedirs(f"{dir}/out/{format}", exist_ok=True)
with open(f"{dir}/out/{format}/{file}", "w", encoding="utf-8") as f:
f.write(outstr)
self.outfiles.add(file)
def load(self, dir: str) -> int:
unitsdct = self.load_file(dir + "/units.yaml")
unitsschema = self.load_file("schemas/units.json")
jsonschema.validate(instance=unitsdct, schema=unitsschema)
self.ctx.units.load(unitsdct)
if len(self.ctx.issues) != 0:
for issue in self.ctx.issues:
print("ERROR in units.yaml:", issue)
return 1
ingredientsdct = self.load_file(dir + "/ingredients.yaml")
ingredientsschema = self.load_file("schemas/ingredients.json")
jsonschema.validate(instance=ingredientsdct, schema=ingredientsschema)
self.ctx.ingredients.load(ingredientsdct)
if len(self.ctx.issues) != 0:
for issue in self.ctx.issues:
print("ERROR in ingredients.yaml:", issue)
return 1
return 0
def run(self, dir: str) -> int:
files = []
for _, _, filesx in os.walk(dir + "/recipes"):
files = filesx
files.sort()
recipes: List[Recipe] = []
recipeschema = self.load_file("schemas/recipe.json")
for file in files:
if not file.endswith(".yaml"):
print(f"unknown extension of {file}")
continue
recipedct = self.load_file(dir + "/recipes/" + file)
jsonschema.validate(instance=recipedct, schema=recipeschema)
recipe = Recipe(self.ctx, recipedct)
recipe.srcpath = file
recipe.outpath = file[:-5] + ".html"
recipes.append(recipe)
if len(self.ctx.issues) != 0:
for issue in self.ctx.issues:
print("ERROR", issue)
return 1
self.rendertemplate(
templatepath="index.html",
format="html",
file="index.html",
dir=dir,
args={"recipes": recipes},
)
for recipe in recipes:
self.rendertemplate(
templatepath="recipe.html",
format="html",
file=recipe.outpath,
dir=dir,
args={"recipe": recipe},
)
return 0
def finish(self, dir: str) -> int:
files = set()
for _, _, filesx in os.walk(f"{dir}/out/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"{dir}/out/html/{file}")
return 0
def build(self, path: str) -> int:
fcs = [self.load, self.run, self.finish]
for func in fcs:
try:
ret = func(path)
if ret != 0:
return ret
except jsonschema.exceptions.ValidationError as e:
print("ERROR:", e)
return 1
return 0
if __name__ == "__main__":
builder = Builder()
sys.exit(builder.build("recipes"))