162 lines
4.4 KiB
Python
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()
|