Compare commits

..

No commits in common. "dca63bd51a64029b0c18bd0aff551b605dbf0dab" and "b17bb1d687c5c7c4d80caced783299b769740cee" have entirely different histories.

7 changed files with 225 additions and 317 deletions

View file

@ -1,23 +0,0 @@
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/doof.sqlite') -> aiosqlite.Connection:
async def connect(path = './data/your_database.db') -> 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=-1,
return Ingredient(id=0,
line=ingredient.sentence,
name=name,
quantity=quantity,

View file

@ -38,7 +38,7 @@ async def insert_ingredient(conn, ingredient: Ingredient):
ingredient.product_id = ingredient.product.id
if ingredient.product_id < 0:
ingredient.product_id = None
raise ValueError('Product must be inserted before ingredient')
async with conn.execute('''
INSERT INTO Ingredient (name, line, preparation, unit, quantity, product_id, recipe_id, meal_id)

111
main.py
View file

@ -3,7 +3,7 @@ import products, recipes, db, meals, persons, ingredients, shopping
import datetime
from pydantic import BaseModel
from typing import Dict, List, Annotated, Optional, Union
from typing import List, Annotated, Optional, Union
from fastapi import FastAPI, Depends, Query, Cookie
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
@ -109,6 +109,10 @@ async def create_recipe(recipe: recipes.Recipe, conn: sqlite3.Connection = Depen
if not recipe.ingredients:
return JSONResponse(status_code=400, content={'message': 'Recipe must have at least one ingredient'})
for ingredient in recipe.ingredients:
if not ingredient.product:
return JSONResponse(status_code=400, content={'message': 'Ingredient must have a product'})
if recipe.id >= 0:
await recipes.hide_recipe(conn, recipe.id, user)
recipe.based_on_recipe = recipe.id
@ -118,9 +122,7 @@ async def create_recipe(recipe: recipes.Recipe, conn: sqlite3.Connection = Depen
await recipes.insert_recipe(conn, recipe)
for ingredient in recipe.ingredients:
ingredient.recipe_id = recipe.id
if ingredient.product:
ingredient.product_id = ingredient.product.id
ingredient.product_id = ingredient.product.id
await ingredients.insert_ingredient(conn, ingredient)
await conn.commit()
@ -234,111 +236,82 @@ async def mark_consumed(meal_id: int, consumed_date: Optional[datetime.datetime]
return JSONResponse(status_code=404, content={'message': 'Meal not found'})
await meals.mark_consumed(conn, meal, consumed_date or datetime.datetime.now().astimezone())
await shopping.remove_request(conn, person, meal=meal)
await shopping.unrequest_meal(conn, meal)
await conn.commit()
return meal
@app.delete("/api/meals/{meal_id}")
async def delete_meal(meal_id: int, conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> meals.Meal:
async def delete_meal(meal_id: int, conn: sqlite3.Connection = Depends(get_db)) -> meals.Meal:
meal = await meals.find_meal_by_id(conn, meal_id)
if not meal:
return JSONResponse(status_code=404, content={'message': 'Meal not found'})
await shopping.remove_request(conn, person, meal=meal)
await meals.delete_meal(conn, meal.id)
await shopping.unrequest_meal(conn, meal)
await conn.commit()
return meal
class CurrentShoppingList(BaseModel):
outstanding_items: List[shopping.ShoppingListItem]
requested_meals: List[shopping.ShoppingListItem]
purchased_items: List[shopping.ShoppingListItem] = []
ingredients_lookup: Dict[int, ingredients.Ingredient] = {}
meals_lookup: Dict[int, meals.Meal] = {}
shopping_list_lookup: Dict[int, shopping.ShoppingList] = {}
recipes_lookup: Dict[int, recipes.Recipe] = {}
requests: List[shopping.ShoppingListRequest]
overlapping_previous_shops: List[shopping.ShoppingList]
@app.get("/api/shopping/current")
async def get_current_shopping_list(conn: sqlite3.Connection = Depends(get_db)) -> CurrentShoppingList:
outstanding_requests, purchased_requests, meal_requests = await shopping.get_outstanding_requests(conn)
other_shopping_list_ids = {item.list_id for item in purchased_requests}
current_requests = [r async for r in shopping.get_current_requests(conn)]
shopping_list_lookup = { list_id: await shopping.load_shopping_list(conn, list_id) for list_id in other_shopping_list_ids }
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))]
# Reduce the data structure to items and lookups
items = meal_requests + outstanding_requests + purchased_requests + [item for sl in shopping_list_lookup.values() for item in sl.items]
meals_lookup, recipes_lookup, ingredients_lookup = shopping.remove_references(items)
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(
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(BaseModel):
list: shopping.ShoppingList
meals_lookup: Dict[int, meals.Meal] = {}
ingredients_lookup: Dict[int, ingredients.Ingredient] = {}
recipes_lookup: Dict[int, recipes.Recipe] = {}
return CurrentShoppingList(requests=current_requests, overlapping_previous_shops=list(overlapping.values()))
@app.get("/api/shopping/{list_id}")
async def get_shopping_list(list_id: int, conn: sqlite3.Connection = Depends(get_db)) -> PurchasedShoppingList:
shopping_list = await shopping.load_shopping_list(conn, list_id)
meals_lookup, recipes_lookup, ingredients_lookup = shopping.remove_references(shopping_list.items)
return PurchasedShoppingList(list=shopping_list, meals_lookup=meals_lookup, recipes_lookup=recipes_lookup, ingredients_lookup=ingredients_lookup)
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)
class ShoppingListPurchase(shopping.ShoppingList):
completed_requests: List[shopping.ShoppingListRequest] = []
@app.post("/api/shopping/")
async def purchase_ingredients(shopping_list: shopping.ShoppingList, conn: sqlite3.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)
async def purchase_ingredients(lst: ShoppingListPurchase, conn: sqlite3.Connection = Depends(get_db)) -> shopping.ShoppingList:
await shopping.insert_shopping_list(conn, lst)
for request in lst.completed_requests:
if request.meal_id:
meal = await meals.find_meal_by_id(conn, request.meal_id)
if meal:
await meals.mark_purchased(conn, meal)
await shopping.remove_request(conn, request)
await shopping.purchase(conn, shopping_list)
await conn.commit()
result = PurchasedShoppingList(list=shopping_list)
shopping.remove_references(shopping_list.items, result.meals_lookup, result.recipes_lookup, result.ingredients_lookup)
return result
return lst
@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[ingredients.Ingredient]:
return [r.ingredient async for r in shopping.get_persons_requests(conn, person.id)]
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]
@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[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)
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]
await conn.commit()
return await get_my_shopping_list(conn, person)
return result
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.ShoppingListItem:
async def request_meal(r: MealIdWrapper, conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> shopping.ShoppingListRequest:
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(conn, person, meal=meal)
response = await shopping.request_meal(conn, person, meal)
await conn.commit()
return response
@ -348,7 +321,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.remove_request(conn, person, meal=meal)
await shopping.unrequest_meal(conn, meal)
await conn.commit()
return {}

View file

@ -1,53 +1,2 @@
from typing import Any, AsyncIterator, Dict, Iterator, List, Tuple
from shopping.db import ShoppingList, ShoppingListItem, load_shopping_list, purchase, remove_request, request
from shopping.db import ShoppingList, ShoppingListRequest, ShoppingListResult, sync_persons_requested_ingredients, load_shopping_list, get_current_requests, request_meal, unrequest_meal, insert_shopping_list, get_shopping_list_with_meal, remove_request
from shopping.db import find_items_by_list_id as _find_items_by_list_id, get_purchased_ingredients as _get_purchased_ingredients
def remove_references(items: List[ShoppingListItem], meals_lookup = None, recipes_lookup = None, ingredients_lookup = None) -> Tuple[Dict[int, Any], Dict[int, Any], Dict[int, Any]]:
meals_lookup = meals_lookup or {}
recipes_lookup = recipes_lookup or {}
ingredients_lookup = ingredients_lookup or {}
for item in items:
if item.meal and not item.meal.id in meals_lookup:
meals_lookup[item.meal.id] = item.meal
item.meal = None
if item.ingredient and not item.ingredient.id in ingredients_lookup:
ingredients_lookup[item.ingredient.id] = item.ingredient
item.ingredient = None
if item.recipe and not item.recipe.id in recipes_lookup:
recipes_lookup[item.recipe.id] = item.recipe
item.recipe = None
return meals_lookup, recipes_lookup, ingredients_lookup
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 mealRecipe in item.meal.recipes:
for ingredient in mealRecipe.recipe.ingredients:
yield ShoppingListItem(ingredient=ingredient, meal=item.meal, recipe=mealRecipe.recipe, person_id=item.person_id, created_date=item.created_date)
for ingredient in 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], List[ShoppingListItem]]:
current_requests = [r async for r in _find_items_by_list_id(conn, None)]
meal_requests = [r for r in current_requests if r.meal_id is not None and r.meal_id > 0 and r.meal is not None]
meals = {r.meal_id: r.meal for r in meal_requests}
purchased_ingredients = {r.ingredient_id async for r in _get_purchased_ingredients(conn, list(meals.keys()))}
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, meal_requests

View file

@ -1,33 +1,38 @@
from common import BaseLinkedModel
from recipes import Recipe
from meals import Meal, find_meal_by_id, mark_purchased
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 ShoppingListItem(BaseLinkedModel):
class ShoppingListRequest(BaseModel):
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
recipe_id: Optional[int] = None
recipe: Optional[Recipe] = 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
@ -36,14 +41,14 @@ class StoreEnum(str, Enum):
coles = 'coles'
home = ''
class ShoppingList(BaseLinkedModel):
class ShoppingList(BaseModel):
KEYS: ClassVar[List[str]] = ['id', 'created_date', 'store_name']
id: int = -1
created_date: datetime = datetime.now().astimezone()
store_name: StoreEnum = ''
purchased_by_id: int = -1
purchased_by: Optional[Person] = None
items: List[ShoppingListItem] = []
requests: List[ShoppingListRequest] = []
results: List[ShoppingListResult] = []
async def create(conn):
await conn.execute('''
@ -51,186 +56,123 @@ 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 ShoppingListItem (
CREATE TABLE IF NOT EXISTS ShoppingListRequest (
id INTEGER PRIMARY KEY,
ingredient_id INTEGER,
list_id INTEGER,
person_id INTEGER,
meal_id INTEGER,
recipe_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(recipe_id) REFERENCES Recipe(id)
FOREIGN KEY(list_id) REFERENCES ShoppingList(id)
);''')
def validate_request(request: ShoppingListItem) -> None:
if request.person_id < 0:
raise ValueError('Requests must have a person')
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')
# 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')
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 request.ingredient and request.meal:
raise ValueError('Request cannot have both an ingredient and a meal')
if shopping_list.items is None or len(shopping_list.items) == 0:
raise ValueError('Shopping list must have items')
if shopping_list.purchased_by_id < 0:
raise ValueError('Shopping list must have a valid person id')
# 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 insert_shopping_list(conn, shopping_list: ShoppingList):
shopping_list.created_date = datetime.now().astimezone()
async with conn.execute('''
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:
INSERT INTO ShoppingList (created_date, store_name)
VALUES (?, ?)
''', (shopping_list.created_date.isoformat(), shopping_list.store_name,)) as cursor:
shopping_list.id = cursor.lastrowid
for item in shopping_list.items:
for request in shopping_list.requests:
request.list_id = shopping_list.id
validate_request(request)
if request.ingredient and request.ingredient.id < 0:
await insert_ingredient(conn, request.ingredient)
if request.ingredient:
request.ingredient_id = request.ingredient.id
if request.meal:
request.meal_id = request.meal.id
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
for item in shopping_list.results:
item.product_id = item.product.id
item.list_id = shopping_list.id
validate_request(item)
if item.ingredient and item.ingredient.id < 0:
await insert_ingredient(conn, item.ingredient)
if item.ingredient_id is None or item.ingredient_id < 0:
raise ValueError('Ingredient request must have a valid ingredient id')
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:
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 list_id IS NULL
AND person_id = ?
AND meal_id IS NULL
AND recipe_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, recipe_id, created_date)
VALUES (?, ?, ?, ?, ?, ?)
''', (item.ingredient_id, shopping_list.id, item.person_id, item.meal_id, item.recipe_id, item.created_date.isoformat())) as cursor:
item.id = cursor.lastrowid
meal_ids = list({ item.meal_id for item in shopping_list.items if item.meal_id is not None and item.meal_id >= 0 })
await update_purchased_meals(conn, meal_ids)
async def update_purchased_meals(conn, meal_ids: List[int]) -> None:
if not meal_ids:
return
purchased_ingredient_ids = {item.ingredient_id async for item in get_purchased_ingredients(conn, meal_ids)}
for meal_id in meal_ids:
meal = await find_meal_by_id(conn, meal_id)
ingredients = {ingredient.id for recipe in meal.recipes for ingredient in recipe.recipe.ingredients} | \
{ingredient.id for ingredient in meal.extra_ingredients}
remaining_ingredients = ingredients - purchased_ingredient_ids
if not remaining_ingredients:
await mark_purchased(conn, meal)
# If all ingredients are purchased, update the meal status
await conn.execute('''
UPDATE Meal
SET purchase_date = ?
WHERE id = ?
''', (datetime.now().isoformat(), meal.id))
async def is_requested(conn, meal: Meal) -> bool:
if meal.id < 0:
return False
async with conn.execute('''
SELECT COUNT(*) FROM ShoppingListItem
WHERE meal_id = ? AND list_id IS NULL
''', (meal.id,)) as cursor:
row = await cursor.fetchone()
return row[0] > 0
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)
if meal is not None and await is_requested(conn, meal):
raise ValueError('Meal is already requested')
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) -> bool:
if meal is not None:
async with conn.execute('''
DELETE FROM ShoppingListItem
WHERE list_id IS NULL AND meal_id = ?
''', (meal.id,)) as cursor:
return cursor.rowcount > 0
INSERT INTO ShoppingListResult (product_id, list_id, quantity, unit)
VALUES (?, ?, ?, ?)
''', (item.product_id, item.list_id, item.quantity, item.unit)) as cursor:
item.id = cursor.lastrowid
elif ingredient is not None:
async with conn.execute('''
DELETE FROM ShoppingListItem
WHERE list_id IS NULL AND ingredient_id = ? AND person_id = ?
''', (ingredient.id, person.id)) as cursor:
return cursor.rowcount > 0
async def remove_request(conn, request: ShoppingListRequest) -> None:
if request.list_id != None:
raise ValueError('Request is already completed')
raise ValueError('Must specify either a meal or an ingredient to remove')
if request.meal and not request.meal_id:
raise ValueError('Meal request must have a meal id')
async def find_items_by_list_id(conn, list_id: Optional[int]) -> AsyncIterator[ShoppingListItem]:
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]:
# 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'shoppinglistitem.{key}' for key in ShoppingListItem.KEYS]
request_keys = [f'shoppinglistrequest.{key}' for key in ShoppingListRequest.KEYS]
person_keys = [f'person.{key}' for key in Person.KEYS]
select = f'''
SELECT {','.join(ingredient_keys + product_keys + request_keys + person_keys)}
FROM ShoppingListItem
LEFT JOIN Ingredient ON ShoppingListItem.ingredient_id = Ingredient.id
FROM ShoppingListRequest
LEFT JOIN Ingredient ON ShoppingListRequest.ingredient_id = Ingredient.id
LEFT JOIN Product ON Ingredient.product_id = Product.id
LEFT JOIN Person ON ShoppingListItem.person_id = Person.id
LEFT JOIN Person ON ShoppingListRequest.person_id = Person.id
'''
where, params = ' WHERE list_id IS NULL', ()
@ -249,14 +191,39 @@ async def find_items_by_list_id(conn, list_id: Optional[int]) -> AsyncIterator[S
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(ShoppingListItem.KEYS, row[len(Ingredient.KEYS) + len(Product.KEYS):-len(Person.KEYS)])}
request = ShoppingListItem(**request_keys, ingredient=ingredient, person=person)
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)
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'''
@ -269,19 +236,61 @@ async def load_shopping_list(conn, id: int) -> ShoppingList:
break
if shopping_list:
async for item in find_items_by_list_id(conn, shopping_list.id):
shopping_list.items.append(item)
await fill_related(conn, shopping_list)
return shopping_list
async def get_purchased_ingredients(conn, meal_ids: List[int]) -> AsyncIterator[ShoppingListItem]:
if not meal_ids:
return
async def request_ingredient(conn, person: Person, ingredient: Ingredient) -> ShoppingListRequest:
if ingredient.id >= 0:
raise ValueError('How did you get an existing ingredient?')
async with conn.execute(f'''
SELECT {','.join(ShoppingListItem.KEYS)}
FROM ShoppingListItem
WHERE meal_id IN ({','.join(['?'] * len(meal_ids))})
''', meal_ids) as cursor:
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 for row in cursor:
yield ShoppingListItem(**{k:v for k,v in zip(ShoppingListItem.KEYS, row)})
list_id = row[0]
yield await load_shopping_list(conn, list_id)