Unit management
This commit is contained in:
parent
75ecb5d0f7
commit
df7a41e028
5 changed files with 101 additions and 14 deletions
26
main.py
26
main.py
|
|
@ -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[
|
||||||
|
|
@ -45,4 +49,22 @@ 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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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,19 +31,24 @@ 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):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _dump_json_data_to_log(data: dict) -> str:
|
def _dump_json_data_to_log(data: dict) -> str:
|
||||||
import os, re
|
import os, re
|
||||||
dir = './dump'
|
dir = './dump'
|
||||||
|
|
|
||||||
46
units.py
Normal file
46
units.py
Normal 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
|
||||||
Loading…
Reference in a new issue