Unit management

This commit is contained in:
jableader 2024-01-13 16:40:10 +11:00
parent 75ecb5d0f7
commit df7a41e028
5 changed files with 101 additions and 14 deletions

24
main.py
View file

@ -4,6 +4,7 @@ import product, recipe, db
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Annotated from typing import List, Annotated
from fastapi import FastAPI, Depends, Query from fastapi import FastAPI, Depends, Query
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
app = FastAPI() app = FastAPI()
@ -27,7 +28,10 @@ async def get_db():
@app.get("/recipes/parse") @app.get("/recipes/parse")
async def parse_recipe_handler(url: str, conn: sqlite3.Connection = Depends(get_db)) -> recipe.Recipe: async def parse_recipe_handler(url: str, conn: sqlite3.Connection = Depends(get_db)) -> recipe.Recipe:
return await recipe.parse_recipe(conn, url) parsed = await recipe.parse_recipe(conn, url)
if not parsed:
return JSONResponse(status_code=400, content={'message': 'Recipe not found'})
return parsed
@app.get("/recipes/ingredients/parse") @app.get("/recipes/ingredients/parse")
async def parse_ingredients(lines: Annotated[ async def parse_ingredients(lines: Annotated[
@ -46,3 +50,21 @@ class ProductUrl(BaseModel):
@app.post("/products/") @app.post("/products/")
async def create_product(url: ProductUrl, conn: sqlite3.Connection = Depends(get_db)) -> product.Product: async def create_product(url: ProductUrl, conn: sqlite3.Connection = Depends(get_db)) -> product.Product:
return await product.get_or_create(conn, url.url, url.tags) return await product.get_or_create(conn, url.url, url.tags)
@app.post('/recipes/')
async def create_recipe(item: recipe.Recipe, conn: sqlite3.Connection = Depends(get_db)) -> recipe.Recipe:
if not item.ingredients:
return JSONResponse(status_code=400, content={'message': 'Recipe must have at least one ingredient'})
for ingredient in item.ingredients:
if not ingredient.product:
return JSONResponse(status_code=400, content={'message': 'Ingredient must have a product'})
await recipe.insert_recipe(conn, item)
for ingredient in item.ingredients:
ingredient.recipe_id = item.id
ingredient.product_id = ingredient.product.id
await recipe.insert_ingredient(conn, ingredient)
await conn.commit()
return item

View file

@ -1,12 +1,12 @@
from product import Product from product import Product
from recipe.db import Recipe, Ingredient from recipe.db import Recipe, Ingredient, insert_recipe, insert_ingredient
from recipe.scraping import scrape_recipe from recipe.scraping import scrape_recipe
from ingredient_parser import parse_multiple_ingredients from ingredient_parser import parse_multiple_ingredients
from typing import List from typing import List
from product import find_product_by_tag from product import find_product_by_tag
import json import json, units
async def find_existing_product(conn, ingredient: str) -> Product: async def find_existing_product(conn, ingredient: str) -> Product:
async for item in find_product_by_tag(conn, ingredient): async for item in find_product_by_tag(conn, ingredient):
@ -18,10 +18,13 @@ def parse_ingredient_from_nlp(ingredients: List[str]) -> Ingredient:
for ingredient in parse_multiple_ingredients(ingredients): for ingredient in parse_multiple_ingredients(ingredients):
name = ingredient.name.text if ingredient.name else '' name = ingredient.name.text if ingredient.name else ''
quantity, unit = None, None quantity, unit = 0, units.ITEMS.name
if ingredient.amount: if ingredient.amount:
amount = ingredient.amount[0] amount = ingredient.amount[0]
quantity, unit = amount.quantity, amount.unit
real_unit = units.get_unit(unit)
unit = real_unit.name if real_unit else units.ITEMS.name
quantity = amount.quantity if amount.quantity else 0
results.append(Ingredient(id=0, results.append(Ingredient(id=0,
line=ingredient.sentence, line=ingredient.sentence,
@ -54,7 +57,7 @@ async def _get_recipe_from_ldata(conn, url: str, ldata: dict) -> dict:
) )
async def parse_recipe(conn, url: str) -> dict: async def parse_recipe(conn, url: str) -> dict:
ldata = await scrape_recipe(conn, url) ldata = await scrape_recipe(url)
if ldata: if ldata:
return await _get_recipe_from_ldata(conn, url, ldata) return await _get_recipe_from_ldata(conn, url, ldata)
return None return None

View file

@ -3,17 +3,18 @@ import aiosqlite
from product import Product from product import Product
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, ClassVar from typing import List, ClassVar, Optional
class Ingredient(BaseModel): class Ingredient(BaseModel):
KEYS: ClassVar[List[str]] = ['id', 'name', 'line', 'preparation', 'unit', 'quantity', 'product_id'] KEYS: ClassVar[List[str]] = ['id', 'name', 'line', 'preparation', 'unit', 'quantity', 'product_id', 'recipe_id']
id: int id: int
name: str name: str
line: str line: str
unit: str unit: str
quantity: float quantity: float
preparation: str preparation: str
product_id: int product_id: Optional[int] = None
recipe_id: Optional[int] = None
product: Product = None product: Product = None
class Recipe(BaseModel): class Recipe(BaseModel):

View file

@ -2,8 +2,18 @@ from bs4 import BeautifulSoup
import httpx import httpx
import json import json
def _is_recipe_ldata(ldata_node):
if '@type' in ldata_node:
typ = ldata_node['@type']
if isinstance(typ, list):
typ = typ[0]
async def scrape_recipe(conn, url: str) -> dict: if isinstance(typ, str) and typ.lower() == 'recipe':
return True
return None
async def scrape_recipe(url: str) -> dict:
headers = { headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Accept-Language": "en-US,en;q=0.9", "Accept-Language": "en-US,en;q=0.9",
@ -21,12 +31,17 @@ async def scrape_recipe(conn, url: str) -> dict:
try: try:
data = json.loads(ld.text) data = json.loads(ld.text)
#_dump_json_data_to_log(data) #_dump_json_data_to_log(data)
if '@type' in data and data['@type'].lower() == 'recipe': if _is_recipe_ldata(data):
return data return data
if '@graph' in data: if '@graph' in data:
for item in data['@graph']: for item in data['@graph']:
if '@type' in item and item['@type'].lower() == 'recipe': if _is_recipe_ldata(item):
return item
if isinstance(data, list):
for item in data:
if _is_recipe_ldata(item):
return item return item
except (json.decoder.JSONDecodeError, KeyError): except (json.decoder.JSONDecodeError, KeyError):

46
units.py Normal file
View file

@ -0,0 +1,46 @@
from typing import Union
class Unit:
def __init__(self, name: str, symbols: list, unit_type: str, conversion_to_base: float = 1.0):
self.name = name
self.symbols = symbols
self.unit_type = unit_type
self.conversion_to_base = conversion_to_base
def convert_to_base(self, quantity: float) -> float:
"""Converts a quantity to the base unit."""
return quantity * self.conversion_to_base
def convert_from_base(self, quantity: float) -> float:
"""Converts a quantity from the base unit to this unit."""
return quantity / self.conversion_to_base
# Define common base units in SI units
ITEMS = Unit("Items", ["item", "items"], "count", 1)
LITRE = Unit("Litre", ["litre", "liter", "l"], "volume", 1)
GRAM = Unit("Gram", ["gram", "g"], "weight", 1)
# Add conversion factors for common cooking measurements
CUP = Unit("Cup", ["cup", "c"], "volume", 240)
TABLESPOON = Unit("Tablespoon", ["tablespoon", "tablespoons", "tbsp"], "volume", 15)
TEASPOON = Unit("Teaspoon", ["teaspoon", "teaspoons", "tsp"], "volume", 5)
OUNCE = Unit("Ounce", ["ounce", "ounces", "oz"], "weight", 28.3495)
POUND = Unit("Pound", ["pound", "pounds", "lb"], "weight", 453.592)
FLUID_OUNCE = Unit("Fluid Ounce", ["fluid ounce", "fl oz"], "volume", 29.5735)
PINT = Unit("Pint", ["pint", "pt"], "volume", 473.176)
QUART = Unit("Quart", ["quart", "qt"], "volume", 946.353)
GALLON = Unit("Gallon", ["gallon", "gal"], "volume", 3785.41)
MILLILITRE = Unit("Millilitre", ["millilitre", "millilitres", "ml"], "volume", 1)
MILLIGRAM = Unit("Milligram", ["milligram", "milligrams", "mg"], "weight", 1)
KILOGRAM = Unit("Kilogram", ["kilogram", "kilograms", "kg"], "weight", 1000)
# Big list of units
ALL_UNITS = [ITEMS, LITRE, GRAM, CUP, TABLESPOON, TEASPOON, OUNCE, POUND, FLUID_OUNCE, PINT, QUART, GALLON, MILLILITRE, MILLIGRAM, KILOGRAM]
def get_unit(alias: str) -> Union[Unit, None]:
"""Returns the corresponding unit based on alias or abbreviation."""
alias_lower = alias.lower()
for unit in ALL_UNITS:
if alias_lower in unit.symbols or alias_lower == unit.name.lower():
return unit
return None