restructure parsing, add pricedb support
This commit is contained in:
parent
89cbf282bd
commit
da38086e98
2 changed files with 224 additions and 89 deletions
301
recipes.py
301
recipes.py
|
@ -1,10 +1,187 @@
|
||||||
from typing import Dict, List, Any
|
from typing import Dict, List, Any, Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
import jinja2
|
import jinja2
|
||||||
|
|
||||||
|
|
||||||
|
class Unit:
|
||||||
|
def __init__(self, name: str) -> None:
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
class Units:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.units: List[Unit] = []
|
||||||
|
|
||||||
|
def load(self, lst: List[Any]) -> List[str]:
|
||||||
|
assert_list(lst)
|
||||||
|
for unit in lst:
|
||||||
|
assert_type(unit, "", str)
|
||||||
|
self.units.append(Unit(unit))
|
||||||
|
return []
|
||||||
|
|
||||||
|
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:
|
||||||
|
def load(self, dct: Dict[str, Any]) -> List[str]:
|
||||||
|
issues = []
|
||||||
|
issues += assert_dict(dct, ["name"], ["wdid", "pricedb"])
|
||||||
|
|
||||||
|
assert_type(dct, "name", str)
|
||||||
|
self.name = dct["name"]
|
||||||
|
|
||||||
|
self.wdid = -1
|
||||||
|
if "wdid" in dct:
|
||||||
|
assert_type(dct, "wdid", int)
|
||||||
|
self.wdid = dct["wdid"]
|
||||||
|
|
||||||
|
self.pricedb = None
|
||||||
|
if "pricedb" in dct:
|
||||||
|
assert_list(dct["pricedb"])
|
||||||
|
self.pricedb = PriceDBs()
|
||||||
|
issues += self.pricedb.load(dct["pricedb"])
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
|
class Ingredients:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.ingredients: List[Ingredient] = []
|
||||||
|
|
||||||
|
def load(self, lst: List[Any]) -> List[str]:
|
||||||
|
issues = []
|
||||||
|
assert_list(lst)
|
||||||
|
for ingdct in lst:
|
||||||
|
ing = Ingredient()
|
||||||
|
issues += ing.load(ingdct)
|
||||||
|
self.ingredients.append(ing)
|
||||||
|
return issues
|
||||||
|
|
||||||
|
def get(self, name: str) -> Optional[Ingredient]:
|
||||||
|
for ing in self.ingredients:
|
||||||
|
if ing.name == name:
|
||||||
|
return ing
|
||||||
|
raise RuntimeError(f"ingredient {name} not found")
|
||||||
|
|
||||||
|
|
||||||
|
class PriceDBs:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.pricedbs: List[PriceDB] = []
|
||||||
|
|
||||||
|
def load(self, lst: List[Any]) -> List[str]:
|
||||||
|
issues = []
|
||||||
|
assert_list(lst)
|
||||||
|
for elem in lst:
|
||||||
|
pricedb = PriceDB()
|
||||||
|
issues += pricedb.load(elem)
|
||||||
|
self.pricedbs.append(pricedb)
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
|
class PriceDB:
|
||||||
|
def load(self, dct: Dict[str, Any]) -> List[str]:
|
||||||
|
issues = []
|
||||||
|
issues += assert_dict(dct, ["price"], ["amount", "unit"])
|
||||||
|
|
||||||
|
if isinstance(dct["price"], float):
|
||||||
|
self.price = dct["price"]
|
||||||
|
elif isinstance(dct["price"], int):
|
||||||
|
self.price = float(dct["price"])
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"{dct['price']} has to be int or float")
|
||||||
|
|
||||||
|
self.amount = 1.0
|
||||||
|
if "amount" in dct:
|
||||||
|
if isinstance(dct["amount"], float):
|
||||||
|
self.amount = dct["amount"]
|
||||||
|
elif isinstance(dct["amount"], int):
|
||||||
|
self.amount = float(dct["amount"])
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"{dct['amount']} has to be int or float")
|
||||||
|
|
||||||
|
self.unit = None
|
||||||
|
if "unit" in dct:
|
||||||
|
assert_type(dct, "unit", str)
|
||||||
|
self.unit = units.get(dct["unit"])
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientInstance:
|
||||||
|
def load(self, dct: Dict[str, Any]) -> List[str]:
|
||||||
|
issues = []
|
||||||
|
issues += assert_dict(dct, ["name"], ["amount", "unit", "note"])
|
||||||
|
|
||||||
|
assert_type(dct, "name", str)
|
||||||
|
self.name = dct["name"]
|
||||||
|
|
||||||
|
self.ingredient = ingredients.get(self.name)
|
||||||
|
|
||||||
|
self.amount = 1.0
|
||||||
|
if "amount" in dct:
|
||||||
|
if isinstance(dct["amount"], float):
|
||||||
|
self.amount = dct["amount"]
|
||||||
|
elif isinstance(dct["amount"], int):
|
||||||
|
self.amount = float(dct["amount"])
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"{dct['amount']} has to be int or float")
|
||||||
|
|
||||||
|
self.unit = None
|
||||||
|
if "unit" in dct:
|
||||||
|
assert_type(dct, "unit", str)
|
||||||
|
self.unit = units.get(dct["unit"])
|
||||||
|
|
||||||
|
self.note = ""
|
||||||
|
if "note" in dct:
|
||||||
|
assert_type(dct, "note", str)
|
||||||
|
self.note = dct["note"]
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
|
class RecipePart:
|
||||||
|
def load(self, dct: Dict[str, Any]) -> List[str]:
|
||||||
|
issues = []
|
||||||
|
issues += assert_dict(dct, ["title", "ingredients", "steps"], [])
|
||||||
|
|
||||||
|
assert_type(dct, "title", str)
|
||||||
|
self.title = dct["title"]
|
||||||
|
|
||||||
|
assert_type(dct, "ingredients", list)
|
||||||
|
self.ingredients: List[IngredientInstance] = []
|
||||||
|
for ing in dct["ingredients"]:
|
||||||
|
ingredient = IngredientInstance()
|
||||||
|
issues += ingredient.load(ing)
|
||||||
|
self.ingredients.append(ingredient)
|
||||||
|
|
||||||
|
assert_type(dct, "steps", list)
|
||||||
|
for elem in dct["steps"]:
|
||||||
|
assert_type(elem, "", str)
|
||||||
|
self.steps: List[str] = dct["steps"]
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
|
class Recipe:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.parts: List[RecipePart] = []
|
||||||
|
self.srcpath = ""
|
||||||
|
self.outpath = ""
|
||||||
|
self.title = ""
|
||||||
|
|
||||||
|
def load(self, dct: Dict[str, Any]) -> List[str]:
|
||||||
|
rp = RecipePart()
|
||||||
|
issues = rp.load(dct)
|
||||||
|
self.parts = [rp]
|
||||||
|
self.title = rp.title
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
def load_file(file: str) -> Any:
|
def load_file(file: str) -> Any:
|
||||||
print(f"loading {file}")
|
print(f"loading {file}")
|
||||||
with open(file, encoding="utf-8") as f:
|
with open(file, encoding="utf-8") as f:
|
||||||
|
@ -27,86 +204,17 @@ def assert_dict(
|
||||||
return issues
|
return issues
|
||||||
|
|
||||||
|
|
||||||
def assert_type(dct: Dict[str, Any], key: str, type: type) -> List[str]:
|
def assert_type(dct: Dict[str, Any], key: str, type: type) -> None:
|
||||||
if key == "":
|
if key == "":
|
||||||
if not isinstance(dct, type):
|
if not isinstance(dct, type):
|
||||||
return [f"{key} has to be a {type}"]
|
raise RuntimeError(f"{key} has to be a {type}")
|
||||||
elif key in dct and not isinstance(dct[key], type):
|
elif key in dct and not isinstance(dct[key], type):
|
||||||
return [f"{key} has to be a {type}"]
|
raise RuntimeError(f"{key} has to be a {type}")
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def validate_units(units: List[Any]) -> List[str]:
|
def assert_list(lst: List[Any]) -> None:
|
||||||
if not isinstance(units, list):
|
if not isinstance(lst, list):
|
||||||
raise RuntimeError(f"{units} has to be a list")
|
raise RuntimeError(f"{lst} has to be a {list}")
|
||||||
issues = []
|
|
||||||
for unit in units:
|
|
||||||
issues += assert_type(unit, "", str)
|
|
||||||
return issues
|
|
||||||
|
|
||||||
|
|
||||||
def validate_ingredients(ingredients: Any) -> List[str]:
|
|
||||||
if not isinstance(ingredients, list):
|
|
||||||
raise RuntimeError(f"{ingredients} has to be a list")
|
|
||||||
issues = []
|
|
||||||
for ing in ingredients:
|
|
||||||
issues += assert_dict(ing, ["name"], ["wdid"])
|
|
||||||
issues += assert_type(ing, "name", str)
|
|
||||||
return issues
|
|
||||||
|
|
||||||
|
|
||||||
def validate_recipe(recipe: Any, units: List[Any], ingredients: List[Any]) -> List[str]:
|
|
||||||
issues = []
|
|
||||||
issues += assert_dict(recipe, ["title", "ingredients", "steps"], [])
|
|
||||||
|
|
||||||
issues += assert_type(recipe, "title", str)
|
|
||||||
if not isinstance(recipe["ingredients"], list):
|
|
||||||
raise RuntimeError(f'{recipe["ingredients"]} has to be a list')
|
|
||||||
if not isinstance(recipe["steps"], list):
|
|
||||||
raise RuntimeError(f'{recipe["steps"]} has to be a list')
|
|
||||||
|
|
||||||
for ingredient in recipe["ingredients"]:
|
|
||||||
issues += assert_dict(ingredient, ["name"], ["amount", "unit", "note"])
|
|
||||||
issues += assert_type(ingredient, "name", str)
|
|
||||||
if (
|
|
||||||
"amount" in ingredient
|
|
||||||
and not isinstance(ingredient["amount"], int)
|
|
||||||
and not isinstance(ingredient["amount"], float)
|
|
||||||
):
|
|
||||||
issues.append("each ingredient has to have a int or float amount")
|
|
||||||
issues += assert_type(ingredient, "unit", str)
|
|
||||||
issues += assert_type(ingredient, "note", str)
|
|
||||||
if ingredient["name"] not in [ing["name"] for ing in ingredients]:
|
|
||||||
issues.append(f"unknown ingredient {ingredient['name']}")
|
|
||||||
|
|
||||||
for step in recipe["steps"]:
|
|
||||||
if not isinstance(step, str):
|
|
||||||
issues.append("each step has to be a string")
|
|
||||||
return issues
|
|
||||||
|
|
||||||
|
|
||||||
def get_recipes(units: List[Any], ingredients: List[Any]) -> List[Any]:
|
|
||||||
recipes = []
|
|
||||||
for _, _, files in os.walk("recipes/recipes"):
|
|
||||||
files.sort()
|
|
||||||
for file in files:
|
|
||||||
recipe = load_file("recipes/recipes/" + file)
|
|
||||||
try:
|
|
||||||
issues = validate_recipe(recipe, units, ingredients)
|
|
||||||
except RuntimeError as e:
|
|
||||||
print("WARNING:", e, "skipping")
|
|
||||||
continue
|
|
||||||
if len(issues) != 0:
|
|
||||||
for issue in issues:
|
|
||||||
print("WARNING:", file, issue, "skipping")
|
|
||||||
continue
|
|
||||||
recipe["srcpath"] = file
|
|
||||||
if not file.endswith(".yaml"):
|
|
||||||
print(f"unknown extension of {file}")
|
|
||||||
continue
|
|
||||||
recipe["outpath"] = file[:-5] + ".html"
|
|
||||||
recipes.append(recipe)
|
|
||||||
return recipes
|
|
||||||
|
|
||||||
|
|
||||||
def rendertemplate(
|
def rendertemplate(
|
||||||
|
@ -129,20 +237,45 @@ def rendertemplate(
|
||||||
f.write(outstr)
|
f.write(outstr)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
units = Units()
|
||||||
issues = []
|
ingredients = Ingredients()
|
||||||
units = load_file("recipes/units.yaml")
|
|
||||||
issues += validate_units(units)
|
|
||||||
|
|
||||||
ingredients = load_file("recipes/ingredients.yaml")
|
|
||||||
issues += validate_ingredients(ingredients)
|
def main() -> None:
|
||||||
|
issues: List[str] = []
|
||||||
|
|
||||||
|
unitsdct = load_file("recipes/units.yaml")
|
||||||
|
issues += units.load(unitsdct)
|
||||||
|
|
||||||
|
ingredientsdct = load_file("recipes/ingredients.yaml")
|
||||||
|
issues += ingredients.load(ingredientsdct)
|
||||||
|
|
||||||
if len(issues) != 0:
|
if len(issues) != 0:
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
print("ERROR", issue)
|
print("ERROR", issue)
|
||||||
return
|
return
|
||||||
|
|
||||||
recipes = get_recipes(units, ingredients)
|
files = []
|
||||||
|
for _, _, filesx in os.walk("recipes/recipes"):
|
||||||
|
files = filesx
|
||||||
|
files.sort()
|
||||||
|
|
||||||
|
recipes: List[Recipe] = []
|
||||||
|
for file in files:
|
||||||
|
if not file.endswith(".yaml"):
|
||||||
|
print(f"unknown extension of {file}")
|
||||||
|
continue
|
||||||
|
recipe = Recipe()
|
||||||
|
recipedct = load_file("recipes/recipes/" + file)
|
||||||
|
issues += recipe.load(recipedct)
|
||||||
|
recipe.srcpath = file
|
||||||
|
recipe.outpath = file[:-5] + ".html"
|
||||||
|
recipes.append(recipe)
|
||||||
|
|
||||||
|
if len(issues) != 0:
|
||||||
|
for issue in issues:
|
||||||
|
print("ERROR", issue)
|
||||||
|
return
|
||||||
|
|
||||||
env = jinja2.Environment(
|
env = jinja2.Environment(
|
||||||
loader=jinja2.FileSystemLoader("templates"),
|
loader=jinja2.FileSystemLoader("templates"),
|
||||||
|
@ -154,7 +287,7 @@ def main() -> None:
|
||||||
|
|
||||||
recipetemplate = env.get_template("recipe.html")
|
recipetemplate = env.get_template("recipe.html")
|
||||||
for recipe in recipes:
|
for recipe in recipes:
|
||||||
rendertemplate(recipetemplate, "html", recipe["outpath"], recipe)
|
rendertemplate(recipetemplate, "html", recipe.outpath, {"recipe": recipe})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{title}}</title>
|
<title>{{recipe.title}}</title>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>{{title}}</h1>
|
<h1>{{recipe.title}}</h1>
|
||||||
|
{% for part in recipe.parts %}
|
||||||
<h2>Ingredients</h2>
|
<h2>Ingredients</h2>
|
||||||
{% for ing in ingredients %}
|
{% for ing in part.ingredients %}
|
||||||
<li>{{ing.amount}} {{ing.unit}} {{ ing.name }}</li>
|
<li>{{ing.amount}} {{ing.unit.name}} {{ ing.name }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<h2>Steps</h2>
|
<h2>Steps</h2>
|
||||||
{% for step in steps %}
|
{% for step in part.steps %}
|
||||||
<li>{{ step }}</li>
|
<li>{{ step }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
Loading…
Add table
Add a link
Reference in a new issue