Added some tests

This commit is contained in:
jableader 2024-04-25 14:57:39 +10:00
parent edbe082387
commit cdd4cc1f3b
9 changed files with 288 additions and 29 deletions

16
db.py
View file

@ -1,11 +1,10 @@
import aiosqlite import aiosqlite
async def connect() -> aiosqlite.Connection: async def connect(path = './data/your_database.db') -> aiosqlite.Connection:
return await aiosqlite.connect('./data/your_database.db') return await aiosqlite.connect(path)
async def create(): async def create(conn: aiosqlite.Connection):
import products.db as product_db import products.db as product_db
conn = await connect()
await product_db.create(conn) await product_db.create(conn)
import ingredients.db as ingredient_db import ingredients.db as ingredient_db
@ -20,9 +19,10 @@ async def create():
import meals.db as meals_db import meals.db as meals_db
await meals_db.create(conn) await meals_db.create(conn)
await conn.commit()
await conn.close()
if __name__ == '__main__': if __name__ == '__main__':
import asyncio import asyncio
asyncio.run(create()) async def initdb():
conn = await connect()
await create(conn)
asyncio.run(initdb)

View file

@ -35,6 +35,12 @@ async def create(conn):
async def insert_ingredient(conn, ingredient: Ingredient): 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(''' async with conn.execute('''
INSERT INTO Ingredient (name, line, preparation, unit, quantity, product_id, recipe_id, meal_id) INSERT INTO Ingredient (name, line, preparation, unit, quantity, product_id, recipe_id, meal_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)

10
main.py
View file

@ -1,5 +1,5 @@
import sqlite3 import sqlite3
import products, recipes, db, meals, persons, ingredients import products, recipes, db as db, meals, persons, ingredients
import datetime import datetime
from pydantic import BaseModel from pydantic import BaseModel
@ -14,7 +14,7 @@ app = FastAPI()
# Add CORS middleware # Add CORS middleware
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=["*", "http://localhost:8080", "https://localhost:8080"],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"] allow_headers=["*"]
@ -29,7 +29,7 @@ async def get_db():
await sql_db.close() await sql_db.close()
async def cookie_person(user_id: Annotated[int, Cookie(alias='user_id')], conn: sqlite3.Connection = Depends(get_db)) -> persons.Person: 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") @app.get("/recipes/parse")
async def parse_recipe_handler(url: str, conn: sqlite3.Connection = Depends(get_db)) -> recipes.Recipe: 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): async for ingredient in ingredients.find_ingredients_by_recipe_id(conn, id):
r.ingredients.append(ingredient) r.ingredients.append(ingredient)
r.created_by = await persons.get_by_id(conn, r.created_by_id)
return r return r
@app.get("/recipes/") @app.get("/recipes/")
@ -135,7 +137,7 @@ async def get_meal(meal_id: int, conn: sqlite3.Connection = Depends(get_db)) ->
@app.post("/meals/") @app.post("/meals/")
async def create_meal(meal: meals.Meal, conn: sqlite3.Connection = Depends(get_db)) -> meals.Meal: 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'}) return JSONResponse(status_code=400, content={'message': 'Meal must have at least one chef'})
if not meal.cleanup: if not meal.cleanup:

View file

@ -1,16 +1,19 @@
from typing import List, ClassVar from typing import List, ClassVar, Optional
from pydantic import BaseModel from pydantic import BaseModel
from persons import Person
from ingredients import Ingredient, insert_ingredient, find_ingredients_by_meal_id 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 import datetime
class Meal(BaseModel): class Meal(BaseModel):
KEYS: ClassVar[List[str]] = ['id', 'date'] KEYS: ClassVar[List[str]] = ['id', 'meal_date']
id: int id: int
date: datetime.datetime meal_date: datetime.datetime
chefs: List[Person] = [] chefs: List[Person] = []
cleanup: List[Person] = [] cleanup: List[Person] = []
consumers: List[Person] = [] consumers: List[Person] = []
@ -21,7 +24,7 @@ async def create(conn):
await conn.execute(''' await conn.execute('''
CREATE TABLE IF NOT EXISTS Meal ( CREATE TABLE IF NOT EXISTS Meal (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
date TEXT UNIQUE meal_date TEXT
);''') );''')
await conn.execute(''' 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)) ''', (meal_id, person_id, role))
async def insert_meal_recipe(conn, meal_id: int, recipe_id: int): 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(''' await conn.execute('''
INSERT INTO MealRecipe (meal_id, recipe_id) INSERT INTO MealRecipe (meal_id, recipe_id)
VALUES (?, ?) 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 def insert_meal(conn, meal: Meal):
async with conn.execute(''' async with conn.execute('''
INSERT INTO Meal (date) INSERT INTO Meal (meal_date)
VALUES (?) VALUES (?)
''', (meal.date,)) as cursor: ''', (meal.meal_date,)) as cursor:
meal.id = cursor.lastrowid meal.id = cursor.lastrowid
for person in meal.chefs: for person in meal.chefs:
@ -110,7 +116,7 @@ async def load_participants(conn, meal: Meal) -> None:
WHERE meal_id = ? WHERE meal_id = ?
''', (meal.id,)) as cursor: ''', (meal.id,)) as cursor:
async for row in 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': if row[1] == 'chef':
meal.chefs.append(person) meal.chefs.append(person)
elif row[1] == 'cleanup': elif row[1] == 'cleanup':
@ -127,7 +133,10 @@ async def load_recipes(conn, meal: Meal) -> None:
WHERE MealRecipe.meal_id = ? WHERE MealRecipe.meal_id = ?
''', (meal.id,)) as cursor: ''', (meal.id,)) as cursor:
async for row in 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 def load_extra_ingredients(conn, meal: Meal) -> None:
async for ingredient in find_ingredients_by_meal_id(conn, meal.id): async for ingredient in find_ingredients_by_meal_id(conn, meal.id):

View file

@ -12,10 +12,10 @@ async def create(conn):
name TEXT UNIQUE name TEXT UNIQUE
);''') );''')
await insert(conn, Person(id=0, name='Jacob')) await insert(conn, Person(id=1, name='Jacob'))
await insert(conn, Person(id=0, name='Ellie')) await insert(conn, Person(id=2, name='Ryan'))
await insert(conn, Person(id=0, name='Ryan')) await insert(conn, Person(id=3, name='Ellie'))
await insert(conn, Person(id=0, name='Chris')) await insert(conn, Person(id=4, name='Chris'))
async def get_by_name(conn, name: str) -> Person: async def get_by_name(conn, name: str) -> Person:
cursor = await conn.execute(''' cursor = await conn.execute('''

View file

@ -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 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 from ingredients import parse_ingredient_from_nlp as _parse_ingredient_from_nlp, match_existing_products as _match_existing_products

View file

@ -79,6 +79,6 @@ async def get_all(conn) -> List[Recipe]:
async for row in cursor: async for row in cursor:
yield row_to_recipe(zip(Recipe.KEYS, row)) 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): async for ingredient in find_ingredients_by_recipe_id(conn, recipe.id):
recipe.ingredients.append(ingredient) recipe.ingredients.append(ingredient)

94
test_data.py Normal file
View file

@ -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],
)

148
tests.py Normal file
View file

@ -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()