Schritt 1: Fundament der Lebensmittel-Lagerverwaltung (Pantry)
Backend (FastAPI + PostgreSQL): Chargen mit MHD, FEFO-Auslagern, Einheiten-Umrechnung (Stueck/g/ml + Packungen), Open-Food-Facts-Lookup mit lokalem Fallback, JWT-Auth mit Rollen (Admin/Nutzer), erster Admin beim Setup, Einkaufsliste, Ablaufwarnung, Lagerorte, pytest fuer FEFO. Web-UI (React/Vite): Login, Dashboard, Ein-/Auslagern, Produkte, Lagerorte, Benutzerverwaltung, Einkaufsliste - rollenabhaengig. Deploy: docker-compose + install.sh (Docker-Autoinstall, Secrets), README und Roadmap fuer Schritt 2 (iOS) und Schritt 3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
72
backend/app/off.py
Normal file
72
backend/app/off.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Open Food Facts Lookup-Client."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import httpx
|
||||
|
||||
from .config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
def _guess_base_unit(quantity: str | None) -> str:
|
||||
"""Rät die Basiseinheit aus dem OFF-Feld 'quantity' (z.B. '500 g', '1 l')."""
|
||||
if not quantity:
|
||||
return "piece"
|
||||
q = quantity.lower()
|
||||
if "ml" in q or "cl" in q or "l" in q or "liter" in q:
|
||||
return "milliliter"
|
||||
if "kg" in q or " g" in q or "gramm" in q or q.strip().endswith("g"):
|
||||
return "gram"
|
||||
return "piece"
|
||||
|
||||
|
||||
def lookup_barcode(barcode: str) -> dict | None:
|
||||
"""Fragt Open Food Facts nach einem Barcode.
|
||||
|
||||
Gibt ein vorbefülltes Produkt-Dict zurück oder None, wenn nicht gefunden.
|
||||
"""
|
||||
url = f"{settings.off_base_url}/api/v2/product/{barcode}.json"
|
||||
try:
|
||||
resp = httpx.get(
|
||||
url,
|
||||
timeout=settings.off_timeout_seconds,
|
||||
headers={"User-Agent": "Pantry-Selfhosted/1.0 (github: pantry)"},
|
||||
)
|
||||
except httpx.HTTPError:
|
||||
return None
|
||||
|
||||
if resp.status_code != 200:
|
||||
return None
|
||||
|
||||
data = resp.json()
|
||||
if data.get("status") != 1:
|
||||
return None
|
||||
|
||||
product = data.get("product", {})
|
||||
name = (
|
||||
product.get("product_name_de")
|
||||
or product.get("product_name")
|
||||
or product.get("generic_name")
|
||||
or ""
|
||||
).strip()
|
||||
if not name:
|
||||
return None
|
||||
|
||||
categories = product.get("categories") or ""
|
||||
category_tags = product.get("categories_tags") or []
|
||||
|
||||
return {
|
||||
"barcode": barcode,
|
||||
"name": name,
|
||||
"brand": (product.get("brands") or "").strip() or None,
|
||||
"image_url": product.get("image_front_url") or product.get("image_url") or None,
|
||||
"base_unit": _guess_base_unit(product.get("quantity")),
|
||||
"quantity_text": product.get("quantity"),
|
||||
"category_suggestion": categories.split(",")[0].strip() if categories else None,
|
||||
"category_tags": category_tags,
|
||||
"source": "off",
|
||||
"off_raw": json.dumps(product)[:20000],
|
||||
}
|
||||
Reference in New Issue
Block a user