EPA-Chips und mg/µg vom Etikett; fehlende Parameter ohne Admin. Co-authored-by: Cursor <cursoragent@cursor.com>
188 lines
5.9 KiB
Python
188 lines
5.9 KiB
Python
"""Catalog attribute definitions and numeric values (BLS EAV + extensions)."""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
_ATTR_KEY_RE = re.compile(r"[^A-Z0-9]+")
|
|
_ALLOWED_UNITS = {"g": "g", "mg": "mg", "µg": "µg", "ug": "µg", "mcg": "µg", "kcal": "kcal", "kj": "kJ"}
|
|
|
|
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, keys: list[str] | None = None
|
|
) -> list[dict[str, Any]]:
|
|
lim = min(max(int(limit or 40), 1), 80)
|
|
wanted = [str(k).strip() for k in (keys or []) if str(k).strip()]
|
|
if wanted:
|
|
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 = ANY(%s)
|
|
ORDER BY sort_order, attr_key
|
|
""",
|
|
(wanted,),
|
|
)
|
|
return [dict(r) for r in cur.fetchall()]
|
|
q = (query or "").strip()
|
|
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()}
|
|
|
|
|
|
def normalize_attr_key(raw: str | None) -> str:
|
|
key = _ATTR_KEY_RE.sub("_", (raw or "").strip().upper()).strip("_")
|
|
return key[:64]
|
|
|
|
|
|
def normalize_attr_unit(raw: str | None) -> str:
|
|
return _ALLOWED_UNITS.get((raw or "g").strip().lower(), "g")
|
|
|
|
|
|
def create_extension_attribute(cur, name_de: str, unit: str = "g", attr_key: str | None = None) -> dict[str, Any]:
|
|
name = (name_de or "").strip()
|
|
if not name:
|
|
raise ValueError("Name fehlt")
|
|
key = normalize_attr_key(attr_key or name)
|
|
if not key:
|
|
raise ValueError("attr_key fehlt")
|
|
unit_n = normalize_attr_unit(unit)
|
|
cur.execute(
|
|
"""
|
|
SELECT id, attr_key, name_de, name_en, unit, category, origin
|
|
FROM food_attributes
|
|
WHERE attr_key = %s AND is_active = true
|
|
""",
|
|
(key,),
|
|
)
|
|
existing = cur.fetchone()
|
|
if existing:
|
|
return dict(existing)
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO food_attributes
|
|
(attr_key, name_de, name_en, unit, category, data_type, origin, sort_order)
|
|
VALUES (%s, %s, %s, %s, 'extension', 'num_per_100g', 'extension', 9100)
|
|
RETURNING id, attr_key, name_de, name_en, unit, category, origin
|
|
""",
|
|
(key, name, None, unit_n),
|
|
)
|
|
return dict(cur.fetchone())
|