This commit is contained in:
Andre
2026-03-29 20:52:20 +02:00
commit e832c4e7ce
29 changed files with 3730 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
"""Tandoor REST API client."""
from __future__ import annotations
import logging
from typing import Optional
import httpx
from app.config import get_settings
from app.models import (
Ingredient,
Recipe,
TandoorIngredient,
TandoorRecipe,
TandoorStep,
)
log = logging.getLogger(__name__)
def recipe_to_tandoor(recipe: Recipe) -> TandoorRecipe:
"""Convert internal Recipe to Tandoor-compatible import format."""
working_time = (recipe.prep_time_minutes or 0) + (recipe.cook_time_minutes or 0)
# All ingredients go into step[0] (Tandoor convention)
tandoor_ingredients = [_to_tandoor_ingredient(ing) for ing in recipe.ingredients]
# Build steps: first step carries all ingredients, subsequent steps are plain text
steps: list[TandoorStep] = []
for i, step_text in enumerate(recipe.steps):
step = TandoorStep(
instruction=step_text,
ingredients=tandoor_ingredients if i == 0 else [],
time=0,
)
steps.append(step)
if not steps:
# If no steps, create a placeholder with all ingredients
steps = [TandoorStep(instruction="Zubereitung", ingredients=tandoor_ingredients)]
keywords = [{"label": tag, "name": tag} for tag in recipe.tags]
servings_text = "Portionen"
if recipe.nutrition and recipe.nutrition.kcal_per_serving:
kcal = round(recipe.nutrition.kcal_per_serving)
servings_text = f"Portionen (ca. {kcal} kcal)"
return TandoorRecipe(
name=recipe.title,
description=recipe.description or "",
servings=recipe.servings or 1,
working_time=working_time,
source_url=recipe.source_url,
image=recipe.image_url,
steps=steps,
keywords=keywords,
servings_text=servings_text,
)
def _to_tandoor_ingredient(ing: Ingredient) -> TandoorIngredient:
amount = 0.0
if ing.amount:
try:
from app.services.nutrition import _parse_float
val = _parse_float(ing.amount)
amount = val if val is not None else 0.0
except Exception:
amount = 0.0
unit = {"name": ing.unit} if ing.unit else None
return TandoorIngredient(
food={"name": ing.name},
amount=amount,
unit=unit,
note=ing.note or "",
)
async def import_to_tandoor(recipe: Recipe) -> dict:
"""Import a recipe to Tandoor via REST API. Returns created recipe data."""
settings = get_settings()
if not settings.tandoor_api_token:
raise RuntimeError("TANDOOR_API_TOKEN nicht konfiguriert.")
if not settings.tandoor_base_url:
raise RuntimeError("TANDOOR_BASE_URL nicht konfiguriert.")
base_url = settings.tandoor_base_url.rstrip("/")
headers = {
"Authorization": f"Bearer {settings.tandoor_api_token}",
"Content-Type": "application/json",
}
tandoor_recipe = recipe_to_tandoor(recipe)
payload = tandoor_recipe.model_dump(exclude={"image"})
async with httpx.AsyncClient(timeout=30) as client:
# POST recipe
resp = await client.post(
f"{base_url}/api/recipe/",
json=payload,
headers=headers,
)
if resp.status_code not in (200, 201):
raise RuntimeError(
f"Tandoor API Fehler {resp.status_code}: {resp.text[:300]}"
)
created = resp.json()
recipe_id = created.get("id")
# Upload image if available
if recipe.image_url and recipe_id:
await _upload_image(client, base_url, recipe_id, recipe.image_url, settings.tandoor_api_token)
return created
async def _upload_image(
client: httpx.AsyncClient,
base_url: str,
recipe_id: int,
image_url: str,
token: str,
) -> None:
"""Download image from URL and upload to Tandoor."""
try:
img_resp = await client.get(image_url, follow_redirects=True, timeout=15)
img_resp.raise_for_status()
image_data = img_resp.content
# Determine content type
content_type = img_resp.headers.get("content-type", "image/jpeg").split(";")[0]
ext = {"image/jpeg": "jpg", "image/png": "png", "image/webp": "webp"}.get(content_type, "jpg")
files = {"image": (f"recipe.{ext}", image_data, content_type)}
patch_resp = await client.patch(
f"{base_url}/api/recipe/{recipe_id}/",
files=files,
headers={"Authorization": f"Bearer {token}"}, # NO Content-Type here
)
if patch_resp.status_code not in (200, 201):
log.warning("Image upload failed: %s", patch_resp.text[:200])
except Exception as e:
log.warning("Could not upload image: %s", e)