mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-06 16:03:12 -05:00
refactor(backend): ♻️ cleanup HTTP service classes and remove database singleton (#687)
* refactor(backend): ♻️ cleanup duplicate code in http services * refactor(backend): ♻️ refactor database away from singleton design removed the database single and instead injected the session into a new Database class that is created during each request life-cycle. Now sessions no longer need to be passed into each method on the database All tests pass, but there are likely some hidden breaking changes that were not discovered. * fix venv * disable venv cache * fix install script * bump poetry version * postgres fixes * revert install * fix db initialization for postgres * add postgres to docker * refactor(backend): ♻️ cleanup unused and duplicate code in http services * refactor(backend): remove sessions from arguments * refactor(backend): ♻️ convert units and ingredients to use http service class * test(backend): ✅ add unit and food tests * lint * update tags * re-enable cache * fix missing fraction in db * fix lint Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
37
mealie/services/recipe/recipe_food_service.py
Normal file
37
mealie/services/recipe/recipe_food_service.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import cached_property
|
||||
|
||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientFood, IngredientFood
|
||||
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
|
||||
from mealie.services._base_http_service.http_services import UserHttpService
|
||||
from mealie.services.events import create_recipe_event
|
||||
|
||||
|
||||
class RecipeFoodService(
|
||||
CrudHttpMixins[IngredientFood, CreateIngredientFood, CreateIngredientFood],
|
||||
UserHttpService[int, IngredientFood],
|
||||
):
|
||||
event_func = create_recipe_event
|
||||
_restrict_by_group = False
|
||||
_schema = IngredientFood
|
||||
|
||||
@cached_property
|
||||
def dal(self):
|
||||
return self.db.ingredient_foods
|
||||
|
||||
def populate_item(self, id: int) -> IngredientFood:
|
||||
self.item = self.dal.get_one(id)
|
||||
return self.item
|
||||
|
||||
def get_all(self) -> list[IngredientFood]:
|
||||
return self.dal.get_all()
|
||||
|
||||
def create_one(self, data: CreateIngredientFood) -> IngredientFood:
|
||||
return self._create_one(data)
|
||||
|
||||
def update_one(self, data: IngredientFood, item_id: int = None) -> IngredientFood:
|
||||
return self._update_one(data, item_id)
|
||||
|
||||
def delete_one(self, id: int = None) -> IngredientFood:
|
||||
return self._delete_one(id)
|
||||
@@ -1,13 +1,15 @@
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from shutil import copytree, rmtree
|
||||
from typing import Union
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from mealie.core.dependencies.grouped import PublicDeps, UserDeps
|
||||
from mealie.core.root_logger import get_logger
|
||||
from mealie.db.data_access_layer.recipe_access_model import RecipeDataAccessModel
|
||||
from mealie.schema.recipe.recipe import CreateRecipe, Recipe, RecipeSummary
|
||||
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
|
||||
from mealie.services._base_http_service.http_services import UserHttpService
|
||||
from mealie.services.events import create_recipe_event
|
||||
from mealie.services.recipe.mixins import recipe_creation_factory
|
||||
@@ -15,7 +17,7 @@ from mealie.services.recipe.mixins import recipe_creation_factory
|
||||
logger = get_logger(module=__name__)
|
||||
|
||||
|
||||
class RecipeService(UserHttpService[str, Recipe]):
|
||||
class RecipeService(CrudHttpMixins[CreateRecipe, Recipe, Recipe], UserHttpService[str, Recipe]):
|
||||
"""
|
||||
Class Methods:
|
||||
`read_existing`: Reads an existing recipe from the database.
|
||||
@@ -25,6 +27,10 @@ class RecipeService(UserHttpService[str, Recipe]):
|
||||
|
||||
event_func = create_recipe_event
|
||||
|
||||
@cached_property
|
||||
def dal(self) -> RecipeDataAccessModel:
|
||||
return self.db.recipes
|
||||
|
||||
@classmethod
|
||||
def write_existing(cls, slug: str, deps: UserDeps = Depends()):
|
||||
return super().write_existing(slug, deps)
|
||||
@@ -35,75 +41,49 @@ class RecipeService(UserHttpService[str, Recipe]):
|
||||
|
||||
def assert_existing(self, slug: str):
|
||||
self.populate_item(slug)
|
||||
|
||||
if not self.item:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if not self.item.settings.public and not self.user:
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def populate_item(self, slug: str) -> Recipe:
|
||||
self.item = self.db.recipes.get(self.session, slug)
|
||||
return self.item
|
||||
|
||||
# CRUD METHODS
|
||||
def get_all(self, start=0, limit=None):
|
||||
return self.db.recipes.multi_query(
|
||||
self.session, {"group_id": self.user.group_id}, start=start, limit=limit, override_schema=RecipeSummary
|
||||
{"group_id": self.user.group_id},
|
||||
start=start,
|
||||
limit=limit,
|
||||
override_schema=RecipeSummary,
|
||||
)
|
||||
|
||||
def create_one(self, create_data: Union[Recipe, CreateRecipe]) -> Recipe:
|
||||
create_data = recipe_creation_factory(self.user, name=create_data.name, additional_attrs=create_data.dict())
|
||||
|
||||
try:
|
||||
self.item = self.db.recipes.create(self.session, create_data)
|
||||
except IntegrityError:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": "RECIPE_ALREADY_EXISTS"})
|
||||
|
||||
self._create_one(create_data, "RECIPE_ALREAD_EXISTS")
|
||||
self._create_event(
|
||||
"Recipe Created",
|
||||
f"'{self.item.name}' by {self.user.username} \n {self.settings.BASE_URL}/recipe/{self.item.slug}",
|
||||
)
|
||||
|
||||
return self.item
|
||||
|
||||
def update_one(self, update_data: Recipe) -> Recipe:
|
||||
original_slug = self.item.slug
|
||||
|
||||
try:
|
||||
self.item = 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)
|
||||
|
||||
self._update_one(update_data, original_slug)
|
||||
self.check_assets(original_slug)
|
||||
return self.item
|
||||
|
||||
def patch_one(self, patch_data: Recipe) -> Recipe:
|
||||
original_slug = self.item.slug
|
||||
|
||||
try:
|
||||
self.item = 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)
|
||||
|
||||
self._patch_one(patch_data, original_slug)
|
||||
self.check_assets(original_slug)
|
||||
return self.item
|
||||
|
||||
def delete_one(self) -> Recipe:
|
||||
try:
|
||||
recipe: Recipe = self.db.recipes.delete(self.session, self.item.slug)
|
||||
self._delete_assets()
|
||||
except Exception:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
self._delete_one(self.item.slug)
|
||||
self.delete_assets()
|
||||
self._create_event("Recipe Delete", f"'{self.item.name}' deleted by {self.user.full_name}")
|
||||
return self.item
|
||||
|
||||
self._create_event("Recipe Delete", f"'{recipe.name}' deleted by {self.user.full_name}")
|
||||
return recipe
|
||||
|
||||
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."""
|
||||
if original_slug != self.item.slug:
|
||||
current_dir = self.app_dirs.RECIPE_DATA_DIR.joinpath(original_slug)
|
||||
@@ -123,7 +103,7 @@ class RecipeService(UserHttpService[str, Recipe]):
|
||||
if file.name not in all_asset_files:
|
||||
file.unlink()
|
||||
|
||||
def _delete_assets(self) -> None:
|
||||
def delete_assets(self) -> None:
|
||||
recipe_dir = self.item.directory
|
||||
rmtree(recipe_dir, ignore_errors=True)
|
||||
logger.info(f"Recipe Directory Removed: {self.item.slug}")
|
||||
|
||||
37
mealie/services/recipe/recipe_unit_service.py
Normal file
37
mealie/services/recipe/recipe_unit_service.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import cached_property
|
||||
|
||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit, IngredientUnit
|
||||
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
|
||||
from mealie.services._base_http_service.http_services import UserHttpService
|
||||
from mealie.services.events import create_recipe_event
|
||||
|
||||
|
||||
class RecipeUnitService(
|
||||
CrudHttpMixins[IngredientUnit, CreateIngredientUnit, CreateIngredientUnit],
|
||||
UserHttpService[int, IngredientUnit],
|
||||
):
|
||||
event_func = create_recipe_event
|
||||
_restrict_by_group = False
|
||||
_schema = IngredientUnit
|
||||
|
||||
@cached_property
|
||||
def dal(self):
|
||||
return self.db.ingredient_units
|
||||
|
||||
def populate_item(self, id: int) -> IngredientUnit:
|
||||
self.item = self.dal.get_one(id)
|
||||
return self.item
|
||||
|
||||
def get_all(self) -> list[IngredientUnit]:
|
||||
return self.dal.get_all()
|
||||
|
||||
def create_one(self, data: CreateIngredientUnit) -> IngredientUnit:
|
||||
return self._create_one(data)
|
||||
|
||||
def update_one(self, data: IngredientUnit, item_id: int = None) -> IngredientUnit:
|
||||
return self._update_one(data, item_id)
|
||||
|
||||
def delete_one(self, id: int = None) -> IngredientUnit:
|
||||
return self._delete_one(id)
|
||||
Reference in New Issue
Block a user