mitai-jinkendo/backend/data_layer/food_attributes.py
Lars a7e5511039
All checks were successful
Deploy Development / deploy (push) Successful in 1m1s
Build Test / pytest-backend (push) Successful in 8s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 29s
feat: Rezepte beim Zuordnen anlegen und offene Zutaten listen
Lebensmittel oder Rezept wählbar; gemappte Zutaten werden übernommen, offene erscheinen in der Liste. Dazu Fettgehalt-Suche, EPA-Stoffe, Rezept-CRUD und Wechsel bestehender Zuordnungen.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-12 16:59:24 +02:00

129 lines
4.0 KiB
Python

"""Catalog attribute definitions and numeric values (BLS EAV + extensions)."""
from __future__ import annotations
from typing import Any
MACRO_TO_KEY = {
"kcal": "ENERCC",
"protein_g": "PROT625",
"fat_g": "FAT",
"carbs_g": "CHO",
}
def scale_to_per_100g(value: float, serving_g: float | None) -> float:
if not serving_g or serving_g <= 0 or serving_g == 100:
return float(value)
return float(value) * (100.0 / float(serving_g))
def list_numeric_attributes(cur, query: str = "", limit: int = 40) -> list[dict[str, Any]]:
q = (query or "").strip()
lim = min(max(int(limit or 40), 1), 80)
if q:
like = f"%{q}%"
cur.execute(
"""
SELECT id, attr_key, name_de, name_en, unit, category, origin
FROM food_attributes
WHERE is_active = true AND data_type = 'num_per_100g'
AND (
attr_key ILIKE %s OR name_de ILIKE %s
OR COALESCE(name_en, '') ILIKE %s
)
ORDER BY
CASE WHEN attr_key ILIKE %s THEN 0
WHEN name_de ILIKE %s THEN 1
ELSE 2 END,
sort_order, attr_key
LIMIT %s
""",
(like, like, like, q, f"{q}%", lim),
)
else:
cur.execute(
"""
SELECT id, attr_key, name_de, name_en, unit, category, origin
FROM food_attributes
WHERE is_active = true AND data_type = 'num_per_100g'
ORDER BY sort_order, attr_key
LIMIT %s
""",
(lim,),
)
return [dict(r) for r in cur.fetchall()]
def write_numeric_attributes(cur, food_id: str, values: dict[str, Any] | None) -> int:
if not values:
return 0
written = 0
for raw_key, raw_val in values.items():
key = str(raw_key or "").strip()
if not key or raw_val is None or raw_val == "":
continue
try:
num = float(raw_val)
except (TypeError, ValueError):
continue
cur.execute(
"""
SELECT id FROM food_attributes
WHERE attr_key = %s AND is_active = true AND data_type = 'num_per_100g'
""",
(key,),
)
row = cur.fetchone()
if not row:
continue
cur.execute(
"""
INSERT INTO food_attribute_values (food_id, attribute_id, value_num, is_trace)
VALUES (%s, %s, %s, false)
ON CONFLICT (food_id, attribute_id)
DO UPDATE SET value_num = EXCLUDED.value_num, updated_at = NOW()
""",
(food_id, row["id"], num),
)
written += 1
return written
def macros_and_attributes_to_values(
macros: dict[str, Any] | None,
attributes: dict[str, Any] | None,
serving_g: float | None = None,
) -> dict[str, float]:
out: dict[str, float] = {}
for field, key in MACRO_TO_KEY.items():
if not macros or field not in macros or macros[field] is None or macros[field] == "":
continue
try:
out[key] = scale_to_per_100g(float(macros[field]), serving_g)
except (TypeError, ValueError):
continue
for key, raw in (attributes or {}).items():
if raw is None or raw == "":
continue
try:
out[str(key)] = scale_to_per_100g(float(raw), serving_g)
except (TypeError, ValueError):
continue
return out
def extra_attributes_for_food(cur, food_id: str) -> dict[str, float]:
cur.execute(
"""
SELECT a.attr_key, v.value_num
FROM food_attribute_values v
JOIN food_attributes a ON a.id = v.attribute_id
WHERE v.food_id = %s AND a.data_type = 'num_per_100g'
AND v.value_num IS NOT NULL AND v.is_trace = false
AND NOT (a.attr_key = ANY(%s))
ORDER BY a.sort_order, a.attr_key
""",
(food_id, list(MACRO_TO_KEY.values())),
)
return {r["attr_key"]: float(r["value_num"]) for r in cur.fetchall()}