refactor(backend): ⚗️ experimental dependency injection framework (WIP)

This commit is contained in:
hay-kot 2021-08-28 14:18:38 -08:00
parent da501adce8
commit 1c11f6a3d7
5 changed files with 221 additions and 65 deletions

View File

@ -1,16 +1,15 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
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.deps import is_logged_in
from mealie.schema.recipe import RecipeSummary from mealie.schema.recipe import RecipeSummary
from mealie.services.recipe.all_recipes import get_all_recipes_public, get_all_recipes_user from mealie.services.recipe.all_recipes import AllRecipesService
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
router = APIRouter() router = APIRouter()
@router.get("") @router.get("")
def get_recipe_summary(start=0, limit=9999, user: bool = Depends(is_logged_in)): def get_recipe_summary(all_recipes_service: AllRecipesService.query = Depends()):
""" """
Returns key the recipe summary data for recipes in the database. You can perform Returns key the recipe summary data for recipes in the database. You can perform
slice operations to set the skip/end amounts for recipes. All recipes are sorted by the added date. slice operations to set the skip/end amounts for recipes. All recipes are sorted by the added date.
@ -23,11 +22,7 @@ def get_recipe_summary(start=0, limit=9999, user: bool = Depends(is_logged_in)):
""" """
if user: return all_recipes_service.get_recipes()
return get_all_recipes_user(limit, start)
else:
return get_all_recipes_public(limit, start)
@router.get("/summary/untagged", response_model=list[RecipeSummary]) @router.get("/summary/untagged", response_model=list[RecipeSummary])

View File

@ -2,20 +2,21 @@ import json
import shutil import shutil
from zipfile import ZipFile from zipfile import ZipFile
from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, status from fastapi import APIRouter, BackgroundTasks, Depends, File
from fastapi.datastructures import UploadFile from fastapi.datastructures import UploadFile
from mealie.core.config import settings from mealie.core.config import settings
from mealie.core.root_logger import get_logger from mealie.core.root_logger import get_logger
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.deps import get_current_user, is_logged_in, temporary_zip_path from mealie.routes.deps import get_current_user, temporary_zip_path
from mealie.routes.routers import UserAPIRouter from mealie.routes.routers import UserAPIRouter
from mealie.schema.recipe import CreateRecipeByURL, Recipe, RecipeImageTypes from mealie.schema.recipe import CreateRecipeByURL, Recipe, RecipeImageTypes
from mealie.schema.recipe.recipe import CreateRecipe from mealie.schema.recipe.recipe import CreateRecipe
from mealie.schema.user import UserInDB from mealie.schema.user import UserInDB
from mealie.services.events import create_recipe_event from mealie.services.events import create_recipe_event
from mealie.services.image.image import write_image from mealie.services.image.image import write_image
from mealie.services.recipe.media import check_assets, delete_assets from mealie.services.recipe.media import check_assets
from mealie.services.recipe.recipe_service import RecipeService
from mealie.services.scraper.scraper import create_from_url from mealie.services.scraper.scraper import create_from_url
from scrape_schema_recipe import scrape_url from scrape_schema_recipe import scrape_url
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -27,27 +28,9 @@ logger = get_logger()
@user_router.post("", status_code=201, response_model=str) @user_router.post("", status_code=201, response_model=str)
def create_from_name( def create_from_name(data: CreateRecipe, recipe_service: RecipeService = Depends(RecipeService.base)) -> str:
background_tasks: BackgroundTasks,
data: CreateRecipe,
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
) -> str:
""" Takes in a JSON string and loads data into the database as a new entry""" """ Takes in a JSON string and loads data into the database as a new entry"""
return recipe_service.create_recipe(data).slug
data = Recipe(name=data.name)
recipe: Recipe = db.recipes.create(session, data.dict())
background_tasks.add_task(
create_recipe_event,
"Recipe Created (URL)",
f"'{recipe.name}' by {current_user.full_name} \n {settings.BASE_URL}/recipe/{recipe.slug}",
session=session,
attachment=recipe.image_dir.joinpath("min-original.webp"),
)
return recipe.slug
@user_router.post("/test-scrape-url") @user_router.post("/test-scrape-url")
@ -78,20 +61,10 @@ def parse_recipe_url(
return recipe.slug return recipe.slug
@public_router.get("/{recipe_slug}", response_model=Recipe) @public_router.get("/{slug}", response_model=Recipe)
def get_recipe(recipe_slug: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)): def get_recipe(recipe_service: RecipeService = Depends(RecipeService.read_existing)):
""" Takes in a recipe slug, returns all data for a recipe """ """ Takes in a recipe slug, returns all data for a recipe """
return recipe_service.recipe
recipe: Recipe = db.recipes.get(session, recipe_slug)
if not recipe:
raise HTTPException(status.HTTP_404_NOT_FOUND)
if recipe.settings.public or is_user:
return recipe
else:
raise HTTPException(status.HTTP_403_FORBIDDEN)
@user_router.post("/create-from-zip") @user_router.post("/create-from-zip")
@ -174,23 +147,7 @@ def patch_recipe(
return recipe return recipe
@user_router.delete("/{recipe_slug}") @user_router.delete("/{slug}")
def delete_recipe( def delete_recipe(recipe_service: RecipeService = Depends(RecipeService.write_existing)):
background_tasks: BackgroundTasks,
recipe_slug: str,
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
):
""" Deletes a recipe by slug """ """ Deletes a recipe by slug """
return recipe_service.delete_recipe()
try:
recipe: Recipe = db.recipes.delete(session, recipe_slug)
delete_assets(recipe_slug=recipe_slug)
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
background_tasks.add_task(
create_recipe_event, "Recipe Deleted", f"'{recipe.name}' deleted by {current_user.full_name}", session=session
)
return recipe

View File

@ -1,16 +1,44 @@
import json import json
from functools import lru_cache from functools import lru_cache
from fastapi import Response from fastapi import Depends, Response
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from mealie.core.root_logger import get_logger from mealie.core.root_logger import get_logger
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import SessionLocal from mealie.db.db_setup import SessionLocal, generate_session
from mealie.routes.deps import is_logged_in
from mealie.schema.recipe import RecipeSummary from mealie.schema.recipe import RecipeSummary
from sqlalchemy.orm.session import Session
logger = get_logger() logger = get_logger()
class AllRecipesService:
def __init__(self, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)):
self.start = 0
self.limit = 9999
self.session = session or SessionLocal()
self.is_user = is_user
@classmethod
def query(
cls, start=0, limit=9999, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)
):
set_query = cls(session, is_user)
set_query.start = start
set_query.limit = limit
return set_query
def get_recipes(self):
if self.is_user:
return get_all_recipes_user(self.limit, self.start)
else:
return get_all_recipes_public(self.limit, self.start)
@lru_cache(maxsize=1) @lru_cache(maxsize=1)
def get_all_recipes_user(limit, start): def get_all_recipes_user(limit, start):
with SessionLocal() as session: with SessionLocal() as session:

