mitai-jinkendo/backend/bls/attribute_labels.py
Lars 44e3e216ab
All checks were successful
Deploy Development / deploy (push) Successful in 1m4s
Build Test / pytest-backend (push) Successful in 5s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 21s
feat: Stoffe nach BLS-Gruppen sortieren und aufklappbar listen
Energie und Makros zuerst, restliche Naehrstoffgruppen zuklappbar, damit die Liste bedienbar bleibt.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 13:09:44 +02:00

322 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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