mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-04 06:53:12 -05: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:
@@ -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