View File

@ -0,0 +1,40 @@
from typing import Any
from fastapi import BackgroundTasks, Depends
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user, is_logged_in
from pydantic import BaseModel
from sqlalchemy.orm.session import Session
class CommonDeps(BaseModel):
session: Session
background_tasks: BackgroundTasks
user: Any
class Config:
arbitrary_types_allowed = True
def _read_deps(
background_tasks: BackgroundTasks,
session: Session = Depends(generate_session),
current_user=Depends(is_logged_in),
):
return CommonDeps(
session=session,
background_tasks=background_tasks,
user=current_user,
)
def _write_deps(
background_tasks: BackgroundTasks,
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
):
return CommonDeps(
session=session,
background_tasks=background_tasks,
user=current_user,
)

View File

@ -0,0 +1,136 @@
from fastapi import BackgroundTasks, Depends, HTTPException, status
from mealie.core.config import get_settings
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 UserInDB
from mealie.services.events import create_recipe_event
from mealie.services.recipe.media import delete_assets
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.session import Session
from .common_deps import CommonDeps, _read_deps, _write_deps
class RecipeService:
recipe: Recipe
def __init__(self, session: Session, user: UserInDB, background_tasks: BackgroundTasks = None) -> None:
self.session = session or SessionLocal()
self.user = user
self.background_tasks = background_tasks
self.recipe: Recipe = None
# Static Globals
self.db = get_database()
self.settings = get_settings()
@classmethod
def read_existing(cls, slug: str, local_deps: CommonDeps = Depends(_read_deps)):
"""
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(local_deps.session, local_deps.user, local_deps.background_tasks)
new_class.assert_existing(slug)
return new_class
@classmethod
def write_existing(cls, slug: str, local_deps: CommonDeps = Depends(_write_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(local_deps.session, local_deps.user, local_deps.background_tasks)
new_class.assert_existing(slug)
return new_class
@classmethod
def base(cls, local_deps: CommonDeps = Depends(_write_deps)) -> Recipe:
"""A Base instance to be used as a router dependency
Raises:
HTTPException: 400 Bad Request
"""
return cls(local_deps.session, local_deps.user, local_deps.background_tasks)
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):
self.pupulate_recipe(slug)
if not self.recipe:
raise HTTPException(status.HTTP_404_NOT_FOUND)
if not self.recipe.settings.public and not self.user:
raise HTTPException(status.HTTP_403_FORBIDDEN)
# CRUD METHODS
def create_recipe(self, new_recipe: CreateRecipe) -> Recipe:
try:
create_data = Recipe(name=new_recipe.name)
self.recipe = self.db.recipes.create(self.session, create_data)
except IntegrityError:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": "RECIPE_ALREADY_EXISTS"})
self._create_event(
"Recipe Created (URL)",
f"'{self.recipe.name}' by {self.user.username} \n {self.settings.BASE_URL}/recipe/{self.recipe.slug}",
)
return self.recipe
def delete_recipe(self) -> Recipe:
"""removes a recipe from the database and purges the existing files from the filesystem.
Raises:
HTTPException: 400 Bad Request
Returns:
Recipe: The deleted recipe
"""
try:
recipe: Recipe = self.db.recipes.delete(self.session, self.recipe.slug)
delete_assets(recipe_slug=self.recipe.slug)
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
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)