feat: autoayuno-api (FastAPI) — alimentos (OFF) y recetas

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Carlos Narro
2026-07-01 19:35:02 +02:00
commit 150a323a37
9 changed files with 369 additions and 0 deletions

141
main.py Normal file
View File

@@ -0,0 +1,141 @@
"""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
Base.metadata.create_all(bind=engine)
app = FastAPI(title="autoayuno-api")
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()