1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-07 06:25:21 +02:00

update dependency injection methods

This commit is contained in:
hay-kot 2021-08-29 17:10:29 -08:00
parent 0b27e8af45
commit 086098899d
13 changed files with 193 additions and 114 deletions

View file

@ -1,5 +1,6 @@
import os import os
import secrets import secrets
from functools import lru_cache
from pathlib import Path from pathlib import Path
from typing import Any, Optional, Union from typing import Any, Optional, Union
@ -175,11 +176,13 @@ class AppSettings(BaseSettings):
settings = AppSettings() settings = AppSettings()
@lru_cache
def get_app_dirs() -> AppDirectories: def get_app_dirs() -> AppDirectories:
global app_dirs global app_dirs
return app_dirs return app_dirs
@lru_cache
def get_settings() -> AppSettings: def get_settings() -> AppSettings:
global settings global settings
return settings return settings

View file

@ -9,7 +9,7 @@ from sqlalchemy.orm.session import Session
from mealie.core.config import app_dirs, settings from mealie.core.config import app_dirs, settings
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session 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 = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
oauth2_scheme_soft_fail = OAuth2PasswordBearer(tokenUrl="/api/auth/token", auto_error=False) oauth2_scheme_soft_fail = OAuth2PasswordBearer(tokenUrl="/api/auth/token", auto_error=False)

View file

@ -27,7 +27,7 @@ from mealie.schema.recipe import (
RecipeCategoryResponse, RecipeCategoryResponse,
RecipeTagResponse, 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 ._base_access_model import BaseAccessModel
from .recipe_access_model import RecipeDataAccessModel from .recipe_access_model import RecipeDataAccessModel

View file

@ -82,5 +82,41 @@
"description": "", "description": "",
"fraction": true, "fraction": true,
"abbreviation": "mg" "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": ""
} }
] ]

View file

@ -5,7 +5,7 @@ from mealie.core.dependencies import get_current_user
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter 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 from mealie.services.events import create_group_event
admin_router = AdminAPIRouter(prefix="/groups", tags=["Groups: CRUD"]) admin_router = AdminAPIRouter(prefix="/groups", tags=["Groups: CRUD"])

View file

@ -8,7 +8,7 @@ from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed 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 from mealie.services.events import create_user_event
user_router = UserAPIRouter(prefix="") user_router = UserAPIRouter(prefix="")

View file

@ -6,7 +6,7 @@ from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.routers import UserAPIRouter from mealie.routes.routers import UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed 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() user_router = UserAPIRouter()

View file

@ -8,7 +8,7 @@ from mealie.core.security import hash_password
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.routers import AdminAPIRouter 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 from mealie.services.events import create_user_event
public_router = APIRouter(prefix="/sign-ups") public_router = APIRouter(prefix="/sign-ups")

View file

@ -0,0 +1,2 @@
from .base_http_service import *
from .base_service import *

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

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

View file

@ -2,106 +2,35 @@ from pathlib import Path
from shutil import copytree, rmtree from shutil import copytree, rmtree
from typing import Union from typing import Union
from fastapi import BackgroundTasks, Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from sqlalchemy.exc import IntegrityError 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.dependencies.grouped import WriteDeps
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.db.db_setup import SessionLocal
from mealie.schema.recipe.recipe import CreateRecipe, Recipe 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 from mealie.services.events import create_recipe_event
logger = get_logger(module=__name__) logger = get_logger(module=__name__)
class RecipeService: class RecipeService(BaseHttpService[str, str]):
""" """
Class Methods: Class Methods:
`read_existing`: Reads an existing recipe from the database. `read_existing`: Reads an existing recipe from the database.
`write_existing`: Updates an existing recipe in the database. `write_existing`: Updates an existing recipe in the database.
`base`: Requires write permissions, but doesn't perform recipe checks `base`: Requires write permissions, but doesn't perform recipe checks
""" """
event_func = create_recipe_event
recipe: Recipe # Required for proper type hints 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 @classmethod
def write_existing(cls, slug: str, deps: WriteDeps = Depends()): def write_existing(cls, slug: str, deps: WriteDeps = Depends()):
""" return super().write_existing(slug, deps)
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
@classmethod @classmethod
def base(cls, deps: WriteDeps = Depends()) -> Recipe: def read_existing(cls, slug: str, deps: WriteDeps = Depends()):
"""A Base instance to be used as a router dependency return super().write_existing(slug, deps)
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 assert_existing(self, slug: str): def assert_existing(self, slug: str):
self.pupulate_recipe(slug) self.pupulate_recipe(slug)
@ -112,6 +41,10 @@ class RecipeService:
if not self.recipe.settings.public and not self.user: if not self.recipe.settings.public and not self.user:
raise HTTPException(status.HTTP_403_FORBIDDEN) 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 # CRUD METHODS
def create_recipe(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe: def create_recipe(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe:
if isinstance(create_data, CreateRecipe): 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}") self._create_event("Recipe Delete", f"'{recipe.name}' deleted by {self.user.full_name}")
return recipe 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: 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.""" """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: if original_slug != self.recipe.slug:

View file

@ -1,37 +1,22 @@
from fastapi import BackgroundTasks, Depends, HTTPException, status from fastapi import HTTPException, status
from sqlalchemy.orm.session import Session
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.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.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.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 from mealie.services.events import create_user_event
logger = get_logger(module=__name__) logger = get_logger(module=__name__)
class UserService: class UserService(BaseHttpService[int, str]):
def __init__(self, session: Session, acting_user: PrivateUser, background_tasks: BackgroundTasks = None) -> None: event_func = create_user_event
self.session = session or SessionLocal() acting_user: PrivateUser = None
self.acting_user = acting_user
self.background_tasks = background_tasks
self.recipe: Recipe = None
# Global Singleton Dependency Injection def assert_existing(self, id) -> PrivateUser:
self.db = get_database() self._populate_target_user(id)
self.app_dirs = get_app_dirs() self._assert_user_change_allowed()
self.settings = get_settings() return self.target_user
@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_user_change_allowed(self) -> None: def _assert_user_change_allowed(self) -> None:
if self.acting_user.id != self.target_user.id and not self.acting_user.admin: if self.acting_user.id != self.target_user.id and not self.acting_user.admin:
@ -46,9 +31,6 @@ class UserService:
else: else:
self.target_user = self.acting_user 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: def change_password(self, password_change: ChangePassword) -> PrivateUser:
"""""" """"""
if not verify_password(password_change.current_password, self.target_user.password): if not verify_password(password_change.current_password, self.target_user.password):