Initial commit
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, HTTPException, Query, status
|
||||
from sqlmodel import select
|
||||
|
||||
from app.access import product_barcode_accessible
|
||||
from app.deps import CurrentUserDep, SessionDep
|
||||
from app.models import FoodLog, Product
|
||||
from app.schemas import ProductCreate, ProductRead, ProductSearchResult
|
||||
|
||||
router = APIRouter(prefix="/api/products", tags=["products"])
|
||||
|
||||
OFF_API_URL = "https://world.openfoodfacts.org/api/v2/product/{barcode}.json"
|
||||
|
||||
|
||||
def _extract_macros(nutriments: dict) -> dict:
|
||||
calories = nutriments.get("energy-kcal_100g")
|
||||
if calories is None:
|
||||
energy_kj = nutriments.get("energy_100g")
|
||||
calories = energy_kj / 4.184 if energy_kj is not None else 0.0
|
||||
|
||||
return {
|
||||
"calories": calories,
|
||||
"carbs": nutriments.get("carbohydrates_100g", 0.0) or 0.0,
|
||||
"protein": nutriments.get("proteins_100g", 0.0) or 0.0,
|
||||
"fat": nutriments.get("fat_100g", 0.0) or 0.0,
|
||||
"sugar": nutriments.get("sugars_100g", 0.0) or 0.0,
|
||||
"fiber": nutriments.get("fiber_100g", 0.0) or 0.0,
|
||||
"saturated_fat": nutriments.get("saturated-fat_100g", 0.0) or 0.0,
|
||||
"salt": nutriments.get("salt_100g", 0.0) or 0.0,
|
||||
}
|
||||
|
||||
|
||||
async def _fetch_from_open_food_facts(barcode: str) -> Product | None:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(OFF_API_URL.format(barcode=barcode))
|
||||
except httpx.RequestError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Open Food Facts ist gerade nicht erreichbar",
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
|
||||
data = response.json()
|
||||
if data.get("status") != 1:
|
||||
return None
|
||||
|
||||
product_data = data.get("product", {})
|
||||
macros = _extract_macros(product_data.get("nutriments", {}))
|
||||
name = product_data.get("product_name") or product_data.get("generic_name") or "Unbekanntes Produkt"
|
||||
|
||||
return Product(barcode=barcode, name=name, **macros)
|
||||
|
||||
|
||||
@router.post("", response_model=ProductRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_product(product_in: ProductCreate, current_user: CurrentUserDep, session: SessionDep):
|
||||
barcode = product_in.barcode or f"manual-{uuid4().hex[:12]}"
|
||||
|
||||
existing = session.exec(select(Product).where(Product.barcode == barcode)).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Barcode existiert bereits")
|
||||
|
||||
product = Product(**{**product_in.model_dump(), "barcode": barcode})
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.refresh(product)
|
||||
return product
|
||||
|
||||
|
||||
@router.get("/search", response_model=list[ProductSearchResult])
|
||||
def search_products(
|
||||
current_user: CurrentUserDep,
|
||||
session: SessionDep,
|
||||
q: str = Query(default=""),
|
||||
limit: int = Query(default=20, ge=1, le=50),
|
||||
):
|
||||
query = select(Product)
|
||||
if q:
|
||||
query = query.where(Product.name.ilike(f"%{q}%"))
|
||||
products = [
|
||||
product
|
||||
for product in session.exec(query).all()
|
||||
if product_barcode_accessible(session, product.barcode, current_user.id)
|
||||
]
|
||||
|
||||
recent_logs = session.exec(
|
||||
select(FoodLog)
|
||||
.where(FoodLog.user_id == current_user.id)
|
||||
.order_by(FoodLog.timestamp.desc())
|
||||
.limit(500)
|
||||
).all()
|
||||
last_used: dict[str, FoodLog] = {}
|
||||
for log in recent_logs:
|
||||
if log.barcode and log.barcode not in last_used:
|
||||
last_used[log.barcode] = log
|
||||
|
||||
def sort_key(product: Product):
|
||||
log = last_used.get(product.barcode)
|
||||
if log:
|
||||
return (0, -log.timestamp.timestamp())
|
||||
return (1, product.name.lower())
|
||||
|
||||
products.sort(key=sort_key)
|
||||
|
||||
results = []
|
||||
for product in products[:limit]:
|
||||
log = last_used.get(product.barcode)
|
||||
results.append(
|
||||
ProductSearchResult(
|
||||
**product.model_dump(),
|
||||
last_amount_g=log.amount_g if log else None,
|
||||
is_dish=product.barcode.startswith("dish-"),
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/{barcode}", response_model=ProductRead)
|
||||
async def get_product(barcode: str, current_user: CurrentUserDep, session: SessionDep):
|
||||
product = session.exec(select(Product).where(Product.barcode == barcode)).first()
|
||||
if product:
|
||||
if not product_barcode_accessible(session, barcode, current_user.id):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Produkt nicht gefunden")
|
||||
return product
|
||||
|
||||
product = await _fetch_from_open_food_facts(barcode)
|
||||
if product is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Produkt nicht gefunden")
|
||||
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.refresh(product)
|
||||
return product
|
||||
Reference in New Issue
Block a user