feat(backend): 🗃️ Add CRUD opertaions for Food and Units

This commit is contained in:
hay-kot 2021-08-22 13:10:18 -08:00
parent c894d3d880
commit 122d35ec09
9 changed files with 255 additions and 99 deletions

View 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)

View file

@ -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 *

View 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

View file

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

View file

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

View file

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

View file

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

View 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