mirror of
https://github.com/Andre-Bzk/Rezept0r.git
synced 2026-11-15 09:33:05 +01:00
147 lines
4.6 KiB
Python
147 lines
4.6 KiB
Python
"""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)
|