munch-ease-backend/shopping/db.py

359 lines
15 KiB
Python
Raw Normal View History

2024-05-18 07:05:01 +00:00
from meals import Meal, find_meals_by_date_range, find_meal_by_id
2024-05-17 09:09:03 +00:00
from ingredients import Ingredient, insert_ingredient
from persons import Person
from products import Product
from pydantic import BaseModel
2024-05-19 03:43:06 +00:00
from typing import AsyncIterator, List, ClassVar, Optional, Tuple
2024-05-17 09:09:03 +00:00
2024-05-18 07:05:01 +00:00
from datetime import datetime, timedelta
2024-05-17 09:09:03 +00:00
class ShoppingListRequest(BaseModel):
KEYS: ClassVar[List[str]] = ['id', 'ingredient_id', 'list_id', 'person_id', 'meal_id', 'created_date']
id: int = 0
list_id: int
2024-05-18 07:05:01 +00:00
ingredient_id: Optional[int] = None
2024-05-17 09:09:03 +00:00
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()
class ShoppingListResult(BaseModel):
KEYS: ClassVar[List[str]] = ['id', 'product_id', 'list_id', 'quantity', 'unit', 'created_date', 'found_date']
id: int = 0
list_id: int
2024-05-19 03:43:06 +00:00
product_id: int
product: Optional[Product] = None
2024-05-17 09:09:03 +00:00
quantity: float
unit: str
created_date: datetime = datetime.now()
found_date: Optional[datetime] = None
class ShoppingList(BaseModel):
KEYS: ClassVar[List[str]] = ['id', 'created_date', 'purchased_date']
id: int = 0
created_date: datetime = datetime.now()
purchased_date: Optional[datetime] = None
requests: List[ShoppingListRequest] = []
2024-05-19 03:43:06 +00:00
results: List[ShoppingListResult] = []
2024-05-17 09:09:03 +00:00
async def create(conn):
await conn.execute('''
CREATE TABLE IF NOT EXISTS ShoppingList (
id INTEGER PRIMARY KEY,
created_date TEXT,
purchased_date TEXT
);''')
await conn.execute('''
CREATE TABLE IF NOT EXISTS ShoppingListRequest (
id INTEGER PRIMARY KEY,
ingredient_id INTEGER,
list_id INTEGER,
person_id INTEGER,
meal_id INTEGER,
created_date TEXT,
FOREIGN KEY(ingredient_id) REFERENCES Ingredient(id),
FOREIGN KEY(person_id) REFERENCES Person(id),
FOREIGN KEY(meal_id) REFERENCES Meal(id),
FOREIGN KEY(list_id) REFERENCES ShoppingList(id)
);''')
await conn.execute('''
CREATE TABLE IF NOT EXISTS ShoppingListResult (
id INTEGER PRIMARY KEY,
product_id INTEGER,
list_id INTEGER,
quantity REAL,
unit TEXT,
created_date TEXT,
found_date TEXT,
FOREIGN KEY(product_id) REFERENCES Product(id),
FOREIGN KEY(list_id) REFERENCES ShoppingList(id)
);''')
async def insert_shopping_list(conn, shopping_list: ShoppingList):
async with conn.execute('''
INSERT INTO ShoppingList (id, created_date, purchased_date)
VALUES (?, ?, ?)
''', (shopping_list.id, shopping_list.created_date, shopping_list.purchased_date)) as cursor:
shopping_list.id = cursor.lastrowid
for request in shopping_list.requests:
if not request.ingredient_id:
await insert_ingredient(conn, request.ingredient)
request.ingredient_id = request.ingredient.id
await conn.execute('''
INSERT INTO ShoppingListRequest (id, ingredient_id, list_id, person_id, meal_id, created_date)
VALUES (?, ?, ?, ?, ?, ?)
''', (request.id, request.ingredient_id, shopping_list.id, request.person_id, request.meal_id, request.created_date))
2024-05-19 03:43:06 +00:00
for item in shopping_list.results:
2024-05-17 09:09:03 +00:00
item.product_id = item.product.id
await conn.execute('''
INSERT INTO ShoppingListResult (id, product_id, list_id, quantity, unit, created_date, found_date)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (item.id, item.product_id, shopping_list.id, item.quantity, item.unit, item.created_date, item.found_date))
2024-05-18 07:05:01 +00:00
async def find_request(conn, id: int) -> Optional[ShoppingListRequest]:
# Join Ingredient and Product to also load ingredient and product
product_keys = [f'product.{key}' for key in Product.KEYS]
2024-05-19 03:43:06 +00:00
ingredient_keys = [f'ingredient.{key}' for key in Ingredient.KEYS]
2024-05-18 07:05:01 +00:00
person_keys = [f'person.{key}' for key in Person.KEYS]
2024-05-19 03:43:06 +00:00
request_keys = [f'shoppinglistrequest.{key}' for key in ShoppingListRequest.KEYS]
2024-05-18 07:05:01 +00:00
async with conn.execute(f'''
2024-05-19 03:43:06 +00:00
SELECT {','.join(product_keys + ingredient_keys + person_keys + request_keys)}
2024-05-18 07:05:01 +00:00
FROM ShoppingListRequest
LEFT JOIN Ingredient ON ShoppingListRequest.ingredient_id = Ingredient.id
LEFT JOIN Product ON Ingredient.product_id = Product.id
LEFT JOIN Person ON ShoppingListRequest.person_id = Person.id
WHERE ShoppingListRequest.id = ?
''', (id,)) as cursor:
async for row in cursor:
2024-05-19 03:43:06 +00:00
product_keys = {k:v for k,v in zip(Product.KEYS, row[:len(Product.KEYS)])}
2024-05-18 07:05:01 +00:00
product = Product(**product_keys) if product_keys['id'] else None
2024-05-19 03:43:06 +00:00
ingredient_keys = {k:v for k,v in zip(Ingredient.KEYS, row[len(Product.KEYS):len(Product.KEYS) + len(Ingredient.KEYS)])}
ingredient = Ingredient(**ingredient_keys, product=product) if ingredient_keys['id'] else None
2024-05-18 07:05:01 +00:00
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
2024-05-19 03:43:06 +00:00
request_keys = {k:v for k,v in zip(ShoppingListRequest.KEYS, row[len(Product.KEYS) + len(Ingredient.KEYS):-len(Person.KEYS)])}
2024-05-18 07:05:01 +00:00
request = ShoppingListRequest(**request_keys, ingredient=ingredient, person=person)
2024-05-19 03:43:06 +00:00
if request.meal_id:
request.meal = await find_meal_by_id(conn, request.meal_id)
2024-05-18 07:05:01 +00:00
return request
2024-05-17 09:09:03 +00:00
async def find_requests_by_list_id(conn, list_id: 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'shoppinglistrequest.{key}' for key in ShoppingListRequest.KEYS]
person_keys = [f'person.{key}' for key in Person.KEYS]
cursor = await conn.execute(f'''
SELECT {','.join(ingredient_keys + product_keys + request_keys + person_keys)}
FROM ShoppingListRequest
LEFT JOIN Ingredient ON ShoppingListRequest.ingredient_id = Ingredient.id
LEFT JOIN Product ON Ingredient.product_id = Product.id
LEFT JOIN Person ON ShoppingListRequest.person_id = Person.id
WHERE list_id = ?
''', (list_id,))
async for row in cursor:
product_keys = {k:v for k,v in zip(Product.KEYS, row[len(Ingredient.KEYS):len(Ingredient.KEYS) + len(Product.KEYS)])}
product = Product(**product_keys) if product_keys['id'] else None
ingredient_keys = {k:v for k,v in zip(Ingredient.KEYS, row[:len(Ingredient.KEYS)])}
2024-05-18 07:05:01 +00:00
ingredient = Ingredient(**ingredient_keys, product=product) if ingredient_keys['id'] else None
2024-05-17 09:09:03 +00:00
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)
2024-05-18 07:05:01 +00:00
if request.meal_id:
request.meal = await find_meal_by_id(conn, request.meal_id)
2024-05-17 09:09:03 +00:00
yield request
async def find_items_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_items_by_list_id(conn, shopping_list.id):
2024-05-19 03:43:06 +00:00
shopping_list.results.append(item)
2024-05-17 09:09:03 +00:00
async def load_shopping_list(conn, id: int) -> ShoppingList:
shopping_list = None
async with conn.execute(f'''
SELECT {','.join(ShoppingList.KEYS)} FROM ShoppingList
WHERE id = ?
LIMIT 1
''', (id,)) as cursor:
async for row in cursor:
shopping_list = ShoppingList(**{k:v for k,v in zip(ShoppingList.KEYS, row)})
break
if shopping_list:
await fill_related(conn, shopping_list)
return shopping_list
2024-05-18 07:05:01 +00:00
async def _upcoming_meals(conn) -> AsyncIterator[Meal]:
start = datetime.now()
end = start + timedelta(days=7)
async for meal in find_meals_by_date_range(conn, start, end):
yield meal
2024-05-17 09:09:03 +00:00
async def current_shopping_list(conn) -> ShoppingList:
shopping_list = None
async with conn.execute(f'''
SELECT {','.join(ShoppingList.KEYS)} FROM ShoppingList
WHERE purchased_date IS NULL
LIMIT 1
''') as cursor:
async for row in cursor:
shopping_list = ShoppingList(**{k:v for k,v in zip(ShoppingList.KEYS, row)})
await fill_related(conn, shopping_list)
break
if not shopping_list:
shopping_list = ShoppingList()
2024-05-18 07:05:01 +00:00
async for meal in _upcoming_meals(conn):
shopping_list.requests.append(ShoppingListRequest(list_id=shopping_list.id, meal_id=meal.id, created_date=datetime.now(),))
2024-05-17 09:09:03 +00:00
await insert_shopping_list(conn, shopping_list)
return shopping_list
async def find_existing_result(conn, product: Product, shopping_list: ShoppingList) -> ShoppingListResult:
async with conn.execute(f'''
SELECT {','.join(ShoppingListResult.KEYS)} FROM ShoppingListResult
WHERE product_id = ? AND list_id = ?
LIMIT 1
''', (product.id, shopping_list.id)) as cursor:
async for row in cursor:
return ShoppingListResult(**{k:v for k,v in zip(ShoppingListResult.KEYS, row)}, product=product)
return None
async def get_persons_requests(conn, shopping_list: ShoppingList, person: Person) -> AsyncIterator[ShoppingListRequest]:
async for request in find_requests_by_list_id(conn, shopping_list.id):
if request.person_id == person.id:
yield request
2024-05-18 07:05:01 +00:00
async def request_ingredient(conn, shopping_list: ShoppingList, person: Person, ingredient: Ingredient) -> ShoppingListRequest:
2024-05-17 09:09:03 +00:00
if ingredient.id:
raise ValueError('How did you get an existing ingredient?')
await insert_ingredient(conn, ingredient)
2024-05-18 07:05:01 +00:00
request = ShoppingListRequest(ingredient_id=ingredient.id, ingredient=ingredient, list_id=shopping_list.id, person_id=person.id, created_date=datetime.now())
2024-05-17 09:09:03 +00:00
async with conn.execute('''
INSERT INTO ShoppingListRequest (ingredient_id, list_id, person_id, created_date)
VALUES (?, ?, ?, ?)
''', (request.ingredient_id, request.list_id, request.person_id, request.created_date)) as cursor:
request.id = cursor.lastrowid
shopping_list.requests.append(request)
2024-05-18 07:05:01 +00:00
return request
async def request_meal(conn, shopping_list: ShoppingList, person: Person, meal: Meal) -> ShoppingListRequest:
request = ShoppingListRequest(meal_id=meal.id, meal=meal, list_id=shopping_list.id, person_id=person.id, created_date=datetime.now())
async with conn.execute('''
INSERT INTO ShoppingListRequest (meal_id, list_id, person_id, created_date)
VALUES (?, ?, ?, ?)
''', (request.meal_id, request.list_id, request.person_id, request.created_date)) as cursor:
request.id = cursor.lastrowid
return request
async def delete_requests(conn, shopping_list: ShoppingList, meal: Meal) -> None:
await conn.execute('''
DELETE FROM ShoppingListRequest
WHERE list_id = ? AND meal_id = ?
''', (shopping_list.id, meal.id))
2024-05-17 09:09:03 +00:00
2024-05-18 07:05:01 +00:00
async def sync_persons_requested_ingredients(conn, shopping_list: ShoppingList, person: Person, requests: List[Ingredient]) -> AsyncIterator[ShoppingListRequest]:
2024-05-17 09:09:03 +00:00
# Delete existing and insert all as new
2024-05-18 07:05:01 +00:00
await conn.execute('''
DELETE FROM ShoppingListRequest
WHERE list_id = ? AND person_id = ? AND ingredient_id IS NOT NULL
''', (shopping_list.id, person.id))
2024-05-17 09:09:03 +00:00
for ingredient in requests:
ingredient.id = 0
2024-05-18 07:05:01 +00:00
yield await request_ingredient(conn, shopping_list, person, ingredient)
2024-05-19 03:43:06 +00:00
async def find_existing_result(conn, shopping_list: ShoppingList, product: Product, unit: str) -> Optional[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 ShoppingListResult.list_id = ? AND ShoppingListResult.product_id = ? AND ShoppingListResult.unit = ?
LIMIT 1
''', (shopping_list.id, product.id, unit)) 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):])}
return ShoppingListResult(**result_keys, product=product)
return None
async def mark_found(conn, ingredient: Ingredient, date_found: datetime) -> Tuple[ShoppingListResult, ShoppingListResult]:
product, quantity, unit = ingredient.product, ingredient.quantity, ingredient.unit
2024-05-17 09:09:03 +00:00
shopping_list = await current_shopping_list(conn)
2024-05-19 03:43:06 +00:00
created = ShoppingListResult(product=product, product_id=product.id, list_id=shopping_list.id, quantity=quantity, unit=unit, found_date=date_found)
removed = await find_existing_result(conn, shopping_list, product, unit)
2024-05-17 09:09:03 +00:00
2024-05-19 03:43:06 +00:00
async with conn.execute('''
INSERT INTO ShoppingListResult (product_id, list_id, quantity, unit, created_date, found_date)
VALUES (?, ?, ?, ?, ?, ?)
''', (created.product_id, created.list_id, quantity, unit, created.created_date, created.found_date)) as cursor:
created.id = cursor.lastrowid
if removed:
shopping_list.results = [r for r in shopping_list.results if r.id != removed.id]
2024-05-17 09:09:03 +00:00
await conn.execute('''
2024-05-19 03:43:06 +00:00
DELETE FROM ShoppingListResult
2024-05-17 09:09:03 +00:00
WHERE id = ?
2024-05-19 03:43:06 +00:00
''', (removed.id,))
return removed, created
2024-05-17 09:09:03 +00:00
2024-05-19 03:43:06 +00:00
async def unmark_found(conn, product_id: int) -> List[ShoppingListRequest]:
shopping_list = await current_shopping_list(conn)
requests = []
async with conn.execute('''
DELETE FROM ShoppingListResult
WHERE list_id = ? AND product_id = ?
''', (shopping_list.id, product_id)) as cursor:
async for row in cursor:
requests.append(row)
deleted = [r for r in shopping_list.results if r.product_id == product_id]
shopping_list.results = [r for r in shopping_list.results if r.product_id != product_id]
return deleted