refactor(backend): ⚗️ experimental dependency injection framework (WIP)
This commit is contained in:
parent
da501adce8
commit
1c11f6a3d7
5 changed files with 221 additions and 65 deletions
|
@ -1,16 +1,15 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from mealie.db.database import db
|
||||
from mealie.db.db_setup import generate_session
|
||||
from mealie.routes.deps import is_logged_in
|
||||
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
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@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
|
||||
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 get_all_recipes_user(limit, start)
|
||||
|
||||
else:
|
||||
return get_all_recipes_public(limit, start)
|
||||
return all_recipes_service.get_recipes()
|
||||
|
||||
|
||||
@router.get("/summary/untagged", response_model=list[RecipeSummary])
|
||||
|
|
|
@ -2,20 +2,21 @@ import json
|
|||
import shutil
|
||||
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 mealie.core.config import settings
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.db.database import db
|
||||
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.schema.recipe import CreateRecipeByURL, Recipe, RecipeImageTypes
|
||||
from mealie.schema.recipe.recipe import CreateRecipe
|
||||
from mealie.schema.user import UserInDB
|
||||
from mealie.services.events import create_recipe_event
|
||||
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 scrape_schema_recipe import scrape_url
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
@ -27,27 +28,9 @@ logger = get_logger()
|
|||
|
||||
|
||||
@user_router.post("", status_code=201, response_model=str)
|
||||
def create_from_name(
|
||||
background_tasks: BackgroundTasks,
|
||||
data: CreateRecipe,
|
||||
session: Session = Depends(generate_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> str:
|
||||
def create_from_name(data: CreateRecipe, recipe_service: RecipeService = Depends(RecipeService.base)) -> str:
|
||||
""" Takes in a JSON string and loads data into the database as a new entry"""
|
||||
|
||||
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
|
||||
return recipe_service.create_recipe(data).slug
|
||||
|
||||
|
||||
@user_router.post("/test-scrape-url")
|
||||
|
@ -78,20 +61,10 @@ def parse_recipe_url(
|
|||
return recipe.slug
|
||||
|
||||
|
||||
@public_router.get("/{recipe_slug}", response_model=Recipe)
|
||||
def get_recipe(recipe_slug: str, session: Session = Depends(generate_session), is_user: bool = Depends(is_logged_in)):
|
||||
@public_router.get("/{slug}", response_model=Recipe)
|
||||
def get_recipe(recipe_service: RecipeService = Depends(RecipeService.read_existing)):
|
||||
""" Takes in a recipe slug, returns all data for a 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)
|
||||
return recipe_service.recipe
|
||||
|
||||
|
||||
@user_router.post("/create-from-zip")
|
||||
|
@ -174,23 +147,7 @@ def patch_recipe(
|
|||
return recipe
|
||||
|
||||
|
||||
@user_router.delete("/{recipe_slug}")
|
||||
def delete_recipe(
|
||||
background_tasks: BackgroundTasks,
|
||||
recipe_slug: str,
|
||||
session: Session = Depends(generate_session),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
@user_router.delete("/{slug}")
|
||||
def delete_recipe(recipe_service: RecipeService = Depends(RecipeService.write_existing)):
|
||||
""" Deletes a recipe by slug """
|
||||
|
||||
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
|
||||
return recipe_service.delete_recipe()
|
||||
|
|
|
@ -1,16 +1,44 @@
|
|||
import json
|
||||
from functools import lru_cache
|
||||
|
||||
from fastapi import Response
|
||||
from fastapi import Depends, Response
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from mealie.core.root_logger import get_logger
|
||||
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 sqlalchemy.orm.session import Session
|
||||
|
||||
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)
|
||||
def get_all_recipes_user(limit, start):
|
||||
with SessionLocal() as session:
|
||||
|
|
40
mealie/services/recipe/common_deps.py
Normal file
40
mealie/services/recipe/common_deps.py
Normal 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,
|
||||
)
|
136
mealie/services/recipe/recipe_service.py
Normal file
136
mealie/services/recipe/recipe_service.py
Normal 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)
|
Loading…
Reference in a new issue