mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-11-03 19:18:22 -05:00 
			
		
		
		
	feat(backend): ✨ add initial cookbook support
This commit is contained in:
		
							parent
							
								
									83ab858e46
								
							
						
					
					
						commit
						d24e95c091
					
				@ -24,7 +24,7 @@ class ReadDeps:
 | 
			
		||||
        user=Depends(is_logged_in),
 | 
			
		||||
    ):
 | 
			
		||||
        self.session: Session = session
 | 
			
		||||
        self.bg_tasks: BackgroundTasks = background_tasks
 | 
			
		||||
        self.bg_task: BackgroundTasks = background_tasks
 | 
			
		||||
        self.user: bool = user
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,5 @@
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
from typing import Callable, Generic, TypeVar, Union
 | 
			
		||||
 | 
			
		||||
from sqlalchemy import func
 | 
			
		||||
@ -22,11 +24,8 @@ class BaseAccessModel(Generic[T, D]):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, primary_key: Union[str, int], sql_model: D, schema: T) -> None:
 | 
			
		||||
        self.primary_key = primary_key
 | 
			
		||||
 | 
			
		||||
        self.sql_model = sql_model
 | 
			
		||||
 | 
			
		||||
        self.schema = schema
 | 
			
		||||
 | 
			
		||||
        self.observers: list = []
 | 
			
		||||
 | 
			
		||||
    def subscribe(self, func: Callable) -> None:
 | 
			
		||||
@ -82,40 +81,52 @@ class BaseAccessModel(Generic[T, D]):
 | 
			
		||||
        return [x.get(self.primary_key) for x in results_as_dict]
 | 
			
		||||
 | 
			
		||||
    def _query_one(self, session: Session, match_value: str, match_key: str = None) -> D:
 | 
			
		||||
        """Query the sql database for one item an return the sql alchemy model
 | 
			
		||||
        """
 | 
			
		||||
        Query the sql database for one item an return the sql alchemy model
 | 
			
		||||
        object. If no match key is provided the primary_key attribute will be used.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            session (Session): Database Session object
 | 
			
		||||
            match_value (str): The value to use in the query
 | 
			
		||||
            match_key (str, optional): the key/property to match against. Defaults to None.
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            Union[Session, SqlAlchemyBase]: Will return both the session and found model
 | 
			
		||||
        """
 | 
			
		||||
        if match_key is None:
 | 
			
		||||
            match_key = self.primary_key
 | 
			
		||||
 | 
			
		||||
        return session.query(self.sql_model).filter_by(**{match_key: match_value}).one()
 | 
			
		||||
 | 
			
		||||
    def get_one(self, session: Session, value: str | int, key: str = None, any_case=False, override_schema=None) -> T:
 | 
			
		||||
        key = key or self.primary_key
 | 
			
		||||
 | 
			
		||||
        if any_case:
 | 
			
		||||
            search_attr = getattr(self.sql_model, key)
 | 
			
		||||
            result = session.query(self.sql_model).filter(func.lower(search_attr) == key.lower()).one_or_none()
 | 
			
		||||
 | 
			
		||||
        result = session.query(self.sql_model).filter_by(**{key: value}).one_or_none()
 | 
			
		||||
 | 
			
		||||
        if not result:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        eff_schema = override_schema or self.schema
 | 
			
		||||
        return eff_schema.from_orm(result)
 | 
			
		||||
 | 
			
		||||
    def get_many(
 | 
			
		||||
        self, session: Session, value: str, key: str = None, limit=1, any_case=False, override_schema=None
 | 
			
		||||
    ) -> list[T]:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def get(
 | 
			
		||||
        self, session: Session, match_value: str, match_key: str = None, limit=1, any_case=False, override_schema=None
 | 
			
		||||
    ) -> Union[T, list[T]]:
 | 
			
		||||
    ) -> T | list[T]:
 | 
			
		||||
        """Retrieves an entry from the database by matching a key/value pair. If no
 | 
			
		||||
        key is provided the class objects primary key will be used to match against.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            match_value (str): A value used to match against the key/value in the database \n
 | 
			
		||||
            match_key (str, optional): They key to match the value against. Defaults to None. \n
 | 
			
		||||
            limit (int, optional): A limit to returned responses. Defaults to 1. \n
 | 
			
		||||
            match_value (str): A value used to match against the key/value in the database
 | 
			
		||||
            match_key (str, optional): They key to match the value against. Defaults to None.
 | 
			
		||||
            limit (int, optional): A limit to returned responses. Defaults to 1.
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            dict or list[dict]:
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        if match_key is None:
 | 
			
		||||
            match_key = self.primary_key
 | 
			
		||||
        match_key = match_key or self.primary_key
 | 
			
		||||
 | 
			
		||||
        if any_case:
 | 
			
		||||
            search_attr = getattr(self.sql_model, match_key)
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ from logging import getLogger
 | 
			
		||||
from sqlalchemy.orm.session import Session
 | 
			
		||||
 | 
			
		||||
from mealie.db.data_access_layer.group_access_model import GroupDataAccessModel
 | 
			
		||||
from mealie.db.models.cookbook import CookBook
 | 
			
		||||
from mealie.db.models.event import Event, EventNotification
 | 
			
		||||
from mealie.db.models.group import Group
 | 
			
		||||
from mealie.db.models.mealplan import MealPlan
 | 
			
		||||
@ -10,12 +11,12 @@ from mealie.db.models.recipe.category import Category
 | 
			
		||||
from mealie.db.models.recipe.comment import RecipeComment
 | 
			
		||||
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel
 | 
			
		||||
from mealie.db.models.recipe.recipe import RecipeModel, Tag
 | 
			
		||||
from mealie.db.models.settings import CustomPage, SiteSettings
 | 
			
		||||
from mealie.db.models.settings import SiteSettings
 | 
			
		||||
from mealie.db.models.shopping_list import ShoppingList
 | 
			
		||||
from mealie.db.models.sign_up import SignUp
 | 
			
		||||
from mealie.db.models.users import LongLiveToken, User
 | 
			
		||||
from mealie.schema.admin import CustomPageOut
 | 
			
		||||
from mealie.schema.admin import SiteSettings as SiteSettingsSchema
 | 
			
		||||
from mealie.schema.cookbook import ReadCookBook
 | 
			
		||||
from mealie.schema.events import Event as EventSchema
 | 
			
		||||
