commit fcd005b8624023547f28b7b28e59e6099bcfc7d4
Author: jableader <jacobdunk@gmail.com>
Date: Sun Oct 19 20:24:07 2025 +1100
Openapi tightening
commit f93bd8f641d561052c7bd075bae321b4ff3b676d
Author: jableader <jacobdunk@gmail.com>
Date: Sun Oct 19 19:03:52 2025 +1100
Removed refactor strategy doc
commit 0c5a61092f522be0c47cbbe86917c8a7e4e2d339
Author: jableader <jacobdunk@gmail.com>
Date: Sun Oct 19 17:48:33 2025 +1100
mypy & ruff checks
commit 23d66d6b18984127e17c73c3063f6120385935e9
Author: jableader <jacobdunk@gmail.com>
Date: Sun Oct 19 16:49:35 2025 +1100
Final removal of db.py files
commit f454aed1ca9783cc558cc203f29a7fe31b62a975
Author: jableader <jacobdunk@gmail.com>
Date: Sun Oct 19 15:42:31 2025 +1100
Finalise restructure, remove db.py files
commit 7187f6dd89489521538791c6bdebb426514beb99
Author: jableader <jacobdunk@gmail.com>
Date: Sun Oct 19 15:34:54 2025 +1100
commit 6fea227ae20d32b8eb1e7a4885006a620fcc7bb1
Author: jableader <jacobdunk@gmail.com>
Date: Sun Oct 19 15:32:53 2025 +1100
commit 27415e7e02d89195ad514cb017a9dbbf84d7a5e4
Author: jableader <jacobdunk@gmail.com>
Date: Sun Oct 19 15:31:10 2025 +1100
commit b773428033d855f9ad82005602e049c1a2e3c585
Author: jableader <jacobdunk@gmail.com>
Date: Sun Oct 19 15:28:58 2025 +1100
commit 116592c95278d995f4c516e87f2cea43cf5b7735
Author: jableader <jacobdunk@gmail.com>
Date: Sun Oct 19 15:25:21 2025 +1100
commit 03ec565faea088971968ee2f9bb83e2de16b21f3
Author: jableader <jacobdunk@gmail.com>
Date: Sun Oct 19 15:21:29 2025 +1100
Plan
141 lines
5.5 KiB
Python
141 lines
5.5 KiB
Python
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
|