from typing import AsyncIterator, List, ClassVar, Optional from pydantic import BaseModel from ingredients import Ingredient, insert_ingredient, find_ingredients_by_meal_id, delete_ingredients_by_meal_id from recipes import Recipe, row_to_recipe, load_recipe_ingredients import persons from persons import Person import datetime class Meal(BaseModel): KEYS: ClassVar[List[str]] = ['id', 'suggested_date'] id: int = -1 suggested_date: datetime.datetime purchase_date: Optional[datetime.datetime] = None chefs: List[Person] = [] cleanup: List[Person] = [] consumers: List[Person] = [] recipes: List[Recipe] = [] extra_ingredients: List[Ingredient] = [] async def create(conn): await conn.execute(''' CREATE TABLE IF NOT EXISTS Meal ( id INTEGER PRIMARY KEY, suggested_date TEXT );''') await conn.execute(''' CREATE TABLE IF NOT EXISTS MealParticipant ( meal_id INTEGER, person_id INTEGER, role TEXT, FOREIGN KEY(meal_id) REFERENCES Meal(id), FOREIGN KEY(person_id) REFERENCES Person(id) );''') await conn.execute(''' CREATE TABLE IF NOT EXISTS MealRecipe ( meal_id INTEGER, recipe_id INTEGER, FOREIGN KEY(meal_id) REFERENCES Meal(id), FOREIGN KEY(recipe_id) REFERENCES Recipe(id) );''') async def insert_meal_participant(conn, meal_id: int, person_id: int, role: str): await conn.execute(''' INSERT INTO MealParticipant (meal_id, person_id, role) VALUES (?, ?, ?) ''', (meal_id, person_id, role)) async def sync_meal_participants(conn, meal_id: int, participants: List[Person], role: str): await conn.execute(''' DELETE FROM MealParticipant WHERE meal_id = ? AND role = ? ''', (meal_id, role)) for person in participants: await insert_meal_participant(conn, meal_id, person.id, role) async def insert_meal_recipe(conn, meal_id: int, recipe_id: int): if recipe_id < 0: raise ValueError('Recipe must be inserted before meal') await conn.execute(''' INSERT INTO MealRecipe (meal_id, recipe_id) VALUES (?, ?) ''', (meal_id, recipe_id)) async def insert_meal(conn, meal: Meal): async with conn.execute(''' INSERT INTO Meal (suggested_date) VALUES (?) ''', (meal.suggested_date,)) as cursor: meal.id = cursor.lastrowid await sync_meal_participants(conn, meal.id, meal.chefs, 'chef') await sync_meal_participants(conn, meal.id, meal.cleanup, 'cleanup') await sync_meal_participants(conn, meal.id, meal.consumers, 'consumer') for recipe in meal.recipes: await insert_meal_recipe(conn, meal.id, recipe.id) await sync_extra_ingredients(conn, meal.id, meal.extra_ingredients) async def find_meal_by_id(conn, meal_id: int) -> Meal: async with conn.execute(f''' SELECT {','.join(Meal.KEYS)} FROM Meal WHERE id = ? LIMIT 1 ''', (meal_id,)) as cursor: async for row in cursor: meal = await with_purchase_date(conn, Meal(**{k:v for k,v in zip(Meal.KEYS, row)})) await load_participants(conn, meal) await load_recipes(conn, meal) await load_extra_ingredients(conn, meal) return meal async def find_meals_by_date_range(conn, start: datetime, end: datetime) -> AsyncIterator[Meal]: async with conn.execute(f''' SELECT {','.join(Meal.KEYS)} FROM Meal WHERE suggested_date >= ? AND suggested_date <= ? ''', (start, end)) as cursor: async for row in cursor: yield await with_purchase_date(conn, Meal(**{k:v for k,v in zip(Meal.KEYS, row)})) async def load_participants(conn, meal: Meal) -> None: async with conn.execute(f''' SELECT person_id, role FROM MealParticipant WHERE meal_id = ? ''', (meal.id,)) as cursor: async for row in cursor: person = await persons.get_by_id(conn, row[0]) if row[1] == 'chef': meal.chefs.append(person) elif row[1] == 'cleanup': meal.cleanup.append(person) elif row[1] == 'consumer': meal.consumers.append(person) else: raise Exception(f'Unknown role: {row[1]}') async def load_recipes(conn, meal: Meal) -> None: async with conn.execute(f''' SELECT {','.join(Recipe.KEYS)} FROM Recipe JOIN MealRecipe ON MealRecipe.recipe_id = Recipe.id WHERE MealRecipe.meal_id = ? ''', (meal.id,)) as cursor: async for row in cursor: recipe = row_to_recipe(zip(Recipe.KEYS, row)) await load_recipe_ingredients(conn, recipe) meal.recipes.append(recipe) async def load_extra_ingredients(conn, meal: Meal) -> None: async for ingredient in find_ingredients_by_meal_id(conn, meal.id): meal.extra_ingredients.append(ingredient) async def delete_meal(conn, meal_id: int) -> None: await conn.execute(''' DELETE FROM MealParticipant WHERE meal_id = ? ''', (meal_id,)) await conn.execute(''' DELETE FROM MealRecipe WHERE meal_id = ? ''', (meal_id,)) await conn.execute(''' DELETE FROM Ingredient WHERE meal_id = ? ''', (meal_id,)) await conn.execute(''' DELETE FROM Meal WHERE id = ? ''', (meal_id,)) async def sync_extra_ingredients(conn, meal_id: int, ingredients: List[Ingredient]) -> None: await delete_ingredients_by_meal_id(conn, meal_id) for ingredient in ingredients: ingredient.meal_id = meal_id ingredient.recipe_id = None await insert_ingredient(conn, ingredient) async def sync_recipes(conn, meal_id: int, recipes: List[Recipe]) -> None: await conn.execute(''' DELETE FROM MealRecipe WHERE meal_id = ? ''', (meal_id,)) for recipe in recipes: await insert_meal_recipe(conn, meal_id, recipe.id) async def update_meal(conn, meal: Meal) -> None: await conn.execute(''' UPDATE Meal SET suggested_date = ? WHERE id = ? ''', (meal.suggested_date, meal.id)) await sync_meal_participants(conn, meal.id, meal.chefs, 'chef') await sync_meal_participants(conn, meal.id, meal.cleanup, 'cleanup') await sync_meal_participants(conn, meal.id, meal.consumers, 'consumer') await sync_extra_ingredients(conn, meal.id, meal.extra_ingredients) await sync_recipes(conn, meal.id, meal.recipes) async def with_purchase_date(conn, meal: Meal) -> Meal: async with conn.execute(''' SELECT purchased_date FROM ShoppingList WHERE id = ( SELECT list_id FROM ShoppingListRequest WHERE meal_id = ? LIMIT 1 ) ''', (meal.id,)) as cursor: async for row in cursor: meal.purchase_date = row[0] return meal