From efd0dabc8775dca12b8764a6f6676e5802f9eddf Mon Sep 17 00:00:00 2001 From: jableader Date: Sun, 27 Jul 2025 15:24:54 +1000 Subject: [PATCH] Can edit own ingredient requests --- common.py | 23 +++ db.py | 2 +- ingredients/__init__.py | 2 +- main.py | 62 ++++---- shopping/__init__.py | 29 +++- shopping/db.py | 314 +++++++++++++++++----------------------- 6 files changed, 223 insertions(+), 209 deletions(-) create mode 100644 common.py diff --git a/common.py b/common.py new file mode 100644 index 0000000..25407d2 --- /dev/null +++ b/common.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel, Field, model_validator +from typing import Optional, Any + +class BaseLinkedModel(BaseModel): + model_config = dict(arbitrary_types_allowed=True) + + @model_validator(mode="before") + @classmethod + def auto_populate_ids(cls, data: dict[str, Any]) -> dict[str, Any]: + if isinstance(data, dict): + for key, value in data.copy().items(): + if not key.endswith("_id") and hasattr(value, "id") and value is not None: + id_key = key + "_id" + + if id_key in data: + # If the id_key already exists, ensure it matches the value's id + if data[id_key] != value.id: + raise ValueError(f"ID mismatch for {key}: {data[id_key]} != {value.id}") + else: + # If the id_key does not exist, set it to the value's id + data[id_key] = value.id + + return data \ No newline at end of file diff --git a/db.py b/db.py index 604bcf5..3c12fee 100644 --- a/db.py +++ b/db.py @@ -1,6 +1,6 @@ import aiosqlite -async def connect(path = './data/your_database.db') -> aiosqlite.Connection: +async def connect(path = './data/doof.sqlite') -> aiosqlite.Connection: return await aiosqlite.connect(path) async def create(conn: aiosqlite.Connection): diff --git a/ingredients/__init__.py b/ingredients/__init__.py index 0d4799d..f23244a 100644 --- a/ingredients/__init__.py +++ b/ingredients/__init__.py @@ -55,7 +55,7 @@ def parse_ingredient_from_nlp(ingredient_string: str) -> Ingredient: if unit is None: unit = units.ITEMS.name - return Ingredient(id=0, + return Ingredient(id=-1, line=ingredient.sentence, name=name, quantity=quantity, diff --git a/main.py b/main.py index f94cf04..bb801e9 100644 --- a/main.py +++ b/main.py @@ -252,53 +252,65 @@ async def delete_meal(meal_id: int, conn: sqlite3.Connection = Depends(get_db)) return meal class CurrentShoppingList(BaseModel): - requests: List[shopping.ShoppingListRequest] - overlapping_previous_shops: List[shopping.ShoppingList] + requests: List[shopping.ShoppingListItem] + previously_purchased: List[shopping.ShoppingListItem] = [] + other_shopping_lists: List[shopping.ShoppingList] = [] @app.get("/api/shopping/current") async def get_current_shopping_list(conn: sqlite3.Connection = Depends(get_db)) -> CurrentShoppingList: - current_requests = [r async for r in shopping.get_current_requests(conn)] - - requested_meals = [r.meal_id for r in current_requests if r.meal_id] - upcoming_meals = [m.id async for m in meals.find_upcoming_meals_by_date_range(conn, datetime.datetime.now().astimezone(), datetime.datetime.now().astimezone() + datetime.timedelta(days=14))] + outstanding_requests, purchased_requests = await shopping.get_outstanding_requests(conn) + other_shopping_list_ids = {item.shopping_list for item in purchased_requests} + other_shopping_lists = [await shopping.load_shopping_list(conn, list_id) for list_id in other_shopping_list_ids if list_id is not None] - overlapping = {} - for meal_id in set(requested_meals + upcoming_meals): - async for r in shopping.get_shopping_list_with_meal(conn, meal_id): - overlapping[r.id] = r - - return CurrentShoppingList(requests=current_requests, overlapping_previous_shops=list(overlapping.values())) + return CurrentShoppingList(requests=outstanding_requests, previously_purchased=purchased_requests, other_shopping_lists=other_shopping_lists) @app.get("/api/shopping/{list_id}") async def get_shopping_list(list_id: int, conn: sqlite3.Connection = Depends(get_db)) -> shopping.ShoppingList: return await shopping.load_shopping_list(conn, list_id) @app.post("/api/shopping/") -async def purchase_ingredients(lst: shopping.ShoppingListPurchase, conn: sqlite3.Connection = Depends(get_db)) -> shopping.ShoppingList: - await shopping.purchase_ingredients(conn, lst) +async def purchase_ingredients(shopping_list: shopping.ShoppingList, conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> shopping.ShoppingList: + 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() - return lst - + + return shopping_list + @app.get("/api/shopping/current/me/ingredients") -async def get_my_shopping_list(conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> List[shopping.ShoppingListRequest]: - return [r async for r in shopping.get_current_requests(conn) if r.ingredient and r.person_id == person.id] +async def get_my_shopping_list(conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> List[ingredients.Ingredient]: + return [r.ingredient async for r in shopping.get_persons_requests(conn, person.id)] @app.post("/api/shopping/current/me/ingredients") -async def sync_my_shopping_list(requests: List[ingredients.Ingredient], conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> List[shopping.ShoppingListRequest]: - result = [r async for r in shopping.sync_persons_requested_ingredients(conn, person, requests) if r.ingredient] +async def sync_my_shopping_list(requests: List[ingredients.Ingredient], conn: sqlite3.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 = [r.ingredient async for r in shopping.get_persons_requests(conn, person.id) if r.ingredient is not None] + 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 result + return await get_my_shopping_list(conn, person) class MealIdWrapper(BaseModel): meal_id: int @app.post("/api/shopping/current/meals/me") -async def request_meal(r: MealIdWrapper, conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> shopping.ShoppingListRequest: +async def request_meal(r: MealIdWrapper, conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> shopping.ShoppingListItem: meal = await meals.find_meal_by_id(conn, r.meal_id) if not meal: return JSONResponse(status_code=404, content={'message': 'Meal not found'}) - - response = await shopping.request_meal(conn, person, meal) + + response = await shopping.request(conn, person, meal=meal) await conn.commit() return response @@ -308,7 +320,7 @@ async def unrequest_meal(meal_id: int, conn: sqlite3.Connection = Depends(get_db if not meal: return JSONResponse(status_code=404, content={'message': 'Meal not found'}) - await shopping.unrequest_meal(conn, meal) + await shopping.remove_request(conn, person, meal=meal) await conn.commit() return {} diff --git a/shopping/__init__.py b/shopping/__init__.py index a569739..cc69981 100644 --- a/shopping/__init__.py +++ b/shopping/__init__.py @@ -1,2 +1,29 @@ -from shopping.db import ShoppingList, ShoppingListRequest, ShoppingListResult, ShoppingListPurchase, sync_persons_requested_ingredients, load_shopping_list, get_current_requests, request_meal, unrequest_meal, purchase_ingredients, get_shopping_list_with_meal, remove_request +from typing import AsyncIterator, Iterator, List, Tuple +from shopping.db import ShoppingList, ShoppingListItem, load_shopping_list, purchase, remove_request, request +from shopping.db import find_items_by_list_id as _find_items_by_list_id, get_purchased_ingredients as _get_purchased_ingredients + +async def get_persons_requests(conn, person_id: int) -> AsyncIterator[ShoppingListItem]: + async for item in _find_items_by_list_id(conn, None): + if item.person_id == person_id and item.ingredient_id is not None: + yield item + +def flatten_items(items: Iterator[ShoppingListItem]) -> Iterator[ShoppingListItem]: + for item in items: + if item.meal: + for ingredient in item.meal.ingredients + item.meal.extra_ingredients: + yield ShoppingListItem(ingredient=ingredient, meal=item.meal, person_id=item.person_id, created_date=item.created_date) + else: + yield item + +async def get_outstanding_requests(conn) -> Tuple[List[ShoppingListItem], List[ShoppingListItem]]: + current_requests = [r async for r in _find_items_by_list_id(conn, None)] + + meal_ids = {item.meal_id for item in current_requests if item.meal_id is not None and item.meal_id > 0} + purchased_ingredients = {r.ingredient_id async for r in _get_purchased_ingredients(conn, meal_ids)} + + flattened = flatten_items(current_requests) + outstanding_items = [r for r in flattened if r.ingredient_id not in purchased_ingredients] + purchased_items = [r for r in flattened if r.ingredient_id in purchased_ingredients] + + return outstanding_items, purchased_items diff --git a/shopping/db.py b/shopping/db.py index e4207aa..1a378a5 100644 --- a/shopping/db.py +++ b/shopping/db.py @@ -1,38 +1,29 @@ +from common import BaseLinkedModel from meals import Meal, find_meal_by_id from ingredients import Ingredient, insert_ingredient from persons import Person from products import Product -from pydantic import BaseModel from typing import AsyncIterator, List, ClassVar, Optional from datetime import datetime -class ShoppingListRequest(BaseModel): +class ShoppingListItem(BaseLinkedModel): KEYS: ClassVar[List[str]] = ['id', 'ingredient_id', 'list_id', 'person_id', 'meal_id', 'created_date'] id: int = -1 list_id: Optional[int] = None + person_id: int = -1 + person: Optional[Person] = None + ingredient_id: Optional[int] = None ingredient: Optional[Ingredient] = None - person_id: Optional[int] = None - person: Optional[Person] = None - meal_id: Optional[int] = None meal: Optional[Meal] = None created_date: datetime = datetime.now().astimezone() -class ShoppingListResult(BaseModel): - KEYS: ClassVar[List[str]] = ['id', 'product_id', 'list_id', 'quantity', 'unit' ] - id: int = -1 - list_id: int - product_id: int - product: Optional[Product] = None - - quantity: float - unit: str from enum import Enum @@ -41,17 +32,14 @@ class StoreEnum(str, Enum): coles = 'coles' home = '' -class ShoppingList(BaseModel): +class ShoppingList(BaseLinkedModel): KEYS: ClassVar[List[str]] = ['id', 'created_date', 'store_name'] id: int = -1 created_date: datetime = datetime.now().astimezone() store_name: StoreEnum = '' - - requests: List[ShoppingListRequest] = [] - results: List[ShoppingListResult] = [] - -class ShoppingListPurchase(ShoppingList): - completed_requests: List[ShoppingListRequest] = [] + purchased_by_id: int = -1 + purchased_by: Optional[Person] = None + items: List[ShoppingListItem] = [] async def create(conn): await conn.execute(''' @@ -59,10 +47,12 @@ async def create(conn): id INTEGER PRIMARY KEY, created_date DATETIME NOT NULL, store_name TEXT NOT NULL, + purchased_by_id INTEGER, + FOREIGN KEY(purchased_by_id) REFERENCES Person(id) );''') - + await conn.execute(''' - CREATE TABLE IF NOT EXISTS ShoppingListRequest ( + CREATE TABLE IF NOT EXISTS ShoppingListItem ( id INTEGER PRIMARY KEY, ingredient_id INTEGER, list_id INTEGER, @@ -70,115 +60,144 @@ async def create(conn): meal_id INTEGER, created_date DATETIME NOT NULL, FOREIGN KEY(ingredient_id) REFERENCES Ingredient(id), + FOREIGN KEY(list_id) REFERENCES ShoppingList(id), FOREIGN KEY(person_id) REFERENCES Person(id), - FOREIGN KEY(meal_id) REFERENCES Meal(id), - FOREIGN KEY(list_id) REFERENCES ShoppingList(id) + FOREIGN KEY(meal_id) REFERENCES Meal(id) );''') - await conn.execute(''' - CREATE TABLE IF NOT EXISTS ShoppingListResult ( - id INTEGER PRIMARY KEY, - product_id INTEGER, - list_id INTEGER, - quantity REAL, - unit TEXT, - FOREIGN KEY(product_id) REFERENCES Product(id), - FOREIGN KEY(list_id) REFERENCES ShoppingList(id) - );''') -def validate_request(request: ShoppingListRequest) -> None: - # A request must always have a list id - if request.list_id < 0: - raise ValueError('Request must have a list id') +def validate_request(request: ShoppingListItem) -> None: + if request.person_id < 0: + raise ValueError('Requests must have a person') # A request must have either an ingredient or a meal, but not both if not request.ingredient and not request.meal: raise ValueError('Request must have either an ingredient or a meal') - - # If an ingredient is provided, it must have a person - if request.ingredient and not request.person: - raise ValueError('Ingredient requests must have a person') -async def purchase_ingredients(conn, shopping_list: ShoppingListPurchase): +async def purchase(conn, shopping_list: ShoppingList) -> None: + if shopping_list.purchased_by_id is None: + raise ValueError('Shopping list must have a person id') + + if shopping_list.items is None or len(shopping_list.items) == 0: + raise ValueError('Shopping list must have items') + shopping_list.created_date = datetime.now().astimezone() async with conn.execute(''' - INSERT INTO ShoppingList (created_date, store_name) - VALUES (?, ?) - ''', (shopping_list.created_date.isoformat(), shopping_list.store_name,)) as cursor: + INSERT INTO ShoppingList (created_date, store_name, purchased_by_id) + VALUES (?, ?, ?) + ''', (shopping_list.created_date.isoformat(), shopping_list.store_name, shopping_list.purchased_by_id)) as cursor: shopping_list.id = cursor.lastrowid - for request in shopping_list.completed_requests: - request.list_id = shopping_list.id + for item in shopping_list.items: + item.list_id = shopping_list.id + validate_request(item) - validate_request(request) + if item.ingredient and item.ingredient.id < 0: + await insert_ingredient(conn, item.ingredient) - if request.ingredient and request.ingredient.id < 0: - await insert_ingredient(conn, request.ingredient) + if item.ingredient_id is None or item.ingredient_id < 0: + raise ValueError('Ingredient request must have a valid ingredient id') - if request.ingredient: - request.ingredient_id = request.ingredient.id + isMeal = item.meal_id is None or item.meal_id < 0 + isPersonRequest = (not isMeal) and item.person_id is not None and item.person_id >= 0 - if request.meal: - request.meal_id = request.meal.id + if not isMeal and not isPersonRequest: + raise ValueError('Ingredient request must have either a meal or a person id') + + if isPersonRequest: + # Update existing request from its null id, or throw + async with conn.execute(''' + UPDATE ShoppingListItem + SET list_id = ? + WHERE ingredient_id = ? AND person_id = ? AND meal_id IS NULL + ''', (shopping_list.id, item.ingredient_id, item.person_id)) as cursor: + if cursor.rowcount == 0: + raise ValueError('Ingredient request must have a valid person id and ingredient id') + + elif isMeal: + # Insert new request for meal + if item.meal_id is None or item.meal_id < 0: + raise ValueError('Meal request must have a valid meal id') + + async with conn.execute(''' + INSERT INTO ShoppingListItem (ingredient_id, list_id, person_id, meal_id, created_date) + VALUES (?, ?, ?, ?, ?) + ''', (item.ingredient_id, shopping_list.id, item.person_id, item.meal_id, item.created_date.isoformat())) as cursor: + item.id = cursor.lastrowid + + # TODO: Calculate which meals have been fulfilled and update meal status +async def requested_meal_ids(conn) -> AsyncIterator[ShoppingListItem]: + async with conn.execute(f''' + SELECT {ShoppingListItem.KEYS} + FROM ShoppingListItem + WHERE list_id IS NULL AND meal_id IS NOT NULL AND meal_id > 0 + ''') as cursor: + async for row in cursor: + yield ShoppingListItem(**{k:v for k,v in zip(ShoppingListItem.KEYS, row)}) + +async def request(conn, person: Person, ingredient: Optional[Ingredient] = None, meal: Optional[Meal] = None) -> ShoppingListItem: + if ingredient is not None and meal is not None: + raise ValueError('Cannot request both an ingredient and a meal') + + if ingredient is None and meal is None: + raise ValueError('Must specify either an ingredient or a meal to request') + + if meal is not None and meal.id < 0: + raise ValueError('Meal must have a valid id') + + if ingredient is not None and ingredient.id < 0: + await insert_ingredient(conn, ingredient) + + item = ShoppingListItem(ingredient=ingredient, person=person, meal=meal) + + validate_request(item) + + async with conn.execute(''' + INSERT INTO ShoppingListItem (ingredient_id, person_id, meal_id, created_date) + VALUES (?, ?, ?, ?) + ''', (item.ingredient_id, item.person_id, item.meal_id, item.created_date.isoformat())) as cursor: + item.id = cursor.lastrowid + + return item + +async def remove_request(conn, person: Person, meal: Optional[Meal] = None, ingredient: Optional[Ingredient] = None) -> None: + failed = True + if meal is not None: + # Ensure a record is removed async with conn.execute(''' - INSERT INTO ShoppingListRequest (ingredient_id, list_id, person_id, meal_id, created_date) - VALUES (?, ?, ?, ?, ?) - ''', (request.ingredient_id, shopping_list.id, request.person_id, request.meal_id, request.created_date.isoformat())) as cursor: - request.id = cursor.lastrowid + DELETE FROM ShoppingListItem + WHERE list_id IS NULL AND meal_id = ? + ''', (meal.id,)) as cursor: + if cursor.rowcount > 0: + failed = False - if request.ingredient_id and request.person_id: - await conn.execute(''' - DELETE FROM ShoppingListRequest - WHERE ingredient_id = ? AND person_id = ? AND list_id IS NULL - ''', (request.ingredient_id, request.person_id)) + elif ingredient is not None: + # Ensure a record is removed + async with conn.execute(''' + DELETE FROM ShoppingListItem + WHERE list_id IS NULL AND ingredient_id = ? AND person_id = ? + ''', (ingredient.id, person.id)) as cursor: + if cursor.rowcount > 0: + failed = False -from meals import find_meal_by_id -async def find_completed_meals(conn, shopping_list_request: List[ShoppingListRequest]) -> AsyncIterator[Meal]: - meal_ids = {request.meal_id for request in shopping_list_request if request.meal_id is not None} - if not meal_ids: - return - - meals = [await find_meal_by_id(conn, meal_id) for meal_id in meal_ids] - ingredients_as_requests = [ShoppingListRequest(meal_id=meal.id, meal=meal, list_id=shopping_list_request[0].list_id) for meal in meals] + if failed: + raise ValueError('Must specify either a meal or an ingredient to remove') - -async def remove_request(conn, request: ShoppingListRequest) -> None: - if request.list_id != None: - raise ValueError('Request is already completed') - - if request.meal and not request.meal_id: - raise ValueError('Meal request must have a meal id') - - if request.meal_id != None: - await conn.execute(''' - DELETE FROM ShoppingListRequest - WHERE meal_id = ? AND list_id IS NULL - ''', (request.meal_id,)) - - elif request.person_id != None and request.ingredient_id != None: - await conn.execute(''' - DELETE FROM ShoppingListRequest - WHERE person_id = ? AND ingredient_id = ? AND list_id IS NULL - ''', (request.person_id, request.ingredient_id)) - - else: - raise ValueError('Request is invalid') - -async def find_requests_by_list_id(conn, list_id: Optional[int]) -> AsyncIterator[ShoppingListRequest]: +async def find_items_by_list_id(conn, list_id: Optional[int]) -> AsyncIterator[ShoppingListItem]: # Join Ingredient and Product to also load ingredient and product ingredient_keys = [f'ingredient.{key}' for key in Ingredient.KEYS] product_keys = [f'product.{key}' for key in Product.KEYS] - request_keys = [f'shoppinglistrequest.{key}' for key in ShoppingListRequest.KEYS] + request_keys = [f'shoppinglistitem.{key}' for key in ShoppingListItem.KEYS] person_keys = [f'person.{key}' for key in Person.KEYS] select = f''' SELECT {','.join(ingredient_keys + product_keys + request_keys + person_keys)} - FROM ShoppingListRequest - LEFT JOIN Ingredient ON ShoppingListRequest.ingredient_id = Ingredient.id + FROM ShoppingListItem + LEFT JOIN Ingredient ON ShoppingListItem.ingredient_id = Ingredient.id LEFT JOIN Product ON Ingredient.product_id = Product.id - LEFT JOIN Person ON ShoppingListRequest.person_id = Person.id + LEFT JOIN Person ON ShoppingListItem.person_id = Person.id ''' where, params = ' WHERE list_id IS NULL', () @@ -197,39 +216,14 @@ async def find_requests_by_list_id(conn, list_id: Optional[int]) -> AsyncIterato person_keys = {k:v for k,v in zip(Person.KEYS, row[-len(Person.KEYS):])} person = Person(**person_keys) if person_keys['id'] else None - request_keys = {k:v for k,v in zip(ShoppingListRequest.KEYS, row[len(Ingredient.KEYS) + len(Product.KEYS):-len(Person.KEYS)])} - request = ShoppingListRequest(**request_keys, ingredient=ingredient, person=person) + request_keys = {k:v for k,v in zip(ShoppingListItem.KEYS, row[len(Ingredient.KEYS) + len(Product.KEYS):-len(Person.KEYS)])} + request = ShoppingListItem(**request_keys, ingredient=ingredient, person=person) if request.meal_id is not None: request.meal = await find_meal_by_id(conn, request.meal_id) yield request -async def find_results_by_list_id(conn, list_id: int) -> AsyncIterator[ShoppingListResult]: - product_keys = [f'product.{key}' for key in Product.KEYS] - result_keys = [f'shoppinglistresult.{key}' for key in ShoppingListResult.KEYS] - - async with conn.execute(f''' - SELECT {','.join(product_keys + result_keys)} - FROM ShoppingListResult - LEFT JOIN Product ON ShoppingListResult.product_id = Product.id - WHERE list_id = ? - ''', (list_id,)) as cursor: - async for row in cursor: - product_keys = {k:v for k,v in zip(Product.KEYS, row[:len(Product.KEYS)])} - product = Product(**product_keys) if product_keys['id'] else None - - result_keys = {k:v for k,v in zip(ShoppingListResult.KEYS, row[len(Product.KEYS):])} - result = ShoppingListResult(**result_keys, product=product) - yield result - -async def fill_related(conn, shopping_list: ShoppingList) -> ShoppingList: - async for request in find_requests_by_list_id(conn, shopping_list.id): - shopping_list.requests.append(request) - - async for item in find_results_by_list_id(conn, shopping_list.id): - shopping_list.results.append(item) - async def load_shopping_list(conn, id: int) -> ShoppingList: shopping_list = None async with conn.execute(f''' @@ -242,61 +236,19 @@ async def load_shopping_list(conn, id: int) -> ShoppingList: break if shopping_list: - await fill_related(conn, shopping_list) + async for item in find_items_by_list_id(conn, shopping_list.id): + shopping_list.items.append(item) return shopping_list -async def request_ingredient(conn, person: Person, ingredient: Ingredient) -> ShoppingListRequest: - if ingredient.id >= 0: - raise ValueError('How did you get an existing ingredient?') +async def get_purchased_ingredients(conn, meal_ids: List[int]) -> AsyncIterator[ShoppingListItem]: + if not meal_ids: + return - await insert_ingredient(conn, ingredient) - - request = ShoppingListRequest(ingredient_id=ingredient.id, ingredient=ingredient, person_id=person.id, created_date=datetime.now().astimezone()) - async with conn.execute(''' - INSERT INTO ShoppingListRequest (ingredient_id, person_id, created_date) - VALUES (?, ?, ?) - ''', (request.ingredient_id, request.person_id, request.created_date.isoformat())) as cursor: - request.id = cursor.lastrowid - - return request - -async def request_meal(conn, person: Person, meal: Meal) -> ShoppingListRequest: - request = ShoppingListRequest(meal_id=meal.id, meal=meal, person_id=person.id, created_date=datetime.now().astimezone()) - - async with conn.execute(''' - INSERT INTO ShoppingListRequest (meal_id, person_id, created_date) - VALUES (?, ?, ?) - ''', (request.meal_id, request.person_id, request.created_date.isoformat())) as cursor: - request.id = cursor.lastrowid - - return request - -async def unrequest_meal(conn, meal: Meal) -> None: - await conn.execute(''' - DELETE FROM ShoppingListRequest - WHERE list_id IS NULL AND meal_id = ? - ''', (meal.id,)) - -def get_current_requests(conn) -> AsyncIterator[ShoppingListRequest]: - return find_requests_by_list_id(conn, None) - -async def sync_persons_requested_ingredients(conn, person: Person, requests: List[Ingredient]) -> AsyncIterator[ShoppingListRequest]: - # Delete existing and insert all as new - await conn.execute(''' - DELETE FROM ShoppingListRequest - WHERE list_id IS NULL AND person_id = ? AND ingredient_id IS NOT NULL - ''', (person.id,)) - - for ingredient in requests: - ingredient.id = -1 - yield await request_ingredient(conn, person, ingredient) - -async def get_shopping_list_with_meal(conn, meal_id: int) -> AsyncIterator[ShoppingList]: - async with conn.execute(''' - SELECT list_id FROM ShoppingListRequest - WHERE meal_id = ? AND list_id IS NOT NULL - ''', (meal_id,)) as cursor: + async with conn.execute(f''' + SELECT {','.join(ShoppingListItem.KEYS)} + FROM ShoppingListItem + WHERE meal_id IN ({','.join(['?'] * len(meal_ids))}) + ''', meal_ids) as cursor: async for row in cursor: - list_id = row[0] - yield await load_shopping_list(conn, list_id) \ No newline at end of file + yield ShoppingListItem(**{k:v for k,v in zip(ShoppingListItem.KEYS, row)}) \ No newline at end of file