feat(backend): 🗃️ Add CRUD opertaions for Food and Units
This commit is contained in:
parent
c894d3d880
commit
122d35ec09
9 changed files with 255 additions and 99 deletions
0
mealie/db/data_initialization/__init__.py
Normal file
0
mealie/db/data_initialization/__init__.py
Normal file
34
mealie/db/data_initialization/init_units_foods.py
Normal file
34
mealie/db/data_initialization/init_units_foods.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from mealie.schema.recipe.recipe import IngredientUnit
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from ..data_access_layer import DatabaseAccessLayer
|
||||
|
||||
|
||||
def get_default_units():
|
||||
return [
|
||||
# Volume
|
||||
IngredientUnit(name="teaspoon", abbreviation="tsp"),
|
||||
IngredientUnit(name="tablespoon", abbreviation="tbsp"),
|
||||
IngredientUnit(name="fluid ounce", abbreviation="fl oz"),
|
||||
IngredientUnit(name="cup", abbreviation="cup"),
|
||||
IngredientUnit(name="pint", abbreviation="pt"),
|
||||
IngredientUnit(name="quart", abbreviation="qt"),
|
||||
IngredientUnit(name="gallon", abbreviation="gal"),
|
||||
IngredientUnit(name="milliliter", abbreviation="ml"),
|
||||
IngredientUnit(name="liter", abbreviation="l"),
|
||||
# Mass Weight
|
||||
IngredientUnit(name="pound", abbreviation="lb"),
|
||||
IngredientUnit(name="ounce", abbreviation="oz"),
|
||||
IngredientUnit(name="gram", abbreviation="g"),
|
||||
IngredientUnit(name="kilogram", abbreviation="kg"),
|
||||
IngredientUnit(name="milligram", abbreviation="mg"),
|
||||
]
|
||||
|
||||
|
||||
def default_recipe_unit_init(db: DatabaseAccessLayer, session: Session) -> None:
|
||||
for unit in get_default_units():
|
||||
try:
|
||||
db.ingredient_units.create(session, unit)
|
||||
print("Ingredient Unit Committed")
|
||||
except Exception as e:
|
||||
print(e)
|
|
@ -1,9 +1,9 @@
|
|||
from mealie.db.models.event import *
|
||||
from mealie.db.models.group import *
|
||||
from mealie.db.models.mealplan import *
|
||||
from mealie.db.models.recipe.recipe import *
|
||||
from mealie.db.models.settings import *
|
||||
from mealie.db.models.shopping_list import *
|
||||
from mealie.db.models.sign_up import *
|
||||
from mealie.db.models.theme import *
|
||||
from mealie.db.models.users import *
|
||||
from .event import *
|
||||
from .group import *
|
||||
from .mealplan import *
|
||||
from .recipe.recipe import *
|
||||
from .settings import *
|
||||
from .shopping_list import *
|
||||
from .sign_up import *
|
||||
from .theme import *
|
||||
from .users import *
|
||||
|
|
112
mealie/db/models/_model_utils.py
Normal file
112
mealie/db/models/_model_utils.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
from functools import wraps
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy.orm import MANYTOMANY, MANYTOONE, ONETOMANY
|
||||
|
||||
|
||||
def handle_one_to_many_list(relation_cls, all_elements: list[dict]):
|
||||
elems_to_create = []
|
||||
updated_elems = []
|
||||
|
||||
for elem in all_elements:
|
||||
elem_id = elem.get("id", None)
|
||||
|
||||
existing_elem = relation_cls.get_ref(match_value=elem_id)
|
||||
|
||||
if existing_elem is None:
|
||||
|
||||
elems_to_create.append(elem)
|
||||
|
||||
else:
|
||||
for key, value in elem.items():
|
||||
setattr(existing_elem, key, value)
|
||||
|
||||
updated_elems.append(existing_elem)
|
||||
|
||||
new_elems = []
|
||||
for elem in elems_to_create:
|
||||
new_elems = [relation_cls(**elem) for elem in all_elements]
|
||||
|
||||
return new_elems
|
||||
|
||||
|
||||
def auto_init(exclude: Union[set, list] = None): # sourcery no-metrics
|
||||
"""Wraps the `__init__` method of a class to automatically set the common
|
||||
attributes.
|
||||
|
||||
Args:
|
||||
exclude (Union[set, list], optional): [description]. Defaults to None.
|
||||
"""
|
||||
|
||||
exclude = exclude or set()
|
||||
exclude.add("id")
|
||||
|
||||
def decorator(init):
|
||||
@wraps(init)
|
||||
def wrapper(self, *args, **kwargs): # sourcery no-metrics
|
||||
"""
|
||||
Custom initializer that allows nested children initialization.
|
||||
Only keys that are present as instance's class attributes are allowed.
|
||||
These could be, for example, any mapped columns or relationships.
|
||||
|
||||
Code inspired from GitHub.
|
||||
Ref: https://github.com/tiangolo/fastapi/issues/2194
|
||||
"""
|
||||
cls = self.__class__
|
||||
model_columns = self.__mapper__.columns
|
||||
relationships = self.__mapper__.relationships
|
||||
|
||||
for key, val in kwargs.items():
|
||||
if key in exclude:
|
||||
continue
|
||||
|
||||
if not hasattr(cls, key):
|
||||
continue
|
||||
# raise TypeError(f"Invalid keyword argument: {key}")
|
||||
|
||||
if key in model_columns:
|
||||
setattr(self, key, val)
|
||||
continue
|
||||
|
||||
if key in relationships:
|
||||
relation_dir = relationships[key].direction.name
|
||||
relation_cls = relationships[key].mapper.entity
|
||||
use_list = relationships[key].uselist
|
||||
|
||||
if relation_dir == ONETOMANY.name and use_list:
|
||||
instances = handle_one_to_many_list(relation_cls, val)
|
||||
setattr(self, key, instances)
|
||||
|
||||
if relation_dir == ONETOMANY.name and not use_list:
|
||||
instance = relation_cls(**val)
|
||||
setattr(self, key, instance)
|
||||
|
||||
elif relation_dir == MANYTOONE.name and not use_list:
|
||||
if isinstance(val, dict):
|
||||
val = val.get("id")
|
||||
|
||||
if val is None:
|
||||
raise ValueError(
|
||||
f"Expected 'id' to be provided for {key}"
|
||||
)
|
||||
|
||||
if isinstance(val, (str, int)):
|
||||
instance = relation_cls.get_ref(match_value=val)
|
||||
setattr(self, key, instance)
|
||||
|
||||
elif relation_dir == MANYTOMANY.name:
|
||||
if not isinstance(val, list):
|
||||
raise ValueError(
|
||||
f"Expected many to many input to be of type list for {key}"
|
||||
)
|
||||
|
||||
if isinstance(val[0], dict):
|
||||
val = [elem.get("id") for elem in val]
|
||||
intstances = [relation_cls.get_ref(elem) for elem in val]
|
||||
setattr(self, key, intstances)
|
||||
|
||||
return init(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
|
@ -2,6 +2,8 @@ from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
|||
from requests import Session
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String, Table, orm
|
||||
|
||||
from .._model_utils import auto_init
|
||||
|
||||
ingredients_to_units = Table(
|
||||
"ingredients_to_units",
|
||||
SqlAlchemyBase.metadata,
|
||||
|
@ -17,54 +19,29 @@ ingredients_to_foods = Table(
|
|||
)
|
||||
|
||||
|
||||
class IngredientUnit(SqlAlchemyBase, BaseMixins):
|
||||
class IngredientUnitModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "ingredient_units"
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String)
|
||||
description = Column(String)
|
||||
abbreviation = Column(String)
|
||||
ingredients = orm.relationship("RecipeIngredient", secondary=ingredients_to_units, back_populates="unit")
|
||||
|
||||
def __init__(self, name: str, description: str = None) -> None:
|
||||
self.name = name
|
||||
self.description = description
|
||||
|
||||
@classmethod
|
||||
def get_ref_or_create(cls, session: Session, obj: dict):
|
||||
# sourcery skip: flip-comparison
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
name = obj.get("name")
|
||||
|
||||
unit = session.query(cls).filter("name" == name).one_or_none()
|
||||
|
||||
if not unit:
|
||||
return cls(**obj)
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class IngredientFood(SqlAlchemyBase, BaseMixins):
|
||||
class IngredientFoodModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "ingredient_foods"
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String)
|
||||
description = Column(String)
|
||||
ingredients = orm.relationship("RecipeIngredient", secondary=ingredients_to_foods, back_populates="food")
|
||||
|
||||
def __init__(self, name: str, description: str = None) -> None:
|
||||
self.name = name
|
||||
self.description = description
|
||||
|
||||
@classmethod
|
||||
def get_ref_or_create(cls, session: Session, obj: dict):
|
||||
# sourcery skip: flip-comparison
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
name = obj.get("name")
|
||||
|
||||
unit = session.query(cls).filter("name" == name).one_or_none()
|
||||
|
||||
if not unit:
|
||||
return cls(**obj)
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class RecipeIngredient(SqlAlchemyBase):
|
||||
|
@ -77,8 +54,8 @@ class RecipeIngredient(SqlAlchemyBase):
|
|||
note = Column(String) # Force Show Text - Overrides Concat
|
||||
|
||||
# Scaling Items
|
||||
unit = orm.relationship(IngredientUnit, secondary=ingredients_to_units, uselist=False)
|
||||
food = orm.relationship(IngredientFood, secondary=ingredients_to_foods, uselist=False)
|
||||
unit = orm.relationship(IngredientUnitModel, secondary=ingredients_to_units, uselist=False)
|
||||
food = orm.relationship(IngredientFoodModel, secondary=ingredients_to_foods, uselist=False)
|
||||
quantity = Column(Integer)
|
||||
|
||||
# Extras
|
||||
|
@ -86,6 +63,6 @@ class RecipeIngredient(SqlAlchemyBase):
|
|||
def __init__(self, title: str, note: str, unit: dict, food: dict, quantity: int, session: Session, **_) -> None:
|
||||
self.title = title
|
||||
self.note = note
|
||||
self.unit = IngredientUnit.get_ref_or_create(session, unit)
|
||||
self.food = IngredientFood.get_ref_or_create(session, food)
|
||||
self.unit = IngredientUnitModel.get_ref_or_create(session, unit)
|
||||
self.food = IngredientFoodModel.get_ref_or_create(session, food)
|
||||
self.quantity = quantity
|
||||
|
|
|
@ -1,33 +1,43 @@
|
|||
from mealie.core.root_logger import get_logger
|
||||
from fastapi import Depends, status
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import Session, generate_session
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.recipe.units_and_foods import CreateIngredientFood, IngredientFood
|
||||
|
||||
router = UserAPIRouter()
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_food():
|
||||
""" Create food in the Database """
|
||||
# Create food
|
||||
pass
|
||||
@router.get("", response_model=list[IngredientFood])
|
||||
async def get_all(
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Get unit from the Database """
|
||||
# Get unit
|
||||
return db.ingredient_foods.get_all(session)
|
||||
|
||||
|
||||
@router.post("", response_model=IngredientFood, status_code=status.HTTP_201_CREATED)
|
||||
async def create_unit(unit: CreateIngredientFood, session: Session = Depends(generate_session)):
|
||||
""" Create unit in the Database """
|
||||
|
||||
return db.ingredient_foods.create(session, unit)
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
async def get_food():
|
||||
""" Get food from the Database """
|
||||
# Get food
|
||||
pass
|
||||
async def get_unit(id: str, session: Session = Depends(generate_session)):
|
||||
""" Get unit from the Database """
|
||||
|
||||
return db.ingredient_foods.get(session, id)
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
async def update_food():
|
||||
""" Update food in the Database """
|
||||
# Update food
|
||||
pass
|
||||
async def update_unit(id: str, unit: CreateIngredientFood, session: Session = Depends(generate_session)):
|
||||
""" Update unit in the Database """
|
||||
|
||||
return db.ingredient_foods.update(session, id, unit)
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_food():
|
||||
""" Delete food from the Database """
|
||||
# Delete food
|
||||
pass
|
||||
async def delete_unit(id: str, session: Session = Depends(generate_session)):
|
||||
""" Delete unit from the Database """
|
||||
return db.ingredient_foods.delete(session, id)
|
||||
|
|
|
@ -1,33 +1,43 @@
|
|||
from mealie.core.root_logger import get_logger
|
||||
from fastapi import Depends, status
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import Session, generate_session
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.schema.recipe.units_and_foods import CreateIngredientUnit, IngredientUnit
|
||||
|
||||
router = UserAPIRouter()
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_food():
|
||||
""" Create food in the Database """
|
||||
# Create food
|
||||
pass
|
||||
@router.get("", response_model=list[IngredientUnit])
|
||||
async def get_all(
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Get unit from the Database """
|
||||
# Get unit
|
||||
return db.ingredient_units.get_all(session)
|
||||
|
||||
|
||||
@router.post("", response_model=IngredientUnit, status_code=status.HTTP_201_CREATED)
|
||||
async def create_unit(unit: CreateIngredientUnit, session: Session = Depends(generate_session)):
|
||||
""" Create unit in the Database """
|
||||
|
||||
return db.ingredient_units.create(session, unit)
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
async def get_food():
|
||||
""" Get food from the Database """
|
||||
# Get food
|
||||
pass
|
||||
async def get_unit(id: str, session: Session = Depends(generate_session)):
|
||||
""" Get unit from the Database """
|
||||
|
||||
return db.ingredient_units.get(session, id)
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
async def update_food():
|
||||
""" Update food in the Database """
|
||||
# Update food
|
||||
pass
|
||||
async def update_unit(id: str, unit: CreateIngredientUnit, session: Session = Depends(generate_session)):
|
||||
""" Update unit in the Database """
|
||||
|
||||
return db.ingredient_units.update(session, id, unit)
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_food():
|
||||
""" Delete food from the Database """
|
||||
# Delete food
|
||||
pass
|
||||
async def delete_unit(id: str, session: Session = Depends(generate_session)):
|
||||
""" Delete unit from the Database """
|
||||
return db.ingredient_units.delete(session, id)
|
||||
|
|
|
@ -11,6 +11,7 @@ from pydantic.utils import GetterDict
|
|||
from slugify import slugify
|
||||
|
||||
from .comments import CommentOut
|
||||
from .units_and_foods import IngredientFood, IngredientUnit
|
||||
|
||||
|
||||
class CreateRecipe(CamelModel):
|
||||
|
@ -73,23 +74,11 @@ class Nutrition(CamelModel):
|
|||
orm_mode = True
|
||||
|
||||
|
||||
class RecipeIngredientFood(CamelModel):
|
||||
name: str = ""
|
||||
description: str = ""
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class RecipeIngredientUnit(RecipeIngredientFood):
|
||||
pass
|
||||
|
||||
|
||||
class RecipeIngredient(CamelModel):
|
||||
title: Optional[str]
|
||||
note: Optional[str]
|
||||
unit: Optional[RecipeIngredientUnit]
|
||||
food: Optional[RecipeIngredientFood]
|
||||
unit: Optional[IngredientUnit]
|
||||
food: Optional[IngredientFood]
|
||||
disable_amount: bool = True
|
||||
quantity: int = 1
|
||||
|
||||
|
|
24
mealie/schema/recipe/units_and_foods.py
Normal file
24
mealie/schema/recipe/units_and_foods.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from fastapi_camelcase import CamelModel
|
||||
|
||||
|
||||
class CreateIngredientFood(CamelModel):
|
||||
name: str
|
||||
description: str = ""
|
||||
|
||||
|
||||
class CreateIngredientUnit(CreateIngredientFood):
|
||||
abbreviation: str = ""
|
||||
|
||||
|
||||
class IngredientFood(CreateIngredientFood):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class IngredientUnit(CreateIngredientUnit):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
Loading…
Reference in a new issue