munch-ease-backend/api/shopping.py
jableader f51c90f922 - [x] Wire routers in main with minimal app code
- [x] Create api/deps module for get_db, cookie_person, and error_response
- [x] Use settings.py (DOOF_DB) for DB path in main and deps
2025-10-18 17:14:03 +11:00

233 lines
9.9 KiB
Python

from __future__ import annotations
from typing import Dict, List
import aiosqlite
from fastapi import APIRouter, Depends, Request
import ingredients
import meals
import persons
import recipes
import shopping
from common import ProblemDetails
from api.deps import get_db, cookie_person, error_response
router = APIRouter(prefix="/shopping", tags=["shopping"])
from common import ApiModel, Field
from typing import Optional
class CurrentShoppingList(ApiModel):
outstanding_items: List[shopping.ShoppingListItem]
requested_meals: List[shopping.ShoppingListItem]
purchased_items: List[shopping.ShoppingListItem] = Field(default_factory=list)
ingredients_lookup: Dict[int, ingredients.Ingredient] = Field(default_factory=dict)
meals_lookup: Dict[int, meals.Meal] = Field(default_factory=dict)
shopping_list_lookup: Dict[int, shopping.ShoppingList] = Field(default_factory=dict)
recipes_lookup: Dict[int, recipes.Recipe] = Field(default_factory=dict)
@router.get(
"/current",
response_model=CurrentShoppingList,
operation_id="getCurrentShoppingList",
summary="Get the current aggregated shopping list",
)
async def get_current_shopping_list(conn: aiosqlite.Connection = Depends(get_db)) -> CurrentShoppingList:
(
outstanding_requests,
purchased_requests,
meal_requests,
meals_lookup,
recipes_lookup,
ingredients_lookup,
) = await shopping.get_outstanding_requests(conn)
other_shopping_list_ids = {item.list_id for item in purchased_requests}
shopping_list_lookup = {}
for list_id in other_shopping_list_ids:
if list_id is not None:
sl = await shopping.load_shopping_list(conn, list_id)
if sl is not None:
shopping_list_lookup[list_id] = sl
# Add any additional items from shopping lists to the existing lookups
additional_items = [item for sl in shopping_list_lookup.values() for item in sl.items]
if additional_items:
await shopping.to_lookups(
conn, additional_items, meals_lookup, recipes_lookup, ingredients_lookup
)
return CurrentShoppingList(
outstanding_items=outstanding_requests,
requested_meals=meal_requests,
purchased_items=purchased_requests,
meals_lookup=meals_lookup,
shopping_list_lookup=shopping_list_lookup,
ingredients_lookup=ingredients_lookup,
recipes_lookup=recipes_lookup,
)
class PurchasedShoppingList(ApiModel):
list: shopping.ShoppingList
meals_lookup: Dict[int, meals.Meal] = Field(default_factory=dict)
ingredients_lookup: Dict[int, ingredients.Ingredient] = Field(default_factory=dict)
recipes_lookup: Dict[int, recipes.Recipe] = Field(default_factory=dict)
@router.get("/{list_id}", response_model=PurchasedShoppingList, operation_id="getShoppingList", summary="Get a purchased shopping list by id",
responses={404: {"model": ProblemDetails, "description": "Shopping list not found", "content": {"application/problem+json": {}}}})
async def get_shopping_list(list_id: int, conn: aiosqlite.Connection = Depends(get_db), request: Request | None = None) -> PurchasedShoppingList | ProblemDetails:
shopping_list = await shopping.load_shopping_list(conn, list_id)
if not shopping_list:
return error_response(request, 404, "Shopping list not found")
meals_lookup, recipes_lookup, ingredients_lookup = await shopping.to_lookups(
conn, shopping_list.items
)
return PurchasedShoppingList(
list=shopping_list,
meals_lookup=meals_lookup,
recipes_lookup=recipes_lookup,
ingredients_lookup=ingredients_lookup,
)
@router.post("", operation_id="purchaseIngredients", summary="Purchase ingredients for a shopping list")
async def purchase_ingredients(shopping_list: shopping.ShoppingList, conn: aiosqlite.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> PurchasedShoppingList:
shopping_list = shopping.ShoppingList(
purchased_by=person, items=shopping_list.items, store_name=shopping_list.store_name
)
await shopping.purchase(conn, shopping_list)
await conn.commit()
result = PurchasedShoppingList(list=shopping_list)
await shopping.to_lookups(
conn,
shopping_list.items,
result.meals_lookup,
result.recipes_lookup,
result.ingredients_lookup,
)
return result
@router.get("/current/me/ingredients", operation_id="getMyShoppingList", summary="Get my outstanding ingredient requests")
async def get_my_shopping_list(conn: aiosqlite.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> List[ingredients.Ingredient]:
return await shopping.get_persons_requests(conn, person.id)
@router.post("/current/me/ingredients", operation_id="syncMyShoppingList", summary="Sync my outstanding ingredient requests")
async def sync_my_shopping_list(requests: List[ingredients.Ingredient], conn: aiosqlite.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> List[ingredients.Ingredient]:
def isMatching(a: ingredients.Ingredient, b: ingredients.Ingredient) -> bool:
return a.id == b.id or a.line == b.line
my_shopping_list = await shopping.get_persons_requests(conn, person.id)
to_remove = [r for r in my_shopping_list if not any(isMatching(r, req) for req in requests)]
to_add = [req for req in requests if not any(isMatching(req, r) for r in my_shopping_list)]
for r in to_remove:
await shopping.remove_request(conn, person, ingredient=r)
for r in to_add:
if r.id < 0:
await ingredients.insert_ingredient(conn, r)
await shopping.request(conn, person, ingredient=r)
await conn.commit()
return await get_my_shopping_list(conn, person)
class MealIdWrapper(ApiModel):
meal_id: int
@router.post("/current/meals/me", response_model=None, operation_id="requestMeal", summary="Request a meal for shopping",
responses={404: {"model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}}})
async def request_meal(r: MealIdWrapper, conn: aiosqlite.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person), request: Request | None = None) -> shopping.ShoppingListItem | ProblemDetails:
meal = await meals.find_meal_by_id(conn, r.meal_id)
if not meal:
return error_response(request, 404, "Meal not found")
response = await shopping.request(conn, person, meal=meal)
await conn.commit()
return response
@router.delete("/current/meals/{meal_id}", response_model=None, operation_id="unrequestMeal", summary="Remove a meal request",
responses={404: {"model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}}})
async def unrequest_meal(meal_id: int, conn: aiosqlite.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person), request: Request | None = None) -> dict | ProblemDetails:
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 conn.commit()
return {}
@router.get("/current", operation_id="getCurrentShoppingList", summary="Get the current aggregated shopping list")
async def get_current_shopping_list(
conn: aiosqlite.Connection = Depends(get_db),
):
raise NotImplementedError("get_current_shopping_list extraction pending")
@router.get("/{list_id}", operation_id="getShoppingList", summary="Get a purchased shopping list by id",
responses={404: {"model": ProblemDetails, "description": "Shopping list not found", "content": {"application/problem+json": {}}}})
async def get_shopping_list(
list_id: int, conn: aiosqlite.Connection = Depends(get_db), request: Request | None = None
):
raise NotImplementedError("get_shopping_list extraction pending")
@router.post("", operation_id="purchaseIngredients", summary="Purchase ingredients for a shopping list")
async def purchase_ingredients(
shopping_list: shopping.ShoppingList,
conn: aiosqlite.Connection = Depends(get_db),
person: persons.Person = Depends(cookie_person),
):
raise NotImplementedError("purchase_ingredients extraction pending")
@router.get("/current/me/ingredients", operation_id="getMyShoppingList", summary="Get my outstanding ingredient requests")
async def get_my_shopping_list(
conn: aiosqlite.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)
) -> List[ingredients.Ingredient]:
raise NotImplementedError("get_my_shopping_list extraction pending")
@router.post("/current/me/ingredients", operation_id="syncMyShoppingList", summary="Sync my outstanding ingredient requests")
async def sync_my_shopping_list(
requests: List[ingredients.Ingredient],
conn: aiosqlite.Connection = Depends(get_db),
person: persons.Person = Depends(cookie_person),
) -> List[ingredients.Ingredient]:
raise NotImplementedError("sync_my_shopping_list extraction pending")
@router.post("/current/meals/{meal_id}", operation_id="requestMeal", summary="Request a meal for shopping",
responses={404: {"model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}}})
async def request_meal(
meal_id: int,
conn: aiosqlite.Connection = Depends(get_db),
person: persons.Person = Depends(cookie_person),
request: Request | None = None,
):
raise NotImplementedError("request_meal extraction pending")
@router.delete("/current/meals/{meal_id}", operation_id="unrequestMeal", summary="Remove a meal request",
responses={404: {"model": ProblemDetails, "description": "Meal not found", "content": {"application/problem+json": {}}}})
async def unrequest_meal(
meal_id: int,
conn: aiosqlite.Connection = Depends(get_db),
person: persons.Person = Depends(cookie_person),
request: Request | None = None,
):
raise NotImplementedError("unrequest_meal extraction pending")