"""API de autoayuno: catálogo compartido de alimentos (alimentado de Open Food Facts bajo demanda) y recetas publicables. Sin cuentas: publicar es anónimo con un deviceToken.""" import uuid from fastapi import Depends, FastAPI from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from sqlalchemy import func from sqlalchemy.orm import Session import off from db import Base, engine, get_db from models import Food, Recipe def init_db(retries: int = 10): """Crea las tablas, reintentando por si la BD tarda en levantar. No tumba el arranque si falla: /health seguirá respondiendo.""" import time for _ in range(retries): try: Base.metadata.create_all(bind=engine) return True except Exception: time.sleep(2) return False # En import: un intento best-effort (crea tablas ya para tests/SQLite). init_db(retries=1) app = FastAPI(title="autoayuno-api") @app.on_event("startup") def _startup(): init_db() app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) @app.get("/health") def health(): return {"status": "ok"} # --------------------------------------------------------------------------- # Alimentos # --------------------------------------------------------------------------- class FoodIn(BaseModel): name: str kcalPer100g: float zone: str = "otros" portions: list = [] barcode: str | None = None deviceToken: str | None = None @app.get("/foods") def search_foods(q: str = "", limit: int = 20, db: Session = Depends(get_db)): q = (q or "").strip() if len(q) < 2: return [] local = ( db.query(Food) .filter(func.lower(Food.name).like(f"%{q.lower()}%")) .limit(limit) .all() ) results = [f.to_app() for f in local] # Si hay pocos, enriquecemos con Open Food Facts y guardamos. if len(local) < 5: seen_off = {f.off_id for f in local if f.off_id} seen_names = {f.name.lower() for f in local} for item in off.search(q): if item["off_id"] and item["off_id"] in seen_off: continue if item["name"].lower() in seen_names: continue # Dedupe contra la BD por off_id. if item["off_id"] and db.query(Food).filter(Food.off_id == item["off_id"]).first(): continue food = Food( id=str(uuid.uuid4()), name=item["name"], kcal_100g=item["kcal_100g"], zone="otros", portions=[], barcode=item.get("barcode"), source="off", off_id=item.get("off_id"), ) db.add(food) seen_names.add(item["name"].lower()) results.append(food.to_app()) db.commit() return results[:limit] @app.post("/foods") def add_food(body: FoodIn, db: Session = Depends(get_db)): food = Food( id=str(uuid.uuid4()), name=body.name.strip()[:120], kcal_100g=body.kcalPer100g, zone=body.zone, portions=body.portions or [], barcode=body.barcode, source="user", author_token=body.deviceToken, ) db.add(food) db.commit() return food.to_app() # --------------------------------------------------------------------------- # Recetas # --------------------------------------------------------------------------- class RecipeIn(BaseModel): name: str servings: int = 1 items: list = [] deviceToken: str | None = None @app.get("/recipes") def search_recipes(q: str = "", limit: int = 20, db: Session = Depends(get_db)): query = db.query(Recipe) q = (q or "").strip() if q: query = query.filter(func.lower(Recipe.name).like(f"%{q.lower()}%")) return [r.to_app() for r in query.limit(limit).all()] @app.get("/recipes/{recipe_id}") def get_recipe(recipe_id: str, db: Session = Depends(get_db)): r = db.query(Recipe).filter(Recipe.id == recipe_id).first() return r.to_app() if r else {} @app.post("/recipes") def add_recipe(body: RecipeIn, db: Session = Depends(get_db)): r = Recipe( id=str(uuid.uuid4()), name=body.name.strip()[:140], servings=body.servings, items=body.items or [], author_token=body.deviceToken, ) db.add(r) db.commit() return r.to_app()