mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-30 17:53:31 -04:00 
			
		
		
		
	refactor(backend): ♻️ refactor backend services (#669)
* refactor(backend): ♻️ refactor backend services * refactor(backend): ♻️ move user model folder into own directory for future expansion * fix overriding results Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
		| @@ -96,8 +96,8 @@ class BaseAccessModel(Generic[T, D]): | ||||
|         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() | ||||
|         else: | ||||
|             result = session.query(self.sql_model).filter_by(**{key: value}).one_or_none() | ||||
|  | ||||
|         if not result: | ||||
|             return | ||||
|   | ||||
| @@ -3,9 +3,10 @@ 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.group.cookbook import CookBook | ||||
| from mealie.db.models.group.shopping_list import ShoppingList | ||||
| from mealie.db.models.group.webhooks import GroupWebhooksModel | ||||
| from mealie.db.models.mealplan import MealPlan | ||||
| from mealie.db.models.recipe.category import Category | ||||
| @@ -13,7 +14,6 @@ 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 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 SiteSettings as SiteSettingsSchema | ||||
| @@ -39,7 +39,9 @@ from .user_access_model import UserDataAccessModel | ||||
| logger = getLogger() | ||||
|  | ||||
|  | ||||
| DEFAULT_PK = "id" | ||||
| pk_id = "id" | ||||
| pk_slug = "slug" | ||||
| pk_token = "token" | ||||
|  | ||||
|  | ||||
| class CategoryDataAccessModel(BaseAccessModel): | ||||
| @@ -53,38 +55,37 @@ class TagsDataAccessModel(BaseAccessModel): | ||||
|  | ||||
|  | ||||
| class DatabaseAccessLayer: | ||||
|     """ | ||||
|     `DatabaseAccessLayer` class is the data access layer for all database actions within | ||||
|     Mealie. Database uses composition from classes derived from BaseAccessModel. These | ||||
|     can be substantiated from the BaseAccessModel class or through inheritance when | ||||
|     additional methods are required. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self) -> None: | ||||
|         """ | ||||
|         `DatabaseAccessLayer` class is the data access layer for all database actions within | ||||
|         Mealie. Database uses composition from classes derived from BaseAccessModel. These | ||||
|         can be substantiated from the BaseAccessModel class or through inheritance when | ||||
|         additional methods are required. | ||||
|         """ | ||||
|  | ||||
|         # Recipes | ||||
|         self.recipes = RecipeDataAccessModel("slug", RecipeModel, Recipe) | ||||
|         self.ingredient_foods = BaseAccessModel(DEFAULT_PK, IngredientFoodModel, IngredientFood) | ||||
|         self.ingredient_units = BaseAccessModel(DEFAULT_PK, IngredientUnitModel, IngredientUnit) | ||||
|         self.comments = BaseAccessModel(DEFAULT_PK, RecipeComment, CommentOut) | ||||
|         self.recipes = RecipeDataAccessModel(pk_slug, RecipeModel, Recipe) | ||||
|         self.ingredient_foods = BaseAccessModel(pk_id, IngredientFoodModel, IngredientFood) | ||||
|         self.ingredient_units = BaseAccessModel(pk_id, IngredientUnitModel, IngredientUnit) | ||||
|         self.comments = BaseAccessModel(pk_id, RecipeComment, CommentOut) | ||||
|  | ||||
|         # Tags and Categories | ||||
|         self.categories = CategoryDataAccessModel("slug", Category, RecipeCategoryResponse) | ||||
|         self.tags = TagsDataAccessModel("slug", Tag, RecipeTagResponse) | ||||
|         self.categories = CategoryDataAccessModel(pk_slug, Category, RecipeCategoryResponse) | ||||
|         self.tags = TagsDataAccessModel(pk_slug, Tag, RecipeTagResponse) | ||||
|  | ||||
|         # Site | ||||
|         self.settings = BaseAccessModel(DEFAULT_PK, SiteSettings, SiteSettingsSchema) | ||||
|         self.sign_ups = BaseAccessModel("token", SignUp, SignUpOut) | ||||
|         self.event_notifications = BaseAccessModel(DEFAULT_PK, EventNotification, EventNotificationIn) | ||||
|         self.events = BaseAccessModel(DEFAULT_PK, Event, EventSchema) | ||||
|         self.settings = BaseAccessModel(pk_id, SiteSettings, SiteSettingsSchema) | ||||
|         self.sign_ups = BaseAccessModel(pk_token, SignUp, SignUpOut) | ||||
|         self.event_notifications = BaseAccessModel(pk_id, EventNotification, EventNotificationIn) | ||||
|         self.events = BaseAccessModel(pk_id, Event, EventSchema) | ||||
|  | ||||
|         # Users | ||||
|         self.users = UserDataAccessModel(DEFAULT_PK, User, PrivateUser) | ||||
|         self.api_tokens = BaseAccessModel(DEFAULT_PK, LongLiveToken, LongLiveTokenInDB) | ||||
|         self.users = UserDataAccessModel(pk_id, User, PrivateUser) | ||||
|         self.api_tokens = BaseAccessModel(pk_id, LongLiveToken, LongLiveTokenInDB) | ||||
|  | ||||
|         # Group Data | ||||
|         self.groups = GroupDataAccessModel(DEFAULT_PK, Group, GroupInDB) | ||||
|         self.meals = BaseAccessModel(DEFAULT_PK, MealPlan, MealPlanOut) | ||||
|         self.webhooks = BaseAccessModel(DEFAULT_PK, GroupWebhooksModel, ReadWebhook) | ||||
|         self.shopping_lists = BaseAccessModel(DEFAULT_PK, ShoppingList, ShoppingListOut) | ||||
|         self.cookbooks = BaseAccessModel(DEFAULT_PK, CookBook, ReadCookBook) | ||||
|         self.groups = GroupDataAccessModel(pk_id, Group, GroupInDB) | ||||
|         self.meals = BaseAccessModel(pk_id, MealPlan, MealPlanOut) | ||||
|         self.webhooks = BaseAccessModel(pk_id, GroupWebhooksModel, ReadWebhook) | ||||
|         self.shopping_lists = BaseAccessModel(pk_id, ShoppingList, ShoppingListOut) | ||||
|         self.cookbooks = BaseAccessModel(pk_id, CookBook, ReadCookBook) | ||||
|   | ||||
| @@ -3,6 +3,5 @@ from .group import * | ||||
| from .mealplan import * | ||||
| from .recipe.recipe import * | ||||
| from .settings import * | ||||
| from .shopping_list import * | ||||
| from .sign_up import * | ||||
| from .users import * | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import uuid | ||||
| from datetime import datetime | ||||
|  | ||||
| from sqlalchemy import Column, DateTime, Integer | ||||
| @@ -7,22 +6,11 @@ from sqlalchemy.orm import declarative_base | ||||
| from sqlalchemy.orm.session import Session | ||||
|  | ||||
|  | ||||
| def get_uuid_as_hex() -> str: | ||||
|     """ | ||||
|     Generate a UUID as a hex string. | ||||
|     :return: UUID as a hex string. | ||||
|     """ | ||||
|     return uuid.uuid4().hex | ||||
|  | ||||
|  | ||||
| @as_declarative() | ||||
| class Base: | ||||
|     id = Column(Integer, primary_key=True) | ||||
|     created_at = Column(DateTime, default=datetime.now()) | ||||
|  | ||||
|     # @declared_attr | ||||
|     # def __tablename__(cls): | ||||
|     #     return cls.__name__.lower() | ||||
|     update_at = Column(DateTime, default=datetime.now(), onupdate=datetime.now()) | ||||
|  | ||||
|  | ||||
| class BaseMixins: | ||||
|   | ||||
| @@ -2,6 +2,8 @@ from sqlalchemy import Boolean, Column, DateTime, Integer, String | ||||
|  | ||||
| from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase | ||||
|  | ||||
| from ._model_utils import auto_init | ||||
|  | ||||
|  | ||||
| class EventNotification(SqlAlchemyBase, BaseMixins): | ||||
|     __tablename__ = "event_notifications" | ||||
| @@ -19,19 +21,9 @@ class EventNotification(SqlAlchemyBase, BaseMixins): | ||||
|     group = Column(Boolean, default=False) | ||||
|     user = Column(Boolean, default=False) | ||||
|  | ||||
|     def __init__( | ||||
|         self, name, notification_url, type, general, recipe, backup, scheduled, migration, group, user, **_ | ||||
|     ) -> None: | ||||
|         self.name = name | ||||
|         self.notification_url = notification_url | ||||
|         self.type = type | ||||
|         self.general = general | ||||
|         self.recipe = recipe | ||||
|         self.backup = backup | ||||
|         self.scheduled = scheduled | ||||
|         self.migration = migration | ||||
|         self.group = group | ||||
|         self.user = user | ||||
|     @auto_init() | ||||
|     def __init__(self, **_) -> None: | ||||
|         pass | ||||
|  | ||||
|  | ||||
| class Event(SqlAlchemyBase, BaseMixins): | ||||
| @@ -42,8 +34,6 @@ class Event(SqlAlchemyBase, BaseMixins): | ||||
|     time_stamp = Column(DateTime) | ||||
|     category = Column(String) | ||||
|  | ||||
|     def __init__(self, title, text, time_stamp, category, **_) -> None: | ||||
|         self.title = title | ||||
|         self.text = text | ||||
|         self.time_stamp = time_stamp | ||||
|         self.category = category | ||||
|     @auto_init() | ||||
|     def __init__(self, **_) -> None: | ||||
|         pass | ||||
|   | ||||
| @@ -1 +1,3 @@ | ||||
| from .group import * | ||||
| from .shopping_list import * | ||||
| from .webhooks import * | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| 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 | ||||
| from .._model_base import BaseMixins, SqlAlchemyBase | ||||
| from .._model_utils import auto_init | ||||
| from ..recipe.category import Category, cookbooks_to_categories | ||||
| 
 | ||||
| 
 | ||||
| class CookBook(SqlAlchemyBase, BaseMixins): | ||||
| @@ -3,12 +3,12 @@ import sqlalchemy.orm as orm | ||||
| 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.group.webhooks import GroupWebhooksModel | ||||
| from mealie.db.models.recipe.category import Category, group2categories | ||||
|  | ||||
| from .._model_base import BaseMixins, SqlAlchemyBase | ||||
| from .._model_utils import auto_init | ||||
| from ..group.webhooks import GroupWebhooksModel | ||||
| from ..recipe.category import Category, group2categories | ||||
| from .cookbook import CookBook | ||||
|  | ||||
|  | ||||
| class Group(SqlAlchemyBase, BaseMixins): | ||||
|   | ||||
| @@ -3,8 +3,8 @@ from requests import Session | ||||
| from sqlalchemy import Boolean, Column, ForeignKey, Integer, String | ||||
| from sqlalchemy.ext.orderinglist import ordering_list | ||||
| 
 | ||||
| from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase | ||||
| from mealie.db.models.group import Group | ||||
| from .._model_base import BaseMixins, SqlAlchemyBase | ||||
| from .group import Group | ||||
| 
 | ||||
| 
 | ||||
| class ShoppingListItem(SqlAlchemyBase, BaseMixins): | ||||
| @@ -5,7 +5,8 @@ from sqlalchemy.ext.orderinglist import ordering_list | ||||
| from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase | ||||
| from mealie.db.models.group import Group | ||||
| from mealie.db.models.recipe.recipe import RecipeModel | ||||
| from mealie.db.models.shopping_list import ShoppingList | ||||
|  | ||||
| from .group.shopping_list import ShoppingList | ||||
|  | ||||
|  | ||||
| class Meal(SqlAlchemyBase): | ||||
|   | ||||
| @@ -2,6 +2,8 @@ from sqlalchemy import Boolean, Column, Integer, String | ||||
|  | ||||
| from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase | ||||
|  | ||||
| from ._model_utils import auto_init | ||||
|  | ||||
|  | ||||
| class SignUp(SqlAlchemyBase, BaseMixins): | ||||
|     __tablename__ = "sign_ups" | ||||
| @@ -10,13 +12,6 @@ class SignUp(SqlAlchemyBase, BaseMixins): | ||||
|     name = Column(String, index=True) | ||||
|     admin = Column(Boolean, default=False) | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
|         session, | ||||
|         token, | ||||
|         name, | ||||
|         admin, | ||||
|     ) -> None: | ||||
|         self.token = token | ||||
|         self.name = name | ||||
|         self.admin = admin | ||||
|     @auto_init() | ||||
|     def __init__(self, **_) -> None: | ||||
|         pass | ||||
|   | ||||
							
								
								
									
										1
									
								
								mealie/db/models/users/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								mealie/db/models/users/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| from .users import * | ||||
| @@ -27,9 +27,11 @@ class User(SqlAlchemyBase, BaseMixins): | ||||
|     username = Column(String, index=True, unique=True) | ||||
|     email = Column(String, unique=True, index=True) | ||||
|     password = Column(String) | ||||
|     admin = Column(Boolean, default=False) | ||||
| 
 | ||||
|     group_id = Column(Integer, ForeignKey("groups.id")) | ||||
|     group = orm.relationship("Group", back_populates="users") | ||||
|     admin = Column(Boolean, default=False) | ||||
| 
 | ||||
|     tokens: list[LongLiveToken] = orm.relationship( | ||||
|         LongLiveToken, back_populates="user", cascade="all, delete, delete-orphan", single_parent=True | ||||
|     ) | ||||
| @@ -3,7 +3,7 @@ from typing import Optional | ||||
| from fastapi_camelcase import CamelModel | ||||
| from pydantic.utils import GetterDict | ||||
|  | ||||
| from mealie.db.models.shopping_list import ShoppingList | ||||
| from mealie.db.models.group.shopping_list import ShoppingList | ||||
|  | ||||
|  | ||||
| class ListItem(CamelModel): | ||||
|   | ||||
| @@ -1,3 +1,2 @@ | ||||
| from .base_http_service import * | ||||
| from .base_service import * | ||||
| from .http_services import * | ||||
| from .router_factory import * | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| from abc import ABC, abstractmethod | ||||
| from typing import Callable, Generic, Type, TypeVar | ||||
| from typing import Any, Callable, Generic, Type, TypeVar | ||||
|  | ||||
| from fastapi import BackgroundTasks, Depends, HTTPException, status | ||||
| from sqlalchemy.orm.session import Session | ||||
|  | ||||
| from mealie.core.config import get_app_dirs, get_settings | ||||
| from mealie.core.dependencies.grouped import PublicDeps, UserDeps | ||||
| from mealie.core.root_logger import get_logger | ||||
| from mealie.db.data_access_layer.db_access import DatabaseAccessLayer | ||||
| from mealie.db.database import get_database | ||||
| from mealie.db.db_setup import SessionLocal | ||||
| from mealie.schema.user.user import PrivateUser | ||||
| @@ -44,10 +44,10 @@ class BaseHttpService(Generic[T, D], ABC): | ||||
|     delete_one: Callable = None | ||||
|     delete_all: Callable = None | ||||
|  | ||||
|     db_access: DatabaseAccessLayer = None | ||||
|  | ||||
|     # Type Definitions | ||||
|     _schema = None | ||||
|     _create_schema = None | ||||
|     _update_schema = None | ||||
|  | ||||
|     # Function called to create a server side event | ||||
|     event_func: Callable = None | ||||
| @@ -67,14 +67,6 @@ class BaseHttpService(Generic[T, D], ABC): | ||||
|         self.app_dirs = get_app_dirs() | ||||
|         self.settings = get_settings() | ||||
|  | ||||
|     @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") | ||||
|             self._group_id_cache = group.id | ||||
|         return self._group_id_cache | ||||
|  | ||||
|     def _existing_factory(dependency: Type[CLS_DEP]) -> classmethod: | ||||
|         def cls_method(cls, item_id: T, deps: CLS_DEP = Depends(dependency)): | ||||
|             new_class = cls(deps.session, deps.user, deps.bg_task) | ||||
| @@ -89,21 +81,42 @@ class BaseHttpService(Generic[T, D], ABC): | ||||
|  | ||||
|         return classmethod(cls_method) | ||||
|  | ||||
|     # TODO: Refactor to allow for configurable dependencies base on substantiation | ||||
|     read_existing = _existing_factory(PublicDeps) | ||||
|     write_existing = _existing_factory(UserDeps) | ||||
|     @classmethod | ||||
|     @abstractmethod | ||||
|     def public(cls, deps: Any): | ||||
|         pass | ||||
|  | ||||
|     public = _class_method_factory(PublicDeps) | ||||
|     private = _class_method_factory(UserDeps) | ||||
|     @classmethod | ||||
|     @abstractmethod | ||||
|     def private(cls, deps: Any): | ||||
|         pass | ||||
|  | ||||
|     @classmethod | ||||
|     @abstractmethod | ||||
|     def read_existing(cls, deps: Any): | ||||
|         pass | ||||
|  | ||||
|     @classmethod | ||||
|     @abstractmethod | ||||
|     def write_existing(cls, deps: Any): | ||||
|         pass | ||||
|  | ||||
|     @abstractmethod | ||||
|     def populate_item(self) -> None: | ||||
|         pass | ||||
|  | ||||
|     @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") | ||||
|             self._group_id_cache = group.id | ||||
|         return self._group_id_cache | ||||
|  | ||||
|     def assert_existing(self, id: T) -> None: | ||||
|         self.populate_item(id) | ||||
|         self._check_item() | ||||
|  | ||||
|     @abstractmethod | ||||
|     def populate_item(self) -> None: | ||||
|         ... | ||||
|  | ||||
|     def _check_item(self) -> None: | ||||
|         if not self.item: | ||||
|             raise HTTPException(status.HTTP_404_NOT_FOUND) | ||||
| @@ -114,8 +127,38 @@ class BaseHttpService(Generic[T, D], ABC): | ||||
|             if not group_id or group_id != self.group_id: | ||||
|                 raise HTTPException(status.HTTP_403_FORBIDDEN) | ||||
|  | ||||
|         if hasattr(self, "check_item"): | ||||
|             self.check_item() | ||||
|  | ||||
|     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) | ||||
|  | ||||
|     # Generic CRUD Functions | ||||
|     def _create_one(self, data: Any, exception_msg="generic-create-error") -> D: | ||||
|         try: | ||||
|             self.item = self.db_access.create(self.session, data) | ||||
|         except Exception as ex: | ||||
|             logger.exception(ex) | ||||
|             raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": exception_msg, "exception": str(ex)}) | ||||
|  | ||||
|         return self.item | ||||
|  | ||||
|     def _update_one(self, data: Any, id: int = None) -> D: | ||||
|         if not self.item: | ||||
|             return | ||||
|  | ||||
|         target_id = id or self.item.id | ||||
|         self.item = self.db_access.update(self.session, target_id, data) | ||||
|  | ||||
|         return self.item | ||||
|  | ||||
|     def _delete_one(self, id: int = None) -> D: | ||||
|         if not self.item: | ||||
|             return | ||||
|  | ||||
|         target_id = id or self.item.id | ||||
|         self.item = self.db_access.delete(self.session, target_id) | ||||
|         return self.item | ||||
|   | ||||
| @@ -1,17 +0,0 @@ | ||||
| from mealie.core.config import get_app_dirs, get_settings | ||||
| from mealie.core.root_logger import get_logger | ||||
| from mealie.db.database import get_database | ||||
| from mealie.db.db_setup import generate_session | ||||
|  | ||||
| logger = get_logger() | ||||
|  | ||||
|  | ||||
| class BaseService: | ||||
|     def __init__(self) -> None: | ||||
|         # Static Globals Dependency Injection | ||||
|         self.db = get_database() | ||||
|         self.app_dirs = get_app_dirs() | ||||
|         self.settings = get_settings() | ||||
|  | ||||
|     def session_context(self): | ||||
|         return generate_session() | ||||
| @@ -1,5 +1,5 @@ | ||||
| import inspect | ||||
| from typing import Any, Callable, Optional, Sequence, Type, TypeVar | ||||
| from typing import Any, Callable, Optional, Sequence, Type, TypeVar, get_type_hints | ||||
|  | ||||
| from fastapi import APIRouter | ||||
| from fastapi.params import Depends | ||||
| @@ -8,7 +8,7 @@ from pydantic import BaseModel | ||||
|  | ||||
| from .base_http_service import BaseHttpService | ||||
|  | ||||
| """" | ||||
| """ | ||||
| This code is largely based off of the FastAPI Crud Router | ||||
| https://github.com/awtkns/fastapi-crudrouter/blob/master/fastapi_crudrouter/core/_base.py | ||||
| """ | ||||
| @@ -18,25 +18,40 @@ S = TypeVar("S", bound=BaseHttpService) | ||||
| DEPENDENCIES = Optional[Sequence[Depends]] | ||||
|  | ||||
|  | ||||
| def get_return(func: Callable, default) -> Type: | ||||
|     return get_type_hints(func).get("return", default) | ||||
|  | ||||
|  | ||||
| def get_func_args(func: Callable) -> Sequence[str]: | ||||
|     for _, value in get_type_hints(func).items(): | ||||
|         if value: | ||||
|             return value | ||||
|         else: | ||||
|             return None | ||||
|  | ||||
|  | ||||
| class RouterFactory(APIRouter): | ||||
|  | ||||
|     schema: Type[T] | ||||
|     create_schema: Type[T] | ||||
|     update_schema: Type[T] | ||||
|     _base_path: str = "/" | ||||
|  | ||||
|     def __init__(self, service: Type[S], prefix: Optional[str] = None, tags: Optional[list[str]] = None, *_, **kwargs): | ||||
|         """ | ||||
|         RouterFactory takes a concrete service class derived from the BaseHttpService class and returns common | ||||
|         CRUD Routes for the service. The following features are implmeneted in the RouterFactory: | ||||
|  | ||||
|         1. API endpoint Descriptions are read from the docstrings of the methods in the passed in service class | ||||
|         2. Return types are inferred from the concrete service schema, or specified from the return type annotations. | ||||
|         This provides flexibility to return different types based on each route depending on client needs. | ||||
|         3. Arguemnt types are inferred for Post and Put routes where the first type annotated argument is the data that | ||||
|         is beging posted or updated. Note that this is only done for the first argument of the method. | ||||
|         4. The Get and Delete routes assume that you've defined the `write_existing` and `read_existing` methods in the | ||||
|         service class. The dependencies defined in the `write_existing` and `read_existing` methods are passed directly | ||||
|         to the FastAPI router and as such should include the `item_id` or equilivent argument. | ||||
|         """ | ||||
|         self.service: Type[S] = service | ||||
|         self.schema: Type[T] = service._schema | ||||
|  | ||||
|         # HACK: Special Case for Coobooks, not sure this is a good way to handle the abstraction :/ | ||||
|         if hasattr(self.service, "_get_one_schema"): | ||||
|             self.get_one_schema = self.service._get_one_schema | ||||
|         else: | ||||
|             self.get_one_schema = self.schema | ||||
|  | ||||
|         self.update_schema: Type[T] = service._update_schema | ||||
|         self.create_schema: Type[T] = service._create_schema | ||||
|  | ||||
|         prefix = str(prefix or self.schema.__name__).lower() | ||||
|         prefix = self._base_path + prefix.strip("/") | ||||
|         tags = tags or [prefix.strip("/").capitalize()] | ||||
| @@ -88,7 +103,7 @@ class RouterFactory(APIRouter): | ||||
|                 "/{item_id}", | ||||
|                 self._get_one(), | ||||
|                 methods=["GET"], | ||||
|                 response_model=self.get_one_schema, | ||||
|                 response_model=get_type_hints(self.service.populate_item).get("return", self.schema), | ||||
|                 summary="Get One", | ||||
|                 description=inspect.cleandoc(self.service.populate_item.__doc__ or ""), | ||||
|             ) | ||||
| @@ -104,7 +119,6 @@ class RouterFactory(APIRouter): | ||||
|             ) | ||||
|  | ||||
|         if self.service.delete_one: | ||||
|             print(self.service.delete_one.__doc__) | ||||
|             self._add_api_route( | ||||
|                 "/{item_id}", | ||||
|                 self._delete_one(), | ||||
| @@ -160,19 +174,25 @@ class RouterFactory(APIRouter): | ||||
|         return route | ||||
|  | ||||
|     def _create(self, *args: Any, **kwargs: Any) -> Callable[..., Any]: | ||||
|         def route(data: self.create_schema, service: S = Depends(self.service.private)) -> T:  # type: ignore | ||||
|         create_schema = get_func_args(self.service.create_one) or self.schema | ||||
|  | ||||
|         def route(data: create_schema, service: S = Depends(self.service.private)) -> T:  # type: ignore | ||||
|             return service.create_one(data) | ||||
|  | ||||
|         return route | ||||
|  | ||||
|     def _update(self, *args: Any, **kwargs: Any) -> Callable[..., Any]: | ||||
|         def route(data: self.update_schema, service: S = Depends(self.service.write_existing)) -> T:  # type: ignore | ||||
|         update_schema = get_func_args(self.service.update_one) or self.schema | ||||
|  | ||||
|         def route(data: update_schema, service: S = Depends(self.service.write_existing)) -> T:  # type: ignore | ||||
|             return service.update_one(data) | ||||
|  | ||||
|         return route | ||||
|  | ||||
|     def _update_many(self, *args: Any, **kwargs: Any) -> Callable[..., Any]: | ||||
|         def route(data: list[self.update_schema], service: S = Depends(self.service.write_existing)) -> T:  # type: ignore | ||||
|         update_many_schema = get_func_args(self.service.update_many) or list[self.schema] | ||||
|  | ||||
|         def route(data: update_many_schema, service: S = Depends(self.service.private)) -> T:  # type: ignore | ||||
|             return service.update_many(data) | ||||
|  | ||||
|         return route | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| from fastapi import HTTPException, status | ||||
|  | ||||
| from mealie.core.root_logger import get_logger | ||||
| from mealie.db.database import get_database | ||||
| from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook | ||||
| from mealie.services.base_http_service.http_services import UserHttpService | ||||
| from mealie.services.events import create_group_event | ||||
| from mealie.utils.error_messages import ErrorMessages | ||||
|  | ||||
| logger = get_logger(module=__name__) | ||||
|  | ||||
| @@ -15,11 +15,10 @@ class CookbookService(UserHttpService[int, ReadCookBook]): | ||||
|     _restrict_by_group = True | ||||
|  | ||||
|     _schema = ReadCookBook | ||||
|     _create_schema = CreateCookBook | ||||
|     _update_schema = UpdateCookBook | ||||
|     _get_one_schema = RecipeCookBook | ||||
|  | ||||
|     def populate_item(self, item_id: int | str): | ||||
|     db_access = get_database().cookbooks | ||||
|  | ||||
|     def populate_item(self, item_id: int) -> RecipeCookBook: | ||||
|         try: | ||||
|             item_id = int(item_id) | ||||
|         except Exception: | ||||
| @@ -37,25 +36,13 @@ class CookbookService(UserHttpService[int, ReadCookBook]): | ||||
|         return items | ||||
|  | ||||
|     def create_one(self, data: CreateCookBook) -> ReadCookBook: | ||||
|         try: | ||||
|             self.item = 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)} | ||||
|             ) | ||||
|         data = SaveCookBook(group_id=self.group_id, **data.dict()) | ||||
|         return self._create_one(data, ErrorMessages.cookbook_create_failure) | ||||
|  | ||||
|         return self.item | ||||
|     def update_one(self, data: UpdateCookBook, id: int = None) -> ReadCookBook: | ||||
|         return self._update_one(data, id) | ||||
|  | ||||
|     def update_one(self, data: CreateCookBook, id: int = None) -> ReadCookBook: | ||||
|         if not self.item: | ||||
|             return | ||||
|  | ||||
|         target_id = id or self.item.id | ||||
|         self.item = self.db.cookbooks.update(self.session, target_id, data) | ||||
|  | ||||
|         return self.item | ||||
|  | ||||
|     def update_many(self, data: list[ReadCookBook]) -> list[ReadCookBook]: | ||||
|     def update_many(self, data: list[UpdateCookBook]) -> list[ReadCookBook]: | ||||
|         updated = [] | ||||
|  | ||||
|         for cookbook in data: | ||||
| @@ -65,10 +52,4 @@ class CookbookService(UserHttpService[int, ReadCookBook]): | ||||
|         return updated | ||||
|  | ||||
|     def delete_one(self, id: int = None) -> ReadCookBook: | ||||
|         if not self.item: | ||||
|             return | ||||
|  | ||||
|         target_id = id or self.item.id | ||||
|         self.item = self.db.cookbooks.delete(self.session, target_id) | ||||
|  | ||||
|         return self.item | ||||
|         return self._delete_one(id) | ||||
|   | ||||
| @@ -6,13 +6,13 @@ from mealie.core.dependencies.grouped import UserDeps | ||||
| from mealie.core.root_logger import get_logger | ||||
| from mealie.schema.recipe.recipe_category import CategoryBase | ||||
| from mealie.schema.user.user import GroupInDB | ||||
| from mealie.services.base_http_service.base_http_service import BaseHttpService | ||||
| from mealie.services.base_http_service.http_services import UserHttpService | ||||
| from mealie.services.events import create_group_event | ||||
|  | ||||
| logger = get_logger(module=__name__) | ||||
|  | ||||
|  | ||||
| class GroupSelfService(BaseHttpService[int, str]): | ||||
| class GroupSelfService(UserHttpService[int, str]): | ||||
|     _restrict_by_group = True | ||||
|     event_func = create_group_event | ||||
|     item: GroupInDB | ||||
| @@ -36,8 +36,9 @@ class GroupSelfService(BaseHttpService[int, str]): | ||||
|         if self.item.id != self.group_id: | ||||
|             raise HTTPException(status.HTTP_403_FORBIDDEN) | ||||
|  | ||||
|     def populate_item(self, _: str = None): | ||||
|     def populate_item(self, _: str = None) -> GroupInDB: | ||||
|         self.item = self.db.groups.get(self.session, self.group_id) | ||||
|         return self.item | ||||
|  | ||||
|     def update_categories(self, new_categories: list[CategoryBase]): | ||||
|         if not self.item: | ||||
|   | ||||
| @@ -5,13 +5,13 @@ from fastapi import HTTPException, status | ||||
| from mealie.core.root_logger import get_logger | ||||
| from mealie.schema.group import ReadWebhook | ||||
| from mealie.schema.group.webhook import CreateWebhook, SaveWebhook | ||||
| from mealie.services.base_http_service.base_http_service import BaseHttpService | ||||
| from mealie.services.base_http_service.http_services import UserHttpService | ||||
| from mealie.services.events import create_group_event | ||||
|  | ||||
| logger = get_logger(module=__name__) | ||||
|  | ||||
|  | ||||
| class WebhookService(BaseHttpService[int, ReadWebhook]): | ||||
| class WebhookService(UserHttpService[int, ReadWebhook]): | ||||
|     event_func = create_group_event | ||||
|     _restrict_by_group = True | ||||
|  | ||||
| @@ -19,8 +19,9 @@ class WebhookService(BaseHttpService[int, ReadWebhook]): | ||||
|     _create_schema = CreateWebhook | ||||
|     _update_schema = CreateWebhook | ||||
|  | ||||
|     def populate_item(self, id: int | str): | ||||
|     def populate_item(self, id: int) -> ReadWebhook: | ||||
|         self.item = self.db.webhooks.get_one(self.session, id) | ||||
|         return self.item | ||||
|  | ||||
|     def get_all(self) -> list[ReadWebhook]: | ||||
|         return self.db.webhooks.get(self.session, self.group_id, match_key="group_id", limit=9999) | ||||
|   | ||||
| @@ -8,13 +8,13 @@ from sqlalchemy.exc import IntegrityError | ||||
| from mealie.core.dependencies.grouped import PublicDeps, UserDeps | ||||
| 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 | ||||
| from mealie.services.base_http_service.http_services import PublicHttpService | ||||
| from mealie.services.events import create_recipe_event | ||||
|  | ||||
| logger = get_logger(module=__name__) | ||||
|  | ||||
|  | ||||
| class RecipeService(BaseHttpService[str, Recipe]): | ||||
| class RecipeService(PublicHttpService[str, Recipe]): | ||||
|     """ | ||||
|     Class Methods: | ||||
|         `read_existing`: Reads an existing recipe from the database. | ||||
|   | ||||
| @@ -3,13 +3,13 @@ from fastapi import HTTPException, status | ||||
| from mealie.core.root_logger import get_logger | ||||
| from mealie.core.security import hash_password, verify_password | ||||
| from mealie.schema.user.user import ChangePassword, PrivateUser | ||||
| from mealie.services.base_http_service.base_http_service import BaseHttpService | ||||
| from mealie.services.base_http_service.http_services import UserHttpService | ||||
| from mealie.services.events import create_user_event | ||||
|  | ||||
| logger = get_logger(module=__name__) | ||||
|  | ||||
|  | ||||
| class UserService(BaseHttpService[int, str]): | ||||
| class UserService(UserHttpService[int, str]): | ||||
|     event_func = create_user_event | ||||
|     acting_user: PrivateUser = None | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user