diff --git a/db.py b/db.py index fffb900..6121c82 100644 --- a/db.py +++ b/db.py @@ -1,11 +1,10 @@ import aiosqlite -async def connect() -> aiosqlite.Connection: - return await aiosqlite.connect('./data/your_database.db') +async def connect(path = './data/your_database.db') -> aiosqlite.Connection: + return await aiosqlite.connect(path) -async def create(): +async def create(conn: aiosqlite.Connection): import products.db as product_db - conn = await connect() await product_db.create(conn) import ingredients.db as ingredient_db @@ -20,9 +19,10 @@ async def create(): import meals.db as meals_db await meals_db.create(conn) - await conn.commit() - await conn.close() - if __name__ == '__main__': import asyncio - asyncio.run(create()) \ No newline at end of file + async def initdb(): + conn = await connect() + await create(conn) + + asyncio.run(initdb) \ No newline at end of file diff --git a/ingredients/db.py b/ingredients/db.py index d80b50b..08f781a 100644 --- a/ingredients/db.py +++ b/ingredients/db.py @@ -35,6 +35,12 @@ async def create(conn): async def insert_ingredient(conn, ingredient: Ingredient): + if not ingredient.product_id and ingredient.product: + ingredient.product_id = ingredient.product.id + + if not ingredient.product_id: + 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) VALUES (?, ?, ?, ?, ?, ?, ?, ?) diff --git a/main.py b/main.py index b0771cb..f039be1 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,5 @@ import sqlite3 -import products, recipes, db, meals, persons, ingredients +import products, recipes, db as db, meals, persons, ingredients import datetime from pydantic import BaseModel @@ -14,7 +14,7 @@ app = FastAPI() # Add CORS middleware app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=["*", "http://localhost:8080", "https://localhost:8080"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"] @@ -29,7 +29,7 @@ async def get_db(): await sql_db.close() async def cookie_person(user_id: Annotated[int, Cookie(alias='user_id')], conn: sqlite3.Connection = Depends(get_db)) -> persons.Person: - return await persons.find_person_by_id(conn, user_id) + return await persons.get_by_id(conn, user_id) @app.get("/recipes/parse") async def parse_recipe_handler(url: str, conn: sqlite3.Connection = Depends(get_db)) -> recipes.Recipe: @@ -65,6 +65,8 @@ async def load_full_recipe(conn: sqlite3.Connection, id: int) -> recipes.Recipe: async for ingredient in ingredients.find_ingredients_by_recipe_id(conn, id): r.ingredients.append(ingredient) + r.created_by = await persons.get_by_id(conn, r.created_by_id) + return r @app.get("/recipes/") @@ -135,7 +137,7 @@ async def get_meal(meal_id: int, conn: sqlite3.Connection = Depends(get_db)) -> @app.post("/meals/") async def create_meal(meal: meals.Meal, conn: sqlite3.Connection = Depends(get_db)) -> meals.Meal: - if not meal.chef: + if not meal.chefs: return JSONResponse(status_code=400, content={'message': 'Meal must have at least one chef'}) if not meal.cleanup: diff --git a/meals/db.py b/meals/db.py index d85a5cd..9b3a4ec 100644 --- a/meals/db.py +++ b/meals/db.py @@ -1,16 +1,19 @@ -from typing import List, ClassVar +from typing import List, ClassVar, Optional from pydantic import BaseModel -from persons import Person from ingredients import Ingredient, insert_ingredient, find_ingredients_by_meal_id -from products import Product -from recipes import Recipe, row_to_recipe + +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', 'date'] + KEYS: ClassVar[List[str]] = ['id', 'meal_date'] id: int - date: datetime.datetime + meal_date: datetime.datetime + chefs: List[Person] = [] cleanup: List[Person] = [] consumers: List[Person] = [] @@ -21,7 +24,7 @@ async def create(conn): await conn.execute(''' CREATE TABLE IF NOT EXISTS Meal ( id INTEGER PRIMARY KEY, - date TEXT UNIQUE + meal_date TEXT );''') await conn.execute(''' @@ -48,6 +51,9 @@ async def insert_meal_participant(conn, meal_id: int, person_id: int, role: str) ''', (meal_id, person_id, role)) async def insert_meal_recipe(conn, meal_id: int, recipe_id: int): + if not recipe_id: + raise ValueError('Recipe must be inserted before meal') + await conn.execute(''' INSERT INTO MealRecipe (meal_id, recipe_id) VALUES (?, ?) @@ -55,9 +61,9 @@ async def insert_meal_recipe(conn, meal_id: int, recipe_id: int): async def insert_meal(conn, meal: Meal): async with conn.execute(''' - INSERT INTO Meal (date) + INSERT INTO Meal (meal_date) VALUES (?) - ''', (meal.date,)) as cursor: + ''', (meal.meal_date,)) as cursor: meal.id = cursor.lastrowid for person in meal.chefs: @@ -110,7 +116,7 @@ async def load_participants(conn, meal: Meal) -> None: WHERE meal_id = ? ''', (meal.id,)) as cursor: async for row in cursor: - person = await Person.find_person_by_id(conn, row[0]) + person = await persons.get_by_id(conn, row[0]) if row[1] == 'chef': meal.chefs.append(person) elif row[1] == 'cleanup': @@ -127,7 +133,10 @@ async def load_recipes(conn, meal: Meal) -> None: WHERE MealRecipe.meal_id = ? ''', (meal.id,)) as cursor: async for row in cursor: - meal.recipes.append(row_to_recipe(zip(Recipe.KEYS, row))) + 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): diff --git a/persons/db.py b/persons/db.py index 618d4c2..d76064a 100644 --- a/persons/db.py +++ b/persons/db.py @@ -12,10 +12,10 @@ async def create(conn): name TEXT UNIQUE );''') - await insert(conn, Person(id=0, name='Jacob')) - await insert(conn, Person(id=0, name='Ellie')) - await insert(conn, Person(id=0, name='Ryan')) - await insert(conn, Person(id=0, name='Chris')) + await insert(conn, Person(id=1, name='Jacob')) + await insert(conn, Person(id=2, name='Ryan')) + await insert(conn, Person(id=3, name='Ellie')) + await insert(conn, Person(id=4, name='Chris')) async def get_by_name(conn, name: str) -> Person: cursor = await conn.execute(''' diff --git a/recipes/__init__.py b/recipes/__init__.py index 2180908..9d1ef14 100644 --- a/recipes/__init__.py +++ b/recipes/__init__.py @@ -1,4 +1,4 @@ -from recipes.db import Recipe, insert_recipe, find_recipe_by_id, get_all, find_recipes_by_name, row_to_recipe +from recipes.db import Recipe, insert_recipe, find_recipe_by_id, get_all, find_recipes_by_name, row_to_recipe, load_recipe_ingredients from recipes.scraping import scrape_recipe_ldata as _scrape_recipe_ldata from ingredients import parse_ingredient_from_nlp as _parse_ingredient_from_nlp, match_existing_products as _match_existing_products diff --git a/recipes/db.py b/recipes/db.py index d068f4d..2165bba 100644 --- a/recipes/db.py +++ b/recipes/db.py @@ -79,6 +79,6 @@ async def get_all(conn) -> List[Recipe]: async for row in cursor: yield row_to_recipe(zip(Recipe.KEYS, row)) -async def load_ingredients(conn, recipe: Recipe): +async def load_recipe_ingredients(conn, recipe: Recipe) -> None: async for ingredient in find_ingredients_by_recipe_id(conn, recipe.id): recipe.ingredients.append(ingredient) \ No newline at end of file diff --git a/test_data.py b/test_data.py new file mode 100644 index 0000000..275cf87 --- /dev/null +++ b/test_data.py @@ -0,0 +1,94 @@ +from persons import db as persons_db + +class Persons: + jacob = persons_db.Person( + id=1, + name='Jacob') + + ryan = persons_db.Person( + id=2, + name='Ryan') + + ellie = persons_db.Person( + id=3, + name='Ellie') + + chris = persons_db.Person( + id=4, + name='Chris') + +from products import db as products_db + +class Products: + broccoli = products_db.Product( + id=0, + product_id='69', + name='Broccoli', + link='https://en.wikipedia.org/wiki/Broccoli', + tags=['vegetable'], + img_small='https://upload.wikimedia.org/wikipedia/commons/thumb/0/03/Broccoli_and_cross_section_edit.jpg/800px-Broccoli_and_cross_section_edit.jpg', + img_large='https://upload.wikimedia.org/wikipedia/commons/0/03/Broccoli_and_cross_section_edit.jpg', + raw_data={}, + ) + + garlic_bread = products_db.Product( + id=0, + product_id='420', + name='Garlic Bread', + link='https://en.wikipedia.org/wiki/Garlic_bread', + tags=['bread', 'garlic'], + img_small='https://upload.wikimedia.org/wikipedia/commons/thumb/4/4b/Garlic_bread.jpg/800px-Garlic_bread.jpg', + img_large='https://upload.wikimedia.org/wikipedia/commons/4/4b/Garlic_bread.jpg', + raw_data={}, + ) + +from ingredients import db as ingredients_db + +class Ingredients: + broccoli_chopped_1kg = ingredients_db.Ingredient( + id=0, + line='1kg Broccoli, Chopped', + name='Broccoli', + unit='1kg', + quantity='1', + preparation='Chopped', + product=Products.broccoli, + ) + + garlic_bread_1_loaf = ingredients_db.Ingredient( + id=0, + line='1 Loaf Garlic Bread', + name='Garlic Bread', + unit='Loaf', + quantity='1', + preparation='', + product=Products.garlic_bread, + ) + +from recipes import db as recipes_db + +class Recipes: + broccoli_soup = recipes_db.Recipe( + id=0, + name='Broccoli Soup', + link='https://www.bbcgoodfood.com/recipes/broccoli-soup', + image_urls=['https://www.bbcgoodfood.com/sites/default/files/styles/recipe/public/recipe/recipe-image/2018/10/broccoli-soup.jpg'], + ingredients=[Ingredients.broccoli_chopped_1kg], + created_by_id=Persons.jacob.id, + ) + +from meals import db as meals_db +from datetime import datetime + +class Meals: + broccoli_soup_for_jacob = meals_db.Meal( + id=0, + create_date=datetime(2021, 12, 25), + created_by=Persons.jacob, + meal_date=datetime(2021, 12, 25), + chefs=[Persons.jacob], + cleanup=[Persons.ryan], + consumers=[Persons.ellie, Persons.chris], + recipes=[Recipes.broccoli_soup], + extra_ingredients=[Ingredients.garlic_bread_1_loaf], + ) \ No newline at end of file diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..515ee2c --- /dev/null +++ b/tests.py @@ -0,0 +1,148 @@ +import unittest +import test_data + +import pathlib +from db import connect, create + +import products.db as products_db +import recipes.db as recipes_db + +class TestProducts(unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.conn = await connect('./testdb.db') + await create(self.conn) + return await super().asyncSetUp() + + async def asyncTearDown(self) -> None: + await self.conn.close() + pathlib.Path('./testdb.db').unlink(missing_ok=True) + return await super().asyncTearDown() + + async def testCreateAndFind(self) -> None: + product = test_data.Products.broccoli + await products_db.insert_product(self.conn, product, {}) + self.assertIsNotNone(product) + self.assertGreater(product.id, 0) + + product_by_id = await products_db.find_product_by_id(self.conn, product.id) + self.assertIsNotNone(product_by_id) + self.assertEqual(product_by_id.id, product.id) + self.assertEqual(product_by_id.name, product.name) + self.assertEqual(product_by_id.link, product.link) + self.assertEqual(product_by_id.img_large, product.img_large) + self.assertEqual(product_by_id.img_small, product.img_small) + +import main + +class TestRecipe(unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + pathlib.Path('./testdb.db').unlink(missing_ok=True) + self.conn = await connect('./testdb.db') + await create(self.conn) + return await super().asyncSetUp() + + async def asyncTearDown(self) -> None: + await self.conn.close() + pathlib.Path('./testdb.db').unlink(missing_ok=True) + return await super().asyncTearDown() + + async def testCreateAndFind(self) -> None: + recipe = test_data.Recipes.broccoli_soup + ingredient = recipe.ingredients[0] + product = ingredient.product + person = test_data.Persons.jacob + + await products_db.insert_product(self.conn, product, {}) + + create_response = await main.create_recipe(recipe, self.conn, person) + self.assertIsNotNone(create_response) + self.assertIsInstance(create_response, recipes_db.Recipe, msg=create_response.body if hasattr(create_response, 'body') else create_response) + + recipe_by_id = await main.get_recipe(recipe.id, self.conn) + self.assertIsNotNone(recipe_by_id) + self.assertIsInstance(recipe_by_id, recipes_db.Recipe, msg=recipe_by_id.body if hasattr(recipe_by_id, 'body') else recipe_by_id) + + self.assertIsNotNone(recipe_by_id) + self.assertEqual(recipe_by_id.id, recipe.id) + self.assertEqual(recipe_by_id.name, recipe.name) + self.assertEqual(recipe_by_id.link, recipe.link) + self.assertEqual(recipe_by_id.image_urls, recipe.image_urls) + self.assertEqual(len(recipe_by_id.ingredients), 1) + self.assertEqual(recipe_by_id.ingredients[0].id, ingredient.id) + self.assertEqual(recipe_by_id.ingredients[0].line, ingredient.line) + self.assertEqual(recipe_by_id.ingredients[0].name, ingredient.name) + self.assertEqual(recipe_by_id.ingredients[0].unit, ingredient.unit) + self.assertEqual(recipe_by_id.ingredients[0].quantity, ingredient.quantity) + self.assertEqual(recipe_by_id.ingredients[0].preparation, ingredient.preparation) + self.assertEqual(recipe_by_id.ingredients[0].product.id, product.id) + self.assertEqual(recipe_by_id.ingredients[0].product.name, product.name) + self.assertEqual(recipe_by_id.ingredients[0].product.link, product.link) + self.assertEqual(recipe_by_id.ingredients[0].product.img_large, product.img_large) + self.assertEqual(recipe_by_id.ingredients[0].product.img_small, product.img_small) + self.assertEqual(recipe_by_id.created_by.id, person.id) + self.assertEqual(recipe_by_id.created_by.name, person.name) + +import meals + +class TestMeals(unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + pathlib.Path('./testdb.db').unlink(missing_ok=True) + self.conn = await connect('./testdb.db') + await create(self.conn) + return await super().asyncSetUp() + + async def asyncTearDown(self) -> None: + await self.conn.close() + pathlib.Path('./testdb.db').unlink(missing_ok=True) + return await super().asyncTearDown() + + async def testCreateAndFind(self) -> None: + meal = test_data.Meals.broccoli_soup_for_jacob + recipe = meal.recipes[0] + recipe_ingredient = recipe.ingredients[0] + recipe_product = recipe_ingredient.product + extra_ingredient = meal.extra_ingredients[0] + extra_product = extra_ingredient.product + + person = test_data.Persons.jacob + + await products_db.insert_product(self.conn, recipe_product, {}) + await products_db.insert_product(self.conn, extra_product, {}) + created_recipe = await main.create_recipe(recipe, self.conn, person) + recipe.id = created_recipe.id + + create_response = await main.create_meal(meal, self.conn) + self.assertIsNotNone(create_response) + self.assertIsInstance(create_response, meals.Meal, msg=create_response.body if hasattr(create_response, 'body') else create_response) + + meal_by_id = await main.get_meal(meal.id, self.conn) + self.assertIsNotNone(meal_by_id) + self.assertIsInstance(meal_by_id, meals.Meal, msg=meal_by_id.body if hasattr(meal_by_id, 'body') else meal_by_id) + + self.assertIsNotNone(meal_by_id) + self.assertEqual(meal_by_id.id, meal.id) + self.assertEqual(meal_by_id.meal_date, meal.meal_date) + self.assertEqual(len(meal_by_id.chefs), 1) + self.assertEqual(meal_by_id.chefs[0].id, meal.chefs[0].id) + self.assertEqual(meal_by_id.chefs[0].name, meal.chefs[0].name) + self.assertEqual(len(meal_by_id.cleanup), 1) + self.assertEqual(meal_by_id.cleanup[0].id, meal.cleanup[0].id) + self.assertEqual(meal_by_id.cleanup[0].name, meal.cleanup[0].name) + self.assertEqual(len(meal_by_id.consumers), 2) + + expected = [p.id for p in meal.consumers] + actual = [p.id for p in meal_by_id.consumers] + self.assertEqual(sorted(expected), sorted(actual)) + + self.assertEqual(meal_by_id.recipes[0].id, recipe.id) + self.assertEqual(meal_by_id.recipes[0].name, recipe.name) + self.assertEqual(meal_by_id.recipes[0].link, recipe.link) + self.assertEqual(meal_by_id.recipes[0].image_urls, recipe.image_urls) + self.assertEqual(len(meal_by_id.recipes[0].ingredients), 1) + self.assertEqual(meal_by_id.recipes[0].ingredients[0].id, recipe_ingredient.id) + self.assertEqual(meal_by_id.recipes[0].ingredients[0].line, recipe_ingredient.line) + self.assertEqual(meal_by_id.extra_ingredients[0].id, extra_ingredient.id) + self.assertEqual(meal_by_id.extra_ingredients[0].line, extra_ingredient.line) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file