from mealie.schema.events import EventNotificationIn
 | 
			
		||||
from mealie.schema.meal_plan import MealPlanOut, ShoppingListOut
 | 
			
		||||
@ -73,7 +74,6 @@ class DatabaseAccessLayer:
 | 
			
		||||
        # Site
 | 
			
		||||
        self.settings = BaseAccessModel(DEFAULT_PK, SiteSettings, SiteSettingsSchema)
 | 
			
		||||
        self.sign_ups = BaseAccessModel("token", SignUp, SignUpOut)
 | 
			
		||||
        self.custom_pages = BaseAccessModel(DEFAULT_PK, CustomPage, CustomPageOut)
 | 
			
		||||
        self.event_notifications = BaseAccessModel(DEFAULT_PK, EventNotification, EventNotificationIn)
 | 
			
		||||
        self.events = BaseAccessModel(DEFAULT_PK, Event, EventSchema)
 | 
			
		||||
 | 
			
		||||
@ -83,3 +83,4 @@ class DatabaseAccessLayer:
 | 
			
		||||
        self.groups = GroupDataAccessModel(DEFAULT_PK, Group, GroupInDB)
 | 
			
		||||
        self.meals = BaseAccessModel(DEFAULT_PK, MealPlan, MealPlanOut)
 | 
			
		||||
        self.shopping_lists = BaseAccessModel(DEFAULT_PK, ShoppingList, ShoppingListOut)
 | 
			
		||||
        self.cookbooks = BaseAccessModel(DEFAULT_PK, CookBook, ReadCookBook)
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ from typing import Union
 | 
			
		||||
from sqlalchemy.orm import MANYTOMANY, MANYTOONE, ONETOMANY
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def handle_one_to_many_list(relation_cls, all_elements: list[dict]):
 | 
			
		||||
def handle_one_to_many_list(get_attr, relation_cls, all_elements: list[dict]):
 | 
			
		||||
    elems_to_create = []
 | 
			
		||||
    updated_elems = []
 | 
			
		||||
 | 
			
		||||
@ -75,8 +75,17 @@ def auto_init(exclude: Union[set, list] = None):  # sourcery no-metrics
 | 
			
		||||
                    relation_cls = relationships[key].mapper.entity
 | 
			
		||||
                    use_list = relationships[key].uselist
 | 
			
		||||
 | 
			
		||||
                    try:
 | 
			
		||||
                        get_attr = relation_cls.Config.get_attr
 | 
			
		||||
                        if get_attr is None:
 | 
			
		||||
                            get_attr = "id"
 | 
			
		||||
                    except Exception:
 | 
			
		||||
                        get_attr = "id"
 | 
			
		||||
 | 
			
		||||
                    print(get_attr)
 | 
			
		||||
 | 
			
		||||
                    if relation_dir == ONETOMANY.name and use_list:
 | 
			
		||||
                        instances = handle_one_to_many_list(relation_cls, val)
 | 
			
		||||
                        instances = handle_one_to_many_list(get_attr, relation_cls, val)
 | 
			
		||||
                        setattr(self, key, instances)
 | 
			
		||||
 | 
			
		||||
                    if relation_dir == ONETOMANY.name and not use_list:
 | 
			
		||||
@ -85,7 +94,7 @@ def auto_init(exclude: Union[set, list] = None):  # sourcery no-metrics
 | 
			
		||||
 | 
			
		||||
                    elif relation_dir == MANYTOONE.name and not use_list:
 | 
			
		||||
                        if isinstance(val, dict):
 | 
			
		||||
                            val = val.get("id")
 | 
			
		||||
                            val = val.get(get_attr)
 | 
			
		||||
 | 
			
		||||
                            if val is None:
 | 
			
		||||
                                raise ValueError(f"Expected 'id' to be provided for {key}")
 | 
			
		||||
@ -100,7 +109,7 @@ def auto_init(exclude: Union[set, list] = None):  # sourcery no-metrics
 | 
			
		||||
                            raise ValueError(f"Expected many to many input to be of type list for {key}")
 | 
			
		||||
 | 
			
		||||
                        if len(val) > 0 and isinstance(val[0], dict):
 | 
			
		||||
                            val = [elem.get("id") for elem in val]
 | 
			
		||||
                            val = [elem.get(get_attr) for elem in val]
 | 
			
		||||
 | 
			
		||||
                        instances = [x for x in [relation_cls.get_ref(elem, session=session) for elem in val] if x]
 | 
			
		||||
                        setattr(self, key, instances)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								mealie/db/models/cookbook.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								mealie/db/models/cookbook.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
from sqlalchemy import Column, ForeignKey, Integer, String, orm
 | 
			
		||||
 | 
			
		||||
from ._model_base import BaseMixins, SqlAlchemyBase
 | 
			
		||||
from ._model_utils import auto_init
 | 
			
		||||
from .recipe.category import Category, cookbooks_to_categories
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CookBook(SqlAlchemyBase, BaseMixins):
 | 
			
		||||
    __tablename__ = "cookbooks"
 | 
			
		||||
    id = Column(Integer, primary_key=True)
 | 
			
		||||
    position = Column(Integer, nullable=False)
 | 
			
		||||
    name = Column(String, nullable=False)
 | 
			
		||||
    slug = Column(String, nullable=False)
 | 
			
		||||
    categories = orm.relationship(Category, secondary=cookbooks_to_categories, single_parent=True)
 | 
			
		||||
 | 
			
		||||
    group_id = Column(Integer, ForeignKey("groups.id"))
 | 
			
		||||
    group = orm.relationship("Group", back_populates="cookbooks")
 | 
			
		||||
 | 
			
		||||
    @auto_init()
 | 
			
		||||
    def __init__(self, **_) -> None:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def update(self, *args, **kwarg):
 | 
			
		||||
        self.__init__(*args, **kwarg)
 | 
			
		||||
@ -4,6 +4,7 @@ from sqlalchemy.orm.session import Session
 | 
			
		||||
 | 
			
		||||
from mealie.core.config import settings
 | 
			
		||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
 | 
			
		||||
from mealie.db.models.cookbook import CookBook
 | 
			
		||||
