Files
healthy-fit/backend/app/api/v1/endpoints/nutrition.py
Carlos Escalante f279907ae3 Add supplements, kettlebell, calendar, push notifications, and PWA support
- Supplement tracking: CRUD endpoints, /today, /logs, Supplements page
- Kettlebell workouts: session tracking, analytics endpoint, ActiveSession page
- Calendar module: events CRUD, calendar components
- Push notifications: VAPID keys, PushSubscription model, APScheduler reminders,
  service worker with push/notificationclick handlers, Profile notifications UI
- PWA: vite-plugin-pwa, manifest, icons, service worker generation
- Frontend: TypeScript types, API modules, ConfirmModal, toast notifications
- Auth fixes: password hashing, nutrition endpoint auth
- CLAUDE.md: project documentation and development guide

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 18:57:03 -06:00

156 lines
4.4 KiB
Python

from datetime import date as date_type
from datetime import datetime
from typing import Any
import litellm
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile
from pydantic import BaseModel
from sqlmodel import Session, select
from app.ai.nutrition import NutritionalInfo, analyze_nutrition_from_image, nutrition_module
from app.api import deps
from app.models.food import FoodLog # Added FoodItem
router = APIRouter()
class AnalyzeRequest(BaseModel):
description: str
class FoodLogCreate(BaseModel):
name: str
calories: float
protein: float
carbs: float
fats: float
@router.post("/analyze", response_model=NutritionalInfo)
def analyze_food(
request: AnalyzeRequest,
current_user: deps.CurrentUser,
) -> Any:
"""
Analyze food description and return nutritional info using DSPy.
"""
try:
result = nutrition_module(description=request.description)
return result.nutritional_info
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/analyze/image", response_model=NutritionalInfo)
async def analyze_food_image(
current_user: deps.CurrentUser,
file: UploadFile = File(...),
description: str = Form(""),
) -> Any:
"""
Analyze food image and return nutritional info.
"""
try:
contents = await file.read()
return analyze_nutrition_from_image(contents, description)
except litellm.exceptions.BadRequestError as e:
raise HTTPException(status_code=400, detail=f"Invalid image or request: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/log", response_model=FoodLog)
def log_food(
*,
session: Session = Depends(deps.get_session),
nutrition_info: FoodLogCreate,
current_user: deps.CurrentUser,
) -> Any:
"""
Save food log to database.
"""
food_log = FoodLog(
user_id=current_user.id,
name=nutrition_info.name,
calories=nutrition_info.calories,
protein=nutrition_info.protein,
carbs=nutrition_info.carbs,
fats=nutrition_info.fats,
)
session.add(food_log)
session.commit()
session.refresh(food_log)
return food_log
@router.get("/logs", response_model=list[FoodLog])
def read_logs(
current_user: deps.CurrentUser,
session: Session = Depends(deps.get_session),
skip: int = 0,
limit: int = 100,
) -> Any:
"""
Get food logs for current user.
"""
statement = (
select(FoodLog)
.where(FoodLog.user_id == current_user.id)
.order_by(FoodLog.timestamp.desc())
.offset(skip)
.limit(limit)
)
return session.exec(statement).all()
class NutritionSummary(BaseModel):
date: str
total_calories: float
total_protein: float
total_carbs: float
total_fats: float
log_count: int
target_calories: float | None
target_protein: float | None
target_carbs: float | None
target_fat: float | None
@router.get("/summary", response_model=NutritionSummary)
def get_nutrition_summary(
current_user: deps.CurrentUser,
session: Session = Depends(deps.get_session),
date: str = Query(default=None, description="Date in YYYY-MM-DD format, defaults to today"),
) -> Any:
"""
Get aggregated macro totals for a given day.
"""
if date:
target_date = datetime.strptime(date, "%Y-%m-%d").date()
else:
target_date = date_type.today()
start = datetime(target_date.year, target_date.month, target_date.day, 0, 0, 0)
end = datetime(target_date.year, target_date.month, target_date.day, 23, 59, 59)
statement = (
select(FoodLog)
.where(FoodLog.user_id == current_user.id)
.where(FoodLog.timestamp >= start)
.where(FoodLog.timestamp <= end)
)
logs = session.exec(statement).all()
return NutritionSummary(
date=target_date.isoformat(),
total_calories=sum(log.calories for log in logs),
total_protein=sum(log.protein for log in logs),
total_carbs=sum(log.carbs for log in logs),
total_fats=sum(log.fats for log in logs),
log_count=len(logs),
target_calories=getattr(current_user, "target_calories", None),
target_protein=getattr(current_user, "target_protein", None),
target_carbs=getattr(current_user, "target_carbs", None),
target_fat=getattr(current_user, "target_fat", None),
)