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:
Hayden
2021-09-19 15:31:34 -08:00
committed by GitHub
parent c0e3f04c23
commit 476aefeeb0
68 changed files with 1131 additions and 1084 deletions

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

View File

@@ -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}")

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