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
201 lines
7.3 KiB
Python
201 lines
7.3 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Dict, List
|
|
|
|
import aiosqlite
|
|
from fastapi import APIRouter, Depends, Request, Response
|
|
|
|
import ingredients
|
|
import meals
|
|
import persons
|
|
import recipes
|
|
import shopping
|
|
from api.deps import cookie_person, error_response, get_db
|
|
from common import ApiModel, Field, ProblemDetails
|
|
|
|
router = APIRouter(prefix="/shopping", tags=["shopping"])
|
|
|
|
|
|
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 | Response:
|
|
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=shopping.ShoppingListItem,
|
|
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 | Response:
|
|
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
|
|
|
|
class Ok(ApiModel):
|
|
ok: bool = True
|
|
|
|
|
|
@router.delete(
|
|
"/current/meals/{meal_id}",
|
|
response_model=Ok,
|
|
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),
|
|
) -> Ok | 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)
|
|
return Ok()
|
|
|
|
|
|
# Removed duplicate placeholder endpoints left over from earlier scaffolding
|