mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -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:
parent
b550dae593
commit
3d87ffc3a5
@ -96,7 +96,7 @@ class BaseAccessModel(Generic[T, D]):
|
|||||||
if any_case:
|
if any_case:
|
||||||
search_attr = getattr(self.sql_model, key)
|
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(func.lower(search_attr) == key.lower()).one_or_none()
|
||||||
|
else:
|
||||||
result = session.query(self.sql_model).filter_by(**{key: value}).one_or_none()
|
result = session.query(self.sql_model).filter_by(**{key: value}).one_or_none()
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
|
@ -3,9 +3,10 @@ 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.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.group.webhooks import GroupWebhooksModel
|
||||||
from mealie.db.models.mealplan import MealPlan
|
from mealie.db.models.mealplan import MealPlan
|
||||||
from mealie.db.models.recipe.category import Category
|
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.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 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.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 SiteSettings as SiteSettingsSchema
|
from mealie.schema.admin import SiteSettings as SiteSettingsSchema
|
||||||
@ -39,7 +39,9 @@ from .user_access_model import UserDataAccessModel
|
|||||||
logger = getLogger()
|
logger = getLogger()
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_PK = "id"
|
pk_id = "id"
|
||||||
|
pk_slug = "slug"
|
||||||
|
pk_token = "token"
|
||||||
|
|
||||||
|
|
||||||
class CategoryDataAccessModel(BaseAccessModel):
|
class CategoryDataAccessModel(BaseAccessModel):
|
||||||
@ -53,6 +55,7 @@ class TagsDataAccessModel(BaseAccessModel):
|
|||||||
|
|
||||||
|
|
||||||
class DatabaseAccessLayer:
|
class DatabaseAccessLayer:
|
||||||
|
def __init__(self) -> None:
|
||||||
"""
|
"""
|
||||||
`DatabaseAccessLayer` class is the data access layer for all database actions within
|
`DatabaseAccessLayer` class is the data access layer for all database actions within
|
||||||
Mealie. Database uses composition from classes derived from BaseAccessModel. These
|
Mealie. Database uses composition from classes derived from BaseAccessModel. These
|
||||||
@ -60,31 +63,29 @@ class DatabaseAccessLayer:
|
|||||||
additional methods are required.
|
additional methods are required.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
|
|
||||||
# Recipes
|
# Recipes
|
||||||
self.recipes = RecipeDataAccessModel("slug", RecipeModel, Recipe)
|
self.recipes = RecipeDataAccessModel(pk_slug, RecipeModel, Recipe)
|
||||||
self.ingredient_foods = BaseAccessModel(DEFAULT_PK, IngredientFoodModel, IngredientFood)
|
self.ingredient_foods = BaseAccessModel(pk_id, IngredientFoodModel, IngredientFood)
|
||||||
self.ingredient_units = BaseAccessModel(DEFAULT_PK, IngredientUnitModel, IngredientUnit)
|
self.ingredient_units = BaseAccessModel(pk_id, IngredientUnitModel, IngredientUnit)
|
||||||
self.comments = BaseAccessModel(DEFAULT_PK, RecipeComment, CommentOut)
|
self.comments = BaseAccessModel(pk_id, RecipeComment, CommentOut)
|
||||||
|
|
||||||
# Tags and Categories
|
# Tags and Categories
|
||||||
self.categories = CategoryDataAccessModel("slug", Category, RecipeCategoryResponse)
|
self.categories = CategoryDataAccessModel(pk_slug, Category, RecipeCategoryResponse)
|
||||||
self.tags = TagsDataAccessModel("slug", Tag, RecipeTagResponse)
|
self.tags = TagsDataAccessModel(pk_slug, Tag, RecipeTagResponse)
|
||||||
|
|
||||||
# Site
|
# Site
|
||||||
self.settings = BaseAccessModel(DEFAULT_PK, SiteSettings, SiteSettingsSchema)
|
self.settings = BaseAccessModel(pk_id, SiteSettings, SiteSettingsSchema)
|
||||||
self.sign_ups = BaseAccessModel("token", SignUp, SignUpOut)
|
self.sign_ups = BaseAccessModel(pk_token, SignUp, SignUpOut)
|
||||||
self.event_notifications = BaseAccessModel(DEFAULT_PK, EventNotification, EventNotificationIn)
|
self.event_notifications = BaseAccessModel(pk_id, EventNotification, EventNotificationIn)
|
||||||
self.events = BaseAccessModel(DEFAULT_PK, Event, EventSchema)
|
self.events = BaseAccessModel(pk_id, Event, EventSchema)
|
||||||
|
|
||||||
# Users
|
# Users
|
||||||
self.users = UserDataAccessModel(DEFAULT_PK, User, PrivateUser)
|
self.users = UserDataAccessModel(pk_id, User, PrivateUser)
|
||||||
self.api_tokens = BaseAccessModel(DEFAULT_PK, LongLiveToken, LongLiveTokenInDB)
|
self.api_tokens = BaseAccessModel(pk_id, LongLiveToken, LongLiveTokenInDB)
|
||||||
|
|
||||||
# Group Data
|
# Group Data
|
||||||
self.groups = GroupDataAccessModel(DEFAULT_PK, Group, GroupInDB)
|
self.groups = GroupDataAccessModel(pk_id, Group, GroupInDB)
|
||||||
self.meals = BaseAccessModel(DEFAULT_PK, MealPlan, MealPlanOut)
|
self.meals = BaseAccessModel(pk_id, MealPlan, MealPlanOut)
|
||||||
self.webhooks = BaseAccessModel(DEFAULT_PK, GroupWebhooksModel, ReadWebhook)
|
self.webhooks = BaseAccessModel(pk_id, GroupWebhooksModel, ReadWebhook)
|
||||||
self.shopping_lists = BaseAccessModel(DEFAULT_PK, ShoppingList, ShoppingListOut)
|
self.shopping_lists = BaseAccessModel(pk_id, ShoppingList, ShoppingListOut)
|
||||||
self.cookbooks = BaseAccessModel(DEFAULT_PK, CookBook, ReadCookBook)
|
self.cookbooks = BaseAccessModel(pk_id, CookBook, ReadCookBook)
|
||||||
|
@ -3,6 +3,5 @@ from .group import *
|
|||||||
from .mealplan import *
|
from .mealplan import *
|
||||||
from .recipe.recipe import *
|
from .recipe.recipe import *
|
||||||
from .settings import *
|
from .settings import *
|
||||||
from .shopping_list import *
|
|
||||||
from .sign_up import *
|
from .sign_up import *
|
||||||
from .users import *
|
from .users import *
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import uuid
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import Column, DateTime, Integer
|
from sqlalchemy import Column, DateTime, Integer
|
||||||
@ -7,22 +6,11 @@ from sqlalchemy.orm import declarative_base
|
|||||||
from sqlalchemy.orm.session import Session
|
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()
|
@as_declarative()
|
||||||
class Base:
|
class Base:
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
created_at = Column(DateTime, default=datetime.now())
|
created_at = Column(DateTime, default=datetime.now())
|
||||||
|
update_at = Column(DateTime, default=datetime.now(), onupdate=datetime.now())
|
||||||
# @declared_attr
|
|
||||||
# def __tablename__(cls):
|
|
||||||
# return cls.__name__.lower()
|
|
||||||
|
|
||||||
|
|
||||||
class BaseMixins:
|
class BaseMixins:
|
||||||
|
@ -2,6 +2,8 @@ from sqlalchemy import Boolean, Column, DateTime, Integer, String
|
|||||||
|
|
||||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||||
|
|
||||||
|
from ._model_utils import auto_init
|
||||||
|
|
||||||
|
|
||||||
class EventNotification(SqlAlchemyBase, BaseMixins):
|
class EventNotification(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "event_notifications"
|
__tablename__ = "event_notifications"
|
||||||
@ -19,19 +21,9 @@ class EventNotification(SqlAlchemyBase, BaseMixins):
|
|||||||
group = Column(Boolean, default=False)
|
group = Column(Boolean, default=False)
|
||||||
user = Column(Boolean, default=False)
|
user = Column(Boolean, default=False)
|
||||||
|
|
||||||
def __init__(
|
@auto_init()
|
||||||
self, name, notification_url, type, general, recipe, backup, scheduled, migration, group, user, **_
|
def __init__(self, **_) -> None:
|
||||||
) -> None:
|
pass
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class Event(SqlAlchemyBase, BaseMixins):
|
class Event(SqlAlchemyBase, BaseMixins):
|
||||||
@ -42,8 +34,6 @@ class Event(SqlAlchemyBase, BaseMixins):
|
|||||||
time_stamp = Column(DateTime)
|
time_stamp = Column(DateTime)
|
||||||
category = Column(String)
|
category = Column(String)
|
||||||
|
|
||||||
def __init__(self, title, text, time_stamp, category, **_) -> None:
|
@auto_init()
|
||||||
self.title = title
|
def __init__(self, **_) -> None:
|
||||||
self.text = text
|
pass
|
||||||
self.time_stamp = time_stamp
|
|
||||||
self.category = category
|
|
||||||
|
@ -1 +1,3 @@
|
|||||||
from .group import *
|
from .group import *
|
||||||
|
from .shopping_list import *
|
||||||
|
from .webhooks import *
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from sqlalchemy import Column, ForeignKey, Integer, String, orm
|
from sqlalchemy import Column, ForeignKey, Integer, String, orm
|
||||||
|
|
||||||
from ._model_base import BaseMixins, SqlAlchemyBase
|
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||||
from ._model_utils import auto_init
|
from .._model_utils import auto_init
|
||||||
from .recipe.category import Category, cookbooks_to_categories
|
from ..recipe.category import Category, cookbooks_to_categories
|
||||||
|
|
||||||
|
|
||||||
class CookBook(SqlAlchemyBase, BaseMixins):
|
class CookBook(SqlAlchemyBase, BaseMixins):
|
@ -3,12 +3,12 @@ import sqlalchemy.orm as orm
|
|||||||
from sqlalchemy.orm.session import Session
|
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.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 .._model_utils import auto_init
|
||||||
|
from ..group.webhooks import GroupWebhooksModel
|
||||||
|
from ..recipe.category import Category, group2categories
|
||||||
|
from .cookbook import CookBook
|
||||||
|
|
||||||
|
|
||||||
class Group(SqlAlchemyBase, BaseMixins):
|
class Group(SqlAlchemyBase, BaseMixins):
|
||||||
|
@ -3,8 +3,8 @@ from requests import Session
|
|||||||
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
|
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
|
||||||
from sqlalchemy.ext.orderinglist import ordering_list
|
from sqlalchemy.ext.orderinglist import ordering_list
|
||||||
|
|
||||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||||
from mealie.db.models.group import Group
|
from .group import Group
|
||||||
|
|
||||||
|
|
||||||
class ShoppingListItem(SqlAlchemyBase, BaseMixins):
|
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._model_base import BaseMixins, SqlAlchemyBase
|
||||||
from mealie.db.models.group import Group
|
from mealie.db.models.group import Group
|
||||||
from mealie.db.models.recipe.recipe import RecipeModel
|
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):
|
class Meal(SqlAlchemyBase):
|
||||||
|
@ -2,6 +2,8 @@ from sqlalchemy import Boolean, Column, Integer, String
|
|||||||
|
|
||||||
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
|
||||||
|
|
||||||
|
from ._model_utils import auto_init
|
||||||
|
|
||||||
|
|
||||||
class SignUp(SqlAlchemyBase, BaseMixins):
|
class SignUp(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "sign_ups"
|
__tablename__ = "sign_ups"
|
||||||
@ -10,13 +12,6 @@ class SignUp(SqlAlchemyBase, BaseMixins):
|
|||||||
name = Column(String, index=True)
|
name = Column(String, index=True)
|
||||||
admin = Column(Boolean, default=False)
|
admin = Column(Boolean, default=False)
|
||||||
|
|
||||||
def __init__(
|
@auto_init()
|
||||||
self,
|
def __init__(self, **_) -> None:
|
||||||
session,
|
pass
|
||||||
token,
|
|
||||||
name,
|
|
||||||
admin,
|
|
||||||
) -> None:
|
|
||||||
self.token = token
|
|
||||||
self.name = name
|
|
||||||
self.admin = admin
|
|
||||||
|
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)
|
username = Column(String, index=True, unique=True)
|
||||||
email = Column(String, unique=True, index=True)
|
email = Column(String, unique=True, index=True)
|
||||||
password = Column(String)
|
password = Column(String)
|
||||||
|
admin = Column(Boolean, default=False)
|
||||||
|
|
||||||
group_id = Column(Integer, ForeignKey("groups.id"))
|
group_id = Column(Integer, ForeignKey("groups.id"))
|
||||||
group = orm.relationship("Group", back_populates="users")
|
group = orm.relationship("Group", back_populates="users")
|
||||||
admin = Column(Boolean, default=False)
|
|
||||||
tokens: list[LongLiveToken] = orm.relationship(
|
tokens: list[LongLiveToken] = orm.relationship(
|
||||||
LongLiveToken, back_populates="user", cascade="all, delete, delete-orphan", single_parent=True
|
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 fastapi_camelcase import CamelModel
|
||||||
from pydantic.utils import GetterDict
|
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):
|
class ListItem(CamelModel):
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
from .base_http_service import *
|
from .http_services import *
|
||||||
from .base_service import *
|
|
||||||
from .router_factory import *
|
from .router_factory import *
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
from abc import ABC, abstractmethod
|
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 fastapi import BackgroundTasks, Depends, HTTPException, status
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.core.config import get_app_dirs, get_settings
|
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.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.database import get_database
|
||||||
from mealie.db.db_setup import SessionLocal
|
from mealie.db.db_setup import SessionLocal
|
||||||
from mealie.schema.user.user import PrivateUser
|
from mealie.schema.user.user import PrivateUser
|
||||||
@ -44,10 +44,10 @@ class BaseHttpService(Generic[T, D], ABC):
|
|||||||
delete_one: Callable = None
|
delete_one: Callable = None
|
||||||
delete_all: Callable = None
|
delete_all: Callable = None
|
||||||
|
|
||||||
|
db_access: DatabaseAccessLayer = None
|
||||||
|
|
||||||
# Type Definitions
|
# Type Definitions
|
||||||
_schema = None
|
_schema = None
|
||||||
_create_schema = None
|
|
||||||
_update_schema = None
|
|
||||||
|
|
||||||
# Function called to create a server side event
|
# Function called to create a server side event
|
||||||
event_func: Callable = None
|
event_func: Callable = None
|
||||||
@ -67,14 +67,6 @@ class BaseHttpService(Generic[T, D], ABC):
|
|||||||
self.app_dirs = get_app_dirs()
|
self.app_dirs = get_app_dirs()
|
||||||
self.settings = get_settings()
|
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 _existing_factory(dependency: Type[CLS_DEP]) -> classmethod:
|
||||||
def cls_method(cls, item_id: T, deps: CLS_DEP = Depends(dependency)):
|
def cls_method(cls, item_id: T, deps: CLS_DEP = Depends(dependency)):
|
||||||
new_class = cls(deps.session, deps.user, deps.bg_task)
|
new_class = cls(deps.session, deps.user, deps.bg_task)
|
||||||
@ -89,21 +81,42 @@ class BaseHttpService(Generic[T, D], ABC):
|
|||||||
|
|
||||||
return classmethod(cls_method)
|
return classmethod(cls_method)
|
||||||
|
|
||||||
# TODO: Refactor to allow for configurable dependencies base on substantiation
|
@classmethod
|
||||||
read_existing = _existing_factory(PublicDeps)
|
@abstractmethod
|
||||||
write_existing = _existing_factory(UserDeps)
|
def public(cls, deps: Any):
|
||||||
|
pass
|
||||||
|
|
||||||
public = _class_method_factory(PublicDeps)
|
@classmethod
|
||||||
private = _class_method_factory(UserDeps)
|
@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:
|
def assert_existing(self, id: T) -> None:
|
||||||
self.populate_item(id)
|
self.populate_item(id)
|
||||||
self._check_item()
|
self._check_item()
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def populate_item(self) -> None:
|
|
||||||
...
|
|
||||||
|
|
||||||
def _check_item(self) -> None:
|
def _check_item(self) -> None:
|
||||||
if not self.item:
|
if not self.item:
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
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:
|
if not group_id or group_id != self.group_id:
|
||||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
if hasattr(self, "check_item"):
|
||||||
|
self.check_item()
|
||||||
|
|
||||||
def _create_event(self, title: str, message: str) -> None:
|
def _create_event(self, title: str, message: str) -> None:
|
||||||
if not self.__class__.event_func:
|
if not self.__class__.event_func:
|
||||||
raise NotImplementedError("`event_func` must be set by child class")
|
raise NotImplementedError("`event_func` must be set by child class")
|
||||||
|
|
||||||
self.background_tasks.add_task(self.__class__.event_func, title, message, self.session)
|
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
|
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 import APIRouter
|
||||||
from fastapi.params import Depends
|
from fastapi.params import Depends
|
||||||
@ -8,7 +8,7 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
from .base_http_service import BaseHttpService
|
from .base_http_service import BaseHttpService
|
||||||
|
|
||||||
""""
|
"""
|
||||||
This code is largely based off of the FastAPI Crud Router
|
This code is largely based off of the FastAPI Crud Router
|
||||||
https://github.com/awtkns/fastapi-crudrouter/blob/master/fastapi_crudrouter/core/_base.py
|
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]]
|
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):
|
class RouterFactory(APIRouter):
|
||||||
|
|
||||||
schema: Type[T]
|
schema: Type[T]
|
||||||
create_schema: Type[T]
|
|
||||||
update_schema: Type[T]
|
|
||||||
_base_path: str = "/"
|
_base_path: str = "/"
|
||||||
|
|
||||||
def __init__(self, service: Type[S], prefix: Optional[str] = None, tags: Optional[list[str]] = None, *_, **kwargs):
|
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.service: Type[S] = service
|
||||||
self.schema: Type[T] = service._schema
|
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 = str(prefix or self.schema.__name__).lower()
|
||||||
prefix = self._base_path + prefix.strip("/")
|
prefix = self._base_path + prefix.strip("/")
|
||||||
tags = tags or [prefix.strip("/").capitalize()]
|
tags = tags or [prefix.strip("/").capitalize()]
|
||||||
@ -88,7 +103,7 @@ class RouterFactory(APIRouter):
|
|||||||
"/{item_id}",
|
"/{item_id}",
|
||||||
self._get_one(),
|
self._get_one(),
|
||||||
methods=["GET"],
|
methods=["GET"],
|
||||||
response_model=self.get_one_schema,
|
response_model=get_type_hints(self.service.populate_item).get("return", self.schema),
|
||||||
summary="Get One",
|
summary="Get One",
|
||||||
description=inspect.cleandoc(self.service.populate_item.__doc__ or ""),
|
description=inspect.cleandoc(self.service.populate_item.__doc__ or ""),
|
||||||
)
|
)
|
||||||
@ -104,7 +119,6 @@ class RouterFactory(APIRouter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if self.service.delete_one:
|
if self.service.delete_one:
|
||||||
print(self.service.delete_one.__doc__)
|
|
||||||
self._add_api_route(
|
self._add_api_route(
|
||||||
"/{item_id}",
|
"/{item_id}",
|
||||||
self._delete_one(),
|
self._delete_one(),
|
||||||
@ -160,19 +174,25 @@ class RouterFactory(APIRouter):
|
|||||||
return route
|
return route
|
||||||
|
|
||||||
def _create(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
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 service.create_one(data)
|
||||||
|
|
||||||
return route
|
return route
|
||||||
|
|
||||||
def _update(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
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 service.update_one(data)
|
||||||
|
|
||||||
return route
|
return route
|
||||||
|
|
||||||
def _update_many(self, *args: Any, **kwargs: Any) -> Callable[..., Any]:
|
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 service.update_many(data)
|
||||||
|
|
||||||
return route
|
return route
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from fastapi import HTTPException, status
|
|
||||||
|
|
||||||
from mealie.core.root_logger import get_logger
|
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.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook
|
||||||
from mealie.services.base_http_service.http_services import UserHttpService
|
from mealie.services.base_http_service.http_services import UserHttpService
|
||||||
from mealie.services.events import create_group_event
|
from mealie.services.events import create_group_event
|
||||||
|
from mealie.utils.error_messages import ErrorMessages
|
||||||
|
|
||||||
logger = get_logger(module=__name__)
|
logger = get_logger(module=__name__)
|
||||||
|
|
||||||
@ -15,11 +15,10 @@ class CookbookService(UserHttpService[int, ReadCookBook]):
|
|||||||
_restrict_by_group = True
|
_restrict_by_group = True
|
||||||
|
|
||||||
_schema = ReadCookBook
|
_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:
|
try:
|
||||||
item_id = int(item_id)
|
item_id = int(item_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -37,25 +36,13 @@ class CookbookService(UserHttpService[int, ReadCookBook]):
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
def create_one(self, data: CreateCookBook) -> ReadCookBook:
|
def create_one(self, data: CreateCookBook) -> ReadCookBook:
|
||||||
try:
|
data = SaveCookBook(group_id=self.group_id, **data.dict())
|
||||||
self.item = self.db.cookbooks.create(self.session, SaveCookBook(group_id=self.group_id, **data.dict()))
|
return self._create_one(data, ErrorMessages.cookbook_create_failure)
|
||||||
except Exception as ex:
|
|
||||||
raise HTTPException(
|
|
||||||
status.HTTP_400_BAD_REQUEST, detail={"message": "PAGE_CREATION_ERROR", "exception": str(ex)}
|
|
||||||
)
|
|
||||||
|
|
||||||
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:
|
def update_many(self, data: list[UpdateCookBook]) -> list[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]:
|
|
||||||
updated = []
|
updated = []
|
||||||
|
|
||||||
for cookbook in data:
|
for cookbook in data:
|
||||||
@ -65,10 +52,4 @@ class CookbookService(UserHttpService[int, ReadCookBook]):
|
|||||||
return updated
|
return updated
|
||||||
|
|
||||||
def delete_one(self, id: int = None) -> ReadCookBook:
|
def delete_one(self, id: int = None) -> ReadCookBook:
|
||||||
if not self.item:
|
return self._delete_one(id)
|
||||||
return
|
|
||||||
|
|
||||||
target_id = id or self.item.id
|
|
||||||
self.item = self.db.cookbooks.delete(self.session, target_id)
|
|
||||||
|
|
||||||
return self.item
|
|
||||||
|
@ -6,13 +6,13 @@ from mealie.core.dependencies.grouped import UserDeps
|
|||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.schema.recipe.recipe_category import CategoryBase
|
from mealie.schema.recipe.recipe_category import CategoryBase
|
||||||
from mealie.schema.user.user import GroupInDB
|
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
|
from mealie.services.events import create_group_event
|
||||||
|
|
||||||
logger = get_logger(module=__name__)
|
logger = get_logger(module=__name__)
|
||||||
|
|
||||||
|
|
||||||
class GroupSelfService(BaseHttpService[int, str]):
|
class GroupSelfService(UserHttpService[int, str]):
|
||||||
_restrict_by_group = True
|
_restrict_by_group = True
|
||||||
event_func = create_group_event
|
event_func = create_group_event
|
||||||
item: GroupInDB
|
item: GroupInDB
|
||||||
@ -36,8 +36,9 @@ class GroupSelfService(BaseHttpService[int, str]):
|
|||||||
if self.item.id != self.group_id:
|
if self.item.id != self.group_id:
|
||||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
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)
|
self.item = self.db.groups.get(self.session, self.group_id)
|
||||||
|
return self.item
|
||||||
|
|
||||||
def update_categories(self, new_categories: list[CategoryBase]):
|
def update_categories(self, new_categories: list[CategoryBase]):
|
||||||
if not self.item:
|
if not self.item:
|
||||||
|
@ -5,13 +5,13 @@ from fastapi import HTTPException, status
|
|||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.schema.group import ReadWebhook
|
from mealie.schema.group import ReadWebhook
|
||||||
from mealie.schema.group.webhook import CreateWebhook, SaveWebhook
|
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
|
from mealie.services.events import create_group_event
|
||||||
|
|
||||||
logger = get_logger(module=__name__)
|
logger = get_logger(module=__name__)
|
||||||
|
|
||||||
|
|
||||||
class WebhookService(BaseHttpService[int, ReadWebhook]):
|
class WebhookService(UserHttpService[int, ReadWebhook]):
|
||||||
event_func = create_group_event
|
event_func = create_group_event
|
||||||
_restrict_by_group = True
|
_restrict_by_group = True
|
||||||
|
|
||||||
@ -19,8 +19,9 @@ class WebhookService(BaseHttpService[int, ReadWebhook]):
|
|||||||
_create_schema = CreateWebhook
|
_create_schema = CreateWebhook
|
||||||
_update_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)
|
self.item = self.db.webhooks.get_one(self.session, id)
|
||||||
|
return self.item
|
||||||
|
|
||||||
def get_all(self) -> list[ReadWebhook]:
|
def get_all(self) -> list[ReadWebhook]:
|
||||||
return self.db.webhooks.get(self.session, self.group_id, match_key="group_id", limit=9999)
|
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.dependencies.grouped import PublicDeps, UserDeps
|
||||||
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.http_services import PublicHttpService
|
||||||
from mealie.services.events import create_recipe_event
|
from mealie.services.events import create_recipe_event
|
||||||
|
|
||||||
logger = get_logger(module=__name__)
|
logger = get_logger(module=__name__)
|
||||||
|
|
||||||
|
|
||||||
class RecipeService(BaseHttpService[str, Recipe]):
|
class RecipeService(PublicHttpService[str, Recipe]):
|
||||||
"""
|
"""
|
||||||
Class Methods:
|
Class Methods:
|
||||||
`read_existing`: Reads an existing recipe from the database.
|
`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.root_logger import get_logger
|
||||||
from mealie.core.security import hash_password, verify_password
|
from mealie.core.security import hash_password, verify_password
|
||||||
from mealie.schema.user.user import ChangePassword, PrivateUser
|
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
|
from mealie.services.events import create_user_event
|
||||||
|
|
||||||
logger = get_logger(module=__name__)
|
logger = get_logger(module=__name__)
|
||||||
|
|
||||||
|
|
||||||
class UserService(BaseHttpService[int, str]):
|
class UserService(UserHttpService[int, str]):
|
||||||
event_func = create_user_event
|
event_func = create_user_event
|
||||||
acting_user: PrivateUser = None
|
acting_user: PrivateUser = None
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user