Files
autoayuno-api/main.py
2026-07-01 19:42:24 +02:00

162 lines
4.4 KiB
Python

"""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()