munch-ease-backend/api/meals.py

173 lines
6.3 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
from fastapi import APIRouter, Depends, Query, Request
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]:
2025-10-18 06:05:48 +00:00
result = []
async for meal in meals.find_upcoming_meals_by_date_range(conn, date_from, to):
await meals.load_recipes(conn, meal)
await meals.load_extra_ingredients(conn, meal)
await meals.load_participants(conn, meal)
result.append(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(
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
validation_response = validate_meal(meal, request)
if validation_response:
return validation_response
await meals.insert_meal(conn, meal)
await conn.commit()
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)
await conn.commit()
return await get_meal(meal_id, 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)
await conn.commit()
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)
await conn.commit()
return meal
def get_duplicates(items: List[meals.Person]) -> set[str]:
seen: set[int] = set()
duplicates: set[str] = set()
for item in items:
if item.id in seen:
duplicates.add(item.name)
seen.add(item.id)
return duplicates
def validate_meal(meal: meals.Meal, request: Optional[Request] = None) -> Optional[Request]:
if not meal.chefs:
return error_response(request, 400, "Meal must have at least one chef")
if not meal.cleanup:
return error_response(request, 400, "Meal must have at least one cleanup person")
if not meal.consumers:
return error_response(request, 400, "Meal must have at least one consumer")
if len(meal.recipes) == 0 and len(meal.extra_ingredients) == 0:
return error_response(request, 400, "Meal must have at least one recipe or ingredient")
duplicates = get_duplicates(meal.chefs)
if duplicates:
return error_response(request, 400, f'Duplicate chef: {", ".join(duplicates)}')
duplicates = get_duplicates(meal.cleanup)
if duplicates:
return error_response(request, 400, f'Duplicate cleanup person: {", ".join(duplicates)}')
duplicates = get_duplicates(meal.consumers)
if duplicates:
return error_response(request, 400, f'Duplicate consumer: {", ".join(duplicates)}')
zero_servings = [r for r in meal.recipes if r.servings == 0]
if zero_servings:
return error_response(request, 400, "Recipe servings must be greater than 0")
return None