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

@@ -7,7 +7,6 @@ from sqlalchemy.orm.session import Session
from mealie.core.config import get_app_dirs, get_settings
from mealie.core.root_logger import get_logger
from mealie.db.data_access_layer.db_access import DatabaseAccessLayer
from mealie.db.database import get_database
from mealie.db.db_setup import SessionLocal
from mealie.schema.user.user import PrivateUser
@@ -45,8 +44,6 @@ class BaseHttpService(Generic[T, D], ABC):
delete_one: Callable = None
delete_all: Callable = None
db_access: DatabaseAccessLayer = None
# Type Definitions
_schema = None
@@ -64,7 +61,7 @@ class BaseHttpService(Generic[T, D], ABC):
self.background_tasks = background_tasks
# Static Globals Dependency Injection
self.db = get_database()
self.db = get_database(session)
self.app_dirs = get_app_dirs()
self.settings = get_settings()
@@ -110,7 +107,7 @@ class BaseHttpService(Generic[T, D], ABC):
def group_id(self):
# TODO: Populate Group in Private User Call WARNING: May require significant refactoring
if not self._group_id_cache:
group = self.db.groups.get(self.session, self.user.group, "name")
group = self.db.groups.get(self.user.group, "name")
self._group_id_cache = group.id
return self._group_id_cache

View File

@@ -1,27 +1,37 @@
from abc import ABC, abstractmethod
from typing import Generic, TypeVar
from fastapi import HTTPException, status
from pydantic import BaseModel
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from mealie.core.root_logger import get_logger
from mealie.db.data_access_layer.db_access import DatabaseAccessLayer
from mealie.db.data_access_layer._access_model import AccessModel
C = TypeVar("C", bound=BaseModel)
R = TypeVar("R", bound=BaseModel)
U = TypeVar("U", bound=BaseModel)
DAL = TypeVar("DAL", bound=DatabaseAccessLayer)
DAL = TypeVar("DAL", bound=AccessModel)
logger = get_logger()
class CrudHttpMixins(Generic[C, R, U]):
item: C
class CrudHttpMixins(Generic[C, R, U], ABC):
item: R
session: Session
dal: DAL
@property
@abstractmethod
def dal(self) -> DAL:
...
def populate_item(self, id: int) -> R:
self.item = self.dal.get_one(id)
return self.item
def _create_one(self, data: C, exception_msg="generic-create-error") -> R:
try:
self.item = self.dal.create(self.session, data)
self.item = self.dal.create(data)
except Exception as ex:
logger.exception(ex)
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": exception_msg, "exception": str(ex)})
@@ -33,17 +43,26 @@ class CrudHttpMixins(Generic[C, R, U]):
return
target_id = item_id or self.item.id
self.item = self.dal.update(self.session, target_id, data)
self.item = self.dal.update(target_id, data)
return self.item
def _patch_one(self) -> None:
raise NotImplementedError
def _delete_one(self, item_id: int = None) -> None:
if not self.item:
return
def _patch_one(self, data: U, item_id: int) -> None:
try:
self.item = self.dal.patch(item_id, data.dict(exclude_unset=True, exclude_defaults=True))
except IntegrityError:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail={"message": "generic-patch-error"})
def _delete_one(self, item_id: int = None) -> R:
target_id = item_id or self.item.id
self.item = self.dal.delete(self.session, target_id)
logger.info(f"Deleting item with id {target_id}")
try:
self.item = self.dal.delete(target_id)
except Exception as ex:
logger.exception(ex)
raise HTTPException(
status.HTTP_400_BAD_REQUEST, detail={"message": "generic-delete-error", "exception": str(ex)}
)
return self.item

View File

@@ -10,7 +10,7 @@ from pydantic.main import BaseModel
from mealie.core import root_logger
from mealie.core.config import app_dirs
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import create_session
from mealie.services.events import create_backup_event
@@ -114,29 +114,31 @@ def backup_all(
):
db_export = ExportDatabase(tag=tag, templates=templates)
db = get_database(session)
if export_users:
all_users = db.users.get_all(session)
all_users = db.users.get_all()
db_export.export_items(all_users, "users")
if export_groups:
all_groups = db.groups.get_all(session)
all_groups = db.groups.get_all()
db_export.export_items(all_groups, "groups")
if export_recipes:
all_recipes = db.recipes.get_all(session)
all_recipes = db.recipes.get_all()
db_export.export_recipe_dirs()
db_export.export_items(all_recipes, "recipes", export_list=False, slug_folder=True)
db_export.export_templates(all_recipes)
all_comments = db.comments.get_all(session)
all_comments = db.comments.get_all()
db_export.export_items(all_comments, "comments")
if export_settings:
all_settings = db.settings.get_all(session)
all_settings = db.settings.get_all()
db_export.export_items(all_settings, "settings")
if export_notifications:
all_notifications = db.event_notifications.get_all(session)
all_notifications = db.event_notifications.get_all()
db_export.export_items(all_notifications, "notifications")
return db_export.finish_export()

