move from dict based classes to standard classes

This commit is contained in:
Emi Vasilek 2023-11-18 17:16:26 +01:00
parent 3d49bc044d
commit 39d021da46
2 changed files with 247 additions and 141 deletions

View file

@ -2,7 +2,7 @@ from abc import abstractmethod
from collections import defaultdict from collections import defaultdict
import collections import collections
import sys import sys
from typing import Dict, List, Any, Optional, Set from typing import Dict, List, Any, Optional, Self, Set
import os import os
import json import json
@ -51,7 +51,7 @@ class Context:
def __init__(self) -> None: def __init__(self) -> None:
self.settings = Settings() self.settings = Settings()
self.units: AUnits = FakeUnits(self) self.units: AUnits = FakeUnits(self)
self.default_unit = Unit(self, {"name": "piece"}) self.default_unit = Unit(self, "piece", [], [])
self.ingredients: AIngredients = FakeIngredients(self) self.ingredients: AIngredients = FakeIngredients(self)
self.issues = Issues() self.issues = Issues()
@ -87,68 +87,76 @@ class Settings:
class Element: class Element:
def __init__(self, ctx: Context, dct: Dict[str, Any]) -> None: def __init__(self, ctx: Context) -> None:
self.ctx = ctx 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: @classmethod
return item in self.dct def from_dict(cls, ctx: Context, dct: Dict[str, Any]) -> Self:
return cls(ctx)
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): class Conversion(Element):
def load(self, dct: Dict[str, Any]) -> None: def __init__(
fromunit = self.ctx.units.get(dct["from"]) 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: if fromunit is None:
raise RuntimeError(f"unit {dct['from']} doesn't exist") raise RuntimeError(f"unit {dct['from']} doesn't exist")
self["from"] = fromunit
tounit = self.ctx.units.get(dct["to"]) tounit = ctx.units.get(dct["to"])
if tounit is None: if tounit is None:
raise RuntimeError(f"unit {dct['to']} doesn't exist") raise RuntimeError(f"unit {dct['to']} doesn't exist")
self["to"] = tounit return cls(ctx, fromunit, tounit, dct["ratio"])
class Unit(Element): class Unit(Element):
def load(self, dct: Dict[str, Any]) -> None: def __init__(
oldunits = self.ctx.units.units[:] self,
self.ctx.units.units.append(self) ctx: Context,
name: str,
conversions: List[Conversion],
aliases: List[str],
dct: Optional[Dict[str, Any]] = None,
) -> None:
super().__init__(ctx)
self.name = name
self.conversions = conversions
self.aliases = aliases
self.dct = dct
@classmethod
def from_dict(cls, ctx: Context, dct: Dict[str, Any]) -> Self:
# conversions are an empty list which is populated in finish_load
# which should be ran when all units are registered
aliases: List[str] = [] aliases: List[str] = []
if "aliases" in dct: if "aliases" in dct:
for alias in dct["aliases"]: aliases = dct["aliases"]
aliases.append(alias)
self["aliases"] = aliases return cls(ctx, name=dct["name"], conversions=[], aliases=aliases, dct=dct)
self.ctx.units.units = oldunits
def finish_load(self) -> None: def finish_load(self) -> None:
conversions: List[Conversion] = [] if self.dct is None:
if "conversions" in self.dct: return
for convdct in self.dct["conversions"]: if "conversions" not in self.dct:
if "from" in self.dct["conversions"]: return
raise RuntimeError( conversions = []
"conversions in units.yaml cannot have a from field, it is automatically assigned from the unit name" for conv in self.dct["conversions"]:
) if "from" in conv:
convdct["from"] = self["name"] raise RuntimeError(
conversion = Conversion(self.ctx, convdct) "conversions in units.yaml cannot have a from field, it is automatically assigned from the unit name"
conversions.append(conversion) )
self["conversions"] = conversions conv["from"] = self.dct["name"]
conv = Conversion.from_dict(self.ctx, conv)
conversions.append(conv)
self.conversions = conversions
class AUnits: class AUnits:
@ -170,9 +178,9 @@ class AUnits:
class FakeUnits(AUnits): class FakeUnits(AUnits):
def get(self, name: str) -> Optional[Unit]: def get(self, name: str) -> Optional[Unit]:
for unit in self.units: for unit in self.units:
if unit["name"] == name: if unit.name == name:
return unit return unit
unit = Unit(self.ctx, {"name": name}) unit = Unit(self.ctx, name, [], [])
self.units.append(unit) self.units.append(unit)
return unit return unit
@ -180,21 +188,21 @@ class FakeUnits(AUnits):
class Units(AUnits): class Units(AUnits):
def load(self, lst: List[Any]) -> None: def load(self, lst: List[Any]) -> None:
for unitdct in lst: for unitdct in lst:
unit = Unit(self.ctx, unitdct) unit = Unit.from_dict(self.ctx, unitdct)
self.units.append(unit) self.units.append(unit)
for unit in self.units: for unit in self.units:
unit.finish_load() unit.finish_load()
def get(self, name: str) -> Optional[Unit]: def get(self, name: str) -> Optional[Unit]:
for unit in self.units: for unit in self.units:
if unit["name"] == name or "aliases" in unit and name in unit["aliases"]: if unit.name == name or name in unit.aliases:
return unit return unit
return None return None
def validate(self) -> None: def validate(self) -> None:
unitnames = [] unitnames = []
for unit in self.units: for unit in self.units:
unitnames.append(unit["name"]) unitnames.append(unit.name)
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(
@ -204,18 +212,51 @@ class Units(AUnits):
class Ingredient(Element): class Ingredient(Element):
def load(self, dct: Dict[str, Any]) -> None: def __init__(
if "prices" in dct: self,
pricedb = PriceDBs(self.ctx) ctx: Context,
pricedb.load(dct["prices"]) name: str,
self["prices"] = pricedb wdid: Optional[int],
prices: List["PriceDB"],
conversions: List[Conversion],
aliases: List[str],
) -> None:
super().__init__(ctx)
self.name = name
self.wdid = wdid
self.prices = prices
self.conversions = conversions
self.aliases = aliases
conversions = [] @classmethod
def from_dict(cls, ctx: Context, dct: Dict[str, Any]) -> Self:
wdid = None
if "wdid" in dct:
wdid = dct["wdid"]
prices: List[PriceDB] = []
if "prices" in dct:
for pricedct in dct["prices"]:
prices.append(PriceDB.from_dict(ctx, pricedct))
conversions: List[Conversion] = []
if "conversions" in dct: if "conversions" in dct:
for convdct in dct["conversions"]: for convdct in dct["conversions"]:
conversion = Conversion(self.ctx, convdct) conversion = Conversion.from_dict(ctx, convdct)
conversions.append(conversion) conversions.append(conversion)
self["conversions"] = conversions
aliases: List[str] = []
if "aliases" in dct:
aliases = dct["aliases"]
return cls(
ctx,
name=dct["name"],
wdid=wdid,
prices=prices,
conversions=conversions,
aliases=aliases,
)
def dfs( def dfs(
self, self,
@ -240,21 +281,21 @@ class Ingredient(Element):
def convert(self, amount: float, unitfrom: Unit, unitto: Unit) -> Optional[float]: def convert(self, amount: float, unitfrom: Unit, unitto: Unit) -> Optional[float]:
conversions: Dict[str, Dict[str, float]] = defaultdict(dict) conversions: Dict[str, Dict[str, float]] = defaultdict(dict)
# construct node tree # construct node tree
convs = self["conversions"] convs = self.conversions
for unit in self.ctx.units.units: for unit in self.ctx.units.units:
convs += unit["conversions"] convs += unit.conversions
for conv in convs: for conv in convs:
fromname = conv["from"]["name"] fromname = conv.fromunit.name
toname = conv["to"]["name"] toname = conv.tounit.name
conversions[fromname][toname] = conv["ratio"] conversions[fromname][toname] = conv.ratio
conversions[toname][fromname] = 1 / conv["ratio"] conversions[toname][fromname] = 1 / conv.ratio
# find path between conversions # find path between conversions
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, 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
assert len(path) != 0 assert len(path) != 0
@ -265,15 +306,13 @@ class Ingredient(Element):
return amount return amount
def getprice(self, amount: float, unit: Unit) -> Optional["PriceDB"]: def getprice(self, amount: float, unit: Unit) -> Optional["PriceDB"]:
if "prices" not in self.dct:
return None
prices: List[PriceDB] = [] prices: List[PriceDB] = []
pricedbs: PriceDBs = self["prices"] pricedbs: List[PriceDB] = self.prices
for entry in pricedbs.pricedbs: for entry in pricedbs:
assert isinstance(entry, PriceDB) assert isinstance(entry, PriceDB)
entryamount: float = entry["amount"] entryamount: float = entry.amount
entryprice: float = entry["price"] entryprice: float = entry.price
entryunit: Unit = entry["unit"] entryunit: Unit = entry.unit
price = 0.0 price = 0.0
if entryunit == unit: if entryunit == unit:
price = (amount / entryamount) * entryprice price = (amount / entryamount) * entryprice
@ -283,12 +322,10 @@ class Ingredient(Element):
price = (pricex / entryamount) * entryprice price = (pricex / entryamount) * entryprice
newentry = PriceDB( newentry = PriceDB(
self.ctx, self.ctx,
{ price=price,
"price": price, amount=amount,
"amount": amount, unit=unit,
"unit": unit["name"], currency=entry.currency,
"currency": entry["currency"],
},
) )
prices.append(newentry) prices.append(newentry)
if len(prices) == 0: if len(prices) == 0:
@ -313,9 +350,9 @@ class AIngredients:
class FakeIngredients(AIngredients): class FakeIngredients(AIngredients):
def get(self, name: str) -> Optional[Ingredient]: def get(self, name: str) -> Optional[Ingredient]:
for ing in self.ingredients: for ing in self.ingredients:
if ing["name"] == name: if ing.name == name:
return ing return ing
ing = Ingredient(self.ctx, {"name": name}) ing = Ingredient(self.ctx, name, None, [], [], [])
self.ingredients.append(ing) self.ingredients.append(ing)
return ing return ing
@ -323,113 +360,164 @@ class FakeIngredients(AIngredients):
class Ingredients(AIngredients): class Ingredients(AIngredients):
def load(self, lst: List[Any]) -> None: def load(self, lst: List[Any]) -> None:
for ingdct in lst: for ingdct in lst:
ing = Ingredient(self.ctx, ingdct) ing = Ingredient.from_dict(self.ctx, ingdct)
self.ingredients.append(ing) self.ingredients.append(ing)
def get(self, name: str) -> Optional[Ingredient]: def get(self, name: str) -> Optional[Ingredient]:
for ing in self.ingredients: for ing in self.ingredients:
if ing["name"] == name or "aliases" in ing and name in ing["aliases"]: if ing.name == name or name in ing.aliases:
return ing return ing
return None return None
class PriceDBs:
def __init__(self, ctx: Context) -> None:
self.ctx = ctx
self.pricedbs: List[PriceDB] = []
def load(self, lst: List[Any]) -> None:
for elem in lst:
pricedb = PriceDB(self.ctx, elem)
self.pricedbs.append(pricedb)
def __repr__(self) -> str:
return self.pricedbs.__repr__()
class PriceDB(Element): class PriceDB(Element):
def load(self, dct: Dict[str, Any]) -> None: def __init__(
if "amount" not in dct: self,
self["amount"] = 1.0 ctx: Context,
price: float,
amount: float,
unit: Unit,
currency: Optional[str],
) -> None:
super().__init__(ctx)
self.price = price
self.amount = amount
self.unit = unit
self.currency = currency
if "unit" in dct: @classmethod
unitstr = dct["unit"] def from_dict(cls, ctx: Context, dct: Dict[str, Any]) -> Self:
self["unit"] = self.ctx.units.get(unitstr) price = dct["price"]
if self["unit"] is None: amount = dct["amount"]
self.ctx.issues.error(ISSUE_UNKNOWN_UNIT, f"unknown unit {unitstr}")
unitstr = dct["unit"]
unitx = ctx.units.get(unitstr)
# uses the default unit if unit is not known (this won't be
# visible to the user since rendering will stop after
# everything is evaluated)
unit = ctx.default_unit
if unitx is None:
ctx.issues.error(ISSUE_UNKNOWN_UNIT, f"unknown unit {unitstr}")
else: else:
self["unit"] = self.ctx.default_unit unit = unitx
if "currency" not in dct: currency = ctx.settings.default_currency
self["currency"] = self.ctx.settings.default_currency if "currency" in dct:
currency = dct["currency"]
return cls(ctx=ctx, price=price, amount=amount, unit=unit, currency=currency)
class IngredientInstance(Element): class IngredientInstance(Element):
def load(self, dct: Dict[str, Any]) -> None: def __init__(
ingredient = self.ctx.ingredients.get(dct["name"]) 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: if ingredient is None:
self.ctx.issues.error( ctx.issues.error(
ISSUE_UNKNOWN_INGREDIENT, f"unknown ingredient {dct['name']}" ISSUE_UNKNOWN_INGREDIENT, f"unknown ingredient {dct['name']}"
) )
self["ingredient"] = ingredient
if "amount" not in dct: amount = 1.0
self["amount"] = 1.0 if "amount" in dct:
amount = dct["amount"]
unit = ctx.default_unit
if "unit" in dct: if "unit" in dct:
unitstr = dct["unit"] unitstr = dct["unit"]
self["unit"] = self.ctx.units.get(unitstr) unitx = ctx.units.get(unitstr)
if self["unit"] is None: if unitx is None:
self.ctx.issues.error(ISSUE_UNKNOWN_UNIT, "unknown unit {unitstr}") ctx.issues.error(ISSUE_UNKNOWN_UNIT, "unknown unit {unitstr}")
else: else:
self["unit"] = self.ctx.default_unit unit = unitx
if "note" not in dct: note = ""
self["note"] = "" if "note" in dct:
note = dct["note"]
alternatives = [] alternatives = []
if "or" in dct: if "or" in dct:
for ingdct in dct["or"]: for ingdct in dct["or"]:
ing = IngredientInstance(self.ctx, ingdct) ing = IngredientInstance.from_dict(ctx, ingdct)
alternatives.append(ing) alternatives.append(ing)
self["alternatives"] = alternatives
price = None
if ingredient is not None: if ingredient is not None:
self["price"] = ingredient.getprice(self["amount"], self["unit"]) price = ingredient.getprice(amount, unit)
return cls(
ctx=ctx,
name=name,
amount=amount,
unit=unit,
alternatives=alternatives,
note=note,
price=price,
)
class Recipe(Element): class Recipe(Element):
def __init__(self, ctx: Context, dct: Dict[str, Any]) -> None: def __init__(
super().__init__(ctx, dct) self,
ctx: Context,
title: str,
ingredients: List[IngredientInstance],
subrecipes: List["Recipe"],
price: Optional[PriceDB],
steps: List[str],
) -> None:
super().__init__(ctx)
self.srcpath = "" self.srcpath = ""
self.outpath = "" self.outpath = ""
self.title = title
self.ingredients = ingredients
self.subrecipes = subrecipes
self.price = price
self.steps = steps
def load(self, dct: Dict[str, Any]) -> None: @classmethod
def from_dict(cls, ctx: Context, dct: Dict[str, Any]) -> Self:
ingredients: List[IngredientInstance] = [] ingredients: List[IngredientInstance] = []
if "ingredients" in dct: if "ingredients" in dct:
for ing in dct["ingredients"]: for ingdct in dct["ingredients"]:
ingredient = IngredientInstance(self.ctx, ing) ingredient = IngredientInstance.from_dict(ctx, ingdct)
ingredients.append(ingredient) ingredients.append(ingredient)
self["ingredients"] = ingredients
subrecipes: List[Recipe] = [] subrecipes: List[Recipe] = []
if "subrecipes" in dct: if "subrecipes" in dct:
for partdct in dct["subrecipes"]: for partdct in dct["subrecipes"]:
rp = Recipe(self.ctx, partdct) rp = Recipe.from_dict(ctx, partdct)
subrecipes.append(rp) subrecipes.append(rp)
self["subrecipes"] = subrecipes
price: Optional[int] = 0 price: Optional[PriceDB] = None
pricex: float = 0
ingswithprice = 0 ingswithprice = 0
ingswithoutprice = 0 ingswithoutprice = 0
currency = None currency = None
for ing in ingredients: for ing in ingredients:
if ing["price"] is None: if ing.price is None:
ingswithoutprice += 1 ingswithoutprice += 1
continue continue
ingswithprice += 1 ingswithprice += 1
price += ing["price"]["price"] pricex += ing.price.price
cur_currency = ing["price"]["currency"] cur_currency = ing.price.currency
if currency is None: if currency is None:
currency = cur_currency currency = cur_currency
elif currency != cur_currency: elif currency != cur_currency:
@ -437,9 +525,27 @@ class Recipe(Element):
currency = None currency = None
break break
if currency is None or ingswithoutprice != 0 or len(ingredients) == 0: if currency is None or ingswithoutprice != 0 or len(ingredients) == 0:
self["price"] = None price = None
else: else:
self["price"] = PriceDB(self.ctx, {"price": price, "currency": currency}) 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: class Builder:
@ -533,7 +639,7 @@ class Builder:
continue continue
recipedct = self.load_file(dir + "/recipes/" + file) recipedct = self.load_file(dir + "/recipes/" + file)
jsonschema.validate(instance=recipedct, schema=recipeschema) jsonschema.validate(instance=recipedct, schema=recipeschema)
recipe = Recipe(self.ctx, recipedct) recipe = Recipe.from_dict(self.ctx, recipedct)
recipe.srcpath = file recipe.srcpath = file
recipe.outpath = file[:-5] + ".html" recipe.outpath = file[:-5] + ".html"
if self.ctx.issues.check() != 0: if self.ctx.issues.check() != 0:

View file

@ -19,7 +19,7 @@
"required": [ "price", "amount", "unit" ], "required": [ "price", "amount", "unit" ],
"properties": { "properties": {
"price": { "type": "number" }, "price": { "type": "number" },
"amount": { "type": "integer" }, "amount": { "type": "number" },
"unit": { "type": "string" }, "unit": { "type": "string" },
"currency": { "type": "string" } "currency": { "type": "string" }
} }