refactor(backend): ♻️ align variable names, eliminate dead-code, and finalize recipe services

This commit is contained in:
hay-kot 2021-08-28 16:24:14 -08:00
parent 985ad8017d
commit 8e9280efaf
23 changed files with 132 additions and 180 deletions

View file

@ -10,7 +10,7 @@ from mealie.routes.mealplans import meal_plan_router
from mealie.routes.media import media_router
from mealie.routes.site_settings import settings_router
from mealie.services.events import create_general_event
from mealie.services.recipe.all_recipes import subscripte_to_recipe_events
from mealie.services.recipe.all_recipe_service import subscripte_to_recipe_events
logger = get_logger()

View file

@ -1,9 +1,9 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import is_logged_in
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.core.dependencies import is_logged_in
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse

View file

@ -1,9 +1,9 @@
from fastapi import BackgroundTasks, Depends, HTTPException, status
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.core.dependencies import get_current_user
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.schema.user import GroupBase, GroupInDB, UpdateGroup, UserInDB
from mealie.services.events import create_group_event

View file

@ -2,9 +2,9 @@ from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
from mealie.core.dependencies import get_current_user
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.core.dependencies import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.meal_plan import MealPlanIn, MealPlanOut
from mealie.schema.user import GroupInDB, UserInDB

View file

@ -1,10 +1,10 @@
from fastapi import Depends
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user
from mealie.core.root_logger import get_logger
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.core.dependencies import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.meal_plan import ListItem, MealPlanOut, ShoppingListIn, ShoppingListOut
from mealie.schema.recipe import Recipe

View file