from mealie.db.models.recipe.category import Category, group2categories
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -19,19 +20,9 @@ class Group(SqlAlchemyBase, BaseMixins):
 | 
			
		||||
    id = sa.Column(sa.Integer, primary_key=True)
 | 
			
		||||
    name = sa.Column(sa.String, index=True, nullable=False, unique=True)
 | 
			
		||||
    users = orm.relationship("User", back_populates="group")
 | 
			
		||||
    mealplans = orm.relationship(
 | 
			
		||||
        "MealPlan",
 | 
			
		||||
        back_populates="group",
 | 
			
		||||
        single_parent=True,
 | 
			
		||||
        order_by="MealPlan.start_date",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    shopping_lists = orm.relationship(
 | 
			
		||||
        "ShoppingList",
 | 
			
		||||
        back_populates="group",
 | 
			
		||||
        single_parent=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    mealplans = orm.relationship("MealPlan", back_populates="group", single_parent=True, order_by="MealPlan.start_date")
 | 
			
		||||
    shopping_lists = orm.relationship("ShoppingList", back_populates="group", single_parent=True)
 | 
			
		||||
    cookbooks = orm.relationship(CookBook, back_populates="group", single_parent=True)
 | 
			
		||||
    categories = orm.relationship("Category", secondary=group2categories, single_parent=True)
 | 
			
		||||
 | 
			
		||||
    # Webhook Settings
 | 
			
		||||
 | 
			
		||||
@ -29,10 +29,10 @@ recipes2categories = sa.Table(
 | 
			
		||||
    sa.Column("category_id", sa.Integer, sa.ForeignKey("categories.id")),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
custom_pages2categories = sa.Table(
 | 
			
		||||
    "custom_pages2categories",
 | 
			
		||||
cookbooks_to_categories = sa.Table(
 | 
			
		||||
    "cookbooks_to_categories",
 | 
			
		||||
    SqlAlchemyBase.metadata,
 | 
			
		||||
    sa.Column("custom_page_id", sa.Integer, sa.ForeignKey("custom_pages.id")),
 | 
			
		||||
    sa.Column("cookbook_id", sa.Integer, sa.ForeignKey("cookbooks.id")),
 | 
			
		||||
    sa.Column("category_id", sa.Integer, sa.ForeignKey("categories.id")),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -61,6 +61,7 @@ class Category(SqlAlchemyBase, BaseMixins):
 | 
			
		||||
        if not session or not match_value:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        print(match_value)
 | 
			
		||||
        slug = slugify(match_value)
 | 
			
		||||
 | 
			
		||||
        result = session.query(Category).filter(Category.slug == slug).one_or_none()
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ import sqlalchemy.orm as orm
 | 
			
		||||
from sqlalchemy.orm import Session
 | 
			
		||||
 | 
			
		||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
 | 
			
		||||
from mealie.db.models.recipe.category import Category, custom_pages2categories, site_settings2categories
 | 
			
		||||
from mealie.db.models.recipe.category import Category, site_settings2categories
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SiteSettings(SqlAlchemyBase, BaseMixins):
 | 
			
		||||
@ -33,21 +33,3 @@ class SiteSettings(SqlAlchemyBase, BaseMixins):
 | 
			
		||||
 | 
			
		||||
    def update(self, *args, **kwarg):
 | 
			
		||||
        self.__init__(*args, **kwarg)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CustomPage(SqlAlchemyBase, BaseMixins):
 | 
			
		||||
    __tablename__ = "custom_pages"
 | 
			
		||||
    id = sa.Column(sa.Integer, primary_key=True)
 | 
			
		||||
    position = sa.Column(sa.Integer, nullable=False)
 | 
			
		||||
    name = sa.Column(sa.String, nullable=False)
 | 
			
		||||
    slug = sa.Column(sa.String, nullable=False)
 | 
			
		||||
    categories = orm.relationship("Category", secondary=custom_pages2categories, single_parent=True)
 | 
			
		||||
 | 
			
		||||
    def __init__(self, session=None, name=None, slug=None, position=0, categories=[], **_) -> None:
 | 
			
		||||
        self.name = name
 | 
			
		||||
        self.slug = slug
 | 
			
		||||
        self.position = position
 | 
			
		||||
        self.categories = [Category.get_ref(session=session, slug=cat.get("slug")) for cat in categories]
 | 
			
		||||
 | 
			
		||||
    def update(self, *args, **kwarg):
 | 
			
		||||
        self.__init__(*args, **kwarg)
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,9 @@
 | 
			
		||||
from fastapi import APIRouter
 | 
			
		||||
 | 
			
		||||
from . import crud
 | 
			
		||||
from . import cookbooks, crud
 | 
			
		||||
 | 
			
		||||
router = APIRouter()
 | 
			
		||||
 | 
			
		||||
router.include_router(cookbooks.user_router)
 | 
			
		||||
router.include_router(crud.user_router)
 | 
			
		||||
router.include_router(crud.admin_router)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										49
									
								
								mealie/routes/groups/cookbooks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								mealie/routes/groups/cookbooks.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
from fastapi import Depends
 | 
			
		||||
 | 
			
		||||
from mealie.routes.routers import UserAPIRouter
 | 
			
		||||
from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook
 | 
			
		||||
from mealie.services.cookbook import CookbookService
 | 
			
		||||
 | 
			
		||||
user_router = UserAPIRouter(prefix="/groups/cookbooks", tags=["Groups: Cookbooks"])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_router.get("", response_model=list[ReadCookBook])
 | 
			
		||||
def get_all_cookbook(cb_service: CookbookService = Depends(CookbookService.private)):
 | 
			
		||||
    """ Get cookbook from the Database """
 | 
			
		||||
    # Get Item
 | 
			
		||||
    return cb_service.get_all()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_router.post("", response_model=ReadCookBook)
 | 
			
		||||
def create_cookbook(data: CreateCookBook, cb_service: CookbookService = Depends(CookbookService.private)):
 | 
			
		||||
    """ Create cookbook in the Database """
 | 
			
		||||
    # Create Item
 | 
			
		||||
    return cb_service.create_one(data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_router.put("", response_model=list[ReadCookBook])
 | 
			
		||||
def update_many(data: list[ReadCookBook], cb_service: CookbookService = Depends(CookbookService.private)):
 | 
			
		||||
    """ Create cookbook in the Database """
 | 
			
		||||
    # Create Item
 | 
			
		||||
    return cb_service.update_many(data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_router.get("/{id}", response_model=ReadCookBook)
 | 
			
		||||
def get_cookbook(cb_service: CookbookService = Depends(CookbookService.write_existing)):
 | 
			
		||||
    """ Get cookbook from the Database """
 | 
			
		||||
    # Get Item
 | 
			
		||||
    return cb_service.cookbook
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_router.put("/{id}")
 | 
			
		||||
def update_cookbook(data: CreateCookBook, cb_service: CookbookService = Depends(CookbookService.write_existing)):
 | 
			
		||||
    """ Update cookbook in the Database """
 | 
			
		||||
    # Update Item
 | 
			
		||||
    return cb_service.update_one(data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_router.delete("/{id}")
 | 
			
		||||
def delete_cookbook(cd_service: CookbookService = Depends(CookbookService.write_existing)):
 | 
			
		||||
    """ Delete cookbook from the Database """
 | 
			
		||||
    # Delete Item
 | 
			
		||||
    return cd_service.delete_one()
 | 
			
		||||
@ -31,13 +31,13 @@ def get_recipe(recipe_service: RecipeService = Depends(RecipeService.read_existi
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_router.post("", status_code=201, response_model=str)
 | 
			
		||||
def create_from_name(data: CreateRecipe, recipe_service: RecipeService = Depends(RecipeService.base)) -> str:
 | 
			
		||||
def create_from_name(data: CreateRecipe, recipe_service: RecipeService = Depends(RecipeService.private)) -> str:
 | 
			
		||||
    """ Takes in a JSON string and loads data into the database as a new entry"""
 | 
			
		||||
    return recipe_service.create_recipe(data).slug
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_router.post("/create-url", status_code=201, response_model=str)
 | 
			
		||||
def parse_recipe_url(url: CreateRecipeByURL, recipe_service: RecipeService = Depends(RecipeService.base)):
 | 
			
		||||
def parse_recipe_url(url: CreateRecipeByURL, recipe_service: RecipeService = Depends(RecipeService.private)):
 | 
			
		||||
    """ Takes in a URL and attempts to scrape data and load it into the database """
 | 
			
		||||
 | 
			
		||||
    recipe = create_from_url(url.url)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								mealie/schema/cookbook/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								mealie/schema/cookbook/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
from .cookbook import *
 | 
			
		||||
							
								
								
									
										38
									
								
								mealie/schema/cookbook/cookbook.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								mealie/schema/cookbook/cookbook.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
			
		||||
from fastapi_camelcase import CamelModel
 | 
			
		||||
from pydantic import validator
 | 
			
		||||
from slugify import slugify
 | 
			
		||||
 | 
			
		||||
from ..recipe.recipe_category import CategoryBase
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CreateCookBook(CamelModel):
 | 
			
		||||
    name: str
 | 
			
		||||
    slug: str = None
 | 
			
		||||
    position: int = 1
 | 
			
		||||
    categories: list[CategoryBase] = []
 | 
			
		||||
 | 
			
		||||
    @validator("slug", always=True, pre=True)
 | 
			
		||||
    def validate_slug(slug: str, values):
 | 
			
		||||
        name: str = values["name"]
 | 
			
		||||
        calc_slug: str = slugify(name)
 | 
			
		||||
 | 
			
		||||
        if slug != calc_slug:
 | 
			
		||||
            slug = calc_slug
 | 
			
		||||
 | 
			
		||||
        return slug
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdateCookBook(CreateCookBook):
 | 
			
		||||
    id: int
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SaveCookBook(CreateCookBook):
 | 
			
		||||
    group_id: int
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ReadCookBook(UpdateCookBook):
 | 
			
		||||
    group_id: int
 | 
			
		||||
    categories: list[CategoryBase] = []
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        orm_mode = True
 | 
			
		||||
@ -19,7 +19,6 @@ class CategoryBase(CategoryIn):
 | 
			
		||||
        def getter_dict(_cls, name_orm):
 | 
			
		||||
            return {
 | 
			
		||||
                **GetterDict(name_orm),
 | 
			
		||||
                "total_recipes": len(name_orm.recipes),
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +0,0 @@
 | 
			
		||||
from .category import *
 | 
			
		||||
from .comments import *
 | 
			
		||||
from .helpers import *
 | 
			
		||||
from .recipe import *
 | 
			
		||||
@ -1,48 +0,0 @@
 | 
			
		||||
from typing import List, Optional
 | 
			
		||||
 | 
			
		||||
from fastapi_camelcase import CamelModel
 | 
			
		||||
from pydantic.utils import GetterDict
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CategoryIn(CamelModel):
 | 
			
		||||
    name: str
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CategoryBase(CategoryIn):
 | 
			
		||||
    id: int
 | 
			
		||||
    slug: str
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        orm_mode = True
 | 
			
		||||
 | 
			
		||||
        @classmethod
 | 
			
		||||
        def getter_dict(_cls, name_orm):
 | 
			
		||||
            return {
 | 
			
		||||
                **GetterDict(name_orm),
 | 
			
		||||
                "total_recipes": len(name_orm.recipes),
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecipeCategoryResponse(CategoryBase):
 | 
			
		||||
    recipes: Optional[List["Recipe"]]
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        schema_extra = {"example": {"id": 1, "name": "dinner", "recipes": [{}]}}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TagIn(CategoryIn):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TagBase(CategoryBase):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecipeTagResponse(RecipeCategoryResponse):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from .recipe import Recipe
 | 
			
		||||
 | 
			
		||||
RecipeCategoryResponse.update_forward_refs()
 | 
			
		||||
RecipeTagResponse.update_forward_refs()
 | 
			
		||||
@ -1,44 +0,0 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from fastapi_camelcase import CamelModel
 | 
			
		||||
from pydantic.utils import GetterDict
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserBase(CamelModel):
 | 
			
		||||
    id: int
 | 
			
		||||
    username: Optional[str]
 | 
			
		||||
    admin: bool
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        orm_mode = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CommentIn(CamelModel):
 | 
			
		||||
    text: str
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CommentSaveToDB(CommentIn):
 | 
			
		||||
    recipe_slug: str
 | 
			
		||||
    user: int
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        orm_mode = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CommentOut(CommentIn):
 | 
			
		||||
    id: int
 | 
			
		||||
    uuid: str
 | 
			
		||||
    recipe_slug: str
 | 
			
		||||
    date_added: datetime
 | 
			
		||||
    user: UserBase
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        orm_mode = True
 | 
			
		||||
 | 
			
		||||
        @classmethod
 | 
			
		||||
        def getter_dict(_cls, name_orm):
 | 
			
		||||
            return {
 | 
			
		||||
                **GetterDict(name_orm),
 | 
			
		||||
                "recipe_slug": name_orm.recipe.slug,
 | 
			
		||||
            }
 | 
			
		||||
@ -1,5 +0,0 @@
 | 
			
		||||
from fastapi_camelcase import CamelModel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecipeSlug(CamelModel):
 | 
			
		||||
    slug: str
 | 
			
		||||
@ -1,243 +0,0 @@
 | 
			
		||||
import datetime
 | 
			
		||||
from enum import Enum
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from typing import Any, Optional
 | 
			
		||||
 | 
			
		||||
from fastapi_camelcase import CamelModel
 | 
			
		||||
from pydantic import BaseModel, Field, validator
 | 
			
		||||
from pydantic.utils import GetterDict
 | 
			
		||||
from slugify import slugify
 | 
			
		||||
 | 
			
		||||
from mealie.core.config import app_dirs, settings
 | 
			
		||||
from mealie.db.models.recipe.recipe import RecipeModel
 | 
			
		||||
 | 
			
		||||
from .comments import CommentOut
 | 
			
		||||
from .units_and_foods import IngredientFood, IngredientUnit
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CreateRecipe(CamelModel):
 | 
			
		||||
    name: str
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecipeImageTypes(str, Enum):
 | 
			
		||||
    original = "original.webp"
 | 
			
		||||
    min = "min-original.webp"
 | 
			
		||||
    tiny = "tiny-original.webp"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecipeSettings(CamelModel):
 | 
			
		||||
    public: bool = settings.RECIPE_PUBLIC
 | 
			
		||||
    show_nutrition: bool = settings.RECIPE_SHOW_NUTRITION
 | 
			
		||||
    show_assets: bool = settings.RECIPE_SHOW_ASSETS
 | 
			
		||||
    landscape_view: bool = settings.RECIPE_LANDSCAPE_VIEW
 | 
			
		||||
    disable_comments: bool = settings.RECIPE_DISABLE_COMMENTS
 | 
			
		||||
    disable_amount: bool = settings.RECIPE_DISABLE_AMOUNT
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        orm_mode = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecipeNote(BaseModel):
 | 
			
		||||
    title: str
 | 
			
		||||
    text: str
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        orm_mode = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecipeStep(CamelModel):
 | 
			
		||||
    title: Optional[str] = ""
 | 
			
		||||
    text: str
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        orm_mode = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecipeAsset(CamelModel):
 | 
			
		||||
    name: str
 | 
			
		||||
    icon: str
 | 
			
		||||
    file_name: Optional[str]
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        orm_mode = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Nutrition(CamelModel):
 | 
			
		||||
    calories: Optional[str]
 | 
			
		||||
    fat_content: Optional[str]
 | 
			
		||||
    protein_content: Optional[str]
 | 
			
		||||
    carbohydrate_content: Optional[str]
 | 
			
		||||
    fiber_content: Optional[str]
 | 
			
		||||
    sodium_content: Optional[str]
 | 
			
		||||
    sugar_content: Optional[str]
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        orm_mode = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecipeIngredient(CamelModel):
 | 
			
		||||
    title: Optional[str]
 | 
			
		||||
    note: Optional[str]
 | 
			
		||||
    unit: Optional[IngredientUnit]
 | 
			
		||||
    food: Optional[IngredientFood]
 | 
			
		||||
    disable_amount: bool = True
 | 
			
		||||
    quantity: int = 1
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        orm_mode = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecipeSummary(CamelModel):
 | 
			
		||||
    id: Optional[int]
 | 
			
		||||
    name: Optional[str]
 | 
			
		||||
    slug: str = ""
 | 
			
		||||
    image: Optional[Any]
 | 
			
		||||
 | 
			
		||||
    description: Optional[str]
 | 
			
		||||
    recipe_category: Optional[list[str]] = []
 | 
			
		||||
    tags: Optional[list[str]] = []
 | 
			
		||||
    rating: Optional[int]
 | 
			
		||||
 | 
			
		||||
    date_added: Optional[datetime.date]
 | 
			
		||||
    date_updated: Optional[datetime.datetime]
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        orm_mode = True
 | 
			
		||||
 | 
			
		||||
        @classmethod
 | 
			
		||||
        def getter_dict(_cls, name_orm: RecipeModel):
 | 
			
		||||
            return {
 | 
			
		||||
                **GetterDict(name_orm),
 | 
			
		||||
                "recipe_category": [x.name for x in name_orm.recipe_category],
 | 
			
		||||
                "tags": [x.name for x in name_orm.tags],
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Recipe(RecipeSummary):
 | 
			
		||||
    recipe_yield: Optional[str]
 | 
			
		||||
    recipe_ingredient: Optional[list[RecipeIngredient]] = []
 | 
			
		||||
    recipe_instructions: Optional[list[RecipeStep]] = []
 | 
			
		||||
    nutrition: Optional[Nutrition]
 | 
			
		||||
    tools: Optional[list[str]] = []
 | 
			
		||||
 | 
			
		||||
    total_time: Optional[str] = None
 | 
			
		||||
    prep_time: Optional[str] = None
 | 
			
		||||
    perform_time: Optional[str] = None
 | 
			
		||||
 | 
			
		||||
    # Mealie Specific
 | 
			
		||||
    settings: Optional[RecipeSettings] = RecipeSettings()
 | 
			
		||||
    assets: Optional[list[RecipeAsset]] = []
 | 
			
		||||
    notes: Optional[list[RecipeNote]] = []
 | 
			
		||||
    org_url: Optional[str] = Field(None, alias="orgURL")
 | 
			
		||||
    extras: Optional[dict] = {}
 | 
			
		||||
 | 
			
		||||
    comments: Optional[list[CommentOut]] = []
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def directory_from_slug(slug) -> Path:
 | 
			
		||||
        return app_dirs.RECIPE_DATA_DIR.joinpath(slug)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def directory(self) -> Path:
 | 
			
		||||
        dir = app_dirs.RECIPE_DATA_DIR.joinpath(self.slug)
 | 
			
		||||
        dir.mkdir(exist_ok=True, parents=True)
 | 
			
		||||
        return dir
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def asset_dir(self) -> Path:
 | 
			
		||||
        dir = self.directory.joinpath("assets")
 | 
			
		||||
        dir.mkdir(exist_ok=True, parents=True)
 | 
			
		||||
        return dir
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def image_dir(self) -> Path:
 | 
			
		||||
        dir = self.directory.joinpath("images")
 | 
			
		||||
        dir.mkdir(exist_ok=True, parents=True)
 | 
			
		||||
        return dir
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        orm_mode = True
 | 
			
		||||
 | 
			
		||||
        @classmethod
 | 
			
		||||
        def getter_dict(_cls, name_orm: RecipeModel):
 | 
			
		||||
            return {
 | 
			
		||||
                **GetterDict(name_orm),
 | 
			
		||||
                # "recipe_ingredient": [x.note for x in name_orm.recipe_ingredient],
 | 
			
		||||
                "recipe_category": [x.name for x in name_orm.recipe_category],
 | 
			
		||||
                "tags": [x.name for x in name_orm.tags],
 | 
			
		||||
                "tools": [x.tool for x in name_orm.tools],
 | 
			
		||||
                "extras": {x.key_name: x.value for x in name_orm.extras},
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        schema_extra = {
 | 
			
		||||
            "example": {
 | 
			
		||||
                "name": "Chicken and Rice With Leeks and Salsa Verde",
 | 
			
		||||
                "description": "This one-skillet dinner gets deep oniony flavor from lots of leeks cooked down to jammy tenderness.",
 | 
			
		||||
                "image": "chicken-and-rice-with-leeks-and-salsa-verde.jpg",
 | 
			
		||||
                "recipe_yield": "4 Servings",
 | 
			
		||||
                "recipe_ingredient": [
 | 
			
		||||
                    "1 1/2 lb. skinless, boneless chicken thighs (4-8 depending on size)",
 | 
			
		||||
                    "Kosher salt, freshly ground pepper",
 | 
			
		||||
                    "3 Tbsp. unsalted butter, divided",
 | 
			
		||||
                ],
 | 
			
		||||
                "recipe_instructions": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "text": "Season chicken with salt and pepper.",
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
                "slug": "chicken-and-rice-with-leeks-and-salsa-verde",
 | 
			
		||||
                "tags": ["favorite", "yummy!"],
 | 
			
		||||
                "recipe_category": ["Dinner", "Pasta"],
 | 
			
		||||
                "notes": [{"title": "Watch Out!", "text": "Prep the day before!"}],
 | 
			
		||||
                "org_url": "https://www.bonappetit.com/recipe/chicken-and-rice-with-leeks-and-salsa-verde",
 | 
			
		||||
                "rating": 3,
 | 
			
		||||
                "extras": {"message": "Don't forget to defrost the chicken!"},
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @validator("slug", always=True, pre=True)
 | 
			
		||||
    def validate_slug(slug: str, values):
 | 
			
		||||
        if not values["name"]:
 | 
			
		||||
            return slug
 | 
			
		||||
        name: str = values["name"]
 | 
			
		||||
        calc_slug: str = slugify(name)
 | 
			
		||||
 | 
			
		||||
        if slug != calc_slug:
 | 
			
		||||
            slug = calc_slug
 | 
			
		||||
 | 
			
		||||
        return slug
 | 
			
		||||
 | 
			
		||||
    @validator("recipe_ingredient", always=True, pre=True)
 | 
			
		||||
    def validate_ingredients(recipe_ingredient, values):
 | 
			
		||||
        if not recipe_ingredient or not isinstance(recipe_ingredient, list):
 | 
			
		||||
            return recipe_ingredient
 | 
			
		||||
 | 
			
		||||
        if all(isinstance(elem, str) for elem in recipe_ingredient):
 | 
			
		||||
            return [RecipeIngredient(note=x) for x in recipe_ingredient]
 | 
			
		||||
 | 
			
		||||
        return recipe_ingredient
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AllRecipeRequest(BaseModel):
 | 
			
		||||
    properties: list[str]
 | 
			
		||||
    limit: Optional[int]
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        schema_extra = {
 | 
			
		||||
            "example": {
 | 
			
		||||
                "properties": ["name", "slug", "image"],
 | 
			
		||||
                "limit": 100,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecipeURLIn(BaseModel):
 | 
			
		||||
    url: str
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        schema_extra = {"example": {"url": "https://myfavoriterecipes.com/recipes"}}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SlugResponse(BaseModel):
 | 
			
		||||
    class Config:
 | 
			
		||||
        schema_extra = {"example": "adult-mac-and-cheese"}
 | 
			
		||||
@ -1,24 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
@ -48,25 +48,19 @@ class BaseHttpService(Generic[T, D]):
 | 
			
		||||
    def assert_existing(self, data: T) -> None:
 | 
			
		||||
        raise NotImplementedError("`assert_existing` must by implemented by child class")
 | 
			
		||||
 | 
			
		||||
    def _create_event(self, title: str, message: str) -> None:
 | 
			
		||||
        if not self.__class__.event_func:
 | 
			
		||||
            raise NotImplementedError("`event_func` must be set by child class")
 | 
			
		||||
 | 
			
		||||
        self.background_tasks.add_task(self.__class__.event_func, title, message, self.session)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def read_existing(cls, id: T, deps: ReadDeps = Depends()):
 | 
			
		||||
        """
 | 
			
		||||
        Used for dependency injection for routes that require an existing recipe. If the recipe doesn't exist
 | 
			
		||||
        or the user doens't not have the required permissions, the proper HTTP Status code will be raised.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            slug (str): Recipe Slug used to query the database
 | 
			
		||||
            session (Session, optional): The Injected SQLAlchemy Session.
 | 
			
		||||
            user (bool, optional): The injected determination of is_logged_in.
 | 
			
		||||
 | 
			
		||||
        Raises:
 | 
			
		||||
            HTTPException: 404 Not Found
 | 
			
		||||
            HTTPException: 403 Forbidden
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            RecipeService: The Recipe Service class with a populated recipe attribute
 | 
			
		||||
        """
 | 
			
		||||
        new_class = cls(deps.session, deps.user, deps.bg_tasks)
 | 
			
		||||
        new_class = cls(deps.session, deps.user, deps.bg_task)
 | 
			
		||||
        new_class.assert_existing(id)
 | 
			
		||||
        return new_class
 | 
			
		||||
 | 
			
		||||
@ -75,35 +69,21 @@ class BaseHttpService(Generic[T, D]):
 | 
			
		||||
        """
 | 
			
		||||
        Used for dependency injection for routes that require an existing recipe. The only difference between
 | 
			
		||||
        read_existing and write_existing is that the user is required to be logged in on write_existing method.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            slug (str): Recipe Slug used to query the database
 | 
			
		||||
            session (Session, optional): The Injected SQLAlchemy Session.
 | 
			
		||||
            user (bool, optional): The injected determination of is_logged_in.
 | 
			
		||||
 | 
			
		||||
        Raises:
 | 
			
		||||
            HTTPException: 404 Not Found
 | 
			
		||||
            HTTPException: 403 Forbidden
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            RecipeService: The Recipe Service class with a populated recipe attribute
 | 
			
		||||
        """
 | 
			
		||||
        new_class = cls(deps.session, deps.user, deps.bg_task)
 | 
			
		||||
        new_class.assert_existing(id)
 | 
			
		||||
        return new_class
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def base(cls, deps: WriteDeps = Depends()):
 | 
			
		||||
        """A Base instance to be used as a router dependency
 | 
			
		||||
 | 
			
		||||
        Raises:
 | 
			
		||||
            HTTPException: 400 Bad Request
 | 
			
		||||
 | 
			
		||||
    def public(cls, deps: ReadDeps = Depends()):
 | 
			
		||||
        """
 | 
			
		||||
        A Base instance to be used as a router dependency
 | 
			
		||||
        """
 | 
			
		||||
        return cls(deps.session, deps.user, deps.bg_task)
 | 
			
		||||
 | 
			
		||||
    def _create_event(self, title: str, message: str) -> None:
 | 
			
		||||
        if not self.__class__.event_func:
 | 
			
		||||
            raise NotImplementedError("`event_func` must be set by child class")
 | 
			
		||||
 | 
			
		||||
        self.background_tasks.add_task(self.__class__.event_func, title, message, self.session)
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def private(cls, deps: WriteDeps = Depends()):
 | 
			
		||||
        """
 | 
			
		||||
        A Base instance to be used as a router dependency
 | 
			
		||||
        """
 | 
			
		||||
        return cls(deps.session, deps.user, deps.bg_task)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								mealie/services/cookbook/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								mealie/services/cookbook/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
from .cookbook_service import *
 | 
			
		||||
							
								
								
									
										85
									
								
								mealie/services/cookbook/cookbook_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								mealie/services/cookbook/cookbook_service.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,85 @@
 | 
			
		||||
from fastapi import HTTPException, status
 | 
			
		||||
 | 
			
		||||
from mealie.core.root_logger import get_logger
 | 
			
		||||
from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, SaveCookBook
 | 
			
		||||
from mealie.services.base_http_service.base_http_service import BaseHttpService
 | 
			
		||||
from mealie.services.events import create_group_event
 | 
			
		||||
 | 
			
		||||
logger = get_logger(module=__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CookbookService(BaseHttpService[str, str]):
 | 
			
		||||
    """
 | 
			
		||||
    Class Methods:
 | 
			
		||||
        `read_existing`: Reads an existing recipe from the database.
 | 
			
		||||
        `write_existing`: Updates an existing recipe in the database.
 | 
			
		||||
        `base`: Requires write permissions, but doesn't perform recipe checks
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    event_func = create_group_event
 | 
			
		||||
    cookbook: ReadCookBook  # Required for proper type hints
 | 
			
		||||
 | 
			
		||||
    _group_id_cache = None
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def group_id(self):
 | 
			
		||||
        # TODO: Populate Group in Private User Call WARNING: May require significant refactoring
 | 
			
		||||
        if not self._group_id_cache:
 | 
			
		||||
            group = self.db.groups.get(self.session, self.user.group, "name")
 | 
			
		||||
            print(group)
 | 
			
		||||
            self._group_id_cache = group.id
 | 
			
		||||
        return self._group_id_cache
 | 
			
		||||
 | 
			
		||||
    def assert_existing(self, id: str):
 | 
			
		||||
        self.populate_cookbook(id)
 | 
			
		||||
 | 
			
		||||
        if not self.cookbook:
 | 
			
		||||
            raise HTTPException(status.HTTP_404_NOT_FOUND)
 | 
			
		||||
 | 
			
		||||
        if self.cookbook.group_id != self.group_id:
 | 
			
		||||
            raise HTTPException(status.HTTP_403_FORBIDDEN)
 | 
			
		||||
 | 
			
		||||
    def populate_cookbook(self, id):
 | 
			
		||||
        self.cookbook = self.db.cookbooks.get(self.session, id)
 | 
			
		||||
 | 
			
		||||
    def get_all(self) -> list[ReadCookBook]:
 | 
			
		||||
        items = self.db.cookbooks.get(self.session, self.group_id, "group_id", limit=999)
 | 
			
		||||
        items.sort(key=lambda x: x.position)
 | 
			
		||||
        return items
 | 
			
		||||
 | 
			
		||||
    def create_one(self, data: CreateCookBook) -> ReadCookBook:
 | 
			
		||||
        try:
 | 
			
		||||
            self.cookbook = self.db.cookbooks.create(self.session, SaveCookBook(group_id=self.group_id, **data.dict()))
 | 
			
		||||
        except Exception as ex:
 | 
			
		||||
            raise HTTPException(
 | 
			
		||||
                status.HTTP_400_BAD_REQUEST, detail={"message": "PAGE_CREATION_ERROR", "exception": str(ex)}
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return self.cookbook
 | 
			
		||||
 | 
			
		||||
    def update_one(self, data: CreateCookBook, id: int = None) -> ReadCookBook:
 | 
			
		||||
        if not self.cookbook:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        target_id = id or self.cookbook.id
 | 
			
		||||
        self.cookbook = self.db.cookbooks.update(self.session, target_id, data)
 | 
			
		||||
 | 
			
		||||
        return self.cookbook
 | 
			
		||||
 | 
			
		||||
    def update_many(self, data: list[ReadCookBook]) -> list[ReadCookBook]:
 | 
			
		||||
        updated = []
 | 
			
		||||
 | 
			
		||||
        for cookbook in data:
 | 
			
		||||
            cb = self.db.cookbooks.update(self.session, cookbook.id, cookbook)
 | 
			
		||||
            updated.append(cb)
 | 
			
		||||
 | 
			
		||||
        return updated
 | 
			
		||||
 | 
			
		||||
    def delete_one(self, id: int = None) -> ReadCookBook:
 | 
			
		||||
        if not self.cookbook:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        target_id = id or self.cookbook.id
 | 
			
		||||
        self.cookbook = self.db.cookbooks.delete(self.session, target_id)
 | 
			
		||||
 | 
			
		||||
        return self.cookbook
 | 
			
		||||
@ -5,7 +5,7 @@ from typing import Union
 | 
			
		||||
from fastapi import Depends, HTTPException, status
 | 
			
		||||
from sqlalchemy.exc import IntegrityError
 | 
			
		||||
 | 
			
		||||
from mealie.core.dependencies.grouped import WriteDeps
 | 
			
		||||
from mealie.core.dependencies.grouped import ReadDeps, WriteDeps
 | 
			
		||||
from mealie.core.root_logger import get_logger
 | 
			
		||||
from mealie.schema.recipe.recipe import CreateRecipe, Recipe
 | 
			
		||||
from mealie.services.base_http_service.base_http_service import BaseHttpService
 | 
			
		||||
@ -21,6 +21,7 @@ class RecipeService(BaseHttpService[str, str]):
 | 
			
		||||
        `write_existing`: Updates an existing recipe in the database.
 | 
			
		||||
        `base`: Requires write permissions, but doesn't perform recipe checks
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    event_func = create_recipe_event
 | 
			
		||||
    recipe: Recipe  # Required for proper type hints
 | 
			
		||||
 | 
			
		||||
@ -29,7 +30,7 @@ class RecipeService(BaseHttpService[str, str]):
 | 
			
		||||
        return super().write_existing(slug, deps)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def read_existing(cls, slug: str, deps: WriteDeps = Depends()):
 | 
			
		||||
    def read_existing(cls, slug: str, deps: ReadDeps = Depends()):
 | 
			
		||||
        return super().write_existing(slug, deps)
 | 
			
		||||
 | 
			
		||||
    def assert_existing(self, slug: str):
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ import unicodedata
 | 
			
		||||
 | 
			
		||||
replace_abbreviations = {
 | 
			
		||||
    "cup ": "cup ",
 | 
			
		||||
    "g ": "gram ",
 | 
			
		||||
    " g ": "gram ",
 | 
			
		||||
    "kg ": "kilogram ",
 | 
			
		||||
    "lb ": "pound ",
 | 
			
		||||
    "ml ": "milliliter ",
 | 
			
		||||
 | 
			
		||||
@ -55,8 +55,8 @@ def _exec_crf_test(input_text):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def convert_list_to_crf_model(list_of_ingrdeint_text: list[str]):
 | 
			
		||||
    print(list_of_ingrdeint_text)
 | 
			
		||||
    crf_output = _exec_crf_test([pre_process_string(x) for x in list_of_ingrdeint_text])
 | 
			
		||||
 | 
			
		||||
    crf_models = [CRFIngredient(**ingredient) for ingredient in utils.import_data(crf_output.split("\n"))]
 | 
			
		||||
 | 
			
		||||
    for model in crf_models:
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@ def cleanUnicodeFractions(s):
 | 
			
		||||
    """
 | 
			
		||||
    Replace unicode fractions with ascii representation, preceded by a
 | 
			
		||||
    space.
 | 
			
		||||
 | 
			
		||||
    "1\x215e" => "1 7/8"
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
@ -47,7 +48,7 @@ def unclump(s):
 | 
			
		||||
 | 
			
		||||
def normalizeToken(s):
 | 
			
		||||
    """
 | 
			
		||||
    TODO: FIX THIS. We used to use the pattern.en package to singularize words, but
 | 
			
		||||
    ToDo: FIX THIS. We used to use the pattern.en package to singularize words, but
 | 
			
		||||
    in the name of simple deployments, we took it out. We should fix this at some
 | 
			
		||||
    point.
 | 
			
		||||
    """
 | 
			
		||||
@ -133,12 +134,13 @@ def insideParenthesis(token, tokens):
 | 
			
		||||
        return True
 | 
			
		||||
    else:
 | 
			
		||||
        line = " ".join(tokens)
 | 
			
		||||
        return re.match(r".*\(.*" + re.escape(token) + r".*\).*", line) is not None
 | 
			
		||||
        return re.match(r".*\(.*" + re.escape(token) + ".*\).*", line) is not None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def displayIngredient(ingredient):
 | 
			
		||||
    """
 | 
			
		||||
    Format a list of (tag, [tokens]) tuples as an HTML string for display.
 | 
			
		||||
 | 
			
		||||
        displayIngredient([("qty", ["1"]), ("name", ["cat", "pie"])])
 | 
			
		||||
        # => <span class='qty'>1</span> <span class='name'>cat pie</span>
 | 
			
		||||
    """
 | 
			
		||||
@ -220,21 +222,7 @@ def import_data(lines):
 | 
			
		||||
 | 
			
		||||
            # turn B-NAME/123 back into "name"
 | 
			
		||||
            tag, confidence = re.split(r"/", columns[-1], 1)
 | 
			
		||||
            tag = re.sub(r"^[BI]\-", "", tag).lower()
 | 
			
		||||
 | 
			
		||||
            # TODO: Integrate Confidence into API Response
 | 
			
		||||
            print("Confidence", confidence)
 | 
			
		||||
 | 
			
		||||
            # new token
 | 
			
		||||
            if prevTag != tag or token == "n/a":
 | 
			
		||||
                display[-1].append((tag, [token]))
 | 
			
		||||
                data[-1][tag] = []
 | 
			
		||||
                prevTag = tag
 | 
			
		||||
 | 
			
		||||
            # continuation
 | 
			
		||||
            else:
 | 
			
		||||
                display[-1][-1][1].append(token)
 | 
			
		||||
                data[-1][tag].append(token)
 | 
			
		||||
            tag = re.sub("^[BI]\-", "", tag).lower()
 | 
			
		||||
 | 
			
		||||
            # ---- DISPLAY ----
 | 
			
		||||
            # build a structure which groups each token by its tag, so we can
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user