2025-11-01 07:34:40 +00:00
from typing import Any , AsyncIterator , List , Optional
2025-10-18 03:26:42 +00:00
2025-10-19 09:24:23 +00:00
from shopping . models import ShoppingList , ShoppingListItem
2025-10-18 03:26:42 +00:00
2024-05-17 09:09:03 +00:00
async def create ( conn ) :
2025-10-18 03:26:42 +00:00
await conn . execute (
"""
2024-05-17 09:09:03 +00:00
CREATE TABLE IF NOT EXISTS ShoppingList (
id INTEGER PRIMARY KEY ,
2024-10-14 06:36:56 +00:00
created_date DATETIME NOT NULL ,
store_name TEXT NOT NULL ,
2025-07-28 23:23:09 +00:00
purchased_by_id INTEGER ,
2025-11-01 08:07:44 +00:00
household_id INTEGER ,
2025-11-01 08:58:33 +00:00
FOREIGN KEY ( purchased_by_id ) REFERENCES User ( id )
2025-10-18 03:26:42 +00:00
) ; """
)
await conn . execute (
"""
2025-07-28 23:23:09 +00:00
CREATE TABLE IF NOT EXISTS ShoppingListItem (
2024-05-17 09:09:03 +00:00
id INTEGER PRIMARY KEY ,
ingredient_id INTEGER ,
list_id INTEGER ,
person_id INTEGER ,
meal_id INTEGER ,
2025-07-28 23:23:09 +00:00
recipe_id INTEGER ,
2024-10-14 06:36:56 +00:00
created_date DATETIME NOT NULL ,
2025-11-01 08:07:44 +00:00
household_id INTEGER ,
2024-05-17 09:09:03 +00:00
FOREIGN KEY ( ingredient_id ) REFERENCES Ingredient ( id ) ,
2025-07-28 23:23:09 +00:00
FOREIGN KEY ( list_id ) REFERENCES ShoppingList ( id ) ,
2025-11-01 08:58:33 +00:00
FOREIGN KEY ( person_id ) REFERENCES User ( id ) ,
2024-05-17 09:09:03 +00:00
FOREIGN KEY ( meal_id ) REFERENCES Meal ( id ) ,
2025-07-28 23:23:09 +00:00
FOREIGN KEY ( recipe_id ) REFERENCES Recipe ( id )
2025-10-18 03:26:42 +00:00
) ; """
)
2025-10-19 02:12:28 +00:00
# Useful indexes for queries
2025-10-19 13:12:16 +00:00
await conn . execute (
" CREATE INDEX IF NOT EXISTS idx_shopping_item_list_id ON ShoppingListItem(list_id); "
)
await conn . execute (
" CREATE INDEX IF NOT EXISTS idx_shopping_item_meal_id ON ShoppingListItem(meal_id); "
)
2025-10-19 09:24:23 +00:00
await conn . execute (
" CREATE INDEX IF NOT EXISTS idx_shopping_item_person_null_list ON ShoppingListItem(person_id, ingredient_id) WHERE list_id IS NULL; "
)
2025-11-01 08:07:44 +00:00
# Household indices for scoped queries
await conn . execute (
" CREATE INDEX IF NOT EXISTS idx_shopping_list_household_id ON ShoppingList(household_id); "
)
await conn . execute (
" CREATE INDEX IF NOT EXISTS idx_shopping_item_household_id ON ShoppingListItem(household_id); "
)
await conn . execute (
" CREATE INDEX IF NOT EXISTS idx_shopping_item_household_person_ing_null_list ON ShoppingListItem(household_id, person_id, ingredient_id) WHERE list_id IS NULL; "
)
2025-10-18 03:26:42 +00:00
2024-05-17 09:09:03 +00:00
2025-07-28 23:23:09 +00:00
def validate_request ( request : ShoppingListItem ) - > None :
if request . person_id < 0 :
2025-10-18 03:26:42 +00:00
raise ValueError ( " Requests must have a person " )
2024-05-20 10:09:57 +00:00
# A request must have either an ingredient or a meal, but not both
2025-07-30 12:05:29 +00:00
if not request . ingredient_id and not request . meal_id :
2025-10-18 03:26:42 +00:00
raise ValueError ( " Request must have either an ingredient or a meal " )
2025-07-28 23:23:09 +00:00
async def purchase ( conn , shopping_list : ShoppingList ) - > None :
2025-07-29 07:15:38 +00:00
if shopping_list . purchased_by_id is None or shopping_list . purchased_by_id < 0 :
2025-10-18 03:26:42 +00:00
raise ValueError ( " Shopping list must have a person id " )
2025-07-28 23:23:09 +00:00
if shopping_list . items is None or len ( shopping_list . items ) == 0 :
2025-10-18 03:26:42 +00:00
raise ValueError ( " Shopping list must have items " )
2025-10-19 09:24:23 +00:00
from datetime import datetime
2024-10-14 05:59:05 +00:00
shopping_list . created_date = datetime . now ( ) . astimezone ( )
2025-10-18 03:26:42 +00:00
async with conn . execute (
"""
2025-07-28 23:23:09 +00:00
INSERT INTO ShoppingList ( created_date , store_name , purchased_by_id )
VALUES ( ? , ? , ? )
2025-10-18 03:26:42 +00:00
""" ,
(
shopping_list . created_date . isoformat ( ) ,
shopping_list . store_name ,
shopping_list . purchased_by_id ,
) ,
) as cursor :
2024-05-17 09:09:03 +00:00
shopping_list . id = cursor . lastrowid
2025-07-28 23:23:09 +00:00
for item in shopping_list . items :
item . list_id = shopping_list . id
validate_request ( item )
if item . ingredient_id is None or item . ingredient_id < 0 :
2025-10-18 03:26:42 +00:00
raise ValueError ( " Ingredient request must have a valid ingredient id " )
2025-07-28 23:23:09 +00:00
isMeal = item . meal_id is not None and item . meal_id > = 0
isPersonRequest = ( not isMeal ) and item . person_id is not None and item . person_id > = 0
if not isMeal and not isPersonRequest :
2025-10-18 03:26:42 +00:00
raise ValueError ( " Ingredient request must have either a meal or a person id " )
2025-07-28 23:23:09 +00:00
if isPersonRequest :
# Update existing request from its null id, or throw
2025-10-18 03:26:42 +00:00
async with conn . execute (
"""
2025-07-28 23:23:09 +00:00
UPDATE ShoppingListItem
SET list_id = ?
2025-11-01 03:26:23 +00:00
WHERE ingredient_id = ?
AND list_id IS NULL
2025-07-28 23:23:09 +00:00
AND person_id = ?
2025-11-01 03:26:23 +00:00
AND meal_id IS NULL
2025-07-28 23:23:09 +00:00
AND recipe_id IS NULL
2025-10-18 03:26:42 +00:00
""" ,
( shopping_list . id , item . ingredient_id , item . person_id ) ,
) as cursor :
2025-07-28 23:23:09 +00:00
if cursor . rowcount == 0 :
2025-10-18 03:26:42 +00:00
raise ValueError (
" Ingredient request must have a valid person id and ingredient id "
)
2025-07-28 23:23:09 +00:00
elif isMeal :
# Insert new request for meal
if item . meal_id is None or item . meal_id < 0 :
2025-10-18 03:26:42 +00:00
raise ValueError ( " Meal request must have a valid meal id " )
async with conn . execute (
"""
2025-07-28 23:23:09 +00:00
INSERT INTO ShoppingListItem ( ingredient_id , list_id , person_id , meal_id , recipe_id , created_date )
VALUES ( ? , ? , ? , ? , ? , ? )
2025-10-18 03:26:42 +00:00
""" ,
(
item . ingredient_id ,
shopping_list . id ,
item . person_id ,
item . meal_id ,
item . recipe_id ,
item . created_date . isoformat ( ) ,
) ,
) as cursor :
2025-07-28 23:23:09 +00:00
item . id = cursor . lastrowid
2025-10-18 03:26:42 +00:00
meal_ids = list (
{
item . meal_id
for item in shopping_list . items
if item . meal_id is not None and item . meal_id > = 0
}
)
2025-07-28 23:23:09 +00:00
await update_purchased_meals ( conn , meal_ids )
2025-10-18 03:26:42 +00:00
2025-11-01 07:07:13 +00:00
async def get_outstanding_requests_scoped ( conn , household_id : int ) - > List [ ShoppingListItem ] :
# outstanding items are those that are not purchased and either have no meal or have a meal that has not been consumed
# and is not part of a shopping list that has been purchased.
# The subquery for `active_meal_ids` finds meals that are not consumed and not part of a purchased list.
# The main query then selects items linked to these active meals OR items with no meal link at all.
rows = await conn . execute_fetchall (
2025-11-01 03:49:30 +00:00
"""
2025-11-01 07:07:13 +00:00
WITH active_meal_ids AS (
SELECT m . id
FROM meals m
LEFT JOIN shopping_list_items sli ON sli . meal_id = m . id
LEFT JOIN shopping_lists sl ON sl . id = sli . shopping_list_id
WHERE
m . household_id = : household_id
AND m . consumed_date IS NULL
AND ( sl . id IS NULL OR sl . purchased_by_id IS NULL )
GROUP BY m . id
)
SELECT
sli . id ,
sli . meal_id ,
sli . ingredient_id ,
sli . quantity ,
sli . unit ,
sli . added_by_id
FROM shopping_list_items sli
WHERE
sli . household_id = : household_id
AND sli . purchased_at IS NULL
AND ( sli . meal_id IN ( SELECT id FROM active_meal_ids ) OR sli . meal_id IS NULL ) ;
""" ,
{ " household_id " : household_id } ,
2025-11-01 03:49:30 +00:00
)
2025-11-01 07:07:13 +00:00
return [ _to_shopping_list_item ( r ) for r in rows ]
2025-11-01 03:49:30 +00:00
2025-07-28 23:23:09 +00:00
async def update_purchased_meals ( conn , meal_ids : List [ int ] ) - > None :
if not meal_ids :
return
2025-10-18 03:26:42 +00:00
purchased_ingredient_ids = {
item . ingredient_id async for item in get_purchased_ingredients ( conn , meal_ids )
}
2025-10-19 09:24:23 +00:00
from meals . repository import find_meal_by_id , mark_purchased
2025-07-28 23:23:09 +00:00
for meal_id in meal_ids :
meal = await find_meal_by_id ( conn , meal_id )
2025-10-18 03:26:42 +00:00
if not meal :
continue
ingredients = {
ingredient . id
for mr in meal . recipes
for ingredient in ( mr . recipe . ingredients if mr . recipe else [ ] )
} | { ingredient . id for ingredient in meal . extra_ingredients }
2025-07-28 23:23:09 +00:00
remaining_ingredients = ingredients - purchased_ingredient_ids
if not remaining_ingredients :
await mark_purchased ( conn , meal )
2025-10-18 03:26:42 +00:00
await remove_request ( conn , person = None , meal = meal )
2025-07-28 23:23:09 +00:00
2025-10-19 09:24:23 +00:00
async def is_requested ( conn , meal ) - > bool :
2025-07-28 23:23:09 +00:00
if meal . id < 0 :
return False
2024-05-20 10:09:57 +00:00
2025-10-18 03:26:42 +00:00
async with conn . execute (
"""
2025-07-28 23:23:09 +00:00
SELECT COUNT ( * ) FROM ShoppingListItem
WHERE meal_id = ? AND list_id IS NULL
2025-10-18 03:26:42 +00:00
""" ,
( meal . id , ) ,
) as cursor :
2025-07-28 23:23:09 +00:00
row = await cursor . fetchone ( )
return row [ 0 ] > 0
2024-05-20 10:09:57 +00:00
2025-10-18 03:26:42 +00:00
2025-11-01 04:40:45 +00:00
async def is_requested_scoped ( conn , meal_id : int , household_id : int ) - > bool :
async with conn . execute (
"""
SELECT COUNT ( * ) FROM ShoppingListItem
WHERE meal_id = ? AND list_id IS NULL AND household_id = ?
""" ,
( meal_id , household_id ) ,
) as cursor :
row = await cursor . fetchone ( )
return row [ 0 ] > 0
2025-10-18 03:26:42 +00:00
async def request (
2025-10-19 09:24:23 +00:00
conn , person , ingredient : Optional [ Any ] = None , meal : Optional [ Any ] = None
2025-10-18 03:26:42 +00:00
) - > ShoppingListItem :
2025-10-19 09:24:23 +00:00
from ingredients . repository import insert_ingredient
2025-07-28 23:23:09 +00:00
if ingredient is not None and meal is not None :
2025-10-18 03:26:42 +00:00
raise ValueError ( " Cannot request both an ingredient and a meal " )
2024-05-17 09:09:03 +00:00
2025-07-28 23:23:09 +00:00
if ingredient is None and meal is None :
2025-10-18 03:26:42 +00:00
raise ValueError ( " Must specify either an ingredient or a meal to request " )
2024-05-20 10:09:57 +00:00
2025-07-28 23:23:09 +00:00
if meal is not None and meal . id < 0 :
2025-10-18 03:26:42 +00:00
raise ValueError ( " Meal must have a valid id " )
2024-05-20 10:09:57 +00:00
2025-07-28 23:23:09 +00:00
if ingredient is not None and ingredient . id < 0 :
await insert_ingredient ( conn , ingredient )
2024-05-17 09:09:03 +00:00
2025-07-30 12:05:29 +00:00
ingredient_id = ingredient . id if ingredient else None
meal_id = meal . id if meal else None
2025-10-18 03:26:42 +00:00
item = ShoppingListItem ( ingredient_id = ingredient_id , person_id = person . id , meal_id = meal_id )
2025-07-28 23:23:09 +00:00
validate_request ( item )
if meal is not None and await is_requested ( conn , meal ) :
2025-10-18 03:26:42 +00:00
raise ValueError ( " Meal is already requested " )
2025-07-28 23:23:09 +00:00
2025-10-18 03:26:42 +00:00
async with conn . execute (
"""
2025-07-28 23:23:09 +00:00
INSERT INTO ShoppingListItem ( ingredient_id , person_id , meal_id , created_date )
VALUES ( ? , ? , ? , ? )
2025-10-18 03:26:42 +00:00
""" ,
( item . ingredient_id , item . person_id , item . meal_id , item . created_date . isoformat ( ) ) ,
) as cursor :
2025-07-28 23:23:09 +00:00
item . id = cursor . lastrowid
return item
2024-05-20 10:09:57 +00:00
2025-10-18 03:26:42 +00:00
2025-11-01 04:40:45 +00:00
async def request_meal_scoped (
conn , meal : Any , household_id : int , person_id : Optional [ int ] = None
) - > ShoppingListItem :
if meal is None or getattr ( meal , " id " , - 1 ) < 0 :
raise ValueError ( " Meal must have a valid id " )
if await is_requested_scoped ( conn , meal . id , household_id ) :
raise ValueError ( " Meal is already requested " )
# Use 0 for outward personId to satisfy schema without binding to v1 persons
pid = person_id if person_id is not None else 0
item = ShoppingListItem ( ingredient_id = None , person_id = pid , meal_id = meal . id )
async with conn . execute (
"""
INSERT INTO ShoppingListItem ( ingredient_id , person_id , meal_id , created_date , household_id )
VALUES ( ? , ? , ? , ? , ? )
""" ,
( None , pid , meal . id , item . created_date . isoformat ( ) , household_id ) ,
) as cursor :
item . id = cursor . lastrowid
return item
2025-11-01 07:07:13 +00:00
async def request_ingredient_scoped (
conn , ingredient : Any , household_id : int , person_id : int
) - > ShoppingListItem :
if ingredient is None or getattr ( ingredient , " id " , - 1 ) < 0 :
raise ValueError ( " Ingredient must have a valid id " )
2025-11-01 08:07:44 +00:00
# If already requested by this person and not yet purchased in this household, return existing
where = " WHERE list_id IS NULL AND meal_id IS NULL AND ingredient_id = ? AND person_id = ? AND household_id = ? "
params = ( ingredient . id , person_id , household_id )
async with conn . execute (
f """
SELECT id , ingredient_id , list_id , person_id , meal_id , recipe_id , created_date
FROM ShoppingListItem
{ where }
LIMIT 1
""" ,
params ,
) as cur :
row = await cur . fetchone ( )
if row :
return ShoppingListItem ( * * { k : v for k , v in zip ( ShoppingListItem . KEYS , row ) } )
2025-11-01 07:07:13 +00:00
item = ShoppingListItem ( ingredient_id = ingredient . id , person_id = person_id , meal_id = None )
async with conn . execute (
"""
INSERT INTO ShoppingListItem ( ingredient_id , person_id , meal_id , created_date , household_id )
VALUES ( ? , ? , ? , ? , ? )
""" ,
( ingredient . id , person_id , None , item . created_date . isoformat ( ) , household_id ) ,
) as cursor :
item . id = cursor . lastrowid
return item
2025-10-18 03:26:42 +00:00
async def remove_request (
conn ,
2025-10-19 09:24:23 +00:00
person : Optional [ Any ] = None ,
meal : Optional [ Any ] = None ,
ingredient : Optional [ Any ] = None ,
2025-10-18 03:26:42 +00:00
) - > bool :
2025-07-28 23:23:09 +00:00
if meal is not None :
2025-10-18 03:26:42 +00:00
async with conn . execute (
"""
2025-07-28 23:23:09 +00:00
DELETE FROM ShoppingListItem
WHERE list_id IS NULL AND meal_id = ?
2025-10-18 03:26:42 +00:00
""" ,
( meal . id , ) ,
) as cursor :
2025-07-28 23:23:09 +00:00
return cursor . rowcount > 0
elif ingredient is not None :
2025-10-18 03:26:42 +00:00
async with conn . execute (
"""
2025-07-28 23:23:09 +00:00
DELETE FROM ShoppingListItem
WHERE list_id IS NULL AND ingredient_id = ? AND person_id = ?
2025-10-18 03:26:42 +00:00
""" ,
( ingredient . id , person . id if person else - 1 ) ,
) as cursor :
2025-07-28 23:23:09 +00:00
return cursor . rowcount > 0
2025-10-18 03:26:42 +00:00
raise ValueError ( " Must specify either a meal or an ingredient to remove " )
2025-07-28 23:23:09 +00:00
2025-11-01 04:40:45 +00:00
async def remove_meal_request_scoped ( conn , meal_id : int , household_id : int ) - > bool :
async with conn . execute (
"""
DELETE FROM ShoppingListItem
WHERE list_id IS NULL AND meal_id = ? AND household_id = ?
""" ,
( meal_id , household_id ) ,
) as cursor :
return cursor . rowcount > 0
2025-11-01 11:41:39 +00:00
async def remove_ingredient_request_scoped (
conn , ingredient_id : int , person_id : int , household_id : int
) - > bool :
""" Remove an ad-hoc ingredient request for a specific user within a household.
This targets items that are not yet purchased ( list_id IS NULL ) , have no meal / recipe linkage
( pure personal request ) , and match the provided ingredient / person / household ids .
"""
async with conn . execute (
"""
DELETE FROM ShoppingListItem
WHERE list_id IS NULL
AND meal_id IS NULL
AND recipe_id IS NULL
AND ingredient_id = ?
AND person_id = ?
AND household_id = ?
""" ,
( ingredient_id , person_id , household_id ) ,
) as cursor :
return cursor . rowcount > 0
2025-07-28 23:23:09 +00:00
async def find_items_by_list_id ( conn , list_id : Optional [ int ] ) - > AsyncIterator [ ShoppingListItem ] :
2025-10-18 03:26:42 +00:00
request_cols = [ f " shoppinglistitem. { key } " for key in ShoppingListItem . KEYS ]
2024-05-17 09:09:03 +00:00
2025-10-18 03:26:42 +00:00
select = f """
2025-10-19 13:12:16 +00:00
SELECT { " , " . join ( request_cols ) }
2025-07-28 23:23:09 +00:00
FROM ShoppingListItem
2025-10-18 03:26:42 +00:00
"""
where : str
params : tuple [ Any , . . . ]
where , params = ( " WHERE list_id IS NULL " , ( ) )
2024-10-13 08:19:57 +00:00
if list_id is not None :
2025-10-18 03:26:42 +00:00
where , params = " WHERE list_id = ? " , ( list_id , )
2024-10-13 08:19:57 +00:00
cursor = await conn . execute ( select + where , params )
2025-10-18 03:26:42 +00:00
2024-05-17 09:09:03 +00:00
async for row in cursor :
2025-10-18 03:26:42 +00:00
request_map = { k : v for k , v in zip ( ShoppingListItem . KEYS , row ) }
request = ShoppingListItem ( * * request_map )
2024-05-17 09:09:03 +00:00
yield request
2025-10-18 03:26:42 +00:00
2025-11-01 03:26:23 +00:00
async def find_items_by_list_id_scoped (
2025-11-01 07:16:38 +00:00
conn , list_id : Optional [ int ] , household_id : int
2025-11-01 03:26:23 +00:00
) - > AsyncIterator [ ShoppingListItem ] :
2025-11-01 07:16:38 +00:00
request_cols = [ f " shoppinglistitem. { key } " for key in ShoppingListItem . KEYS ]
select = f """
SELECT { " , " . join ( request_cols ) }
FROM ShoppingListItem
2025-11-01 03:26:23 +00:00
"""
2025-11-01 07:16:38 +00:00
where : str
params : tuple [ Any , . . . ]
where , params = ( " WHERE list_id IS NULL AND household_id = ? " , ( household_id , ) )
2025-11-01 03:26:23 +00:00
if list_id is not None :
2025-11-01 07:16:38 +00:00
where , params = (
" WHERE list_id = ? AND household_id = ? " ,
( list_id , household_id ) ,
)
2025-11-01 03:26:23 +00:00
2025-11-01 07:16:38 +00:00
cursor = await conn . execute ( select + where , params )
async for row in cursor :
request_map = { k : v for k , v in zip ( ShoppingListItem . KEYS , row ) }
request = ShoppingListItem ( * * request_map )
yield request
2025-11-01 03:26:23 +00:00
2025-10-18 03:26:42 +00:00
async def load_shopping_list ( conn , id : int ) - > Optional [ ShoppingList ] :
shopping_list : Optional [ ShoppingList ] = None
async with conn . execute (
f """
2025-10-19 13:12:16 +00:00
SELECT { " , " . join ( ShoppingList . KEYS ) } FROM ShoppingList
2024-05-17 09:09:03 +00:00
WHERE id = ?
LIMIT 1
2025-10-18 03:26:42 +00:00
""" ,
( id , ) ,
) as cursor :
2024-05-17 09:09:03 +00:00
async for row in cursor :
2025-10-18 03:26:42 +00:00
shopping_list = ShoppingList ( * * { k : v for k , v in zip ( ShoppingList . KEYS , row ) } )
2024-05-17 09:09:03 +00:00
break
if shopping_list :
2025-07-28 23:23:09 +00:00
async for item in find_items_by_list_id ( conn , shopping_list . id ) :
shopping_list . items . append ( item )
2024-05-17 09:09:03 +00:00
return shopping_list
2025-10-18 03:26:42 +00:00
2025-11-01 04:34:01 +00:00
async def load_shopping_list_scoped ( conn , id : int , household_id : int ) - > Optional [ ShoppingList ] :
2025-11-01 03:26:23 +00:00
shopping_list : Optional [ ShoppingList ] = None
async with conn . execute (
f """
2025-11-01 06:53:07 +00:00
SELECT { " , " . join ( ShoppingList . KEYS ) } , (
SELECT display_name FROM User u WHERE u . id = ShoppingList . purchased_by_id
) as purchased_by_name
FROM ShoppingList
2025-11-01 03:26:23 +00:00
WHERE id = ? AND household_id = ?
LIMIT 1
""" ,
( id , household_id ) ,
) as cursor :
async for row in cursor :
2025-11-01 06:53:07 +00:00
base = { k : v for k , v in zip ( ShoppingList . KEYS , row [ : len ( ShoppingList . KEYS ) ] ) }
shopping_list = ShoppingList ( * * base )
# Attach a lightweight purchased_by with display_name if available
try :
display_name = row [ len ( ShoppingList . KEYS ) ]
if display_name and shopping_list . purchased_by_id is not None :
2025-11-01 08:58:33 +00:00
# Store a minimal object; api layer will map to MemberRef
shopping_list . purchased_by = type ( " _PB " , ( ) , { } ) ( )
setattr ( shopping_list . purchased_by , " id " , int ( shopping_list . purchased_by_id ) )
setattr ( shopping_list . purchased_by , " display_name " , display_name )
2025-11-01 06:53:07 +00:00
except Exception :
pass
2025-11-01 03:26:23 +00:00
break
if shopping_list :
async for item in find_items_by_list_id_scoped ( conn , shopping_list . id , household_id ) :
shopping_list . items . append ( item )
return shopping_list
2025-07-28 23:23:09 +00:00
async def get_purchased_ingredients ( conn , meal_ids : List [ int ] ) - > AsyncIterator [ ShoppingListItem ] :
if not meal_ids :
return
2025-10-18 03:26:42 +00:00
async with conn . execute (
f """
2025-10-19 13:12:16 +00:00
SELECT { " , " . join ( ShoppingListItem . KEYS ) }
2025-07-28 23:23:09 +00:00
FROM ShoppingListItem
2025-10-19 13:12:16 +00:00
WHERE meal_id IN ( { " , " . join ( [ " ? " ] * len ( meal_ids ) ) } ) AND list_id IS NOT NULL
2025-10-18 03:26:42 +00:00
""" ,
meal_ids ,
) as cursor :
2024-05-19 03:43:06 +00:00
async for row in cursor :
2025-10-18 03:26:42 +00:00
yield ShoppingListItem ( * * { k : v for k , v in zip ( ShoppingListItem . KEYS , row ) } )
2025-11-01 03:26:23 +00:00
async def get_purchased_ingredients_scoped (
conn , meal_ids : List [ int ] , household_id : int
) - > AsyncIterator [ ShoppingListItem ] :
if not meal_ids :
return
async with conn . execute (
f """
SELECT { " , " . join ( ShoppingListItem . KEYS ) }
FROM ShoppingListItem
WHERE meal_id IN ( { " , " . join ( [ " ? " ] * len ( meal_ids ) ) } ) AND list_id IS NOT NULL AND household_id = ?
""" ,
( * meal_ids , household_id ) ,
) as cursor :
async for row in cursor :
yield ShoppingListItem ( * * { k : v for k , v in zip ( ShoppingListItem . KEYS , row ) } )
2025-11-01 07:07:13 +00:00
def _to_shopping_list_item ( row : Any ) - > ShoppingListItem :
2025-11-01 07:16:38 +00:00
# Row is a tuple in the order of columns selected; map via KEYS
return ShoppingListItem ( * * { k : v for k , v in zip ( ShoppingListItem . KEYS , row ) } )