restructure
This commit is contained in:
parent
9e44070fe7
commit
81fdafb907
13 changed files with 376 additions and 366 deletions
2
comfyrecipes/__main__.py
Normal file
2
comfyrecipes/__main__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import comfyrecipes.cli
|
||||||
|
comfyrecipes.cli.main()
|
157
comfyrecipes/builder.py
Normal file
157
comfyrecipes/builder.py
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
from typing import Any, List, Set
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
import jinja2
|
||||||
|
import jsonschema
|
||||||
|
import jsonschema.exceptions
|
||||||
|
|
||||||
|
import comfyrecipes.parsing as parsing
|
||||||
|
|
||||||
|
|
||||||
|
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, 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:
|
||||||
|
if os.path.isfile(dir + "/settings.yaml"):
|
||||||
|
settingsdct = self.load_file(dir + "/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(dir + "/units.yaml"):
|
||||||
|
unitsdct = self.load_file(dir + "/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(dir + "/ingredients.yaml"):
|
||||||
|
ingredientsdct = self.load_file(dir + "/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, dir: str) -> int:
|
||||||
|
files = []
|
||||||
|
for _, _, filesx in os.walk(dir + "/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(dir + "/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",
|
||||||
|
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
|
|
@ -4,7 +4,7 @@ import socketserver
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import recipes
|
import comfyrecipes.builder as builder
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
@ -23,7 +23,7 @@ def main() -> None:
|
||||||
|
|
||||||
ret = 0
|
ret = 0
|
||||||
if args.subcommand == "build":
|
if args.subcommand == "build":
|
||||||
ret = recipes.Builder().build(args.directory)
|
ret = builder.Builder().build(args.directory)
|
||||||
elif args.subcommand == "serve":
|
elif args.subcommand == "serve":
|
||||||
os.chdir(f"{args.directory}/out/html")
|
os.chdir(f"{args.directory}/out/html")
|
||||||
httpd = socketserver.TCPServer(
|
httpd = socketserver.TCPServer(
|
36
comfyrecipes/issues.py
Normal file
36
comfyrecipes/issues.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
ISSUE_UNKNOWN_INGREDIENT = "unknown-ingredient"
|
||||||
|
ISSUE_DUPLICATE_UNITS = "duplicate-units"
|
||||||
|
ISSUE_KNOWN_PRICE_UNKNOWN_CONVERSION = "known-price-unknown-conversion"
|
||||||
|
ISSUE_UNKNOWN_UNIT = "unknown-unit"
|
||||||
|
|
||||||
|
class Issue:
|
||||||
|
def __init__(self, id: str, msg: str) -> None:
|
||||||
|
self.id = id
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
|
|
||||||
|
class Issues:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.errors: List[Issue] = []
|
||||||
|
self.warnings: List[Issue] = []
|
||||||
|
|
||||||
|
def error(self, id: str, msg: str) -> None:
|
||||||
|
self.errors.append(Issue(id, msg))
|
||||||
|
|
||||||
|
def warn(self, id: str, msg: str) -> None:
|
||||||
|
self.warnings.append(Issue(id, msg))
|
||||||
|
|
||||||
|
def check(self) -> int:
|
||||||
|
retcode = len(self.errors) != 0
|
||||||
|
|
||||||
|
for msg in self.errors:
|
||||||
|
print(f"ERROR {msg.id}: {msg.msg}")
|
||||||
|
for msg in self.warnings:
|
||||||
|
print(f"WARNING {msg.id}: {msg.msg}")
|
||||||
|
|
||||||
|
self.errors.clear()
|
||||||
|
self.warnings.clear()
|
||||||
|
return retcode
|
|
@ -1,57 +1,19 @@
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
import collections
|
import collections
|
||||||
from typing import Dict, List, Any, Optional, Self, Set
|
from typing import Any, Dict, List, Optional, Self
|
||||||
import os
|
|
||||||
import json
|
import comfyrecipes.settings as settings
|
||||||
|
import comfyrecipes.issues as issues
|
||||||
|
|
||||||
import yaml
|
|
||||||
import jinja2
|
|
||||||
import jsonschema
|
import jsonschema
|
||||||
import jsonschema.exceptions
|
|
||||||
|
|
||||||
ISSUE_UNKNOWN_UNIT = "unknown-unit"
|
|
||||||
ISSUE_UNKNOWN_INGREDIENT = "unknown-ingredient"
|
|
||||||
ISSUE_DUPLICATE_UNITS = "duplicate-units"
|
|
||||||
ISSUE_KNOWN_PRICE_UNKNOWN_CONVERSION = "known-price-unknown-conversion"
|
|
||||||
|
|
||||||
|
|
||||||
class Issue:
|
|
||||||
def __init__(self, id: str, msg: str) -> None:
|
|
||||||
self.id = id
|
|
||||||
self.msg = msg
|
|
||||||
|
|
||||||
|
|
||||||
class Issues:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.errors: List[Issue] = []
|
|
||||||
self.warnings: List[Issue] = []
|
|
||||||
|
|
||||||
def error(self, id: str, msg: str) -> None:
|
|
||||||
self.errors.append(Issue(id, msg))
|
|
||||||
|
|
||||||
def warn(self, id: str, msg: str) -> None:
|
|
||||||
self.warnings.append(Issue(id, msg))
|
|
||||||
|
|
||||||
def check(self) -> int:
|
|
||||||
retcode = len(self.errors) != 0
|
|
||||||
|
|
||||||
for msg in self.errors:
|
|
||||||
print(f"ERROR {msg.id}: {msg.msg}")
|
|
||||||
for msg in self.warnings:
|
|
||||||
print(f"WARNING {msg.id}: {msg.msg}")
|
|
||||||
|
|
||||||
self.errors.clear()
|
|
||||||
self.warnings.clear()
|
|
||||||
return retcode
|
|
||||||
|
|
||||||
|
|
||||||
class Context:
|
class Context:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.settings = Settings()
|
self.settings = settings.Settings()
|
||||||
self.units: AUnits = FakeUnits(self)
|
self.units: AUnits = FakeUnits(self)
|
||||||
self.default_unit = Unit(self, "piece", [], [])
|
self.default_unit = Unit(self, "piece", [], [])
|
||||||
self.ingredients: AIngredients = FakeIngredients(self)
|
self.ingredients: AIngredients = FakeIngredients(self)
|
||||||
self.issues = Issues()
|
self.issues = issues.Issues()
|
||||||
|
|
||||||
def load_units(
|
def load_units(
|
||||||
self, unitsdct: List[Dict[str, Any]], unitsschema: Dict[str, Any]
|
self, unitsdct: List[Dict[str, Any]], unitsschema: Dict[str, Any]
|
||||||
|
@ -75,15 +37,6 @@ class Context:
|
||||||
jsonschema.validate(instance=settingsdct, schema=settingsschema)
|
jsonschema.validate(instance=settingsdct, schema=settingsschema)
|
||||||
self.settings.load(settingsdct)
|
self.settings.load(settingsdct)
|
||||||
|
|
||||||
|
|
||||||
class Settings:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.default_currency: Optional[str] = None
|
|
||||||
|
|
||||||
def load(self, settingsdct: Dict[str, Any]) -> None:
|
|
||||||
self.default_currency = settingsdct["default_currency"]
|
|
||||||
|
|
||||||
|
|
||||||
class Element:
|
class Element:
|
||||||
def __init__(self, ctx: Context) -> None:
|
def __init__(self, ctx: Context) -> None:
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
|
@ -92,34 +45,12 @@ class Element:
|
||||||
def from_dict(cls, ctx: Context, dct: Dict[str, Any]) -> Self:
|
def from_dict(cls, ctx: Context, dct: Dict[str, Any]) -> Self:
|
||||||
return cls(ctx)
|
return cls(ctx)
|
||||||
|
|
||||||
|
|
||||||
class Conversion(Element):
|
|
||||||
def __init__(
|
|
||||||
self, ctx: Context, fromunit: "Unit", tounit: "Unit", ratio: float
|
|
||||||
) -> None:
|
|
||||||
super().__init__(ctx)
|
|
||||||
self.fromunit = fromunit
|
|
||||||
self.tounit = tounit
|
|
||||||
self.ratio = ratio
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, ctx: Context, dct: Dict[str, Any]) -> Self:
|
|
||||||
fromunit = ctx.units.get(dct["from"])
|
|
||||||
if fromunit is None:
|
|
||||||
raise RuntimeError(f"unit {dct['from']} doesn't exist")
|
|
||||||
|
|
||||||
tounit = ctx.units.get(dct["to"])
|
|
||||||
if tounit is None:
|
|
||||||
raise RuntimeError(f"unit {dct['to']} doesn't exist")
|
|
||||||
return cls(ctx, fromunit, tounit, dct["ratio"])
|
|
||||||
|
|
||||||
|
|
||||||
class Unit(Element):
|
class Unit(Element):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
name: str,
|
name: str,
|
||||||
conversions: List[Conversion],
|
conversions: List["Conversion"],
|
||||||
aliases: List[str],
|
aliases: List[str],
|
||||||
dct: Optional[Dict[str, Any]] = None,
|
dct: Optional[Dict[str, Any]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -157,6 +88,7 @@ class Unit(Element):
|
||||||
self.conversions = conversions
|
self.conversions = conversions
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AUnits:
|
class AUnits:
|
||||||
def __init__(self, ctx: Context) -> None:
|
def __init__(self, ctx: Context) -> None:
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
|
@ -204,10 +136,169 @@ class Units(AUnits):
|
||||||
for unitname, num in collections.Counter(unitnames).items():
|
for unitname, num in collections.Counter(unitnames).items():
|
||||||
if num > 1:
|
if num > 1:
|
||||||
self.ctx.issues.error(
|
self.ctx.issues.error(
|
||||||
ISSUE_DUPLICATE_UNITS,
|
issues.ISSUE_DUPLICATE_UNITS,
|
||||||
f"units.yaml: {unitname} should only have one entry, found {num}",
|
f"units.yaml: {unitname} should only have one entry, found {num}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class IngredientInstance(Element):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
ctx: Context,
|
||||||
|
name: str,
|
||||||
|
amount: float,
|
||||||
|
unit: Unit,
|
||||||
|
alternatives: List["IngredientInstance"],
|
||||||
|
note: str,
|
||||||
|
price: Optional["PriceDB"],
|
||||||
|
) -> None:
|
||||||
|
super().__init__(ctx)
|
||||||
|
self.name = name
|
||||||
|
self.amount = amount
|
||||||
|
self.unit = unit
|
||||||
|
self.alternatives = alternatives
|
||||||
|
self.note = note
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, ctx: Context, dct: Dict[str, Any]) -> Self:
|
||||||
|
name = dct["name"]
|
||||||
|
|
||||||
|
ingredient = ctx.ingredients.get(name)
|
||||||
|
if ingredient is None:
|
||||||
|
ctx.issues.error(
|
||||||
|
issues.ISSUE_UNKNOWN_INGREDIENT, f"unknown ingredient {dct['name']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
amount = 1.0
|
||||||
|
if "amount" in dct:
|
||||||
|
amount = dct["amount"]
|
||||||
|
|
||||||
|
unit = ctx.default_unit
|
||||||
|
if "unit" in dct:
|
||||||
|
unitstr = dct["unit"]
|
||||||
|
unitx = ctx.units.get(unitstr)
|
||||||
|
if unitx is None:
|
||||||
|
ctx.issues.error(issues.ISSUE_UNKNOWN_UNIT, "unknown unit {unitstr}")
|
||||||
|
else:
|
||||||
|
unit = unitx
|
||||||
|
|
||||||
|
note = ""
|
||||||
|
if "note" in dct:
|
||||||
|
note = dct["note"]
|
||||||
|
|
||||||
|
alternatives = []
|
||||||
|
if "or" in dct:
|
||||||
|
for ingdct in dct["or"]:
|
||||||
|
ing = IngredientInstance.from_dict(ctx, ingdct)
|
||||||
|
alternatives.append(ing)
|
||||||
|
|
||||||
|
price = None
|
||||||
|
if ingredient is not None:
|
||||||
|
price = ingredient.getprice(amount, unit)
|
||||||
|
return cls(
|
||||||
|
ctx=ctx,
|
||||||
|
name=name,
|
||||||
|
amount=amount,
|
||||||
|
unit=unit,
|
||||||
|
alternatives=alternatives,
|
||||||
|
note=note,
|
||||||
|
price=price,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Recipe(Element):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
ctx: Context,
|
||||||
|
title: str,
|
||||||
|
ingredients: List[IngredientInstance],
|
||||||
|
subrecipes: List["Recipe"],
|
||||||
|
price: Optional["PriceDB"],
|
||||||
|
steps: List[str],
|
||||||
|
) -> None:
|
||||||
|
super().__init__(ctx)
|
||||||
|
self.srcpath = ""
|
||||||
|
self.outpath = ""
|
||||||
|
self.title = title
|
||||||
|
self.ingredients = ingredients
|
||||||
|
self.subrecipes = subrecipes
|
||||||
|
self.price = price
|
||||||
|
self.steps = steps
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, ctx: Context, dct: Dict[str, Any]) -> Self:
|
||||||
|
ingredients: List[IngredientInstance] = []
|
||||||
|
if "ingredients" in dct:
|
||||||
|
for ingdct in dct["ingredients"]:
|
||||||
|
ingredient = IngredientInstance.from_dict(ctx, ingdct)
|
||||||
|
ingredients.append(ingredient)
|
||||||
|
|
||||||
|
subrecipes: List[Recipe] = []
|
||||||
|
if "subrecipes" in dct:
|
||||||
|
for partdct in dct["subrecipes"]:
|
||||||
|
rp = Recipe.from_dict(ctx, partdct)
|
||||||
|
subrecipes.append(rp)
|
||||||
|
|
||||||
|
price: Optional[PriceDB] = None
|
||||||
|
pricex: float = 0
|
||||||
|
ingswithprice = 0
|
||||||
|
ingswithoutprice = 0
|
||||||
|
currency = None
|
||||||
|
for ing in ingredients:
|
||||||
|
if ing.price is None:
|
||||||
|
ingswithoutprice += 1
|
||||||
|
continue
|
||||||
|
ingswithprice += 1
|
||||||
|
pricex += ing.price.price
|
||||||
|
cur_currency = ing.price.currency
|
||||||
|
if currency is None:
|
||||||
|
currency = cur_currency
|
||||||
|
elif currency != cur_currency:
|
||||||
|
# we don't know how to convert currencies yet
|
||||||
|
currency = None
|
||||||
|
break
|
||||||
|
if currency is None or ingswithoutprice != 0 or len(ingredients) == 0:
|
||||||
|
price = None
|
||||||
|
else:
|
||||||
|
price = PriceDB(
|
||||||
|
ctx=ctx,
|
||||||
|
price=pricex,
|
||||||
|
amount=1,
|
||||||
|
unit=ctx.default_unit,
|
||||||
|
currency=currency,
|
||||||
|
)
|
||||||
|
|
||||||
|
steps = []
|
||||||
|
if "steps" in dct:
|
||||||
|
steps = dct["steps"]
|
||||||
|
return cls(
|
||||||
|
ctx=ctx,
|
||||||
|
title=dct["title"],
|
||||||
|
ingredients=ingredients,
|
||||||
|
subrecipes=subrecipes,
|
||||||
|
price=price,
|
||||||
|
steps=steps,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Conversion(Element):
|
||||||
|
def __init__(
|
||||||
|
self, ctx: Context, fromunit: Unit, tounit: Unit, ratio: float
|
||||||
|
) -> None:
|
||||||
|
super().__init__(ctx)
|
||||||
|
self.fromunit = fromunit
|
||||||
|
self.tounit = tounit
|
||||||
|
self.ratio = ratio
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, ctx: Context, dct: Dict[str, Any]) -> Self:
|
||||||
|
fromunit = ctx.units.get(dct["from"])
|
||||||
|
if fromunit is None:
|
||||||
|
raise RuntimeError(f"unit {dct['from']} doesn't exist")
|
||||||
|
|
||||||
|
tounit = ctx.units.get(dct["to"])
|
||||||
|
if tounit is None:
|
||||||
|
raise RuntimeError(f"unit {dct['to']} doesn't exist")
|
||||||
|
return cls(ctx, fromunit, tounit, dct["ratio"])
|
||||||
|
|
||||||
class Ingredient(Element):
|
class Ingredient(Element):
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -292,7 +383,7 @@ class Ingredient(Element):
|
||||||
path = self.dfs(conversions, unitfrom.name, unitto.name)
|
path = self.dfs(conversions, unitfrom.name, unitto.name)
|
||||||
if path is None:
|
if path is None:
|
||||||
self.ctx.issues.warn(
|
self.ctx.issues.warn(
|
||||||
ISSUE_KNOWN_PRICE_UNKNOWN_CONVERSION,
|
issues.ISSUE_KNOWN_PRICE_UNKNOWN_CONVERSION,
|
||||||
f"{self.name} has a known price, but conversion {unitfrom.name} -> {unitto.name} not known",
|
f"{self.name} has a known price, but conversion {unitfrom.name} -> {unitto.name} not known",
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
@ -395,7 +486,7 @@ class PriceDB(Element):
|
||||||
# everything is evaluated)
|
# everything is evaluated)
|
||||||
unit = ctx.default_unit
|
unit = ctx.default_unit
|
||||||
if unitx is None:
|
if unitx is None:
|
||||||
ctx.issues.error(ISSUE_UNKNOWN_UNIT, f"unknown unit {unitstr}")
|
ctx.issues.error(issues.ISSUE_UNKNOWN_UNIT, f"unknown unit {unitstr}")
|
||||||
else:
|
else:
|
||||||
unit = unitx
|
unit = unitx
|
||||||
|
|
||||||
|
@ -403,288 +494,3 @@ class PriceDB(Element):
|
||||||
if "currency" in dct:
|
if "currency" in dct:
|
||||||
currency = dct["currency"]
|
currency = dct["currency"]
|
||||||
return cls(ctx=ctx, price=price, amount=amount, unit=unit, currency=currency)
|
return cls(ctx=ctx, price=price, amount=amount, unit=unit, currency=currency)
|
||||||
|
|
||||||
|
|
||||||
class IngredientInstance(Element):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
ctx: Context,
|
|
||||||
name: str,
|
|
||||||
amount: float,
|
|
||||||
unit: Unit,
|
|
||||||
alternatives: List["IngredientInstance"],
|
|
||||||
note: str,
|
|
||||||
price: Optional[PriceDB],
|
|
||||||
) -> None:
|
|
||||||
super().__init__(ctx)
|
|
||||||
self.name = name
|
|
||||||
self.amount = amount
|
|
||||||
self.unit = unit
|
|
||||||
self.alternatives = alternatives
|
|
||||||
self.note = note
|
|
||||||
self.price = price
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, ctx: Context, dct: Dict[str, Any]) -> Self:
|
|
||||||
name = dct["name"]
|
|
||||||
|
|
||||||
ingredient = ctx.ingredients.get(name)
|
|
||||||
if ingredient is None:
|
|
||||||
ctx.issues.error(
|
|
||||||
ISSUE_UNKNOWN_INGREDIENT, f"unknown ingredient {dct['name']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
amount = 1.0
|
|
||||||
if "amount" in dct:
|
|
||||||
amount = dct["amount"]
|
|
||||||
|
|
||||||
unit = ctx.default_unit
|
|
||||||
if "unit" in dct:
|
|
||||||
unitstr = dct["unit"]
|
|
||||||
unitx = ctx.units.get(unitstr)
|
|
||||||
if unitx is None:
|
|
||||||
ctx.issues.error(ISSUE_UNKNOWN_UNIT, "unknown unit {unitstr}")
|
|
||||||
else:
|
|
||||||
unit = unitx
|
|
||||||
|
|
||||||
note = ""
|
|
||||||
if "note" in dct:
|
|
||||||
note = dct["note"]
|
|
||||||
|
|
||||||
alternatives = []
|
|
||||||
if "or" in dct:
|
|
||||||
for ingdct in dct["or"]:
|
|
||||||
ing = IngredientInstance.from_dict(ctx, ingdct)
|
|
||||||
alternatives.append(ing)
|
|
||||||
|
|
||||||
price = None
|
|
||||||
if ingredient is not None:
|
|
||||||
price = ingredient.getprice(amount, unit)
|
|
||||||
return cls(
|
|
||||||
ctx=ctx,
|
|
||||||
name=name,
|
|
||||||
amount=amount,
|
|
||||||
unit=unit,
|
|
||||||
alternatives=alternatives,
|
|
||||||
note=note,
|
|
||||||
price=price,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Recipe(Element):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
ctx: Context,
|
|
||||||
title: str,
|
|
||||||
ingredients: List[IngredientInstance],
|
|
||||||
subrecipes: List["Recipe"],
|
|
||||||
price: Optional[PriceDB],
|
|
||||||
steps: List[str],
|
|
||||||
) -> None:
|
|
||||||
super().__init__(ctx)
|
|
||||||
self.srcpath = ""
|
|
||||||
self.outpath = ""
|
|
||||||
self.title = title
|
|
||||||
self.ingredients = ingredients
|
|
||||||
self.subrecipes = subrecipes
|
|
||||||
self.price = price
|
|
||||||
self.steps = steps
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, ctx: Context, dct: Dict[str, Any]) -> Self:
|
|
||||||
ingredients: List[IngredientInstance] = []
|
|
||||||
if "ingredients" in dct:
|
|
||||||
for ingdct in dct["ingredients"]:
|
|
||||||
ingredient = IngredientInstance.from_dict(ctx, ingdct)
|
|
||||||
ingredients.append(ingredient)
|
|
||||||
|
|
||||||
subrecipes: List[Recipe] = []
|
|
||||||
if "subrecipes" in dct:
|
|
||||||
for partdct in dct["subrecipes"]:
|
|
||||||
rp = Recipe.from_dict(ctx, partdct)
|
|
||||||
subrecipes.append(rp)
|
|
||||||
|
|
||||||
price: Optional[PriceDB] = None
|
|
||||||
pricex: float = 0
|
|
||||||
ingswithprice = 0
|
|
||||||
ingswithoutprice = 0
|
|
||||||
currency = None
|
|
||||||
for ing in ingredients:
|
|
||||||
if ing.price is None:
|
|
||||||
ingswithoutprice += 1
|
|
||||||
continue
|
|
||||||
ingswithprice += 1
|
|
||||||
pricex += ing.price.price
|
|
||||||
cur_currency = ing.price.currency
|
|
||||||
if currency is None:
|
|
||||||
currency = cur_currency
|
|
||||||
elif currency != cur_currency:
|
|
||||||
# we don't know how to convert currencies yet
|
|
||||||
currency = None
|
|
||||||
break
|
|
||||||
if currency is None or ingswithoutprice != 0 or len(ingredients) == 0:
|
|
||||||
price = None
|
|
||||||
else:
|
|
||||||
price = PriceDB(
|
|
||||||
ctx=ctx,
|
|
||||||
price=pricex,
|
|
||||||
amount=1,
|
|
||||||
unit=ctx.default_unit,
|
|
||||||
currency=currency,
|
|
||||||
)
|
|
||||||
|
|
||||||
steps = []
|
|
||||||
if "steps" in dct:
|
|
||||||
steps = dct["steps"]
|
|
||||||
return cls(
|
|
||||||
ctx=ctx,
|
|
||||||
title=dct["title"],
|
|
||||||
ingredients=ingredients,
|
|
||||||
subrecipes=subrecipes,
|
|
||||||
price=price,
|
|
||||||
steps=steps,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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"):
|
|
||||||
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 = 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:
|
|
||||||
if os.path.isfile(dir + "/settings.yaml"):
|
|
||||||
settingsdct = self.load_file(dir + "/settings.yaml")
|
|
||||||
settingsschema = self.load_file("schemas/settings.json")
|
|
||||||
self.ctx.load_settings(settingsdct, settingsschema)
|
|
||||||
retcode = self.ctx.issues.check()
|
|
||||||
if retcode != 0:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if os.path.isfile(dir + "/units.yaml"):
|
|
||||||
unitsschema = self.load_file("schemas/units.json")
|
|
||||||
unitsdct = self.load_file(dir + "/units.yaml")
|
|
||||||
self.ctx.load_units(unitsdct, unitsschema)
|
|
||||||
retcode = self.ctx.issues.check()
|
|
||||||
if retcode != 0:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if os.path.isfile(dir + "/ingredients.yaml"):
|
|
||||||
ingredientsdct = self.load_file(dir + "/ingredients.yaml")
|
|
||||||
ingredientsschema = self.load_file("schemas/ingredients.json")
|
|
||||||
self.ctx.load_ingredients(ingredientsdct, ingredientsschema)
|
|
||||||
retcode = self.ctx.issues.check()
|
|
||||||
if retcode != 0:
|
|
||||||
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.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",
|
|
||||||
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
|
|
9
comfyrecipes/settings.py
Normal file
9
comfyrecipes/settings.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.default_currency: Optional[str] = None
|
||||||
|
|
||||||
|
def load(self, settingsdct: Dict[str, Any]) -> None:
|
||||||
|
self.default_currency = settingsdct["default_currency"]
|
Loading…
Add table
Add a link
Reference in a new issue