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