feat(backend): add initial cookbook support

This commit is contained in:
hay-kot 2021-08-31 14:39:29 -08:00
parent 83ab858e46
commit d24e95c091
27 changed files with 284 additions and 490 deletions

View File

@ -24,7 +24,7 @@ class ReadDeps:
user=Depends(is_logged_in), user=Depends(is_logged_in),
): ):
self.session: Session = session self.session: Session = session
self.bg_tasks: BackgroundTasks = background_tasks self.bg_task: BackgroundTasks = background_tasks
self.user: bool = user self.user: bool = user

View File

@ -1,3 +1,5 @@
from __future__ import annotations
from typing import Callable, Generic, TypeVar, Union from typing import Callable, Generic, TypeVar, Union
from sqlalchemy import func 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: def __init__(self, primary_key: Union[str, int], sql_model: D, schema: T) -> None:
self.primary_key = primary_key self.primary_key = primary_key
self.sql_model = sql_model self.sql_model = sql_model
self.schema = schema self.schema = schema
self.observers: list = [] self.observers: list = []
def subscribe(self, func: Callable) -> None: 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] 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: 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. 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: if match_key is None:
match_key = self.primary_key match_key = self.primary_key
return session.query(self.sql_model).filter_by(**{match_key: match_value}).one() 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( def get(
self, session: Session, match_value: str, match_key: str = None, limit=1, any_case=False, override_schema=None 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 """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. key is provided the class objects primary key will be used to match against.
Args: Args:
match_value (str): A value used to match against the key/value in the database \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. \n 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. \n limit (int, optional): A limit to returned responses. Defaults to 1.
Returns: Returns:
dict or list[dict]: dict or list[dict]:
""" """
if match_key is None: match_key = match_key or self.primary_key
match_key = self.primary_key
if any_case: if any_case:
search_attr = getattr(self.sql_model, match_key) search_attr = getattr(self.sql_model, match_key)

View File

@ -3,6 +3,7 @@ from logging import getLogger
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from mealie.db.data_access_layer.group_access_model import GroupDataAccessModel 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.event import Event, EventNotification
from mealie.db.models.group import Group from mealie.db.models.group import Group
from mealie.db.models.mealplan import MealPlan 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.comment import RecipeComment
from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel from mealie.db.models.recipe.ingredient import IngredientFoodModel, IngredientUnitModel
from mealie.db.models.recipe.recipe import RecipeModel, Tag 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.shopping_list import ShoppingList
from mealie.db.models.sign_up import SignUp from mealie.db.models.sign_up import SignUp
from mealie.db.models.users import LongLiveToken, User 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.admin import SiteSettings as SiteSettingsSchema
from mealie.schema.cookbook import ReadCookBook
from mealie.schema.events import Event as EventSchema from mealie.schema.events import Event as EventSchema
from mealie.schema.events import EventNotificationIn from mealie.schema.events import EventNotificationIn
from mealie.schema.meal_plan import MealPlanOut, ShoppingListOut from mealie.schema.meal_plan import MealPlanOut, ShoppingListOut
@ -73,7 +74,6 @@ class DatabaseAccessLayer:
# Site # Site
self.settings = BaseAccessModel(DEFAULT_PK, SiteSettings, SiteSettingsSchema) self.settings = BaseAccessModel(DEFAULT_PK, SiteSettings, SiteSettingsSchema)
self.sign_ups = BaseAccessModel("token", SignUp, SignUpOut) self.sign_ups = BaseAccessModel("token", SignUp, SignUpOut)
self.custom_pages = BaseAccessModel(DEFAULT_PK, CustomPage, CustomPageOut)
self.event_notifications = BaseAccessModel(DEFAULT_PK, EventNotification, EventNotificationIn) self.event_notifications = BaseAccessModel(DEFAULT_PK, EventNotification, EventNotificationIn)
self.events = BaseAccessModel(DEFAULT_PK, Event, EventSchema) self.events = BaseAccessModel(DEFAULT_PK, Event, EventSchema)
@ -83,3 +83,4 @@ class DatabaseAccessLayer:
self.groups = GroupDataAccessModel(DEFAULT_PK, Group, GroupInDB) self.groups = GroupDataAccessModel(DEFAULT_PK, Group, GroupInDB)
self.meals = BaseAccessModel(DEFAULT_PK, MealPlan, MealPlanOut) self.meals = BaseAccessModel(DEFAULT_PK, MealPlan, MealPlanOut)
self.shopping_lists = BaseAccessModel(DEFAULT_PK, ShoppingList, ShoppingListOut) self.shopping_lists = BaseAccessModel(DEFAULT_PK, ShoppingList, ShoppingListOut)
self.cookbooks = BaseAccessModel(DEFAULT_PK, CookBook, ReadCookBook)

View File

@ -4,7 +4,7 @@ from typing import Union
from sqlalchemy.orm import MANYTOMANY, MANYTOONE, ONETOMANY 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 = [] elems_to_create = []
updated_elems = [] updated_elems = []
@ -75,8 +75,17 @@ def auto_init(exclude: Union[set, list] = None): # sourcery no-metrics
relation_cls = relationships[key].mapper.entity relation_cls = relationships[key].mapper.entity
use_list = relationships[key].uselist 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: 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) setattr(self, key, instances)
if relation_dir == ONETOMANY.name and not use_list: 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: elif relation_dir == MANYTOONE.name and not use_list:
if isinstance(val, dict): if isinstance(val, dict):
val = val.get("id") val = val.get(get_attr)
if val is None: if val is None:
raise ValueError(f"Expected 'id' to be provided for {key}") 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}") raise ValueError(f"Expected many to many input to be of type list for {key}")
if len(val) > 0 and isinstance(val[0], dict): 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] instances = [x for x in [relation_cls.get_ref(elem, session=session) for elem in val] if x]
setattr(self, key, instances) setattr(self, key, instances)

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

View File

@ -4,6 +4,7 @@ from sqlalchemy.orm.session import Session
from mealie.core.config import settings from mealie.core.config import settings
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase 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 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) id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String, index=True, nullable=False, unique=True) name = sa.Column(sa.String, index=True, nullable=False, unique=True)
users = orm.relationship("User", back_populates="group") users = orm.relationship("User", back_populates="group")
mealplans = orm.relationship( mealplans = orm.relationship("MealPlan", back_populates="group", single_parent=True, order_by="MealPlan.start_date")
"MealPlan", shopping_lists = orm.relationship("ShoppingList", back_populates="group", single_parent=True)
back_populates="group", cookbooks = orm.relationship(CookBook, back_populates="group", single_parent=True)
single_parent=True,
order_by="MealPlan.start_date",
)
shopping_lists = orm.relationship(
"ShoppingList",
back_populates="group",
single_parent=True,
)
categories = orm.relationship("Category", secondary=group2categories, single_parent=True) categories = orm.relationship("Category", secondary=group2categories, single_parent=True)
# Webhook Settings # Webhook Settings

View File

@ -29,10 +29,10 @@ recipes2categories = sa.Table(
sa.Column("category_id", sa.Integer, sa.ForeignKey("categories.id")), sa.Column("category_id", sa.Integer, sa.ForeignKey("categories.id")),
) )
custom_pages2categories = sa.Table( cookbooks_to_categories = sa.Table(
"custom_pages2categories", "cookbooks_to_categories",
SqlAlchemyBase.metadata, 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")), 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: if not session or not match_value:
return None return None
print(match_value)
slug = slugify(match_value) slug = slugify(match_value)
result = session.query(Category).filter(Category.slug == slug).one_or_none() result = session.query(Category).filter(Category.slug == slug).one_or_none()

View File

@ -3,7 +3,7 @@ import sqlalchemy.orm as orm
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase 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): class SiteSettings(SqlAlchemyBase, BaseMixins):
@ -33,21 +33,3 @@ class SiteSettings(SqlAlchemyBase, BaseMixins):
def update(self, *args, **kwarg): def update(self, *args, **kwarg):
self.__init__(*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)

View File

@ -1,8 +1,9 @@
from fastapi import APIRouter from fastapi import APIRouter
from . import crud from . import cookbooks, crud
router = APIRouter() router = APIRouter()
router.include_router(cookbooks.user_router)
router.include_router(crud.user_router) router.include_router(crud.user_router)
router.include_router(crud.admin_router) router.include_router(crud.admin_router)

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

View File

@ -31,13 +31,13 @@ def get_recipe(recipe_service: RecipeService = Depends(RecipeService.read_existi
@user_router.post("", status_code=201, response_model=str) @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""" """ Takes in a JSON string and loads data into the database as a new entry"""
return recipe_service.create_recipe(data).slug return recipe_service.create_recipe(data).slug
@user_router.post("/create-url", status_code=201, response_model=str) @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 """ """ Takes in a URL and attempts to scrape data and load it into the database """
recipe = create_from_url(url.url) recipe = create_from_url(url.url)

View File

@ -0,0 +1 @@
from .cookbook import *

View 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

View File

@ -19,7 +19,6 @@ class CategoryBase(CategoryIn):
def getter_dict(_cls, name_orm): def getter_dict(_cls, name_orm):
return { return {
**GetterDict(name_orm), **GetterDict(name_orm),
"total_recipes": len(name_orm.recipes),
} }

View File

@ -1,4 +0,0 @@
from .category import *
from .comments import *
from .helpers import *
from .recipe import *

View File

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

View File

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

View File

@ -1,5 +0,0 @@
from fastapi_camelcase import CamelModel
class RecipeSlug(CamelModel):
slug: str

View File

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

View File

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

View File

@ -48,25 +48,19 @@ class BaseHttpService(Generic[T, D]):
def assert_existing(self, data: T) -> None: def assert_existing(self, data: T) -> None:
raise NotImplementedError("`assert_existing` must by implemented by child class") 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 @classmethod
def read_existing(cls, id: T, deps: ReadDeps = Depends()): 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 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. 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) new_class.assert_existing(id)
return new_class 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 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. 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 = cls(deps.session, deps.user, deps.bg_task)
new_class.assert_existing(id) new_class.assert_existing(id)
return new_class return new_class
@classmethod @classmethod
def base(cls, deps: WriteDeps = Depends()): def public(cls, deps: ReadDeps = Depends()):
"""A Base instance to be used as a router dependency """
A Base instance to be used as a router dependency
Raises:
HTTPException: 400 Bad Request
""" """
return cls(deps.session, deps.user, deps.bg_task) return cls(deps.session, deps.user, deps.bg_task)
def _create_event(self, title: str, message: str) -> None: @classmethod
if not self.__class__.event_func: def private(cls, deps: WriteDeps = Depends()):
raise NotImplementedError("`event_func` must be set by child class") """
A Base instance to be used as a router dependency
self.background_tasks.add_task(self.__class__.event_func, title, message, self.session) """
return cls(deps.session, deps.user, deps.bg_task)

View File

@ -0,0 +1 @@
from .cookbook_service import *

View 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

View File

@ -5,7 +5,7 @@ from typing import Union
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from sqlalchemy.exc import IntegrityError 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.core.root_logger import get_logger
from mealie.schema.recipe.recipe import CreateRecipe, Recipe from mealie.schema.recipe.recipe import CreateRecipe, Recipe
from mealie.services.base_http_service.base_http_service import BaseHttpService 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. `write_existing`: Updates an existing recipe in the database.
`base`: Requires write permissions, but doesn't perform recipe checks `base`: Requires write permissions, but doesn't perform recipe checks
""" """
event_func = create_recipe_event event_func = create_recipe_event
recipe: Recipe # Required for proper type hints recipe: Recipe # Required for proper type hints
@ -29,7 +30,7 @@ class RecipeService(BaseHttpService[str, str]):
return super().write_existing(slug, deps) return super().write_existing(slug, deps)
@classmethod @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) return super().write_existing(slug, deps)
def assert_existing(self, slug: str): def assert_existing(self, slug: str):

View File

@ -55,8 +55,8 @@ def _exec_crf_test(input_text):
def convert_list_to_crf_model(list_of_ingrdeint_text: list[str]): 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_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"))] crf_models = [CRFIngredient(**ingredient) for ingredient in utils.import_data(crf_output.split("\n"))]
for model in crf_models: for model in crf_models:

View File

@ -11,6 +11,7 @@ def cleanUnicodeFractions(s):
""" """
Replace unicode fractions with ascii representation, preceded by a Replace unicode fractions with ascii representation, preceded by a
space. space.
"1\x215e" => "1 7/8" "1\x215e" => "1 7/8"
""" """
@ -47,7 +48,7 @@ def unclump(s):
def normalizeToken(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 in the name of simple deployments, we took it out. We should fix this at some
point. point.
""" """
@ -133,12 +134,13 @@ def insideParenthesis(token, tokens):
return True return True
else: else:
line = " ".join(tokens) 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): def displayIngredient(ingredient):
""" """
Format a list of (tag, [tokens]) tuples as an HTML string for display. Format a list of (tag, [tokens]) tuples as an HTML string for display.
displayIngredient([("qty", ["1"]), ("name", ["cat", "pie"])]) displayIngredient([("qty", ["1"]), ("name", ["cat", "pie"])])
# => <span class='qty'>1</span> <span class='name'>cat pie</span> # => <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" # turn B-NAME/123 back into "name"
tag, confidence = re.split(r"/", columns[-1], 1) tag, confidence = re.split(r"/", columns[-1], 1)
tag = re.sub(r"^[BI]\-", "", tag).lower() tag = re.sub("^[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)
# ---- DISPLAY ---- # ---- DISPLAY ----
# build a structure which groups each token by its tag, so we can # build a structure which groups each token by its tag, so we can