@ -19,11 +19,11 @@ class ImageType(str, Enum):
tiny = "tiny-original.webp"
@router.get("/{recipe_slug}/images/{file_name}")
async def get_recipe_img(recipe_slug: str, file_name: ImageType = ImageType.original):
@router.get("/{slug}/images/{file_name}")
async def get_recipe_img(slug: str, file_name: ImageType = ImageType.original):
"""Takes in a recipe slug, returns the static image. This route is proxied in the docker image
and should not hit the API in production"""
recipe_image = Recipe(slug=recipe_slug).image_dir.joinpath(file_name.value)
recipe_image = Recipe(slug=slug).image_dir.joinpath(file_name.value)
if recipe_image:
return FileResponse(recipe_image)
@ -31,10 +31,10 @@ async def get_recipe_img(recipe_slug: str, file_name: ImageType = ImageType.orig
raise HTTPException(status.HTTP_404_NOT_FOUND)
@router.get("/{recipe_slug}/assets/{file_name}")
async def get_recipe_asset(recipe_slug: str, file_name: str):
@router.get("/{slug}/assets/{file_name}")
async def get_recipe_asset(slug: str, file_name: str):
""" Returns a recipe asset """
file = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
file = Recipe(slug=slug).asset_dir.joinpath(file_name)
try:
return FileResponse(file)

View file

@ -4,7 +4,7 @@ from sqlalchemy.orm.session import Session
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.schema.recipe import RecipeSummary
from mealie.services.recipe.all_recipes import AllRecipesService
from mealie.services.recipe.all_recipe_service import AllRecipesService
router = APIRouter()

View file

@ -3,9 +3,9 @@ from http.client import HTTPException
from fastapi import Depends, status
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.core.dependencies import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.recipe import CommentOut, CreateComment, SaveComment
from mealie.schema.user import UserInDB

View file

@ -14,33 +14,33 @@ from mealie.services.image.image import scrape_image, write_image
user_router = UserAPIRouter()
@user_router.post("/{recipe_slug}/image")
@user_router.post("/{slug}/image")
def scrape_image_url(
recipe_slug: str,
slug: str,
url: CreateRecipeByURL,
):
""" Removes an existing image and replaces it with the incoming file. """
scrape_image(url.url, recipe_slug)
scrape_image(url.url, slug)
@user_router.put("/{recipe_slug}/image")
@user_router.put("/{slug}/image")
def update_recipe_image(
recipe_slug: str,
slug: str,
image: bytes = File(...),
extension: str = Form(...),
session: Session = Depends(generate_session),
):
""" Removes an existing image and replaces it with the incoming file. """
write_image(recipe_slug, image, extension)
new_version = db.recipes.update_image(session, recipe_slug, extension)
write_image(slug, image, extension)
new_version = db.recipes.update_image(session, slug, extension)
return {"image": new_version}
@user_router.post("/{recipe_slug}/assets", response_model=RecipeAsset)
@user_router.post("/{slug}/assets", response_model=RecipeAsset)
def upload_recipe_asset(
recipe_slug: str,
slug: str,
name: str = Form(...),
icon: str = Form(...),
extension: str = Form(...),
@ -50,7 +50,7 @@ def upload_recipe_asset(
""" Upload a file to store as a recipe asset """
file_name = slugify(name) + "." + extension
asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name)
dest = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
dest = Recipe(slug=slug).asset_dir.joinpath(file_name)
with dest.open("wb") as buffer:
copyfileobj(file.file, buffer)
@ -58,7 +58,7 @@ def upload_recipe_asset(
if not dest.is_file():
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
recipe: Recipe = db.recipes.get(session, recipe_slug)
recipe: Recipe = db.recipes.get(session, slug)
recipe.assets.append(asset_in)
db.recipes.update(session, recipe_slug, recipe.dict())
db.recipes.update(session, slug, recipe.dict())
return asset_in

View file

@ -2,24 +2,20 @@ import json
import shutil
from zipfile import ZipFile
from fastapi import APIRouter, BackgroundTasks, Depends, File
from fastapi import APIRouter, Depends, File
from fastapi.datastructures import UploadFile
from scrape_schema_recipe import scrape_url
from sqlalchemy.orm.session import Session
from starlette.responses import FileResponse
from mealie.core.config import settings
from mealie.core.dependencies import temporary_zip_path
from mealie.core.root_logger import get_logger
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.core.dependencies 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
from mealie.services.recipe.recipe_service import RecipeService
from mealie.services.scraper.scraper import create_from_url
@ -28,44 +24,30 @@ public_router = APIRouter()
logger = get_logger()
@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 """
return recipe_service.recipe
@user_router.post("", status_code=201, response_model=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"""
return recipe_service.create_recipe(data).slug
@user_router.post("/test-scrape-url")
def test_parse_recipe_url(url: CreateRecipeByURL):
return scrape_url(url.url)
@user_router.post("/create-url", status_code=201, response_model=str)
def parse_recipe_url(
background_tasks: BackgroundTasks,
url: CreateRecipeByURL,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
def parse_recipe_url(url: CreateRecipeByURL, recipe_service: RecipeService = Depends(RecipeService.base)):
""" Takes in a URL and attempts to scrape data and load it into the database """
recipe = create_from_url(url.url)
recipe: Recipe = db.recipes.create(session, recipe.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(recipe).slug
@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 """
return recipe_service.recipe
@user_router.post("/test-scrape-url")
def test_parse_recipe_url(url: CreateRecipeByURL):
# TODO: Replace with more current implementation of testing schema
return scrape_url(url.url)
@user_router.post("/create-from-zip")
@ -98,54 +80,36 @@ async def create_recipe_from_zip(
return recipe
@public_router.get("/{recipe_slug}/zip")
@public_router.get("/{slug}/zip")
async def get_recipe_as_zip(
recipe_slug: str, session: Session = Depends(generate_session), temp_path=Depends(temporary_zip_path)
slug: str, session: Session = Depends(generate_session), temp_path=Depends(temporary_zip_path)
):
""" Get a Recipe and It's Original Image as a Zip File """
recipe: Recipe = db.recipes.get(session, recipe_slug)
recipe: Recipe = db.recipes.get(session, slug)
image_asset = recipe.image_dir.joinpath(RecipeImageTypes.original.value)
with ZipFile(temp_path, "w") as myzip:
myzip.writestr(f"{recipe_slug}.json", recipe.json())
myzip.writestr(f"{slug}.json", recipe.json())
if image_asset.is_file():
myzip.write(image_asset, arcname=image_asset.name)
return FileResponse(temp_path, filename=f"{recipe_slug}.zip")
return FileResponse(temp_path, filename=f"{slug}.zip")
@user_router.put("/{recipe_slug}")
def update_recipe(
recipe_slug: str,
data: Recipe,
session: Session = Depends(generate_session),
):
@user_router.put("/{slug}")
def update_recipe(data: Recipe, recipe_service: RecipeService = Depends(RecipeService.write_existing)):
""" Updates a recipe by existing slug and data. """
recipe: Recipe = db.recipes.update(session, recipe_slug, data.dict())
check_assets(original_slug=recipe_slug, recipe=recipe)
return recipe
return recipe_service.update_recipe(data)
@user_router.patch("/{recipe_slug}")
def patch_recipe(
recipe_slug: str,
data: Recipe,
session: Session = Depends(generate_session),
):
@user_router.patch("/{slug}")
def patch_recipe(data: Recipe, recipe_service: RecipeService = Depends(RecipeService.write_existing)):
""" Updates a recipe by existing slug and data. """
recipe: Recipe = db.recipes.patch(
session, recipe_slug, new_data=data.dict(exclude_unset=True, exclude_defaults=True)
)
check_assets(original_slug=recipe_slug, recipe=recipe)
return recipe
return recipe_service.patch_recipe(data)
@user_router.delete("/{slug}")

View file

@ -1,9 +1,9 @@
from fastapi import Depends
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.core.dependencies import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.meal_plan import ShoppingListIn, ShoppingListOut
from mealie.schema.user import UserInDB

View file

@ -1,9 +1,9 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.core.dependencies import get_current_user
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.admin import SiteSettings
from mealie.schema.user import GroupInDB, UserInDB

View file

@ -1,9 +1,9 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import is_logged_in
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.core.dependencies import is_logged_in
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.schema.recipe import RecipeTagResponse, TagIn

View file

@ -4,10 +4,10 @@ from fastapi import HTTPException, status
from fastapi.param_functions import Depends
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user
from mealie.core.security import create_access_token
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.core.dependencies import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.schema.user import CreateToken, LoingLiveTokenIn, LongLiveTokenInDB, UserInDB

View file

@ -2,10 +2,10 @@ from fastapi import BackgroundTasks, Depends, HTTPException, status
from sqlalchemy.orm.session import Session
from mealie.core import security
from mealie.core.dependencies import get_current_user
from mealie.core.security import get_password_hash
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.core.dependencies import get_current_user
from mealie.routes.routers import AdminAPIRouter, UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed
from mealie.schema.user import UserBase, UserIn, UserInDB, UserOut

View file

@ -1,9 +1,9 @@
from fastapi import Depends
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_current_user
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.core.dependencies import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed
from mealie.schema.user import UserFavorites, UserInDB

View file

@ -2,10 +2,10 @@ from fastapi import Depends, HTTPException, status
from sqlalchemy.orm.session import Session
from mealie.core.config import settings
from mealie.core.dependencies import get_current_user
from mealie.core.security import get_password_hash, verify_password
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.core.dependencies import get_current_user
from mealie.routes.routers import UserAPIRouter
from mealie.routes.users._helpers import assert_user_change_allowed
from mealie.schema.user import ChangePassword, UserInDB

View file

@ -3,10 +3,10 @@ import uuid
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import get_admin_user
from mealie.core.security import get_password_hash
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.core.dependencies import get_admin_user
from mealie.routes.routers import AdminAPIRouter
from mealie.schema.user import SignUpIn, SignUpOut, SignUpToken, UserIn, UserInDB
from mealie.services.events import create_user_event

View file

@ -5,10 +5,10 @@ from fastapi import Depends, Response
from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm.session import Session
from mealie.core.dependencies import is_logged_in
from mealie.core.root_logger import get_logger
from mealie.db.database import db
from mealie.db.db_setup import SessionLocal, generate_session
from mealie.core.dependencies import is_logged_in
from mealie.schema.recipe import RecipeSummary
logger = get_logger()

View file

@ -1,41 +0,0 @@
from typing import Any
from fastapi import BackgroundTasks, Depends
from pydantic import BaseModel
from sqlalchemy.orm.session import Session
from mealie.db.db_setup import generate_session
from mealie.core.dependencies import get_current_user, is_logged_in
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

@ -1,34 +0,0 @@
from pathlib import Path
from shutil import copytree, rmtree
from mealie.core.config import app_dirs
from mealie.core.root_logger import get_logger
from mealie.schema.recipe import Recipe
logger = get_logger()
def check_assets(original_slug, recipe: Recipe) -> None:
if original_slug != recipe.slug:
current_dir = app_dirs.RECIPE_DATA_DIR.joinpath(original_slug)
try:
copytree(current_dir, recipe.directory, dirs_exist_ok=True)
except FileNotFoundError:
logger.error(f"Recipe Directory not Found: {original_slug}")
logger.info(f"Renaming Recipe Directory: {original_slug} -> {recipe.slug}")
all_asset_files = [x.file_name for x in recipe.assets]
for file in recipe.asset_dir.iterdir():
file: Path
if file.is_dir():
continue
if file.name not in all_asset_files:
file.unlink()
def delete_assets(recipe_slug):
recipe_dir = Recipe(slug=recipe_slug).directory
rmtree(recipe_dir, ignore_errors=True)
logger.info(f"Recipe Directory Removed: {recipe_slug}")

View file

@ -1,20 +1,33 @@
from pathlib import Path
from shutil import copytree, rmtree
from typing import Union
from fastapi import BackgroundTasks, Depends, HTTPException, status
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.session import Session
from mealie.core.config import get_settings
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 UserInDB
from mealie.services.events import create_recipe_event
from mealie.services.recipe.media import delete_assets
logger = get_logger(module=__name__)
class RecipeService:
recipe: Recipe
"""
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
"""
recipe: Recipe # Required for proper type hints
def __init__(self, session: Session, user: UserInDB, background_tasks: BackgroundTasks = None) -> None:
self.session = session or SessionLocal()
@ -22,8 +35,9 @@ class RecipeService:
self.background_tasks = background_tasks
self.recipe: Recipe = None
# Static Globals
# Static Globals Dependency Injection
self.db = get_database()
self.app_dirs = get_app_dirs()
self.settings = get_settings()
@classmethod
@ -99,10 +113,11 @@ class RecipeService:
raise HTTPException(status.HTTP_403_FORBIDDEN)
# CRUD METHODS
def create_recipe(self, new_recipe: CreateRecipe) -> Recipe:
def create_recipe(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe:
if isinstance(create_data, CreateRecipe):
create_data = Recipe(name=create_data.name)
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"})
@ -114,6 +129,32 @@ class RecipeService:
return self.recipe
def update_recipe(self, update_data: Recipe) -> Recipe:
original_slug = self.recipe.slug
try:
self.recipe = self.db.recipes.update(self.session, original_slug, update_data)
except IntegrityError:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": "RECIPE_ALREADY_EXISTS"})
self._check_assets(original_slug)
return self.recipe
def patch_recipe(self, patch_data: Recipe) -> Recipe:
original_slug = self.recipe.slug
try:
self.recipe = self.db.recipes.patch(
self.session, original_slug, patch_data.dict(exclude_unset=True, exclude_defaults=True)
)
except IntegrityError:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": "RECIPE_ALREADY_EXISTS"})
self._check_assets(original_slug)
return self.recipe
def delete_recipe(self) -> Recipe:
"""removes a recipe from the database and purges the existing files from the filesystem.
@ -126,7 +167,7 @@ class RecipeService:
try:
recipe: Recipe = self.db.recipes.delete(self.session, self.recipe.slug)
delete_assets(recipe_slug=self.recipe.slug)
self._delete_assets()
except Exception:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
@ -135,3 +176,27 @@ class RecipeService:
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) -> None:
if original_slug != self.recipe.slug:
current_dir = self.app_dirs.RECIPE_DATA_DIR.joinpath(original_slug)
try:
copytree(current_dir, self.recipe.directory, dirs_exist_ok=True)
logger.info(f"Renaming Recipe Directory: {original_slug} -> {self.recipe.slug}")
except FileNotFoundError:
logger.error(f"Recipe Directory not Found: {original_slug}")
all_asset_files = [x.file_name for x in self.recipe.assets]
for file in self.recipe.asset_dir.iterdir():
file: Path
if file.is_dir():
continue
if file.name not in all_asset_files:
file.unlink()
def _delete_assets(self) -> None:
recipe_dir = self.recipe.directory
rmtree(recipe_dir, ignore_errors=True)
logger.info(f"Recipe Directory Removed: {self.recipe.slug}")

View file

@ -5,6 +5,4 @@ Cron = collections.namedtuple("Cron", "hours minutes")
def cron_parser(time_str: str) -> Cron:
time = time_str.split(":")
cron = Cron(hours=int(time[0]), minutes=int(time[1]))
return cron
return Cron(hours=int(time[0]), minutes=int(time[1]))