"""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()}