Allowed for null products, move ingredient spreading server-side #1

Merged
jacob merged 7 commits from nullproducts into master 2025-07-28 23:23:10 +00:00
6 changed files with 223 additions and 209 deletions
Showing only changes of commit efd0dabc87 - Show all commits

23
common.py Normal file
View file

@ -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

2
db.py
View file

@ -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):

View file

@ -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,

58
main.py
View file

@ -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)]
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]
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))]
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 {}

View file

@ -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

View file

@ -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(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')
async def purchase_ingredients(conn, shopping_list: ShoppingListPurchase):
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
if failed:
raise ValueError('Must specify either a meal or an ingredient to remove')
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]
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)
yield ShoppingListItem(**{k:v for k,v in zip(ShoppingListItem.KEYS, row)})