"""Rezepte aus Mealie, gegen den eigenen Bestand geprüft. Die Seite beantwortet eine Frage: Kann ich das heute kochen, oder muss ich vorher einkaufen? Das Rechnen steckt in ``services/rezepte.py`` (ohne Netz), das Holen in ``services/mealie.py`` (ohne Rechnen). """ # ACHTUNG: Kein ``from __future__ import annotations`` – siehe package_types.py. from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from ..database import get_db from ..deps import get_current_user from ..models import User from ..schemas import ( RecipeDetail, RecipePage, RecipeSummary, ZutatKandidat, ZutatOut, ) from ..services import mealie, rezepte from ..services.einkaufsliste_text import fehlmengen_text router = APIRouter(prefix="/recipes", tags=["recipes"]) def _fehler(fehler: mealie.MealieFehler) -> HTTPException: """Mealie-Kennung auf einen Statuscode abbilden. 503 heisst „hier ist nichts eingerichtet", 502 „Mealie antwortet nicht" – die Oberfläche soll beides verschieden erklären können. """ code = ( status.HTTP_503_SERVICE_UNAVAILABLE if fehler.kennung == "nicht_eingerichtet" else status.HTTP_502_BAD_GATEWAY ) return HTTPException(code, str(fehler)) def _bild(eintrag: dict, basis: str) -> str | None: """Mealie liefert Bilder unter /api/media/recipes//images/min-original.webp.""" kennung = eintrag.get("id") if not kennung or not basis: return None return f"{basis}/api/media/recipes/{kennung}/images/min-original.webp" @router.get("", response_model=RecipePage) def list_recipes( page: int = 1, per_page: int = 50, q: str | None = None, db: Session = Depends(get_db), _: User = Depends(get_current_user), ) -> RecipePage: """Rezeptliste aus Mealie, blätterbar und durchsuchbar.""" try: daten = mealie.rezepte(db, seite=page, pro_seite=per_page, suche=q) except mealie.MealieFehler as fehler: raise _fehler(fehler) from fehler basis = mealie.basis_adresse(db) eintraege = [ RecipeSummary( slug=str(e.get("slug") or ""), name=str(e.get("name") or e.get("slug") or "Ohne Titel"), description=str(e.get("description") or "")[:300], image_url=_bild(e, basis), servings=e.get("recipeServings") or e.get("recipe_servings") or None, ) for e in (daten.get("items") or []) if e.get("slug") ] return RecipePage( items=eintraege, page=page, per_page=per_page, total=int(daten.get("total") or len(eintraege)), ) @router.get("/{slug}", response_model=RecipeDetail) def get_recipe( slug: str, servings: float | None = None, db: Session = Depends(get_db), _: User = Depends(get_current_user), ) -> RecipeDetail: """Ein Rezept mit allen Zutaten, auf ``servings`` Portionen hochgerechnet.""" try: daten = mealie.rezept(db, slug) except mealie.MealieFehler as fehler: raise _fehler(fehler) from fehler faktor, hinweis = rezepte.faktor_fuer_portionen(daten, servings) zutaten = daten.get("recipeIngredient") or daten.get("recipe_ingredient") or [] pruefungen = rezepte.pruefe_zutaten(db, zutaten, faktor=faktor) basis_portionen = ( daten.get("recipeServings") or daten.get("recipe_servings") or daten.get("recipeYieldQuantity") or daten.get("recipe_yield_quantity") or None ) return RecipeDetail( slug=slug, name=str(daten.get("name") or slug), description=str(daten.get("description") or ""), image_url=_bild(daten, mealie.basis_adresse(db)), base_servings=basis_portionen, servings=servings or basis_portionen, skalierung_hinweis=hinweis, kochbar=rezepte.kochbar(pruefungen), ingredients=[_zutat_out(db, p) for p in pruefungen], ) def _zutat_out(db: Session, p: rezepte.ZutatPruefung) -> ZutatOut: return ZutatOut( text=p.text, menge=p.menge, einheit=p.einheit, art=p.art, ziel_id=p.ziel_id, ziel_name=p.ziel_name, kandidaten=[ ZutatKandidat(art=k.art, id=k.id, name=k.name, score=k.score) for k in p.kandidaten ], benoetigt_base=p.benoetigt_base, bestand_base=p.bestand_base, fehlt_base=p.fehlt_base, basis_einheit=p.basis_einheit, status=p.status, hinweis=p.hinweis, fehlendes_mass=p.fehlendes_mass, kategorie_id=p.kategorie_id, fehlt_text=fehlmengen_text(db, p), )