"""Official BLS 4.0 component labels (MRI documentation) and placeholder repair. ``attr_key`` stays the identity. These names only fill empty or key-equal ``name_de``. """ from __future__ import annotations from typing import Any # Keys that feed nutrition_log kcal / protein_g / fat_g / carbs_g MACRO_KEYS = ("ENERCC", "PROT625", "FAT", "CHO") GROUP_META = ( ("energy", "Energie"), ("macro", "Makronährstoffe"), ("carbs", "Kohlenhydrate"), ("fiber", "Ballaststoffe"), ("fatty_acids", "Fettsäuren"), ("amino_acids", "Aminosäuren"), ("vitamins", "Vitamine"), ("minerals", "Mineralstoffe"), ("organic_acids", "Organische Säuren"), ("polyols", "Zuckeralkohole"), ("other", "Sonstige"), ("extension", "Eigene Erweiterungen"), ("unknown", "Ohne Gruppe"), ) GROUP_LABELS = {key: label for key, label in GROUP_META} GROUP_RANK = {key: i for i, (key, _) in enumerate(GROUP_META)} # Code → (name_de, unit, category) from BLS 4.0 Referenzhandbuch (Jan 2026) _OFFICIAL: dict[str, tuple[str, str, str]] = { "ENERCJ": ("Energie (Kilojoule)", "kJ", "energy"), "ENERCC": ("Energie (Kilokalorien)", "kcal", "energy"), "WATER": ("Wasser", "g", "macro"), "PROT625": ("Protein (N×6,25)", "g", "macro"), "FAT": ("Fett", "g", "macro"), "CHO": ("Kohlenhydrate, verfügbar", "g", "macro"), "FIBT": ("Ballaststoffe, gesamt", "g", "fiber"), "ALC": ("Alkohol (Ethanol)", "g", "macro"), "OA": ("Organische Säuren, gesamt", "g", "organic_acids"), "ASH": ("Rohasche", "g", "macro"), "POLYL": ("Zuckeralkohole, gesamt", "g", "polyols"), "GLUS": ("Glucose", "g", "carbs"), "FRUS": ("Fructose", "g", "carbs"), "GALS": ("Galactose", "g", "carbs"), "MNSAC": ("Monosaccharide, gesamt", "g", "carbs"), "SUCS": ("Saccharose", "g", "carbs"), "MALS": ("Maltose", "g", "carbs"), "LACS": ("Lactose", "g", "carbs"), "DISAC": ("Disaccharide, gesamt", "g", "carbs"), "SUGAR": ("Zucker, gesamt", "g", "carbs"), "OLSAC": ("Oligosaccharide, verfügbar", "g", "carbs"), "STARCH": ("Stärke", "g", "carbs"), "FIBLMW": ("Ballaststoffe, niedermolekular", "g", "fiber"), "FIBHMW": ("Ballaststoffe, hochmolekular", "g", "fiber"), "FIBINS": ("Ballaststoffe, wasserunlöslich", "g", "fiber"), "FIBSOL": ("Ballaststoffe, wasserlöslich", "g", "fiber"), "FIBHMWS": ("Ballaststoffe, hochmolekular, wasserlöslich", "g", "fiber"), "FIBHMWI": ("Ballaststoffe, hochmolekular, wasserunlöslich", "g", "fiber"), "FASAT": ("Fettsäuren, gesättigt, gesamt", "g", "fatty_acids"), "F4:0": ("Buttersäure (C4:0)", "g", "fatty_acids"), "F6:0": ("Capronsäure (C6:0)", "g", "fatty_acids"), "F8:0": ("Caprylsäure (C8:0)", "g", "fatty_acids"), "F10:0": ("Caprinsäure (C10:0)", "g", "fatty_acids"), "F12:0": ("Laurinsäure (C12:0)", "g", "fatty_acids"), "F14:0": ("Myristinsäure (C14:0)", "g", "fatty_acids"), "F15:0": ("Pentadecylsäure (C15:0)", "g", "fatty_acids"), "F16:0": ("Palmitinsäure (C16:0)", "g", "fatty_acids"), "F17:0": ("Margarinsäure (C17:0)", "g", "fatty_acids"), "F18:0": ("Stearinsäure (C18:0)", "g", "fatty_acids"), "F20:0": ("Arachinsäure (C20:0)", "g", "fatty_acids"), "F22:0": ("Behensäure (C22:0)", "g", "fatty_acids"), "F24:0": ("Lignocerinsäure (C24:0)", "g", "fatty_acids"), "FAMS": ("Fettsäuren, einfach ungesättigt, gesamt", "g", "fatty_acids"), "F14:1CN5": ("Myristoleinsäure (C14:1 n-5 cis)", "g", "fatty_acids"), "F16:1CN7": ("Palmitoleinsäure (C16:1 n-7 cis)", "g", "fatty_acids"), "F18:1CN7": ("Vaccensäure (C18:1 n-7 cis)", "g", "fatty_acids"), "F18:1CN9": ("Ölsäure (C18:1 n-9 cis)", "g", "fatty_acids"), "F20:1CN9": ("Gondosäure (C20:1 n-9 cis)", "g", "fatty_acids"), "F22:1CN9": ("Erucasäure (C22:1 n-9 cis)", "g", "fatty_acids"), "FAPU": ("Fettsäuren, mehrfach ungesättigt, gesamt", "g", "fatty_acids"), "FAPUN3": ("Omega-3-Fettsäuren, gesamt", "g", "fatty_acids"), "FAPUN6": ("Omega-6-Fettsäuren, gesamt", "g", "fatty_acids"), "F18:2CN6": ("Linolsäure (C18:2 n-6 cis)", "g", "fatty_acids"), "F18:2C9T11": ("Konjugierte Linolsäure (C18:2 cis-9, trans-11)", "g", "fatty_acids"), "F18:3CN3": ("Alpha-Linolensäure (C18:3 n-3)", "g", "fatty_acids"), "F18:3CN6": ("Gamma-Linolensäure (C18:3 n-6)", "g", "fatty_acids"), "F18:4CN3": ("Stearidonsäure (C18:4 n-3)", "g", "fatty_acids"), "F20:2CN6": ("Eicosadiensäure (C20:2 n-6)", "g", "fatty_acids"), "F20:3CN6": ("Dihomo-Gamma-Linolensäure (C20:3 n-6)", "g", "fatty_acids"), "F20:4CN6": ("Arachidonsäure (C20:4 n-6)", "g", "fatty_acids"), "F20:5CN3": ("Eicosapentaensäure EPA (C20:5 n-3)", "g", "fatty_acids"), "F22:5CN3": ("Docosapentaensäure DPA (C22:5 n-3)", "g", "fatty_acids"), "F22:6CN3": ("Docosahexaensäure DHA (C22:6 n-3)", "g", "fatty_acids"), "FAX": ("Fettsäuren, sonstige", "g", "fatty_acids"), "AAE9": ("Aminosäuren, unentbehrlich, gesamt", "g", "amino_acids"), "ILE": ("Isoleucin", "g", "amino_acids"), "LEU": ("Leucin", "g", "amino_acids"), "LYS": ("Lysin", "g", "amino_acids"), "MET": ("Methionin", "g", "amino_acids"), "PHE": ("Phenylalanin", "g", "amino_acids"), "THR": ("Threonin", "g", "amino_acids"), "TRP": ("Tryptophan", "g", "amino_acids"), "VAL": ("Valin", "g", "amino_acids"), "HIS": ("Histidin", "g", "amino_acids"), "ALA": ("Alanin", "g", "amino_acids"), "ARG": ("Arginin", "g", "amino_acids"), "ASP": ("Asparaginsäure inkl. Asparagin", "g", "amino_acids"), "CYS": ("Cystein", "g", "amino_acids"), "GLU": ("Glutaminsäure inkl. Glutamin", "g", "amino_acids"), "GLY": ("Glycin", "g", "amino_acids"), "PRO": ("Prolin", "g", "amino_acids"), "SER": ("Serin", "g", "amino_acids"), "TYR": ("Tyrosin", "g", "amino_acids"), "VITA": ("Vitamin A, Retinol-Äquivalent (RE)", "µg", "vitamins"), "VITAA": ("Vitamin A, Retinol-Aktivitätsäquivalent (RAE)", "µg", "vitamins"), "RETOL": ("Retinol", "µg", "vitamins"), "CARTB": ("Beta-Carotin", "µg", "vitamins"), "CAROTPAXB": ("Carotinoide außer Beta-Carotin", "µg", "vitamins"), "VITD": ("Vitamin D", "µg", "vitamins"), "ERGCAL": ("Vitamin D2 (Ergocalciferol)", "µg", "vitamins"), "CHOCAL": ("Vitamin D3 (Cholecalciferol)", "µg", "vitamins"), "VITE": ("Vitamin E (Alpha-Tocopherol)", "mg", "vitamins"), "TOCPHA": ("Alpha-Tocopherol", "mg", "vitamins"), "TOCPHB": ("Beta-Tocopherol", "mg", "vitamins"), "TOCPHG": ("Gamma-Tocopherol", "mg", "vitamins"), "TOCPHD": ("Delta-Tocopherol", "mg", "vitamins"), "TOCTRA": ("Alpha-Tocotrienol", "mg", "vitamins"), "VITK": ("Vitamin K", "µg", "vitamins"), "VITK1": ("Vitamin K1 (Phyllochinon)", "µg", "vitamins"), "VITK2": ("Vitamin K2 (Menachinone)", "µg", "vitamins"), "THIA": ("Vitamin B1 (Thiamin)", "mg", "vitamins"), "RIBF": ("Vitamin B2 (Riboflavin)", "mg", "vitamins"), "NIA": ("Niacin", "mg", "vitamins"), "NIAEQ": ("Niacin-Äquivalent", "mg", "vitamins"), "PANTAC": ("Pantothensäure", "mg", "vitamins"), "VITB6": ("Vitamin B6", "µg", "vitamins"), "BIOT": ("Biotin", "µg", "vitamins"), "FOL": ("Folat-Äquivalent", "µg", "vitamins"), "FOLFD": ("Folat", "µg", "vitamins"), "FOLAC": ("Folsäure, synthetisch", "µg", "vitamins"), "VITB12": ("Vitamin B12 (Cobalamine)", "µg", "vitamins"), "VITC": ("Vitamin C", "mg", "vitamins"), "NACL": ("Salz (Natriumchlorid)", "g", "minerals"), "NA": ("Natrium", "mg", "minerals"), "CLD": ("Chlorid", "mg", "minerals"), "K": ("Kalium", "mg", "minerals"), "CA": ("Calcium", "mg", "minerals"), "MG": ("Magnesium", "mg", "minerals"), "P": ("Phosphor", "mg", "minerals"), "S": ("Schwefel", "mg", "minerals"), "FE": ("Eisen", "mg", "minerals"), "ZN": ("Zink", "mg", "minerals"), "ID": ("Iodid", "µg", "minerals"), "CU": ("Kupfer", "µg", "minerals"), "MN": ("Mangan", "µg", "minerals"), "FD": ("Fluorid", "µg", "minerals"), "CR": ("Chrom", "µg", "minerals"), "MO": ("Molybdän", "µg", "minerals"), "ACEAC": ("Essigsäure", "g", "organic_acids"), "CITAC": ("Zitronensäure", "g", "organic_acids"), "LACAC": ("Milchsäure", "g", "organic_acids"), "MALAC": ("Äpfelsäure", "g", "organic_acids"), "TARAC": ("Weinsäure", "g", "organic_acids"), "MANTL": ("Mannit", "g", "polyols"), "SORTL": ("Sorbit", "g", "polyols"), "XYLTL": ("Xylit", "g", "polyols"), "CHORL": ("Cholesterin", "mg", "other"), "NT": ("Stickstoff, gesamt", "g", "other"), "EPA": ("Eicosapentaensäure (EPA)", "g", "fatty_acids"), "DHA": ("Docosahexaensäure (DHA)", "g", "fatty_acids"), "DPA": ("Docosapentaensäure (DPA)", "g", "fatty_acids"), "ALA": ("Alpha-Linolensäure (ALA)", "g", "fatty_acids"), "OMEGA3": ("Omega-3-Fettsäuren gesamt", "g", "fatty_acids"), "OMEGA6": ("Omega-6-Fettsäuren gesamt", "g", "fatty_acids"), } def lookup_key(raw: str | None) -> str: return str(raw or "").strip().upper() def resolve_group(attr_key: str | None, category: str | None = None, origin: str | None = None) -> tuple[str, str]: cat = (category or "").strip().lower() if cat in GROUP_LABELS: return cat, GROUP_LABELS[cat] lab = official_label(attr_key) if lab and lab["category"] in GROUP_LABELS: return lab["category"], GROUP_LABELS[lab["category"]] if (origin or "") == "extension": return "extension", GROUP_LABELS["extension"] return "unknown", GROUP_LABELS["unknown"] def annotate_attribute(row: dict[str, Any]) -> dict[str, Any]: key = row.get("attr_key") group_key, group_label = resolve_group(key, row.get("category"), row.get("origin")) row["is_macro"] = key in MACRO_KEYS row["needs_label"] = label_is_placeholder(row.get("name_de"), key) row["group_key"] = group_key row["group_label"] = group_label row["group_rank"] = GROUP_RANK[group_key] return row def sort_attributes(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: def key_of(row: dict[str, Any]): return ( row.get("group_rank", 99), 0 if row.get("is_macro") else 1, row.get("sort_order") if row.get("sort_order") is not None else 0, (row.get("name_de") or row.get("attr_key") or "").casefold(), row.get("attr_key") or "", ) return sorted(rows, key=key_of) def official_label(attr_key: str | None) -> dict[str, str] | None: key = lookup_key(attr_key) hit = _OFFICIAL.get(key) if not hit: hit = _OFFICIAL.get(key.replace("_", ":")) if not hit: return None name_de, unit, category = hit return {"name_de": name_de, "unit": unit, "category": category} def label_is_placeholder(name_de: str | None, attr_key: str | None) -> bool: name = (name_de or "").strip() key = lookup_key(attr_key) return (not name) or name.upper() == key def apply_official_name(attr_key: str | None, name_de: str | None) -> str: if not label_is_placeholder(name_de, attr_key): return (name_de or "").strip() lab = official_label(attr_key) if lab: return lab["name_de"] return (name_de or "").strip() or lookup_key(attr_key) def enrich_attributes_in_memory(attributes: list[dict[str, Any]]) -> list[dict[str, Any]]: for a in attributes: key = a.get("attr_key") lab = official_label(key) if label_is_placeholder(a.get("name_de"), key) and lab: a["name_de"] = lab["name_de"] a["unit"] = a.get("unit") or lab["unit"] a["category"] = a.get("category") or lab["category"] return attributes def fill_placeholder_labels(cur) -> dict[str, int]: cur.execute( """ SELECT id, attr_key, name_de, unit, category FROM food_attributes WHERE is_active = true """ ) updated = 0 for row in cur.fetchall(): rec = dict(row) lab = official_label(rec.get("attr_key")) if not lab: continue need_name = label_is_placeholder(rec.get("name_de"), rec.get("attr_key")) need_cat = not (rec.get("category") or "").strip() if not need_name and not need_cat: continue cur.execute( """ UPDATE food_attributes SET name_de = %s, unit = COALESCE(NULLIF(BTRIM(COALESCE(unit, '')), ''), %s), category = COALESCE(NULLIF(BTRIM(COALESCE(category, '')), ''), %s), updated_at = NOW() WHERE id = %s """, ( lab["name_de"] if need_name else rec.get("name_de"), lab["unit"], lab["category"], rec["id"], ), ) updated += 1 return {"updated": updated} def enrich_labels_from_food_headers(cur, headers: list[dict[str, Any]] | None) -> int: updated = 0 for h in headers or []: key = h.get("attr_key") name = (h.get("name_de") or "").strip() unit = (h.get("unit") or "").strip() or None if not key or not name or label_is_placeholder(name, key): lab = official_label(key) if not lab: continue name = lab["name_de"] unit = unit or lab["unit"] cur.execute( """ UPDATE food_attributes SET name_de = %s, unit = COALESCE(%s, unit), updated_at = NOW() WHERE attr_key = %s AND ( name_de IS NULL OR BTRIM(name_de) = '' OR name_de = attr_key ) """, (name, unit, key), ) if cur.rowcount: updated += cur.rowcount return updated