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:
Hayden
2022-01-13 13:06:52 -09:00
committed by GitHub
parent 5823a32daf
commit c4540f1395
164 changed files with 3111 additions and 3213 deletions

View File

@@ -0,0 +1,4 @@
from .abc_controller import *
from .controller import *
from .dependencies import *
from .mixins import *

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

View 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

View File

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

View File

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

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