from __future__ import annotations import datetime from typing import List, Optional import aiosqlite from fastapi import APIRouter, Depends, Query, Request, Response import meals import persons import shopping from api.deps import cookie_person, error_response, get_db from common import ProblemDetails router = APIRouter(prefix="/meals", tags=["meals"]) @router.get( "/upcoming", operation_id="getUpcomingMeals", summary="List upcoming meals in a date range" ) async def get_upcoming_meals( date_from: datetime.datetime = Query(..., alias="from"), to: datetime.datetime = Query(...), conn: aiosqlite.Connection = Depends(get_db), ) -> List[meals.Meal]: # Load base meals result: List[meals.Meal] = [] async for meal in meals.find_upcoming_meals_by_date_range(conn, date_from, to): result.append(meal) if not result: return result # Batch load participants for all meals await meals.bulk_load_participants(conn, result) # Load recipes and extra ingredients per meal (recipes include a small join) for meal in result: await meals.load_recipes(conn, meal) await meals.load_extra_ingredients(conn, meal) return result @router.get( "/{meal_id}", response_model=meals.Meal, operation_id="getMeal", summary="Get a meal by id", responses={ 404: { "model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}, } }, ) async def get_meal( meal_id: int, request: Request, conn: aiosqlite.Connection = Depends(get_db) ) -> meals.Meal | Response: meal = await meals.find_meal_by_id(conn, meal_id) if not meal: return error_response(request, 404, "Meal not found") return meal @router.post( "", response_model=meals.Meal, operation_id="createMeal", summary="Create a new meal", responses={ 400: { "model": ProblemDetails, "description": "Validation error", "content": {"application/problem+json": {}}, } }, ) async def create_meal( meal: meals.Meal, request: Request, response: Response, conn: aiosqlite.Connection = Depends(get_db), ) -> meals.Meal | Response: validation_response = validate_meal(meal, request) if validation_response: return validation_response await meals.insert_meal(conn, meal) response.headers["Location"] = f"/api/v1/meals/{meal.id}" return meal @router.put( "/{meal_id}", response_model=meals.Meal, operation_id="updateMeal", summary="Update an existing meal", responses={ 400: { "model": ProblemDetails, "description": "Validation error", "content": {"application/problem+json": {}}, }, 404: { "model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}, }, }, ) async def update_meal( meal_id: int, meal: meals.Meal, request: Request, conn: aiosqlite.Connection = Depends(get_db) ) -> meals.Meal | Response: if meal.id != meal_id: return error_response(request, 400, "Meal ID in URL does not match meal ID in body") existing = await meals.find_meal_by_id(conn, meal_id) if not existing: return error_response(request, 404, "Meal not found") validation_response = validate_meal(meal, request) if validation_response: return validation_response await meals.update_meal(conn, meal) # Re-fetch and return the updated meal. Pass request and conn explicitly to avoid Depends resolution. return await get_meal(meal_id, request, conn) @router.post( "/{meal_id}/consumed", response_model=meals.Meal, operation_id="markMealConsumed", summary="Mark a meal as consumed", responses={ 400: { "model": ProblemDetails, "description": "Validation error", "content": {"application/problem+json": {}}, }, 404: { "model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}, }, }, ) async def mark_consumed( meal_id: int, request: Request, consumed_date: Optional[datetime.datetime] = None, conn: aiosqlite.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person), ) -> meals.Meal | Response: if consumed_date and not consumed_date.tzinfo: return error_response(request, 400, "Consumed date must include timezone") meal = await meals.find_meal_by_id(conn, meal_id) if not meal: return error_response(request, 404, "Meal not found") await meals.mark_consumed(conn, meal, consumed_date or datetime.datetime.now().astimezone()) await shopping.remove_request(conn, person, meal=meal) return meal @router.delete( "/{meal_id}", response_model=meals.Meal, operation_id="deleteMeal", summary="Delete a meal", responses={ 404: { "model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}, } }, ) async def delete_meal( meal_id: int, request: Request, conn: aiosqlite.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person), ) -> meals.Meal | Response: meal = await meals.find_meal_by_id(conn, meal_id) if not meal: return error_response(request, 404, "Meal not found") await shopping.remove_request(conn, person, meal=meal) await meals.delete_meal(conn, meal.id) return meal def validate_meal(meal: meals.Meal, request: Optional[Request] = None) -> Optional[Response]: """HTTP-friendly wrapper that maps service validation to ProblemDetails.""" msg = meals.validate_meal(meal) if msg: return error_response(request, 400, msg) return None