munch-ease-backend/api/meals.py

145 lines
5.5 KiB
Python
Raw Normal View History

2025-10-18 05:50:43 +00:00
from __future__ import annotations
from typing import List, Optional
import datetime
import aiosqlite
2025-10-18 07:21:04 +00:00
from fastapi import APIRouter, Depends, Query, Request, Response
2025-10-18 05:50:43 +00:00
import meals
import persons
import shopping
from common import ProblemDetails
from api.deps import get_db, cookie_person, error_response
2025-10-18 06:05:48 +00:00
from common import ApiModel, Field
import datetime
from typing import Dict
2025-10-18 05:50:43 +00:00
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] = []
2025-10-18 06:05:48 +00:00
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:
2025-10-18 06:05:48 +00:00
await meals.load_recipes(conn, meal)
await meals.load_extra_ingredients(conn, meal)
return result
2025-10-18 05:50:43 +00:00
@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)
2025-10-18 05:50:43 +00:00
) -> meals.Meal:
2025-10-18 06:05:48 +00:00
meal = await meals.find_meal_by_id(conn, meal_id)
if not meal:
return error_response(request, 404, "Meal not found")
return meal
2025-10-18 05:50:43 +00:00
@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(
2025-10-18 07:21:04 +00:00
meal: meals.Meal, request: Request, response: Response, conn: aiosqlite.Connection = Depends(get_db)
2025-10-18 05:50:43 +00:00
) -> meals.Meal:
2025-10-18 06:05:48 +00:00
validation_response = validate_meal(meal, request)
if validation_response:
return validation_response
await meals.insert_meal(conn, meal)
2025-10-18 07:21:04 +00:00
response.headers["Location"] = f"/api/v1/meals/{meal.id}"
2025-10-18 06:05:48 +00:00
return meal
2025-10-18 05:50:43 +00:00
@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)
2025-10-18 05:50:43 +00:00
) -> meals.Meal:
2025-10-18 06:05:48 +00:00
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)
2025-10-18 05:50:43 +00:00
@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,
2025-10-18 05:50:43 +00:00
consumed_date: Optional[datetime.datetime] = None,
conn: aiosqlite.Connection = Depends(get_db),
person: persons.Person = Depends(cookie_person),
) -> meals.Meal:
2025-10-18 06:05:48 +00:00
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
2025-10-18 05:50:43 +00:00
@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,
2025-10-18 05:50:43 +00:00
conn: aiosqlite.Connection = Depends(get_db),
person: persons.Person = Depends(cookie_person),
) -> meals.Meal:
2025-10-18 06:05:48 +00:00
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[Request]:
"""HTTP-friendly wrapper that maps service validation to ProblemDetails."""
msg = meals.validate_meal(meal)
if msg:
return error_response(request, 400, msg)
2025-10-18 06:05:48 +00:00
return None