update dependency injection methods
This commit is contained in:
parent
0b27e8af45
commit
086098899d
13 changed files with 193 additions and 114 deletions
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
import secrets
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
|
@ -175,11 +176,13 @@ class AppSettings(BaseSettings):
|
|||
settings = AppSettings()
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_app_dirs() -> AppDirectories:
|
||||
global app_dirs
|
||||
return app_dirs
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> AppSettings:
|
||||
global settings
|
||||
return settings
|
||||
|
|
|
@ -9,7 +9,7 @@ from sqlalchemy.orm.session import Session
|
|||
from mealie.core.config import app_dirs, settings
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.schema.user import LongLiveTokenInDB, TokenData, PrivateUser
|
||||
from mealie.schema.user import LongLiveTokenInDB, PrivateUser, TokenData
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
|
||||
oauth2_scheme_soft_fail = OAuth2PasswordBearer(tokenUrl="/api/auth/token", auto_error=False)
|
||||
|
|
|
@ -27,7 +27,7 @@ from mealie.schema.recipe import (
|
|||
RecipeCategoryResponse,
|
||||
RecipeTagResponse,
|
||||
)
|
||||
from mealie.schema.user import GroupInDB, LongLiveTokenInDB, SignUpOut, PrivateUser
|
||||
from mealie.schema.user import GroupInDB, LongLiveTokenInDB, PrivateUser, SignUpOut
|
||||
|
||||
from ._base_access_model import BaseAccessModel
|
||||
from .recipe_access_model import RecipeDataAccessModel
|
||||
|
|
|
@ -82,5 +82,41 @@
|
|||
"description": "",
|
||||
"fraction": true,
|
||||
"abbreviation": "mg"
|
||||
},
|
||||
{
|
||||
"name": "splash",
|
||||
"description": "",
|
||||
"fraction": true,
|
||||
"abbreviation": ""
|
||||
},
|
||||
{
|
||||
"name": "dash",
|
||||
"description": "",
|
||||
"fraction": true,
|
||||
"abbreviation": ""
|
||||
},
|
||||
{
|
||||
"name": "serving",
|
||||
"description": "",
|
||||
"fraction": true,
|
||||
"abbreviation": ""
|
||||
},
|
||||
{
|
||||
"name": "head",
|
||||
"description": "",
|
||||
"fraction": true,
|
||||
"abbreviation": ""
|
||||
},
|
||||
{
|
||||
"name": "clove",
|
||||
"description": "",
|
||||
"fraction": true,
|
||||
"abbreviation": ""
|
||||
},
|
||||
{
|
||||
"name": "can",
|
||||
"description": "",
|
||||
"fraction": true,
|
||||
"abbreviation": ""
|
||||
}
|
||||
]
|
|
@ -5,7 +5,7 @@ from mealie.core.dependencies import get_current_user
|
|||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
|
||||
from mealie.schema.user import GroupBase, GroupInDB, UpdateGroup, PrivateUser
|
||||
from mealie.schema.user import GroupBase, GroupInDB, PrivateUser, UpdateGroup
|
||||
from mealie.services.events import create_group_event
|
||||
|
||||
admin_router = AdminAPIRouter(prefix="/groups", tags=["Groups: CRUD"])
|
||||
|
|
|
@ -8,7 +8,7 @@ from mealie.db.database import db
|
|||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
|
||||
from mealie.routes.users._helpers import assert_user_change_allowed
|
||||
from mealie.schema.user import UserBase, UserIn, PrivateUser, UserOut
|
||||
from mealie.schema.user import PrivateUser, UserBase, UserIn, UserOut
|
||||
from mealie.services.events import create_user_event
|
||||
|
||||
user_router = UserAPIRouter(prefix="")
|
||||
|
|
|
@ -6,7 +6,7 @@ from mealie.db.database import db
|
|||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.routers import UserAPIRouter
|
||||
from mealie.routes.users._helpers import assert_user_change_allowed
|
||||
from mealie.schema.user import UserFavorites, PrivateUser
|
||||
from mealie.schema.user import PrivateUser, UserFavorites
|
||||
|
||||
user_router = UserAPIRouter()
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ from mealie.core.security import hash_password
|
|||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.routers import AdminAPIRouter
|
||||
from mealie.schema.user import SignUpIn, SignUpOut, SignUpToken, UserIn, PrivateUser
|
||||
from mealie.schema.user import PrivateUser, SignUpIn, SignUpOut, SignUpToken, UserIn
|
||||
from mealie.services.events import create_user_event
|
||||
|
||||
public_router = APIRouter(prefix="/sign-ups")
|
||||
|
|
2
mealie/services/base_http_service/__init__.py
Normal file
2
mealie/services/base_http_service/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from .base_http_service import *
|
||||
from .base_service import *
|
109
mealie/services/base_http_service/base_http_service.py
Normal file
109
mealie/services/base_http_service/base_http_service.py
Normal file
|
@ -0,0 +1,109 @@
|
|||
from typing import Callable, Generic, TypeVar
|
||||
|
||||
from fastapi import BackgroundTasks, Depends
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core.config import get_app_dirs, get_settings
|
||||
from mealie.core.dependencies.grouped import ReadDeps, WriteDeps
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.db.database import get_database
|
||||
from mealie.db.db_setup import SessionLocal
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
T = TypeVar("T")
|
||||
D = TypeVar("D")
|
||||
|
||||
|
||||
class BaseHttpService(Generic[T, D]):
|
||||
"""The BaseHttpService class is a generic class that can be used to create
|
||||
http services that are injected via `Depends` into a route function. To use,
|
||||
you must define the Generic type arguments:
|
||||
|
||||
`T`: The type passed into the *_existing functions (e.g. id) which is then passed into assert_existing
|
||||
`D`: Not yet implemented
|
||||
|
||||
Child Requirements:
|
||||
Define the following functions:
|
||||
`assert_existing(self, data: T) -> None:`
|
||||
|
||||
Define the following variables:
|
||||
`event_func`: A function that is called when an event is created.
|
||||
"""
|
||||
|
||||
event_func: Callable = None
|
||||
|
||||
def __init__(self, session: Session, user: PrivateUser, background_tasks: BackgroundTasks = None) -> None:
|
||||
self.session = session or SessionLocal()
|
||||
self.user = user
|
||||
self.logged_in = bool(self.user)
|
||||
self.background_tasks = background_tasks
|
||||
|
||||
# Static Globals Dependency Injection
|
||||
self.db = get_database()
|
||||
self.app_dirs = get_app_dirs()
|
||||
self.settings = get_settings()
|
||||
|
||||
def assert_existing(self, data: T) -> None:
|
||||
raise NotImplementedError("`assert_existing` must by implemented by child class")
|
||||
|
||||
@classmethod
|
||||
def read_existing(cls, id: T, deps: ReadDeps = Depends()):
|
||||
"""
|
||||
Used for dependency injection for routes that require an existing recipe. If the recipe doesn't exist
|
||||
or the user doens't not have the required permissions, the proper HTTP Status code will be raised.
|
||||
|
||||
Args:
|
||||
slug (str): Recipe Slug used to query the database
|
||||
session (Session, optional): The Injected SQLAlchemy Session.
|
||||
user (bool, optional): The injected determination of is_logged_in.
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 Not Found
|
||||
HTTPException: 403 Forbidden
|
||||
|
||||
Returns:
|
||||
RecipeService: The Recipe Service class with a populated recipe attribute
|
||||
"""
|
||||
new_class = cls(deps.session, deps.user, deps.bg_tasks)
|
||||
new_class.assert_existing(id)
|
||||
return new_class
|
||||
|
||||
@classmethod
|
||||
def write_existing(cls, id: T, deps: WriteDeps = Depends()):
|
||||
"""
|
||||
Used for dependency injection for routes that require an existing recipe. The only difference between
|
||||
read_existing and write_existing is that the user is required to be logged in on write_existing method.
|
||||
|
||||
Args:
|
||||
slug (str): Recipe Slug used to query the database
|
||||
session (Session, optional): The Injected SQLAlchemy Session.
|
||||
user (bool, optional): The injected determination of is_logged_in.
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 Not Found
|
||||
HTTPException: 403 Forbidden
|
||||
|
||||
Returns:
|
||||
RecipeService: The Recipe Service class with a populated recipe attribute
|
||||
"""
|
||||
new_class = cls(deps.session, deps.user, deps.bg_task)
|
||||
new_class.assert_existing(id)
|
||||
return new_class
|
||||
|
||||
@classmethod
|
||||
def base(cls, deps: WriteDeps = Depends()):
|
||||
"""A Base instance to be used as a router dependency
|
||||
|
||||
Raises:
|
||||
HTTPException: 400 Bad Request
|
||||
|
||||
"""
|
||||
return cls(deps.session, deps.user, deps.bg_task)
|
||||
|
||||
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)
|
17
mealie/services/base_http_service/base_service.py
Normal file
17
mealie/services/base_http_service/base_service.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
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()
|
|
@ -2,106 +2,35 @@ from pathlib import Path
|
|||
from shutil import copytree, rmtree
|
||||
from typing import Union
|
||||
|
||||
from fastapi import BackgroundTasks, Depends, HTTPException, status
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from mealie.core.config import get_app_dirs, get_settings
|
||||
from mealie.core.dependencies import ReadDeps
|
||||
from mealie.core.dependencies.grouped import WriteDeps
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.db.database import get_database
|
||||
from mealie.db.db_setup import SessionLocal
|
||||
from mealie.schema.recipe.recipe import CreateRecipe, Recipe
|
||||
from mealie.schema.user.user import PrivateUser
|
||||
from mealie.services.base_http_service.base_http_service import BaseHttpService
|
||||
from mealie.services.events import create_recipe_event
|
||||
|
||||
logger = get_logger(module=__name__)
|
||||
|
||||
|
||||
class RecipeService:
|
||||
class RecipeService(BaseHttpService[str, str]):
|
||||
"""
|
||||
Class Methods:
|
||||
`read_existing`: Reads an existing recipe from the database.
|
||||
`write_existing`: Updates an existing recipe in the database.
|
||||
`base`: Requires write permissions, but doesn't perform recipe checks
|
||||
"""
|
||||
|
||||
event_func = create_recipe_event
|
||||
recipe: Recipe # Required for proper type hints
|
||||
|
||||
def __init__(self, session: Session, user: PrivateUser, background_tasks: BackgroundTasks = None) -> None:
|
||||
self.session = session or SessionLocal()
|
||||
self.user = user
|
||||
self.background_tasks = background_tasks
|
||||
self.recipe: Recipe = None
|
||||
|
||||
# Static Globals Dependency Injection
|
||||
self.db = get_database()
|
||||
self.app_dirs = get_app_dirs()
|
||||
self.settings = get_settings()
|
||||
|
||||
@classmethod
|
||||
def read_existing(cls, slug: str, deps: ReadDeps = Depends()):
|
||||
"""
|
||||
Used for dependency injection for routes that require an existing recipe. If the recipe doesn't exist
|
||||
or the user doens't not have the required permissions, the proper HTTP Status code will be raised.
|
||||
|
||||
Args:
|
||||
slug (str): Recipe Slug used to query the database
|
||||
session (Session, optional): The Injected SQLAlchemy Session.
|
||||
user (bool, optional): The injected determination of is_logged_in.
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 Not Found
|
||||
HTTPException: 403 Forbidden
|
||||
|
||||
Returns:
|
||||
RecipeService: The Recipe Service class with a populated recipe attribute
|
||||
"""
|
||||
new_class = cls(deps.session, deps.user, deps.bg_tasks)
|
||||
new_class.assert_existing(slug)
|
||||
return new_class
|
||||
|
||||
@classmethod
|
||||
def write_existing(cls, slug: str, deps: WriteDeps = Depends()):
|
||||
"""
|
||||
Used for dependency injection for routes that require an existing recipe. The only difference between
|
||||
read_existing and write_existing is that the user is required to be logged in on write_existing method.
|
||||
|
||||
Args:
|
||||
slug (str): Recipe Slug used to query the database
|
||||
session (Session, optional): The Injected SQLAlchemy Session.
|
||||
user (bool, optional): The injected determination of is_logged_in.
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 Not Found
|
||||
HTTPException: 403 Forbidden
|
||||
|
||||
Returns:
|
||||
RecipeService: The Recipe Service class with a populated recipe attribute
|
||||
"""
|
||||
new_class = cls(deps.session, deps.user, deps.bg_task)
|
||||
new_class.assert_existing(slug)
|
||||
return new_class
|
||||
return super().write_existing(slug, deps)
|
||||
|
||||
@classmethod
|
||||
def base(cls, deps: WriteDeps = Depends()) -> Recipe:
|
||||
"""A Base instance to be used as a router dependency
|
||||
|
||||
Raises:
|
||||
HTTPException: 400 Bad Request
|
||||
|
||||
"""
|
||||
return cls(deps.session, deps.user, deps.bg_task)
|
||||
|
||||
def pupulate_recipe(self, slug: str) -> Recipe:
|
||||
"""Populates the recipe attribute with the recipe from the database.
|
||||
|
||||
Returns:
|
||||
Recipe: The populated recipe
|
||||
"""
|
||||
self.recipe = self.db.recipes.get(self.session, slug)
|
||||
return self.recipe
|
||||
def read_existing(cls, slug: str, deps: WriteDeps = Depends()):
|
||||
return super().write_existing(slug, deps)
|
||||
|
||||
def assert_existing(self, slug: str):
|
||||
self.pupulate_recipe(slug)
|
||||
|
@ -112,6 +41,10 @@ class RecipeService:
|
|||
if not self.recipe.settings.public and not self.user:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def pupulate_recipe(self, slug: str) -> Recipe:
|
||||
self.recipe = self.db.recipes.get(self.session, slug)
|
||||
return self.recipe
|
||||
|
||||
# CRUD METHODS
|
||||
def create_recipe(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe:
|
||||
if isinstance(create_data, CreateRecipe):
|
||||
|
@ -174,9 +107,6 @@ class RecipeService:
|
|||
self._create_event("Recipe Delete", f"'{recipe.name}' deleted by {self.user.full_name}")
|
||||
return recipe
|
||||
|
||||
def _create_event(self, title: str, message: str) -> None:
|
||||
self.background_tasks.add_task(create_recipe_event, title, message, self.session)
|
||||
|
||||
def _check_assets(self, original_slug: str) -> None:
|
||||
"""Checks if the recipe slug has changed, and if so moves the assets to a new file with the new slug."""
|
||||
if original_slug != self.recipe.slug:
|
||||
|
|
|
@ -1,37 +1,22 @@
|
|||
from fastapi import BackgroundTasks, Depends, HTTPException, status
|
||||
from sqlalchemy.orm.session import Session
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from mealie.core.config import get_app_dirs, get_settings
|
||||
from mealie.core.dependencies import WriteDeps
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.core.security import hash_password, verify_password
|
||||
from mealie.db.database import get_database
|
||||
from mealie.db.db_setup import SessionLocal
|
||||
from mealie.schema.recipe.recipe import Recipe
|
||||
from mealie.schema.user.user import ChangePassword, PrivateUser
|
||||
from mealie.services.base_http_service.base_http_service import BaseHttpService
|
||||
from mealie.services.events import create_user_event
|
||||
|
||||
logger = get_logger(module=__name__)
|
||||
|
||||
|
||||
class UserService:
|
||||
def __init__(self, session: Session, acting_user: PrivateUser, background_tasks: BackgroundTasks = None) -> None:
|
||||
self.session = session or SessionLocal()
|
||||
self.acting_user = acting_user
|
||||
self.background_tasks = background_tasks
|
||||
self.recipe: Recipe = None
|
||||
class UserService(BaseHttpService[int, str]):
|
||||
event_func = create_user_event
|
||||
acting_user: PrivateUser = None
|
||||
|
||||
# Global Singleton Dependency Injection
|
||||
self.db = get_database()
|
||||
self.app_dirs = get_app_dirs()
|
||||
self.settings = get_settings()
|
||||
|
||||
@classmethod
|
||||
def write_existing(cls, id: int, deps: WriteDeps = Depends()):
|
||||
new_instance = cls(session=deps.session, acting_user=deps.user, background_tasks=deps.bg_task)
|
||||
new_instance._populate_target_user(id)
|
||||
new_instance._assert_user_change_allowed()
|
||||
return new_instance
|
||||
def assert_existing(self, id) -> PrivateUser:
|
||||
self._populate_target_user(id)
|
||||
self._assert_user_change_allowed()
|
||||
return self.target_user
|
||||
|
||||
def _assert_user_change_allowed(self) -> None:
|
||||
if self.acting_user.id != self.target_user.id and not self.acting_user.admin:
|
||||
|
@ -46,9 +31,6 @@ class UserService:
|
|||
else:
|
||||
self.target_user = self.acting_user
|
||||
|
||||
def _create_event(self, title: str, message: str) -> None:
|
||||
self.background_tasks.add_task(create_user_event, title, message, self.session)
|
||||
|
||||
def change_password(self, password_change: ChangePassword) -> PrivateUser:
|
||||
""""""
|
||||
if not verify_password(password_change.current_password, self.target_user.password):
|
||||
|
|
Loading…
Reference in a new issue