Katalog, lernendes Mapping ohne KI, optionale Items und Import-Policy. Playwright-Smoke und Issue-Audit um Ernährung/Zuordnen/API ergänzt. Co-authored-by: Cursor <cursoragent@cursor.com>
127 lines
5.5 KiB
Python
127 lines
5.5 KiB
Python
"""Apply parsed BLS 4.0 data: upsert attributes and foods, never delete official rows."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
|
|
def upsert_attributes(cur, attributes: list[dict[str, Any]]) -> dict[str, int]:
|
|
inserted = updated = 0
|
|
for a in attributes:
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO food_attributes
|
|
(attr_key, name_de, name_en, unit, category, data_type, origin, sort_order, updated_at)
|
|
VALUES (%s, %s, %s, %s, %s, %s, 'official_bls', %s, NOW())
|
|
ON CONFLICT (attr_key) DO UPDATE SET
|
|
name_de = EXCLUDED.name_de,
|
|
name_en = COALESCE(EXCLUDED.name_en, food_attributes.name_en),
|
|
unit = COALESCE(EXCLUDED.unit, food_attributes.unit),
|
|
category = COALESCE(EXCLUDED.category, food_attributes.category),
|
|
sort_order = EXCLUDED.sort_order,
|
|
updated_at = NOW()
|
|
WHERE food_attributes.origin = 'official_bls'
|
|
RETURNING (xmax = 0) AS inserted
|
|
""",
|
|
(
|
|
a["attr_key"], a["name_de"], a.get("name_en"), a.get("unit"),
|
|
a.get("category"), a.get("data_type") or "num_per_100g",
|
|
a.get("sort_order") or 0,
|
|
),
|
|
)
|
|
row = cur.fetchone()
|
|
if row and row.get("inserted"):
|
|
inserted += 1
|
|
else:
|
|
updated += 1
|
|
return {"inserted": inserted, "updated": updated, "total": len(attributes)}
|
|
|
|
|
|
def upsert_foods(cur, foods: list[dict[str, Any]], bls_version: str = "4.0") -> dict[str, int]:
|
|
cur.execute("SELECT attr_key, id FROM food_attributes")
|
|
attr_ids = {r["attr_key"]: r["id"] for r in cur.fetchall()}
|
|
inserted = updated = values_written = 0
|
|
for f in foods:
|
|
code = f["bls_code"]
|
|
cur.execute("SELECT id FROM food_catalog WHERE bls_code = %s", (code,))
|
|
existing = cur.fetchone()
|
|
if existing:
|
|
food_id = existing["id"]
|
|
cur.execute(
|
|
"""
|
|
UPDATE food_catalog
|
|
SET name_de=%s, name_en=%s, food_group=%s, bls_version=%s,
|
|
catalog_kind='official_bls', source='bls_4.0', is_active=true, updated_at=NOW()
|
|
WHERE id=%s AND catalog_kind='official_bls'
|
|
""",
|
|
(f["name_de"], f.get("name_en"), f.get("food_group"), bls_version, food_id),
|
|
)
|
|
updated += 1
|
|
else:
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO food_catalog
|
|
(bls_code, name_de, name_en, food_group, catalog_kind, bls_version, source)
|
|
VALUES (%s, %s, %s, %s, 'official_bls', %s, 'bls_4.0')
|
|
RETURNING id
|
|
""",
|
|
(code, f["name_de"], f.get("name_en"), f.get("food_group"), bls_version),
|
|
)
|
|
food_id = cur.fetchone()["id"]
|
|
inserted += 1
|
|
for val in f.get("values") or []:
|
|
aid = attr_ids.get(val["attr_key"])
|
|
if not aid:
|
|
continue
|
|
if val.get("is_trace"):
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO food_attribute_values
|
|
(food_id, attribute_id, value_num, is_trace, origin_code, reference_text, updated_at)
|
|
VALUES (%s, %s, NULL, true, %s, %s, NOW())
|
|
ON CONFLICT (food_id, attribute_id) DO UPDATE SET
|
|
value_num = NULL, is_trace = true,
|
|
origin_code = EXCLUDED.origin_code,
|
|
reference_text = EXCLUDED.reference_text,
|
|
updated_at = NOW()
|
|
""",
|
|
(food_id, aid, val.get("origin_code"), val.get("reference_text")),
|
|
)
|
|
elif val.get("value_num") is None:
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO food_attribute_values
|
|
(food_id, attribute_id, value_num, is_trace, origin_code, reference_text, updated_at)
|
|
VALUES (%s, %s, NULL, false, %s, %s, NOW())
|
|
ON CONFLICT (food_id, attribute_id) DO UPDATE SET
|
|
value_num = NULL, is_trace = false,
|
|
origin_code = EXCLUDED.origin_code,
|
|
reference_text = EXCLUDED.reference_text,
|
|
updated_at = NOW()
|
|
""",
|
|
(food_id, aid, val.get("origin_code"), val.get("reference_text")),
|
|
)
|
|
else:
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO food_attribute_values
|
|
(food_id, attribute_id, value_num, is_trace, origin_code, reference_text, updated_at)
|
|
VALUES (%s, %s, %s, false, %s, %s, NOW())
|
|
ON CONFLICT (food_id, attribute_id) DO UPDATE SET
|
|
value_num = EXCLUDED.value_num, is_trace = false,
|
|
origin_code = EXCLUDED.origin_code,
|
|
reference_text = EXCLUDED.reference_text,
|
|
updated_at = NOW()
|
|
""",
|
|
(
|
|
food_id, aid, val["value_num"],
|
|
val.get("origin_code"), val.get("reference_text"),
|
|
),
|
|
)
|
|
values_written += 1
|
|
return {
|
|
"foods_inserted": inserted,
|
|
"foods_updated": updated,
|
|
"values_written": values_written,
|
|
"foods_total": len(foods),
|
|
}
|