View File

@@ -8,7 +8,7 @@ from pydantic.main import BaseModel
from sqlalchemy.orm.session import Session
from mealie.core.config import app_dirs
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.schema.admin import (
CommentImport,
GroupImport,
@@ -44,6 +44,7 @@ class ImportDatabase:
"""
self.user = user
self.session = session
self.db = get_database(session)
self.archive = app_dirs.BACKUP_DIR.joinpath(zip_archive)
self.force_imports = force_import
@@ -72,7 +73,7 @@ class ImportDatabase:
recipe.user_id = self.user.id
import_status = self.import_model(
db_table=db.recipes,
db_table=self.db.recipes,
model=recipe,
return_model=RecipeImport,
name_attr="name",
@@ -101,7 +102,7 @@ class ImportDatabase:
comment: CommentOut
self.import_model(
db_table=db.comments,
db_table=self.db.comments,
model=comment,
return_model=CommentImport,
name_attr="uuid",
@@ -166,7 +167,7 @@ class ImportDatabase:
for notify in notifications:
import_status = self.import_model(
db_table=db.event_notifications,
db_table=self.db.event_notifications,
model=notify,
return_model=NotificationImport,
name_attr="name",
@@ -183,7 +184,7 @@ class ImportDatabase:
settings = settings[0]
try:
db.settings.update(self.session, 1, settings.dict())
self.db.settings.update(1, settings.dict())
import_status = SettingsImport(name="Site Settings", status=True)
except Exception as inst:
@@ -198,7 +199,7 @@ class ImportDatabase:
group_imports = []
for group in groups:
import_status = self.import_model(db.groups, group, GroupImport, search_key="name")
import_status = self.import_model(self.db.groups, group, GroupImport, search_key="name")
group_imports.append(import_status)
return group_imports
@@ -209,13 +210,13 @@ class ImportDatabase:
user_imports = []
for user in users:
if user.id == 1: # Update Default User
db.users.update(self.session, 1, user.dict())
self.db.users.update(1, user.dict())
import_status = UserImport(name=user.full_name, status=True)
user_imports.append(import_status)
continue
import_status = self.import_model(
db_table=db.users,
db_table=self.db.users,
model=user,
return_model=UserImport,
name_attr="full_name",
@@ -283,7 +284,7 @@ class ImportDatabase:
model_name = getattr(model, name_attr)
search_value = getattr(model, search_key)
item = db_table.get(self.session, search_value, search_key)
item = db_table.get(search_value, search_key)
if item:
if not self.force_imports:
return return_model(
@@ -293,9 +294,9 @@ class ImportDatabase:
)
primary_key = getattr(item, db_table.primary_key)
db_table.delete(self.session, primary_key)
db_table.delete(primary_key)
try:
db_table.create(self.session, model.dict())
db_table.create(model.dict())
import_status = return_model(name=model_name, status=True)
except Exception as inst:

View File

@@ -1,13 +1,12 @@
import apprise
from sqlalchemy.orm.session import Session
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import create_session
from mealie.schema.events import Event, EventCategory
def test_notification(notification_url, event=None) -> bool:
if event is None:
event = Event(
title="Test Notification",
@@ -38,9 +37,10 @@ def post_notifications(event: Event, notification_urls=list[str], hard_fail=Fals
def save_event(title, text, category, session: Session, attachment=None):
event = Event(title=title, text=text, category=category)
session = session or create_session()
db.events.create(session, event.dict())
db = get_database(session)
db.events.create(event.dict())
notification_objects = db.event_notifications.get(session=session, match_value=True, match_key=category, limit=9999)
notification_objects = db.event_notifications.get(match_value=True, match_key=category, limit=9999)
notification_urls = [x.notification_url for x in notification_objects]
post_notifications(event, notification_urls, attachment=attachment)

View File

@@ -1,7 +1,8 @@
from __future__ import annotations
from functools import cached_property
from mealie.core.root_logger import get_logger
from mealie.db.database import get_database
from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
from mealie.services._base_http_service.http_services import UserHttpService
@@ -12,17 +13,16 @@ logger = get_logger(module=__name__)
class CookbookService(
UserHttpService[int, ReadCookBook],
CrudHttpMixins[CreateCookBook, ReadCookBook, UpdateCookBook],
UserHttpService[int, ReadCookBook],
):
event_func = create_group_event
_restrict_by_group = True
_schema = ReadCookBook
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.dal = get_database().cookbooks
@cached_property
def dal(self):
return self.db.cookbooks
def populate_item(self, item_id: int) -> RecipeCookBook:
try:
@@ -31,13 +31,13 @@ class CookbookService(
pass
if isinstance(item_id, int):
self.item = self.db.cookbooks.get_one(self.session, item_id, override_schema=RecipeCookBook)
self.item = self.dal.get_one(item_id, override_schema=RecipeCookBook)
else:
self.item = self.db.cookbooks.get_one(self.session, item_id, key="slug", override_schema=RecipeCookBook)
self.item = self.dal.get_one(item_id, key="slug", override_schema=RecipeCookBook)
def get_all(self) -> list[ReadCookBook]:
items = self.db.cookbooks.get(self.session, self.group_id, "group_id", limit=999)
items = self.dal.get(self.group_id, "group_id", limit=999)
items.sort(key=lambda x: x.position)
return items
@@ -52,7 +52,7 @@ class CookbookService(
updated = []
for cookbook in data:
cb = self.db.cookbooks.update(self.session, cookbook.id, cookbook)
cb = self.dal.update(cookbook.id, cookbook)
updated.append(cb)
return updated

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from uuid import uuid4
from fastapi import Depends, HTTPException, status
from fastapi import Depends
from mealie.core.dependencies.grouped import UserDeps
from mealie.core.root_logger import get_logger
@@ -17,7 +17,7 @@ logger = get_logger(module=__name__)
class GroupSelfService(UserHttpService[int, str]):
_restrict_by_group = True
_restrict_by_group = False
event_func = create_group_event
item: GroupInDB
@@ -31,31 +31,21 @@ class GroupSelfService(UserHttpService[int, str]):
"""Override parent method to remove `item_id` from arguments"""
return super().write_existing(item_id=0, deps=deps)
def assert_existing(self, _: str = None):
self.populate_item()
if not self.item:
raise HTTPException(status.HTTP_404_NOT_FOUND)
if self.item.id != self.group_id:
raise HTTPException(status.HTTP_403_FORBIDDEN)
def populate_item(self, _: str = None) -> GroupInDB:
self.item = self.db.groups.get(self.session, self.group_id)
self.item = self.db.groups.get(self.group_id)
return self.item
def update_categories(self, new_categories: list[CategoryBase]):
self.item.categories = new_categories
return self.db.groups.update(self.session, self.group_id, self.item)
return self.db.groups.update(self.group_id, self.item)
def update_preferences(self, new_preferences: UpdateGroupPreferences):
self.db.group_preferences.update(self.session, self.group_id, new_preferences)
self.db.group_preferences.update(self.group_id, new_preferences)
return self.populate_item()
def create_invite_token(self, uses: int = 1) -> None:
token = SaveInviteToken(uses_left=uses, group_id=self.group_id, token=uuid4().hex)
return self.db.group_tokens.create(self.session, token)
return self.db.group_invite_tokens.create(token)
def get_invite_tokens(self) -> list[ReadInviteToken]:
return self.db.group_tokens.multi_query(self.session, {"group_id": self.group_id})
return self.db.group_invite_tokens.multi_query({"group_id": self.group_id})

View File

@@ -1,16 +1,15 @@
from mealie.db.database import get_database
from mealie.db.data_access_layer.access_model_factory import Database
from mealie.schema.group.group_preferences import CreateGroupPreferences
from mealie.schema.user.user import GroupBase, GroupInDB
def create_new_group(session, g_base: GroupBase, g_preferences: CreateGroupPreferences = None) -> GroupInDB:
db = get_database()
created_group = db.groups.create(session, g_base)
def create_new_group(db: Database, g_base: GroupBase, g_preferences: CreateGroupPreferences = None) -> GroupInDB:
created_group = db.groups.create(g_base)
g_preferences = g_preferences or CreateGroupPreferences(group_id=0) # Assign Temporary ID before group is created
g_preferences.group_id = created_group.id
db.group_preferences.create(session, g_preferences)
db.group_preferences.create(g_preferences)
return created_group

View File

@@ -1,9 +1,9 @@
from __future__ import annotations
from datetime import date
from functools import cached_property
from mealie.core.root_logger import get_logger
from mealie.db.database import get_database
from mealie.schema.meal_plan import CreatePlanEntry, ReadPlanEntry, SavePlanEntry, UpdatePlanEntry
from .._base_http_service.crud_http_mixins import CrudHttpMixins
@@ -13,26 +13,27 @@ from ..events import create_group_event
logger = get_logger(module=__name__)
class MealService(UserHttpService[int, ReadPlanEntry], CrudHttpMixins[CreatePlanEntry, ReadPlanEntry, UpdatePlanEntry]):
class MealService(CrudHttpMixins[CreatePlanEntry, ReadPlanEntry, UpdatePlanEntry], UserHttpService[int, ReadPlanEntry]):
event_func = create_group_event
_restrict_by_group = True
_schema = ReadPlanEntry
item: ReadPlanEntry
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.dal = get_database().meals
@cached_property
def dal(self):
return self.db.meals
def populate_item(self, id: int) -> ReadPlanEntry:
self.item = self.db.meals.get_one(self.session, id)
self.item = self.dal.get_one(id)
return self.item
def get_slice(self, start: date = None, end: date = None) -> list[ReadPlanEntry]:
# 2 days ago
return self.db.meals.get_slice(self.session, start, end, group_id=self.group_id)
return self.dal.get_slice(start, end, group_id=self.group_id)
def get_today(self) -> list[ReadPlanEntry]:
return self.db.meals.get_today(self.session, group_id=self.group_id)
return self.dal.get_today(group_id=self.group_id)
def create_one(self, data: CreatePlanEntry) -> ReadPlanEntry:
data = self.cast(data, SavePlanEntry)

View File

@@ -1,7 +1,8 @@
from __future__ import annotations
from functools import cached_property
from mealie.core.root_logger import get_logger
from mealie.db.database import get_database
from mealie.schema.group import ReadWebhook
from mealie.schema.group.webhook import CreateWebhook, SaveWebhook
from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
@@ -11,22 +12,21 @@ from mealie.services.events import create_group_event
logger = get_logger(module=__name__)
class WebhookService(UserHttpService[int, ReadWebhook], CrudHttpMixins[ReadWebhook, CreateWebhook, CreateWebhook]):
class WebhookService(CrudHttpMixins[ReadWebhook, CreateWebhook, CreateWebhook], UserHttpService[int, ReadWebhook]):
event_func = create_group_event
_restrict_by_group = True
_schema = ReadWebhook
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.dal = get_database().webhooks
@cached_property
def dal(self):
return self.db.webhooks
def populate_item(self, id: int) -> ReadWebhook:
self.item = self.db.webhooks.get_one(self.session, id)
self.item = self.dal.get_one(id)
return self.item
def get_all(self) -> list[ReadWebhook]:
return self.db.webhooks.get(self.session, self.group_id, match_key="group_id", limit=9999)
return self.dal.get(self.group_id, match_key="group_id", limit=9999)
def create_one(self, data: CreateWebhook) -> ReadWebhook:
data = self.cast(data, SaveWebhook)

View File

@@ -1,50 +0,0 @@
from datetime import date, timedelta
from typing import Union
from sqlalchemy.orm.session import Session
from mealie.db.database import db
from mealie.db.db_setup import create_session
from mealie.schema.meal_plan import MealDayIn, MealPlanIn
from mealie.schema.recipe import Recipe
from mealie.schema.user import GroupInDB
def set_mealplan_dates(meal_plan_base: MealPlanIn) -> MealPlanIn:
for x, plan_days in enumerate(meal_plan_base.plan_days):
plan_days: MealDayIn
plan_days.date = meal_plan_base.start_date + timedelta(days=x)
def get_todays_meal(session: Session, group: Union[int, GroupInDB]) -> Recipe:
"""Returns the given mealplan for today based off the group. If the group
Type is of type int, then a query will be made to the database to get the
grop object."
Args:
session (Session): SqlAlchemy Session
group (Union[int, GroupInDB]): Either the id of the group or the GroupInDB Object
Returns:
Recipe: Pydantic Recipe Object
"""
session = session or create_session()
if isinstance(group, int):
group: GroupInDB = db.groups.get(session, group)
today_slug = None
for mealplan in group.mealplans:
for plan_day in mealplan.plan_days:
if plan_day.date == date.today():
if plan_day.meals[0].slug and plan_day.meals[0].slug != "":
today_slug = plan_day.meals[0].slug
else:
return plan_day.meals[0]
if today_slug:
return db.recipes.get(session, today_slug)
else:
return None

View File

@@ -7,7 +7,7 @@ import yaml
from pydantic import BaseModel
from mealie.core import root_logger
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.schema.admin import MigrationImport
from mealie.schema.recipe import Recipe
from mealie.schema.user.user import PrivateUser
@@ -37,6 +37,10 @@ class MigrationBase(BaseModel):
user: PrivateUser
@property
def db(self):
return get_database(self.session)
@property
def temp_dir(self) -> TemporaryDirectory:
"""unpacks the migration_file into a temporary directory
@@ -66,7 +70,7 @@ class MigrationBase(BaseModel):
with open(yaml_file, "r") as f:
contents = f.read().split("---")
recipe_data = {}
for x, document in enumerate(contents):
for _, document in enumerate(contents):
# Check if None or Empty String
if document is None or document == "":
@@ -172,7 +176,7 @@ class MigrationBase(BaseModel):
exception = ""
status = False
try:
db.recipes.create(self.session, recipe.dict())
self.db.recipes.create(recipe.dict())
status = True
except Exception as inst:

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)

