- [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
233 lines
9.9 KiB
Python
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")
|