mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-07 00:13:12 -05:00
Refactor/conver to controllers (#923)
* add dependency injection for get_repositories * convert events api to controller * update generic typing * add abstract controllers * update test naming * migrate admin services to controllers * add additional admin route tests * remove print * add public shared dependencies * add types * fix typo * add static variables for recipe json keys * add coverage gutters config * update controller routers * add generic success response * add category/tag/tool tests * add token refresh test * add coverage utilities * covert comments to controller * add todo * add helper properties * delete old service * update test notes * add unit test for pretty_stats * remove dead code from post_webhooks * update group routes to use controllers * add additional group test coverage * abstract common permission checks * convert ingredient parser to controller * update recipe crud to use controller * remove dead-code * add class lifespan tracker for debugging * convert bulk export to controller * migrate tools router to controller * update recipe share to controller * move customer router to _base * ignore prints in flake8 * convert units and foods to new controllers * migrate user routes to controllers * centralize error handling * fix invalid ref * reorder fields * update routers to share common handling * update tests * remove prints * fix cookbooks delete * fix cookbook get * add controller for mealplanner * cover report routes to controller * remove __future__ imports * remove dead code * remove all base_http children and remove dead code
This commit is contained in:
4
mealie/routes/_base/__init__.py
Normal file
4
mealie/routes/_base/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .abc_controller import *
|
||||
from .controller import *
|
||||
from .dependencies import *
|
||||
from .mixins import *
|
||||
58
mealie/routes/_base/abc_controller.py
Normal file
58
mealie/routes/_base/abc_controller.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from abc import ABC
|
||||
from functools import cached_property
|
||||
|
||||
from fastapi import Depends
|
||||
|
||||
from mealie.repos.all_repositories import AllRepositories
|
||||
from mealie.routes._base.checks import OperationChecks
|
||||
from mealie.routes._base.dependencies import SharedDependencies
|
||||
|
||||
|
||||
class BasePublicController(ABC):
|
||||
"""
|
||||
This is a public class for all User restricted controllers in the API.
|
||||
It includes the common SharedDependencies and some common methods used
|
||||
by all Admin controllers.
|
||||
"""
|
||||
|
||||
deps: SharedDependencies = Depends(SharedDependencies.public)
|
||||
|
||||
|
||||
class BaseUserController(ABC):
|
||||
"""
|
||||
This is a base class for all User restricted controllers in the API.
|
||||
It includes the common SharedDependencies and some common methods used
|
||||
by all Admin controllers.
|
||||
"""
|
||||
|
||||
deps: SharedDependencies = Depends(SharedDependencies.user)
|
||||
|
||||
@cached_property
|
||||
def repos(self):
|
||||
return AllRepositories(self.deps.session)
|
||||
|
||||
@property
|
||||
def group_id(self):
|
||||
return self.deps.acting_user.group_id
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return self.deps.acting_user
|
||||
|
||||
@property
|
||||
def group(self):
|
||||
return self.deps.repos.groups.get_one(self.group_id)
|
||||
|
||||
@cached_property
|
||||
def checks(self) -> OperationChecks:
|
||||
return OperationChecks(self.deps.acting_user)
|
||||
|
||||
|
||||
class BaseAdminController(BaseUserController):
|
||||
"""
|
||||
This is a base class for all Admin restricted controllers in the API.
|
||||
It includes the common Shared Dependencies and some common methods used
|
||||
by all Admin controllers.
|
||||
"""
|
||||
|
||||
deps: SharedDependencies = Depends(SharedDependencies.admin)
|
||||
39
mealie/routes/_base/checks.py
Normal file
39
mealie/routes/_base/checks.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
|
||||
|
||||
class OperationChecks:
|
||||
"""
|
||||
OperationChecks class is a mixin class that can be used on routers to provide common permission
|
||||
checks and raise the appropriate http error as necessary
|
||||
"""
|
||||
|
||||
user: PrivateUser
|
||||
|
||||
def __init__(self, user: PrivateUser) -> None:
|
||||
self.user = user
|
||||
|
||||
def _raise_unauthorized(self) -> None:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def _raise_forbidden(self) -> None:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# =========================================
|
||||
# User Permission Checks
|
||||
|
||||
def can_manage(self) -> bool:
|
||||
if not self.user.can_manage:
|
||||
self._raise_forbidden()
|
||||
return True
|
||||
|
||||
def can_invite(self) -> bool:
|
||||
if not self.user.can_invite:
|
||||
self._raise_forbidden()
|
||||
return True
|
||||
|
||||
def can_organize(self) -> bool:
|
||||
if not self.user.can_organize:
|
||||
self._raise_forbidden()
|
||||
return True
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import cached_property
|
||||
from logging import Logger
|
||||
|
||||
@@ -17,34 +15,40 @@ from mealie.repos import AllRepositories
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
|
||||
|
||||
def _get_logger() -> Logger:
|
||||
return get_logger()
|
||||
|
||||
|
||||
class SharedDependencies:
|
||||
session: Session
|
||||
t: AbstractLocaleProvider
|
||||
logger: Logger
|
||||
acting_user: PrivateUser | None
|
||||
|
||||
def __init__(self, session: Session, acting_user: PrivateUser | None) -> None:
|
||||
self.t = get_locale_provider()
|
||||
self.logger = _get_logger()
|
||||
self.session = session
|
||||
self.acting_user = acting_user
|
||||
|
||||
@classmethod
|
||||
def public(cls, session: Session = Depends(generate_session)) -> "SharedDependencies":
|
||||
return cls(session, None)
|
||||
|
||||
@classmethod
|
||||
def user(
|
||||
cls, session: Session = Depends(generate_session), user: PrivateUser = Depends(get_current_user)
|
||||
cls,
|
||||
session: Session = Depends(generate_session),
|
||||
user: PrivateUser = Depends(get_current_user),
|
||||
) -> "SharedDependencies":
|
||||
return cls(session, user)
|
||||
|
||||
@classmethod
|
||||
def admin(
|
||||
cls, session: Session = Depends(generate_session), admin: PrivateUser = Depends(get_admin_user)
|
||||
cls,
|
||||
session: Session = Depends(generate_session),
|
||||
admin: PrivateUser = Depends(get_admin_user),
|
||||
) -> "SharedDependencies":
|
||||
return cls(session, admin)
|
||||
|
||||
@cached_property
|
||||
def logger(self) -> Logger:
|
||||
return get_logger()
|
||||
|
||||
@cached_property
|
||||
def settings(self) -> AppSettings:
|
||||
return get_app_settings()
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from logging import Logger
|
||||
from typing import Callable, Type
|
||||
from typing import Callable, Generic, Type, TypeVar
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from pydantic import UUID4, BaseModel
|
||||
|
||||
from mealie.repos.repository_generic import RepositoryGeneric
|
||||
from mealie.schema.response import ErrorResponse
|
||||
|
||||
C = TypeVar("C", bound=BaseModel)
|
||||
R = TypeVar("R", bound=BaseModel)
|
||||
U = TypeVar("U", bound=BaseModel)
|
||||
|
||||
|
||||
class CrudMixins(Generic[C, R, U]):
|
||||
"""
|
||||
The CrudMixins[C, R, U] class is a mixin class that provides a common set of methods for CRUD operations.
|
||||
This class is inteded to be used in a composition pattern where a class has a mixin property. For example:
|
||||
|
||||
```
|
||||
class MyClass:
|
||||
def __init(self repo, logger):
|
||||
self.mixins = CrudMixins(repo, logger)
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
class CrudMixins:
|
||||
repo: RepositoryGeneric
|
||||
exception_msgs: Callable[[Type[Exception]], str] | None
|
||||
default_message: str = "An unexpected error occurred."
|
||||
@@ -21,17 +36,7 @@ class CrudMixins:
|
||||
exception_msgs: Callable[[Type[Exception]], str] = None,
|
||||
default_message: str = None,
|
||||
) -> None:
|
||||
"""
|
||||
The CrudMixins class is a mixin class that provides a common set of methods for CRUD operations.
|
||||
This class is inteded to be used in a composition pattern where a class has a mixin property. For example:
|
||||
|
||||
```
|
||||
class MyClass:
|
||||
def __init(self repo, logger):
|
||||
self.mixins = CrudMixins(repo, logger)
|
||||
```
|
||||
|
||||
"""
|
||||
self.repo = repo
|
||||
self.logger = logger
|
||||
self.exception_msgs = exception_msgs
|
||||
@@ -39,16 +44,6 @@ class CrudMixins:
|
||||
if default_message:
|
||||
self.default_message = default_message
|
||||
|
||||
def set_default_message(self, default_msg: str) -> "CrudMixins":
|
||||
"""
|
||||
Use this method to set a lookup function for exception messages. When an exception is raised, and
|
||||
no custom message is set, the default message will be used.
|
||||
|
||||
IMPORTANT! The function returns the same instance of the CrudMixins class, so you can chain calls.
|
||||
"""
|
||||
self.default_msg = default_msg
|
||||
return self
|
||||
|
||||
def get_exception_message(self, ext: Exception) -> str:
|
||||
if self.exception_msgs:
|
||||
return self.exception_msgs(type(ext))
|
||||
@@ -67,8 +62,8 @@ class CrudMixins:
|
||||
detail=ErrorResponse.respond(message=msg, exception=str(ex)),
|
||||
)
|
||||
|
||||
def create_one(self, data):
|
||||
item = None
|
||||
def create_one(self, data: C) -> R | None:
|
||||
item: R | None = None
|
||||
try:
|
||||
item = self.repo.create(data)
|
||||
except Exception as ex:
|
||||
@@ -76,8 +71,8 @@ class CrudMixins:
|
||||
|
||||
return item
|
||||
|
||||
def get_one(self, item_id):
|
||||
item = self.repo.get(item_id)
|
||||
def get_one(self, item_id: int | str | UUID4, key: str = None) -> R:
|
||||
item = self.repo.get_one(item_id, key)
|
||||
|
||||
if not item:
|
||||
raise HTTPException(
|
||||
@@ -87,33 +82,35 @@ class CrudMixins:
|
||||
|
||||
return item
|
||||
|
||||
def update_one(self, data, item_id):
|
||||
item = self.repo.get(item_id)
|
||||
def update_one(self, data: U, item_id: int | str | UUID4) -> R:
|
||||
item: R = self.repo.get_one(item_id)
|
||||
|
||||
if not item:
|
||||
return
|
||||
raise HTTPException(
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
detail=ErrorResponse.respond(message="Not found."),
|
||||
)
|
||||
|
||||
try:
|
||||
item = self.repo.update(item.id, data) # type: ignore
|
||||
item = self.repo.update(item_id, data) # type: ignore
|
||||
except Exception as ex:
|
||||
self.handle_exception(ex)
|
||||
|
||||
return item
|
||||
|
||||
def patch_one(self, data, item_id) -> None:
|
||||
self.repo.get(item_id)
|
||||
def patch_one(self, data: U, item_id: int | str | UUID4) -> None:
|
||||
self.repo.get_one(item_id)
|
||||
|
||||
try:
|
||||
self.repo.patch(item_id, data.dict(exclude_unset=True, exclude_defaults=True))
|
||||
except Exception as ex:
|
||||
self.handle_exception(ex)
|
||||
|
||||
def delete_one(self, item_id):
|
||||
self.logger.info(f"Deleting item with id {item_id}")
|
||||
|
||||
def delete_one(self, item_id: int | str | UUID4) -> R | None:
|
||||
item: R | None = None
|
||||
try:
|
||||
item = self.repo.delete(item_id)
|
||||
self.logger.info(item)
|
||||
self.logger.info(f"Deleting item with id {item_id}")
|
||||
except Exception as ex:
|
||||
self.handle_exception(ex)
|
||||
|
||||
|
||||
27
mealie/routes/_base/routers.py
Normal file
27
mealie/routes/_base/routers.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from mealie.core.dependencies import get_admin_user, get_current_user
|
||||
|
||||
|
||||
class AdminAPIRouter(APIRouter):
|
||||
"""Router for functions to be protected behind admin authentication"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tags: Optional[List[str]] = None,
|
||||
prefix: str = "",
|
||||
):
|
||||
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_admin_user)])
|
||||
|
||||
|
||||
class UserAPIRouter(APIRouter):
|
||||
"""Router for functions to be protected behind user authentication"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tags: Optional[List[str]] = None,
|
||||
prefix: str = "",
|
||||
):
|
||||
super().__init__(tags=tags, prefix=prefix, dependencies=[Depends(get_current_user)])
|
||||
Reference in New Issue
Block a user