@@ -0,0 +1,204 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from app import db
|
||||
from app.lab.calc.nutrients import parse_num
|
||||
from app.lab.indicators import label_to_indicator_key
|
||||
from app.lab.models import (
|
||||
LabRationCalcIndicator,
|
||||
LabRationCalcTotal,
|
||||
LabRationCompoundLine,
|
||||
LabRecipeRation,
|
||||
)
|
||||
from app.models.base import default_uuid
|
||||
|
||||
_log = logging.getLogger("app.lab.calc")
|
||||
|
||||
|
||||
def _indicator_key(row: dict[str, Any]) -> str | None:
|
||||
key = row.get("key")
|
||||
if key:
|
||||
return str(key)
|
||||
label = row.get("label")
|
||||
if label:
|
||||
return label_to_indicator_key(str(label))
|
||||
return None
|
||||
|
||||
|
||||
def _log_calc_errors(recipe_id: str, errors: list[str] | None) -> None:
|
||||
for message in errors or []:
|
||||
text = str(message or "").strip()
|
||||
if text:
|
||||
_log.warning("ration calc recipe=%s: %s", recipe_id, text)
|
||||
|
||||
|
||||
def clear_calc(recipe_id: str) -> None:
|
||||
for model in (
|
||||
LabRationCalcTotal,
|
||||
LabRationCalcIndicator,
|
||||
LabRationCompoundLine,
|
||||
):
|
||||
model.query.filter_by(recipe_id=recipe_id).delete(synchronize_session=False)
|
||||
|
||||
|
||||
def _save_totals(recipe_id: str, scope: str, totals: list[dict[str, Any]] | None) -> None:
|
||||
for idx, row in enumerate(totals or []):
|
||||
db.session.add(
|
||||
LabRationCalcTotal(
|
||||
id=default_uuid(),
|
||||
recipe_id=recipe_id,
|
||||
scope=scope,
|
||||
metric_key=str(row.get("key") or f"metric_{idx}"),
|
||||
label=str(row.get("label") or ""),
|
||||
value=parse_num(row.get("value")),
|
||||
sort_order=idx,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _save_indicators(
|
||||
recipe_id: str,
|
||||
scope: str,
|
||||
indicators: list[dict[str, Any]] | None,
|
||||
) -> None:
|
||||
for idx, row in enumerate(indicators or []):
|
||||
db.session.add(
|
||||
LabRationCalcIndicator(
|
||||
id=default_uuid(),
|
||||
recipe_id=recipe_id,
|
||||
scope=scope,
|
||||
indicator_key=_indicator_key(row),
|
||||
label=str(row.get("label") or ""),
|
||||
unit=str(row.get("unit") or ""),
|
||||
min_value=parse_num(row.get("min")),
|
||||
max_value=parse_num(row.get("max")),
|
||||
content=parse_num(row.get("content")),
|
||||
diff=parse_num(row.get("diff")),
|
||||
sort_order=idx,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _save_compound_lines(recipe_id: str, lines: list[dict[str, Any]] | None) -> None:
|
||||
for idx, row in enumerate(lines or []):
|
||||
db.session.add(
|
||||
LabRationCompoundLine(
|
||||
id=default_uuid(),
|
||||
recipe_id=recipe_id,
|
||||
row_index=idx,
|
||||
ingredient_name=row.get("ingredient_name"),
|
||||
daily_kg=parse_num(row.get("daily_kg")),
|
||||
share_pct=parse_num(row.get("share_pct")),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def save_calc_result(recipe_id: str, result: dict[str, Any], header: LabRecipeRation) -> None:
|
||||
clear_calc(recipe_id)
|
||||
_log_calc_errors(recipe_id, result.get("errors"))
|
||||
header.calc_engine = str(result.get("engine") or "native")
|
||||
calculated_at = result.get("calculated_at")
|
||||
if calculated_at:
|
||||
try:
|
||||
header.calculated_at = datetime.fromisoformat(str(calculated_at).replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
header.calculated_at = datetime.utcnow()
|
||||
else:
|
||||
header.calculated_at = datetime.utcnow()
|
||||
|
||||
_save_totals(recipe_id, "ration", result.get("totals"))
|
||||
_save_indicators(recipe_id, "ration", result.get("indicators"))
|
||||
|
||||
compound = result.get("compound")
|
||||
if compound:
|
||||
_save_totals(recipe_id, "compound", compound.get("totals"))
|
||||
_save_indicators(recipe_id, "compound", compound.get("indicators"))
|
||||
_save_compound_lines(recipe_id, compound.get("lines"))
|
||||
|
||||
|
||||
def _load_totals(recipe_id: str, scope: str) -> list[dict[str, Any]]:
|
||||
rows = (
|
||||
LabRationCalcTotal.query.filter_by(recipe_id=recipe_id, scope=scope)
|
||||
.order_by(LabRationCalcTotal.sort_order)
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
{"key": row.metric_key, "label": row.label, "value": row.value}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
def _load_indicators(recipe_id: str, scope: str) -> list[dict[str, Any]]:
|
||||
rows = (
|
||||
LabRationCalcIndicator.query.filter_by(recipe_id=recipe_id, scope=scope)
|
||||
.order_by(LabRationCalcIndicator.sort_order)
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
{
|
||||
"key": row.indicator_key,
|
||||
"label": row.label,
|
||||
"unit": row.unit,
|
||||
"min": row.min_value,
|
||||
"max": row.max_value,
|
||||
"content": row.content,
|
||||
"diff": row.diff,
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
def load_compound_results(recipe_id: str) -> dict[str, Any]:
|
||||
totals = _load_totals(recipe_id, "compound")
|
||||
indicators = _load_indicators(recipe_id, "compound")
|
||||
lines = (
|
||||
LabRationCompoundLine.query.filter_by(recipe_id=recipe_id)
|
||||
.order_by(LabRationCompoundLine.row_index)
|
||||
.all()
|
||||
)
|
||||
if not totals and not indicators and not lines:
|
||||
return {}
|
||||
return {
|
||||
"totals": totals,
|
||||
"indicators": indicators,
|
||||
"lines": [
|
||||
{
|
||||
"ingredient_name": row.ingredient_name,
|
||||
"daily_kg": row.daily_kg,
|
||||
"share_pct": row.share_pct,
|
||||
}
|
||||
for row in lines
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def load_ration_results(recipe_id: str, header: LabRecipeRation | None) -> dict[str, Any]:
|
||||
if header is None or header.calculated_at is None:
|
||||
return {}
|
||||
totals = _load_totals(recipe_id, "ration")
|
||||
indicators = _load_indicators(recipe_id, "ration")
|
||||
if not totals and not indicators:
|
||||
return {}
|
||||
compound = load_compound_results(recipe_id)
|
||||
payload: dict[str, Any] = {
|
||||
"calculated_at": header.calculated_at.isoformat(),
|
||||
"engine": header.calc_engine or "native",
|
||||
"totals": totals,
|
||||
"indicators": indicators,
|
||||
}
|
||||
if compound:
|
||||
payload["compound"] = compound
|
||||
return payload
|
||||
|
||||
|
||||
def load_params(recipe_id: str, header: LabRecipeRation | None) -> dict[str, Any]:
|
||||
if header is None or not header.seed_source:
|
||||
return {}
|
||||
if header.seed_source == "execution":
|
||||
return {"seeded_from": "execution"}
|
||||
if header.seed_source == "synced_from":
|
||||
return {"synced_from": "execution"}
|
||||
return {"source": header.seed_source}
|
||||
Reference in New Issue
Block a user