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 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, request: Request, conn: aiosqlite.Connection = Depends(get_db)) -> 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) 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) 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, request: Request, conn: aiosqlite.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> 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) 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, request: Request, conn: aiosqlite.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> 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) return {} # Removed duplicate placeholder endpoints left over from earlier scaffolding