View File

@@ -3,7 +3,7 @@ import datetime
from apscheduler.schedulers.background import BackgroundScheduler
from mealie.core import root_logger
from mealie.db.database import db
from mealie.db.database import get_database
from mealie.db.db_setup import create_session
from mealie.db.models.event import Event
from mealie.schema.user import GroupInDB
@@ -39,7 +39,8 @@ def update_webhook_schedule():
poll the database for changes and reschedule the webhook time
"""
session = create_session()
all_groups: list[GroupInDB] = db.groups.get_all(session)
db = get_database(session)
all_groups: list[GroupInDB] = db.groups.get_all()
for group in all_groups:
@@ -100,7 +101,8 @@ def add_group_to_schedule(scheduler, group: GroupInDB):
def init_webhook_schedule(scheduler, job_store: dict):
session = create_session()
all_groups: list[GroupInDB] = db.groups.get_all(session)
db = get_database(session)
all_groups: list[GroupInDB] = db.groups.get_all()
for group in all_groups:
job_store.update(add_group_to_schedule(scheduler, group))

View File

@@ -29,14 +29,14 @@ class RegistrationService(PublicHttpService[int, str]):
elif registration.group_token and registration.group_token != "":
token_entry = self.db.group_tokens.get(self.session, registration.group_token)
token_entry = self.db.group_invite_tokens.get(registration.group_token)
print("Token Entry", token_entry)
if not token_entry:
raise HTTPException(status.HTTP_400_BAD_REQUEST, {"message": "Invalid group token"})
group = self.db.groups.get(self.session, token_entry.group_id)
group = self.db.groups.get(token_entry.group_id)
else:
raise HTTPException(status.HTTP_400_BAD_REQUEST, {"message": "Missing group"})
@@ -47,10 +47,10 @@ class RegistrationService(PublicHttpService[int, str]):
token_entry.uses_left = token_entry.uses_left - 1
if token_entry.uses_left == 0:
self.db.group_tokens.delete(self.session, token_entry.token)
self.db.group_invite_tokens.delete(token_entry.token)
else:
self.db.group_tokens.update(self.session, token_entry.token, token_entry)
self.db.group_invite_tokens.update(token_entry.token, token_entry)
return user
@@ -64,7 +64,7 @@ class RegistrationService(PublicHttpService[int, str]):
group=group.name,
)
return self.db.users.create(self.session, new_user)
return self.db.users.create(new_user)
def _register_new_group(self) -> GroupInDB:
group_data = GroupBase(name=self.registration.group)
@@ -81,4 +81,4 @@ class RegistrationService(PublicHttpService[int, str]):
recipe_disable_amount=self.registration.advanced,
)
return create_new_group(self.session, group_data, group_preferences)
return create_new_group(self.db, group_data, group_preferences)

View File

@@ -25,7 +25,7 @@ class UserService(UserHttpService[int, str]):
def _populate_target_user(self, id: int = None):
if id:
self.target_user = self.db.users.get(self.session, id)
self.target_user = self.db.users.get(id)
if not self.target_user:
raise HTTPException(status.HTTP_404_NOT_FOUND)
else:
@@ -38,4 +38,4 @@ class UserService(UserHttpService[int, str]):
self.target_user.password = hash_password(password_change.new_password)
return self.db.users.update_password(self.session, self.target_user.id, self.target_user.password)
return self.db.users.update_password(self.target_user.id, self.target_user.password)