From 11b4d2389abe3ac1ffe8fa87f787bf35a28b37af Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Fri, 25 Mar 2022 10:56:49 -0800 Subject: [PATCH] chore: refactor base schema (#1098) * remove dead backup code * implmenet own base model * refactor to use MealieModel instead of CamelModel * cleanup deps --- dev/code-generation/gen_frontend_types.py | 2 +- dev/code-generation/gen_locales.py | 5 +- mealie/routes/admin/admin_email.py | 8 +- mealie/schema/_mealie/__init__.py | 2 + mealie/schema/_mealie/mealie_model.py | 44 +++ mealie/schema/admin/about.py | 8 +- mealie/schema/admin/maintenance.py | 4 +- mealie/schema/admin/settings.py | 5 +- mealie/schema/cookbook/cookbook.py | 5 +- mealie/schema/group/group.py | 5 +- mealie/schema/group/group_events.py | 9 +- mealie/schema/group/group_exports.py | 5 +- mealie/schema/group/group_migration.py | 4 +- mealie/schema/group/group_permissions.py | 5 +- mealie/schema/group/group_preferences.py | 5 +- mealie/schema/group/group_shopping_list.py | 10 +- mealie/schema/group/invite_token.py | 13 +- mealie/schema/group/webhook.py | 5 +- mealie/schema/labels/multi_purpose_label.py | 5 +- mealie/schema/meal_plan/meal.py | 9 +- mealie/schema/meal_plan/new_meal.py | 6 +- mealie/schema/meal_plan/plan_rules.py | 7 +- mealie/schema/meal_plan/shopping_list.py | 6 +- mealie/schema/query.py | 4 +- mealie/schema/recipe/recipe.py | 8 +- mealie/schema/recipe/recipe_asset.py | 4 +- mealie/schema/recipe/recipe_bulk_actions.py | 9 +- mealie/schema/recipe/recipe_category.py | 5 +- mealie/schema/recipe/recipe_comments.py | 9 +- mealie/schema/recipe/recipe_ingredient.py | 14 +- mealie/schema/recipe/recipe_nutrition.py | 4 +- mealie/schema/recipe/recipe_settings.py | 4 +- mealie/schema/recipe/recipe_share_token.py | 5 +- mealie/schema/recipe/recipe_step.py | 7 +- mealie/schema/recipe/recipe_tool.py | 5 +- mealie/schema/recipe/request_helpers.py | 5 +- mealie/schema/reports/reports.py | 7 +- mealie/schema/response/responses.py | 5 +- mealie/schema/server/tasks.py | 5 +- mealie/schema/user/registration.py | 5 +- mealie/schema/user/user.py | 10 +- mealie/schema/user/user_passwords.py | 9 +- mealie/services/backups/__init__.py | 0 mealie/services/backups/exports.py | 143 -------- mealie/services/backups/imports.py | 307 ------------------ mealie/services/scheduler/tasks/__init__.py | 1 - .../services/scheduler/tasks/auto_backup.py | 20 -- poetry.lock | 38 +-- pyproject.toml | 3 +- .../schema_tests/test_mealie_model.py | 63 ++++ 50 files changed, 253 insertions(+), 623 deletions(-) create mode 100644 mealie/schema/_mealie/mealie_model.py delete mode 100644 mealie/services/backups/__init__.py delete mode 100644 mealie/services/backups/exports.py delete mode 100644 mealie/services/backups/imports.py delete mode 100644 mealie/services/scheduler/tasks/auto_backup.py create mode 100644 tests/unit_tests/schema_tests/test_mealie_model.py diff --git a/dev/code-generation/gen_frontend_types.py b/dev/code-generation/gen_frontend_types.py index 4ece601c9140..ccd2af8f0fd8 100644 --- a/dev/code-generation/gen_frontend_types.py +++ b/dev/code-generation/gen_frontend_types.py @@ -96,7 +96,7 @@ def generate_typescript_types() -> None: try: path_as_module = path_to_module(module) - generate_typescript_defs(path_as_module, str(out_path), exclude=("CamelModel")) # type: ignore + generate_typescript_defs(path_as_module, str(out_path), exclude=("MealieModel")) # type: ignore except Exception as e: failed_modules.append(module) print("\nModule Errors:", module, "-----------------") # noqa diff --git a/dev/code-generation/gen_locales.py b/dev/code-generation/gen_locales.py index 8ce5638a058a..cb3d3a19d68d 100644 --- a/dev/code-generation/gen_locales.py +++ b/dev/code-generation/gen_locales.py @@ -3,11 +3,12 @@ import pathlib import _static import dotenv import requests -from fastapi_camelcase import CamelModel from jinja2 import Template from requests import Response from rich import print +from mealie.schema._mealie import MealieModel + BASE = pathlib.Path(__file__).parent.parent.parent API_KEY = dotenv.get_key(BASE / ".env", "CROWDIN_API_KEY") @@ -57,7 +58,7 @@ export const LOCALES = [{% for locale in locales %} """ -class TargetLanguage(CamelModel): +class TargetLanguage(MealieModel): id: str name: str locale: str diff --git a/mealie/routes/admin/admin_email.py b/mealie/routes/admin/admin_email.py index 1c2678cbee47..d8f8e8eda96c 100644 --- a/mealie/routes/admin/admin_email.py +++ b/mealie/routes/admin/admin_email.py @@ -1,22 +1,22 @@ from fastapi import APIRouter -from fastapi_camelcase import CamelModel from mealie.routes._base import BaseAdminController, controller +from mealie.schema._mealie import MealieModel from mealie.services.email import EmailService router = APIRouter(prefix="/email") -class EmailReady(CamelModel): +class EmailReady(MealieModel): ready: bool -class EmailSuccess(CamelModel): +class EmailSuccess(MealieModel): success: bool error: str = None -class EmailTest(CamelModel): +class EmailTest(MealieModel): email: str diff --git a/mealie/schema/_mealie/__init__.py b/mealie/schema/_mealie/__init__.py index e69de29bb2d1..ad43af60f640 100644 --- a/mealie/schema/_mealie/__init__.py +++ b/mealie/schema/_mealie/__init__.py @@ -0,0 +1,2 @@ +from .mealie_model import * +from .types import * diff --git a/mealie/schema/_mealie/mealie_model.py b/mealie/schema/_mealie/mealie_model.py new file mode 100644 index 000000000000..defcb15a333f --- /dev/null +++ b/mealie/schema/_mealie/mealie_model.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from typing import TypeVar + +from humps.main import camelize +from pydantic import BaseModel + +T = TypeVar("T", bound=BaseModel) + + +class MealieModel(BaseModel): + class Config: + alias_generator = camelize + allow_population_by_field_name = True + + def cast(self, cls: type[T], **kwargs) -> T: + """ + Cast the current model to another with additional arguments. Useful for + transforming DTOs into models that are saved to a database + """ + create_data = {field: getattr(self, field) for field in self.__fields__ if field in cls.__fields__} + create_data.update(kwargs or {}) + return cls(**create_data) + + def map_to(self, dest: T) -> T: + """ + Map matching values from the current model to another model. Model returned + for method chaining. + """ + + for field in self.__fields__: + if field in dest.__fields__: + setattr(dest, field, getattr(self, field)) + + return dest + + def map_from(self, src: BaseModel): + """ + Map matching values from another model to the current model. + """ + + for field in src.__fields__: + if field in self.__fields__: + setattr(self, field, getattr(src, field)) diff --git a/mealie/schema/admin/about.py b/mealie/schema/admin/about.py index 9cfe37ad199e..4bed55019c4d 100644 --- a/mealie/schema/admin/about.py +++ b/mealie/schema/admin/about.py @@ -1,7 +1,7 @@ -from fastapi_camelcase import CamelModel +from mealie.schema._mealie import MealieModel -class AppStatistics(CamelModel): +class AppStatistics(MealieModel): total_recipes: int total_users: int total_groups: int @@ -9,7 +9,7 @@ class AppStatistics(CamelModel): untagged_recipes: int -class AppInfo(CamelModel): +class AppInfo(MealieModel): production: bool version: str demo_status: bool @@ -26,7 +26,7 @@ class AdminAboutInfo(AppInfo): build_id: str -class CheckAppConfig(CamelModel): +class CheckAppConfig(MealieModel): email_ready: bool = False ldap_ready: bool = False base_url_set: bool = False diff --git a/mealie/schema/admin/maintenance.py b/mealie/schema/admin/maintenance.py index 38ffc8433e55..e24e16b11fab 100644 --- a/mealie/schema/admin/maintenance.py +++ b/mealie/schema/admin/maintenance.py @@ -1,7 +1,7 @@ -from fastapi_camelcase import CamelModel +from mealie.schema._mealie import MealieModel -class MaintenanceSummary(CamelModel): +class MaintenanceSummary(MealieModel): data_dir_size: str log_file_size: str cleanable_images: int diff --git a/mealie/schema/admin/settings.py b/mealie/schema/admin/settings.py index 31468754bb3f..83fadbbbc56a 100644 --- a/mealie/schema/admin/settings.py +++ b/mealie/schema/admin/settings.py @@ -1,13 +1,14 @@ from typing import Optional -from fastapi_camelcase import CamelModel from pydantic import validator from slugify import slugify +from mealie.schema._mealie import MealieModel + from ..recipe.recipe_category import RecipeCategoryResponse -class CustomPageBase(CamelModel): +class CustomPageBase(MealieModel): name: str slug: Optional[str] position: int diff --git a/mealie/schema/cookbook/cookbook.py b/mealie/schema/cookbook/cookbook.py index 1ec4b85211e5..1eb80f818332 100644 --- a/mealie/schema/cookbook/cookbook.py +++ b/mealie/schema/cookbook/cookbook.py @@ -1,11 +1,12 @@ -from fastapi_camelcase import CamelModel from pydantic import UUID4, validator from slugify import slugify +from mealie.schema._mealie import MealieModel + from ..recipe.recipe_category import CategoryBase, RecipeCategoryResponse -class CreateCookBook(CamelModel): +class CreateCookBook(MealieModel): name: str description: str = "" slug: str = None diff --git a/mealie/schema/group/group.py b/mealie/schema/group/group.py index 1dbe77a3601a..fe83f7b13e98 100644 --- a/mealie/schema/group/group.py +++ b/mealie/schema/group/group.py @@ -1,10 +1,11 @@ -from fastapi_camelcase import CamelModel from pydantic import UUID4 +from mealie.schema._mealie import MealieModel + from .group_preferences import UpdateGroupPreferences -class GroupAdminUpdate(CamelModel): +class GroupAdminUpdate(MealieModel): id: UUID4 name: str preferences: UpdateGroupPreferences diff --git a/mealie/schema/group/group_events.py b/mealie/schema/group/group_events.py index 3e1582cbde1c..d5f78bd4151a 100644 --- a/mealie/schema/group/group_events.py +++ b/mealie/schema/group/group_events.py @@ -1,11 +1,12 @@ -from fastapi_camelcase import CamelModel from pydantic import UUID4, NoneStr +from mealie.schema._mealie import MealieModel + # ============================================================================= # Group Events Notifier Options -class GroupEventNotifierOptions(CamelModel): +class GroupEventNotifierOptions(MealieModel): """ These events are in-sync with the EventTypes found in the EventBusService. If you modify this, make sure to update the EventBusService as well. @@ -55,7 +56,7 @@ class GroupEventNotifierOptionsOut(GroupEventNotifierOptions): # Notifiers -class GroupEventNotifierCreate(CamelModel): +class GroupEventNotifierCreate(MealieModel): name: str apprise_url: str @@ -71,7 +72,7 @@ class GroupEventNotifierUpdate(GroupEventNotifierSave): apprise_url: NoneStr = None -class GroupEventNotifierOut(CamelModel): +class GroupEventNotifierOut(MealieModel): id: UUID4 name: str enabled: bool diff --git a/mealie/schema/group/group_exports.py b/mealie/schema/group/group_exports.py index c97ff7c50042..e271261de455 100644 --- a/mealie/schema/group/group_exports.py +++ b/mealie/schema/group/group_exports.py @@ -1,10 +1,11 @@ from datetime import datetime -from fastapi_camelcase import CamelModel from pydantic import UUID4 +from mealie.schema._mealie import MealieModel -class GroupDataExport(CamelModel): + +class GroupDataExport(MealieModel): id: UUID4 group_id: UUID4 name: str diff --git a/mealie/schema/group/group_migration.py b/mealie/schema/group/group_migration.py index a20091612fa3..b3a6573d9c41 100644 --- a/mealie/schema/group/group_migration.py +++ b/mealie/schema/group/group_migration.py @@ -1,6 +1,6 @@ import enum -from fastapi_camelcase import CamelModel +from mealie.schema._mealie import MealieModel class SupportedMigrations(str, enum.Enum): @@ -10,5 +10,5 @@ class SupportedMigrations(str, enum.Enum): mealie_alpha = "mealie_alpha" -class DataMigrationCreate(CamelModel): +class DataMigrationCreate(MealieModel): source_type: SupportedMigrations diff --git a/mealie/schema/group/group_permissions.py b/mealie/schema/group/group_permissions.py index b9f716b530d0..22ee0eed3dd1 100644 --- a/mealie/schema/group/group_permissions.py +++ b/mealie/schema/group/group_permissions.py @@ -1,8 +1,9 @@ -from fastapi_camelcase import CamelModel from pydantic import UUID4 +from mealie.schema._mealie import MealieModel -class SetPermissions(CamelModel): + +class SetPermissions(MealieModel): user_id: UUID4 can_manage: bool = False can_invite: bool = False diff --git a/mealie/schema/group/group_preferences.py b/mealie/schema/group/group_preferences.py index 0a9e3e7c2371..4e59477c6e4e 100644 --- a/mealie/schema/group/group_preferences.py +++ b/mealie/schema/group/group_preferences.py @@ -1,10 +1,11 @@ from uuid import UUID -from fastapi_camelcase import CamelModel from pydantic import UUID4 +from mealie.schema._mealie import MealieModel -class UpdateGroupPreferences(CamelModel): + +class UpdateGroupPreferences(MealieModel): private_group: bool = False first_day_of_week: int = 0 diff --git a/mealie/schema/group/group_shopping_list.py b/mealie/schema/group/group_shopping_list.py index eec7cbddc1d5..82058f357132 100644 --- a/mealie/schema/group/group_shopping_list.py +++ b/mealie/schema/group/group_shopping_list.py @@ -2,13 +2,13 @@ from __future__ import annotations from typing import Optional -from fastapi_camelcase import CamelModel from pydantic import UUID4 +from mealie.schema._mealie import MealieModel from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit -class ShoppingListItemRecipeRef(CamelModel): +class ShoppingListItemRecipeRef(MealieModel): recipe_id: UUID4 recipe_quantity: float @@ -21,7 +21,7 @@ class ShoppingListItemRecipeRefOut(ShoppingListItemRecipeRef): orm_mode = True -class ShoppingListItemCreate(CamelModel): +class ShoppingListItemCreate(MealieModel): shopping_list_id: UUID4 checked: bool = False position: int = 0 @@ -51,11 +51,11 @@ class ShoppingListItemOut(ShoppingListItemUpdate): orm_mode = True -class ShoppingListCreate(CamelModel): +class ShoppingListCreate(MealieModel): name: str = None -class ShoppingListRecipeRefOut(CamelModel): +class ShoppingListRecipeRefOut(MealieModel): id: UUID4 shopping_list_id: UUID4 recipe_id: UUID4 diff --git a/mealie/schema/group/invite_token.py b/mealie/schema/group/invite_token.py index 5a87a3535638..dac57448d08e 100644 --- a/mealie/schema/group/invite_token.py +++ b/mealie/schema/group/invite_token.py @@ -1,20 +1,21 @@ from uuid import UUID -from fastapi_camelcase import CamelModel from pydantic import NoneStr +from mealie.schema._mealie import MealieModel -class CreateInviteToken(CamelModel): + +class CreateInviteToken(MealieModel): uses: int -class SaveInviteToken(CamelModel): +class SaveInviteToken(MealieModel): uses_left: int group_id: UUID token: str -class ReadInviteToken(CamelModel): +class ReadInviteToken(MealieModel): token: str uses_left: int group_id: UUID @@ -23,11 +24,11 @@ class ReadInviteToken(CamelModel): orm_mode = True -class EmailInvitation(CamelModel): +class EmailInvitation(MealieModel): email: str token: str -class EmailInitationResponse(CamelModel): +class EmailInitationResponse(MealieModel): success: bool error: NoneStr = None diff --git a/mealie/schema/group/webhook.py b/mealie/schema/group/webhook.py index 24d7a1d5b930..9552e7162aa1 100644 --- a/mealie/schema/group/webhook.py +++ b/mealie/schema/group/webhook.py @@ -1,10 +1,11 @@ from uuid import UUID -from fastapi_camelcase import CamelModel from pydantic import UUID4 +from mealie.schema._mealie import MealieModel -class CreateWebhook(CamelModel): + +class CreateWebhook(MealieModel): enabled: bool = True name: str = "" url: str = "" diff --git a/mealie/schema/labels/multi_purpose_label.py b/mealie/schema/labels/multi_purpose_label.py index ca73ce033d2d..fee425ec9b2d 100644 --- a/mealie/schema/labels/multi_purpose_label.py +++ b/mealie/schema/labels/multi_purpose_label.py @@ -1,10 +1,11 @@ from __future__ import annotations -from fastapi_camelcase import CamelModel from pydantic import UUID4 +from mealie.schema._mealie import MealieModel -class MultiPurposeLabelCreate(CamelModel): + +class MultiPurposeLabelCreate(MealieModel): name: str color: str = "#E0E0E0" diff --git a/mealie/schema/meal_plan/meal.py b/mealie/schema/meal_plan/meal.py index 4799d2b6631c..c8a125d5db23 100644 --- a/mealie/schema/meal_plan/meal.py +++ b/mealie/schema/meal_plan/meal.py @@ -1,11 +1,12 @@ from datetime import date from typing import Optional -from fastapi_camelcase import CamelModel from pydantic import validator +from mealie.schema._mealie import MealieModel -class MealIn(CamelModel): + +class MealIn(MealieModel): slug: Optional[str] name: Optional[str] description: Optional[str] @@ -14,7 +15,7 @@ class MealIn(CamelModel): orm_mode = True -class MealDayIn(CamelModel): +class MealDayIn(MealieModel): date: Optional[date] meals: list[MealIn] @@ -29,7 +30,7 @@ class MealDayOut(MealDayIn): orm_mode = True -class MealPlanIn(CamelModel): +class MealPlanIn(MealieModel): group: str start_date: date end_date: date diff --git a/mealie/schema/meal_plan/new_meal.py b/mealie/schema/meal_plan/new_meal.py index 853d04cb38b0..405e26f1f810 100644 --- a/mealie/schema/meal_plan/new_meal.py +++ b/mealie/schema/meal_plan/new_meal.py @@ -3,9 +3,9 @@ from enum import Enum from typing import Optional from uuid import UUID -from fastapi_camelcase import CamelModel from pydantic import validator +from mealie.schema._mealie import MealieModel from mealie.schema.recipe.recipe import RecipeSummary @@ -16,12 +16,12 @@ class PlanEntryType(str, Enum): side = "side" -class CreatRandomEntry(CamelModel): +class CreatRandomEntry(MealieModel): date: date entry_type: PlanEntryType = PlanEntryType.dinner -class CreatePlanEntry(CamelModel): +class CreatePlanEntry(MealieModel): date: date entry_type: PlanEntryType = PlanEntryType.breakfast title: str = "" diff --git a/mealie/schema/meal_plan/plan_rules.py b/mealie/schema/meal_plan/plan_rules.py index faef106f528e..829d7c13f78b 100644 --- a/mealie/schema/meal_plan/plan_rules.py +++ b/mealie/schema/meal_plan/plan_rules.py @@ -1,11 +1,12 @@ import datetime from enum import Enum -from fastapi_camelcase import CamelModel from pydantic import UUID4 +from mealie.schema._mealie import MealieModel -class Category(CamelModel): + +class Category(MealieModel): id: UUID4 name: str slug: str @@ -46,7 +47,7 @@ class PlanRulesType(str, Enum): unset = "unset" -class PlanRulesCreate(CamelModel): +class PlanRulesCreate(MealieModel): day: PlanRulesDay = PlanRulesDay.unset entry_type: PlanRulesType = PlanRulesType.unset categories: list[Category] = [] diff --git a/mealie/schema/meal_plan/shopping_list.py b/mealie/schema/meal_plan/shopping_list.py index deb0c984295e..6e4d86e22dc9 100644 --- a/mealie/schema/meal_plan/shopping_list.py +++ b/mealie/schema/meal_plan/shopping_list.py @@ -1,12 +1,12 @@ from typing import Optional -from fastapi_camelcase import CamelModel from pydantic.utils import GetterDict from mealie.db.models.group.shopping_list import ShoppingList +from mealie.schema._mealie import MealieModel -class ListItem(CamelModel): +class ListItem(MealieModel): title: Optional[str] text: str = "" quantity: int = 1 @@ -16,7 +16,7 @@ class ListItem(CamelModel): orm_mode = True -class ShoppingListIn(CamelModel): +class ShoppingListIn(MealieModel): name: str group: Optional[str] items: list[ListItem] diff --git a/mealie/schema/query.py b/mealie/schema/query.py index 54eda5520191..740c5ab3323c 100644 --- a/mealie/schema/query.py +++ b/mealie/schema/query.py @@ -1,6 +1,6 @@ -from fastapi_camelcase import CamelModel +from mealie.schema._mealie import MealieModel -class GetAll(CamelModel): +class GetAll(MealieModel): start: int = 0 limit: int = 999 diff --git a/mealie/schema/recipe/recipe.py b/mealie/schema/recipe/recipe.py index 45beb9c604eb..77f961e6107c 100644 --- a/mealie/schema/recipe/recipe.py +++ b/mealie/schema/recipe/recipe.py @@ -5,13 +5,13 @@ from pathlib import Path from typing import Any, Optional from uuid import uuid4 -from fastapi_camelcase import CamelModel from pydantic import UUID4, BaseModel, Field, validator from pydantic.utils import GetterDict from slugify import slugify from mealie.core.config import get_app_dirs from mealie.db.models.recipe.recipe import RecipeModel +from mealie.schema._mealie import MealieModel from .recipe_asset import RecipeAsset from .recipe_comments import RecipeCommentOut @@ -23,7 +23,7 @@ from .recipe_step import RecipeStep app_dirs = get_app_dirs() -class RecipeTag(CamelModel): +class RecipeTag(MealieModel): id: UUID4 = None name: str slug: str @@ -58,11 +58,11 @@ class CreateRecipeByUrlBulk(BaseModel): imports: list[CreateRecipeBulk] -class CreateRecipe(CamelModel): +class CreateRecipe(MealieModel): name: str -class RecipeSummary(CamelModel): +class RecipeSummary(MealieModel): id: Optional[UUID4] user_id: UUID4 = Field(default_factory=uuid4) diff --git a/mealie/schema/recipe/recipe_asset.py b/mealie/schema/recipe/recipe_asset.py index 5cf4d2e1f574..5dfcd4dc3546 100644 --- a/mealie/schema/recipe/recipe_asset.py +++ b/mealie/schema/recipe/recipe_asset.py @@ -1,9 +1,9 @@ from typing import Optional -from fastapi_camelcase import CamelModel +from mealie.schema._mealie import MealieModel -class RecipeAsset(CamelModel): +class RecipeAsset(MealieModel): name: str icon: str file_name: Optional[str] diff --git a/mealie/schema/recipe/recipe_bulk_actions.py b/mealie/schema/recipe/recipe_bulk_actions.py index ba5f504117ab..9a1b003a4f24 100644 --- a/mealie/schema/recipe/recipe_bulk_actions.py +++ b/mealie/schema/recipe/recipe_bulk_actions.py @@ -1,7 +1,6 @@ import enum -from fastapi_camelcase import CamelModel - +from mealie.schema._mealie import MealieModel from mealie.schema.recipe.recipe_category import CategoryBase, TagBase @@ -9,7 +8,7 @@ class ExportTypes(str, enum.Enum): JSON = "json" -class ExportBase(CamelModel): +class ExportBase(MealieModel): recipes: list[str] @@ -29,12 +28,12 @@ class DeleteRecipes(ExportBase): pass -class BulkActionError(CamelModel): +class BulkActionError(MealieModel): recipe: str error: str -class BulkActionsResponse(CamelModel): +class BulkActionsResponse(MealieModel): success: bool message: str errors: list[BulkActionError] = [] diff --git a/mealie/schema/recipe/recipe_category.py b/mealie/schema/recipe/recipe_category.py index 22a07462ecbf..3c18fb994fbe 100644 --- a/mealie/schema/recipe/recipe_category.py +++ b/mealie/schema/recipe/recipe_category.py @@ -1,9 +1,10 @@ -from fastapi_camelcase import CamelModel from pydantic import UUID4 from pydantic.utils import GetterDict +from mealie.schema._mealie import MealieModel -class CategoryIn(CamelModel): + +class CategoryIn(MealieModel): name: str diff --git a/mealie/schema/recipe/recipe_comments.py b/mealie/schema/recipe/recipe_comments.py index 646bb9ede486..89d439fedf95 100644 --- a/mealie/schema/recipe/recipe_comments.py +++ b/mealie/schema/recipe/recipe_comments.py @@ -2,11 +2,12 @@ from datetime import datetime from typing import Optional from uuid import UUID -from fastapi_camelcase import CamelModel from pydantic import UUID4 +from mealie.schema._mealie import MealieModel -class UserBase(CamelModel): + +class UserBase(MealieModel): id: int username: Optional[str] admin: bool @@ -15,7 +16,7 @@ class UserBase(CamelModel): orm_mode = True -class RecipeCommentCreate(CamelModel): +class RecipeCommentCreate(MealieModel): recipe_id: UUID4 text: str @@ -24,7 +25,7 @@ class RecipeCommentSave(RecipeCommentCreate): user_id: UUID4 -class RecipeCommentUpdate(CamelModel): +class RecipeCommentUpdate(MealieModel): id: UUID text: str diff --git a/mealie/schema/recipe/recipe_ingredient.py b/mealie/schema/recipe/recipe_ingredient.py index 3c5842d65dc8..3ad331e5241c 100644 --- a/mealie/schema/recipe/recipe_ingredient.py +++ b/mealie/schema/recipe/recipe_ingredient.py @@ -4,13 +4,13 @@ import enum from typing import Optional, Union from uuid import UUID, uuid4 -from fastapi_camelcase import CamelModel from pydantic import UUID4, Field +from mealie.schema._mealie import MealieModel from mealie.schema._mealie.types import NoneFloat -class UnitFoodBase(CamelModel): +class UnitFoodBase(MealieModel): name: str description: str = "" @@ -47,7 +47,7 @@ class IngredientUnit(CreateIngredientUnit): orm_mode = True -class RecipeIngredient(CamelModel): +class RecipeIngredient(MealieModel): title: Optional[str] note: Optional[str] unit: Optional[Union[IngredientUnit, CreateIngredientUnit]] @@ -64,7 +64,7 @@ class RecipeIngredient(CamelModel): orm_mode = True -class IngredientConfidence(CamelModel): +class IngredientConfidence(MealieModel): average: NoneFloat = None comment: NoneFloat = None name: NoneFloat = None @@ -73,7 +73,7 @@ class IngredientConfidence(CamelModel): food: NoneFloat = None -class ParsedIngredient(CamelModel): +class ParsedIngredient(MealieModel): input: Optional[str] confidence: IngredientConfidence = IngredientConfidence() ingredient: RecipeIngredient @@ -84,12 +84,12 @@ class RegisteredParser(str, enum.Enum): brute = "brute" -class IngredientsRequest(CamelModel): +class IngredientsRequest(MealieModel): parser: RegisteredParser = RegisteredParser.nlp ingredients: list[str] -class IngredientRequest(CamelModel): +class IngredientRequest(MealieModel): parser: RegisteredParser = RegisteredParser.nlp ingredient: str diff --git a/mealie/schema/recipe/recipe_nutrition.py b/mealie/schema/recipe/recipe_nutrition.py index 8b6d0aee3e35..026820504347 100644 --- a/mealie/schema/recipe/recipe_nutrition.py +++ b/mealie/schema/recipe/recipe_nutrition.py @@ -1,9 +1,9 @@ from typing import Optional -from fastapi_camelcase import CamelModel +from mealie.schema._mealie import MealieModel -class Nutrition(CamelModel): +class Nutrition(MealieModel): calories: Optional[str] fat_content: Optional[str] protein_content: Optional[str] diff --git a/mealie/schema/recipe/recipe_settings.py b/mealie/schema/recipe/recipe_settings.py index 4c0e5790cb4a..c188235ad3d7 100644 --- a/mealie/schema/recipe/recipe_settings.py +++ b/mealie/schema/recipe/recipe_settings.py @@ -1,7 +1,7 @@ -from fastapi_camelcase import CamelModel +from mealie.schema._mealie import MealieModel -class RecipeSettings(CamelModel): +class RecipeSettings(MealieModel): public: bool = False show_nutrition: bool = False show_assets: bool = False diff --git a/mealie/schema/recipe/recipe_share_token.py b/mealie/schema/recipe/recipe_share_token.py index 229286449ef8..1267f58af5a7 100644 --- a/mealie/schema/recipe/recipe_share_token.py +++ b/mealie/schema/recipe/recipe_share_token.py @@ -1,8 +1,9 @@ from datetime import datetime, timedelta -from fastapi_camelcase import CamelModel from pydantic import UUID4, Field +from mealie.schema._mealie import MealieModel + from .recipe import Recipe @@ -10,7 +11,7 @@ def defaut_expires_at_time() -> datetime: return datetime.utcnow() + timedelta(days=30) -class RecipeShareTokenCreate(CamelModel): +class RecipeShareTokenCreate(MealieModel): recipe_id: UUID4 expires_at: datetime = Field(default_factory=defaut_expires_at_time) diff --git a/mealie/schema/recipe/recipe_step.py b/mealie/schema/recipe/recipe_step.py index ac08c2454d7b..71a7dfd1721e 100644 --- a/mealie/schema/recipe/recipe_step.py +++ b/mealie/schema/recipe/recipe_step.py @@ -1,11 +1,12 @@ from typing import Optional from uuid import UUID, uuid4 -from fastapi_camelcase import CamelModel from pydantic import UUID4, Field +from mealie.schema._mealie import MealieModel -class IngredientReferences(CamelModel): + +class IngredientReferences(MealieModel): """ A list of ingredient references. """ @@ -16,7 +17,7 @@ class IngredientReferences(CamelModel): orm_mode = True -class RecipeStep(CamelModel): +class RecipeStep(MealieModel): id: Optional[UUID] = Field(default_factory=uuid4) title: Optional[str] = "" text: str diff --git a/mealie/schema/recipe/recipe_tool.py b/mealie/schema/recipe/recipe_tool.py index c5daa0cba629..dad1c6e01b94 100644 --- a/mealie/schema/recipe/recipe_tool.py +++ b/mealie/schema/recipe/recipe_tool.py @@ -1,10 +1,11 @@ import typing -from fastapi_camelcase import CamelModel from pydantic import UUID4 +from mealie.schema._mealie import MealieModel -class RecipeToolCreate(CamelModel): + +class RecipeToolCreate(MealieModel): name: str on_hand: bool = False diff --git a/mealie/schema/recipe/request_helpers.py b/mealie/schema/recipe/request_helpers.py index 2c7cf2562a37..16e5ca2c7f69 100644 --- a/mealie/schema/recipe/request_helpers.py +++ b/mealie/schema/recipe/request_helpers.py @@ -1,10 +1,11 @@ -from fastapi_camelcase import CamelModel from pydantic import BaseModel +from mealie.schema._mealie import MealieModel + # TODO: Should these exist?!?!?!?!? -class RecipeSlug(CamelModel): +class RecipeSlug(MealieModel): slug: str diff --git a/mealie/schema/reports/reports.py b/mealie/schema/reports/reports.py index baccbc522ce7..c925893d4c38 100644 --- a/mealie/schema/reports/reports.py +++ b/mealie/schema/reports/reports.py @@ -1,10 +1,11 @@ import datetime import enum -from fastapi_camelcase import CamelModel from pydantic import Field from pydantic.types import UUID4 +from mealie.schema._mealie import MealieModel + class ReportCategory(str, enum.Enum): backup = "backup" @@ -19,7 +20,7 @@ class ReportSummaryStatus(str, enum.Enum): partial = "partial" -class ReportEntryCreate(CamelModel): +class ReportEntryCreate(MealieModel): report_id: UUID4 timestamp: datetime.datetime = Field(default_factory=datetime.datetime.utcnow) success: bool = True @@ -34,7 +35,7 @@ class ReportEntryOut(ReportEntryCreate): orm_mode = True -class ReportCreate(CamelModel): +class ReportCreate(MealieModel): timestamp: datetime.datetime = Field(default_factory=datetime.datetime.utcnow) category: ReportCategory group_id: UUID4 diff --git a/mealie/schema/response/responses.py b/mealie/schema/response/responses.py index 4b4e2f24f3c2..977d9cc231bf 100644 --- a/mealie/schema/response/responses.py +++ b/mealie/schema/response/responses.py @@ -1,8 +1,9 @@ from typing import Optional -from fastapi_camelcase import CamelModel from pydantic import BaseModel +from mealie.schema._mealie import MealieModel + class ErrorResponse(BaseModel): message: str @@ -31,7 +32,7 @@ class SuccessResponse(BaseModel): return cls(message=message).dict() -class FileTokenResponse(CamelModel): +class FileTokenResponse(MealieModel): file_token: str @classmethod diff --git a/mealie/schema/server/tasks.py b/mealie/schema/server/tasks.py index 03411054f49f..9ea4e5ce51a1 100644 --- a/mealie/schema/server/tasks.py +++ b/mealie/schema/server/tasks.py @@ -2,9 +2,10 @@ import datetime import enum from uuid import UUID -from fastapi_camelcase import CamelModel from pydantic import Field +from mealie.schema._mealie import MealieModel + class ServerTaskNames(str, enum.Enum): default = "Background Task" @@ -18,7 +19,7 @@ class ServerTaskStatus(str, enum.Enum): failed = "failed" -class ServerTaskCreate(CamelModel): +class ServerTaskCreate(MealieModel): group_id: UUID name: ServerTaskNames = ServerTaskNames.default created_at: datetime.datetime = Field(default_factory=datetime.datetime.now) diff --git a/mealie/schema/user/registration.py b/mealie/schema/user/registration.py index a790f776fa3a..79926e418d5a 100644 --- a/mealie/schema/user/registration.py +++ b/mealie/schema/user/registration.py @@ -1,9 +1,10 @@ -from fastapi_camelcase import CamelModel from pydantic import validator from pydantic.types import NoneStr, constr +from mealie.schema._mealie import MealieModel -class CreateUserRegistration(CamelModel): + +class CreateUserRegistration(MealieModel): group: NoneStr = None group_token: NoneStr = None email: constr(to_lower=True, strip_whitespace=True) # type: ignore diff --git a/mealie/schema/user/user.py b/mealie/schema/user/user.py index e41dd93bf055..596bbf0f170a 100644 --- a/mealie/schema/user/user.py +++ b/mealie/schema/user/user.py @@ -3,13 +3,13 @@ from pathlib import Path from typing import Any, Optional from uuid import UUID -from fastapi_camelcase import CamelModel from pydantic import UUID4 from pydantic.types import constr from pydantic.utils import GetterDict from mealie.core.config import get_app_dirs, get_app_settings from mealie.db.models.users import User +from mealie.schema._mealie import MealieModel from mealie.schema.group.group_preferences import ReadGroupPreferences from mealie.schema.recipe import RecipeSummary @@ -18,7 +18,7 @@ from ..recipe import CategoryBase settings = get_app_settings() -class LoingLiveTokenIn(CamelModel): +class LoingLiveTokenIn(MealieModel): name: str @@ -38,19 +38,19 @@ class CreateToken(LoingLiveTokenIn): orm_mode = True -class ChangePassword(CamelModel): +class ChangePassword(MealieModel): current_password: str new_password: str -class GroupBase(CamelModel): +class GroupBase(MealieModel): name: str class Config: orm_mode = True -class UserBase(CamelModel): +class UserBase(MealieModel): username: Optional[str] full_name: Optional[str] = None email: constr(to_lower=True, strip_whitespace=True) # type: ignore diff --git a/mealie/schema/user/user_passwords.py b/mealie/schema/user/user_passwords.py index e29fa7dacbc0..f3f2de8ad782 100644 --- a/mealie/schema/user/user_passwords.py +++ b/mealie/schema/user/user_passwords.py @@ -1,14 +1,15 @@ -from fastapi_camelcase import CamelModel from pydantic import UUID4 +from mealie.schema._mealie import MealieModel + from .user import PrivateUser -class ForgotPassword(CamelModel): +class ForgotPassword(MealieModel): email: str -class ValidateResetToken(CamelModel): +class ValidateResetToken(MealieModel): token: str @@ -18,7 +19,7 @@ class ResetPassword(ValidateResetToken): passwordConfirm: str -class SavePasswordResetToken(CamelModel): +class SavePasswordResetToken(MealieModel): user_id: UUID4 token: str diff --git a/mealie/services/backups/__init__.py b/mealie/services/backups/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/mealie/services/backups/exports.py b/mealie/services/backups/exports.py deleted file mode 100644 index af43abaf5ed6..000000000000 --- a/mealie/services/backups/exports.py +++ /dev/null @@ -1,143 +0,0 @@ -import json -import shutil -from datetime import datetime -from pathlib import Path -from typing import Union - -from jinja2 import Template -from pathvalidate import sanitize_filename -from pydantic.main import BaseModel - -from mealie.core import root_logger -from mealie.core.config import get_app_dirs - -app_dirs = get_app_dirs() -from mealie.repos.all_repositories import get_repositories - -logger = root_logger.get_logger() - - -class ExportDatabase: - def __init__(self, tag=None, templates=None) -> None: - """Export a Mealie database. Export interacts directly with class objects and can be used - with any supported backend database platform. By default tags are timestamps, and no - Jinja2 templates are rendered - - Args: - tag ([str], optional): A str to be used as a file tag. Defaults to None. - templates (list, optional): A list of template file names. Defaults to None. - """ - if tag: - export_tag = tag + "_" + datetime.now().strftime("%Y-%b-%d") - else: - export_tag = datetime.now().strftime("%Y-%b-%d") - - self.main_dir = app_dirs.TEMP_DIR.joinpath(export_tag) - self.recipes = self.main_dir.joinpath("recipes") - self.templates_dir = self.main_dir.joinpath("templates") - - try: - self.templates = [app_dirs.TEMPLATE_DIR.joinpath(x) for x in templates] - except Exception: - self.templates = [] - logger.info("No Jinja2 Templates Registered for Export") - - required_dirs = [ - self.main_dir, - self.recipes, - self.templates_dir, - ] - - for dir in required_dirs: - dir.mkdir(parents=True, exist_ok=True) - - def export_templates(self, recipe_list: list[BaseModel]): - if self.templates: - for template_path in self.templates: - out_dir = self.templates_dir.joinpath(template_path.name) - out_dir.mkdir(parents=True, exist_ok=True) - - with open(template_path, "r") as f: - template = Template(f.read()) - - for recipe in recipe_list: - filename = recipe.slug + template_path.suffix - out_file = out_dir.joinpath(filename) - - content = template.render(recipe=recipe) - - with open(out_file, "w") as f: - f.write(content) - - def export_recipe_dirs(self): - shutil.copytree(app_dirs.RECIPE_DATA_DIR, self.recipes, dirs_exist_ok=True) - - def export_items(self, items: list[BaseModel], folder_name: str, export_list=True, slug_folder=False): - items = [x.dict() for x in items] - out_dir = self.main_dir.joinpath(folder_name) - out_dir.mkdir(parents=True, exist_ok=True) - - if export_list: - ExportDatabase._write_json_file(items, out_dir.joinpath(f"{folder_name}.json")) - else: - for item in items: - final_dest = out_dir if not slug_folder else out_dir.joinpath(item.get("slug")) - final_dest.mkdir(exist_ok=True) - filename = sanitize_filename(f"{item.get('slug')}.json") - ExportDatabase._write_json_file(item, final_dest.joinpath(filename)) - - @staticmethod - def _write_json_file(data: Union[dict, list], out_file: Path): - json_data = json.dumps(data, indent=4, default=str) - - with open(out_file, "w") as f: - f.write(json_data) - - def finish_export(self): - zip_path = app_dirs.BACKUP_DIR.joinpath(f"{self.main_dir.name}") - shutil.make_archive(zip_path, "zip", self.main_dir) - - shutil.rmtree(app_dirs.TEMP_DIR, ignore_errors=True) - return str(zip_path.absolute()) + ".zip" - - -def backup_all( - session, - tag=None, - templates=None, - export_recipes=True, - export_settings=False, - export_users=True, - export_groups=True, - export_notifications=True, -): - db_export = ExportDatabase(tag=tag, templates=templates) - - db = get_repositories(session) - - if export_users: - all_users = db.users.get_all() - db_export.export_items(all_users, "users") - - if export_groups: - all_groups = db.groups.get_all() - db_export.export_items(all_groups, "groups") - - if export_recipes: - 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() - db_export.export_items(all_comments, "comments") - - if export_settings: - all_settings = db.settings.get_all() - db_export.export_items(all_settings, "settings") - - if export_notifications: - all_notifications = db.event_notifications.get_all() - db_export.export_items(all_notifications, "notifications") - - return db_export.finish_export() diff --git a/mealie/services/backups/imports.py b/mealie/services/backups/imports.py deleted file mode 100644 index afbfb7f1a274..000000000000 --- a/mealie/services/backups/imports.py +++ /dev/null @@ -1,307 +0,0 @@ -import json -import shutil -import zipfile -from collections.abc import Callable -from pathlib import Path - -from pydantic.main import BaseModel -from sqlalchemy.orm.session import Session - -from mealie.core.config import get_app_dirs -from mealie.repos.all_repositories import get_repositories -from mealie.schema.admin import CommentImport, GroupImport, RecipeImport, UserImport -from mealie.schema.recipe import Recipe, RecipeCommentOut -from mealie.schema.user import PrivateUser, UpdateGroup - -app_dirs = get_app_dirs() - - -class ImportDatabase: - def __init__( - self, - user: PrivateUser, - session: Session, - zip_archive: str, - force_import: bool = False, - ) -> None: - """Import a database.zip file exported from mealie. - - Args: - session (Session): SqlAlchemy Session - zip_archive (str): The filename contained in the backups directory - force_import (bool, optional): Force import will update all existing recipes. If False existing recipes are skipped. Defaults to False. - - Raises: - Exception: If the zip file does not exists an exception raise. - """ - self.user = user - self.session = session - self.db = get_repositories(session) - self.archive = app_dirs.BACKUP_DIR.joinpath(zip_archive) - self.force_imports = force_import - - if self.archive.is_file(): - self.import_dir = app_dirs.TEMP_DIR.joinpath("active_import") - self.import_dir.mkdir(parents=True, exist_ok=True) - - with zipfile.ZipFile(self.archive, "r") as zip_ref: - zip_ref.extractall(self.import_dir) - else: - raise Exception("Import file does not exist") - - def import_recipes(self): - recipe_dir: Path = self.import_dir.joinpath("recipes") - imports = [] - successful_imports = {} - - recipes = ImportDatabase.read_models_file( - file_path=recipe_dir, - model=Recipe, - single_file=False, - migrate=ImportDatabase._recipe_migration, - ) - - for recipe in recipes: - recipe: Recipe - - recipe.group_id = self.user.group_id - recipe.user_id = self.user.id - - import_status = self.import_model( - db_table=self.db.recipes, - model=recipe, - return_model=RecipeImport, - name_attr="name", - search_key="slug", - slug=recipe.slug, - ) - - if import_status.status: - successful_imports[recipe.slug] = recipe - - imports.append(import_status) - - self._import_images(successful_imports) - - return imports - - def import_comments(self): - comment_dir: Path = self.import_dir.joinpath("comments", "comments.json") - - if not comment_dir.exists(): - return - - comments = ImportDatabase.read_models_file(file_path=comment_dir, model=RecipeCommentOut) - - for comment in comments: - comment: RecipeCommentOut - - self.import_model( - db_table=self.db.comments, - model=comment, - return_model=CommentImport, - name_attr="uuid", - search_key="uuid", - ) - - @staticmethod - def _recipe_migration(recipe_dict: dict) -> dict: - if recipe_dict.get("categories", False): - recipe_dict["recipeCategory"] = recipe_dict.get("categories") - del recipe_dict["categories"] - try: - del recipe_dict["_id"] - del recipe_dict["date_added"] - except Exception: - pass - # Migration from list to Object Type Data - try: - if "" in recipe_dict["tags"]: - recipe_dict["tags"] = [tag for tag in recipe_dict["tags"] if tag != ""] - except Exception: - pass - - try: - if "" in recipe_dict["categories"]: - recipe_dict["categories"] = [cat for cat in recipe_dict["categories"] if cat != ""] - - except Exception: - pass - - if type(recipe_dict["extras"]) == list: - recipe_dict["extras"] = {} - - recipe_dict["comments"] = [] - - return recipe_dict - - def _import_images(self, successful_imports: list[Recipe]): - image_dir = self.import_dir.joinpath("images") - - if image_dir.exists(): # Migrate from before v0.5.0 - for image in image_dir.iterdir(): - item: Recipe = successful_imports.get(image.stem) # type: ignore - - if item: - dest_dir = item.image_dir - - if image.is_dir(): - shutil.copytree(image, dest_dir, dirs_exist_ok=True) - - if image.is_file(): - shutil.copy(image, dest_dir) - - else: - recipe_dir = self.import_dir.joinpath("recipes") - shutil.copytree(recipe_dir, app_dirs.RECIPE_DATA_DIR, dirs_exist_ok=True) - - def import_settings(self): - return [] - - def import_groups(self): - groups_file = self.import_dir.joinpath("groups", "groups.json") - groups = ImportDatabase.read_models_file(groups_file, UpdateGroup) - group_imports = [] - - for group in groups: - import_status = self.import_model(self.db.groups, group, GroupImport, search_key="name") - group_imports.append(import_status) - - return group_imports - - def import_users(self): - users_file = self.import_dir.joinpath("users", "users.json") - users = ImportDatabase.read_models_file(users_file, PrivateUser) - user_imports = [] - for user in users: - if user.id == 1: # Update Default User - 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=self.db.users, - model=user, - return_model=UserImport, - name_attr="full_name", - search_key="email", - ) - - user_imports.append(import_status) - - return user_imports - - @staticmethod - def read_models_file(file_path: Path, model: BaseModel, single_file=True, migrate: Callable = None): - """A general purpose function that is used to process a backup `.json` file created by mealie - note that if the file doesn't not exists the function will return any empty list - - Args: - file_path (Path): The path to the .json file or directory - model (BaseModel): The pydantic model that will be created from the .json file entries - single_file (bool, optional): If true, the json data will be treated as list, if false it will use glob style matches and treat each file as its own entry. Defaults to True. - migrate (Callable, optional): A migrate function that will be called on the data prior to creating a model. Defaults to None. - - Returns: - [type]: [description] - """ - if not file_path.exists(): - return [] - - if single_file: - with open(file_path, "r") as f: - file_data = json.loads(f.read()) - - if migrate: - file_data = [migrate(x) for x in file_data] - - return [model(**g) for g in file_data] - - all_models = [] - for file in file_path.glob("**/*.json"): - with open(file, "r") as f: - file_data = json.loads(f.read()) - - if migrate: - file_data = migrate(file_data) - - all_models.append(model(**file_data)) - - return all_models - - def import_model(self, db_table, model, return_model, name_attr="name", search_key="id", **kwargs): - """A general purpose function used to insert a list of pydantic modelsi into the database. - The assumption at this point is that the models that are inserted. If self.force_imports is true - any existing entries will be removed prior to creation - - Args: - db_table ([type]): A database table like `db.users` - model ([type]): The Pydantic model that matches the database - return_model ([type]): The return model that will be used for the 'report' - name_attr (str, optional): The name property on the return model. Defaults to "name". - search_key (str, optional): The key used to identify if an the entry already exists. Defaults to "id" - **kwargs (): Any kwargs passed will be used to set attributes on the `return_model` - - Returns: - [type]: Returns the `return_model` specified. - """ - model_name = getattr(model, name_attr) - search_value = getattr(model, search_key) - - item = db_table.get(search_value, search_key) - if item: - if not self.force_imports: - return return_model( - name=model_name, - status=False, - exception=f"Table entry with matching '{search_key}': '{search_value}' exists", - ) - - primary_key = getattr(item, db_table.primary_key) - db_table.delete(primary_key) - try: - db_table.create(model.dict()) - import_status = return_model(name=model_name, status=True) - - except Exception as inst: - self.session.rollback() - import_status = return_model(name=model_name, status=False, exception=str(inst)) - - for key, value in kwargs.items(): - setattr(return_model, key, value) - - return import_status - - def clean_up(self): - shutil.rmtree(app_dirs.TEMP_DIR) - - -def import_database( - session: Session, - user: PrivateUser, - archive, - import_recipes=True, - import_settings=True, - import_users=True, - import_groups=True, - force_import: bool = False, - **_, -): - import_session = ImportDatabase(user, session, archive, force_import) - - recipe_report = import_session.import_recipes() if import_recipes else [] - settings_report = import_session.import_settings() if import_settings else [] - group_report = import_session.import_groups() if import_groups else [] - user_report = import_session.import_users() if import_users else [] - notification_report: list = [] - - import_session.clean_up() - - return { - "recipeImports": recipe_report, - "settingsImports": settings_report, - "groupImports": group_report, - "userImports": user_report, - "notificationImports": notification_report, - } diff --git a/mealie/services/scheduler/tasks/__init__.py b/mealie/services/scheduler/tasks/__init__.py index 32a4858a5dc0..d4446d33bb1e 100644 --- a/mealie/services/scheduler/tasks/__init__.py +++ b/mealie/services/scheduler/tasks/__init__.py @@ -1,4 +1,3 @@ -from .auto_backup import * from .purge_group_exports import * from .purge_password_reset import * from .purge_registration import * diff --git a/mealie/services/scheduler/tasks/auto_backup.py b/mealie/services/scheduler/tasks/auto_backup.py deleted file mode 100644 index 171805c8da7b..000000000000 --- a/mealie/services/scheduler/tasks/auto_backup.py +++ /dev/null @@ -1,20 +0,0 @@ -from mealie.core import root_logger -from mealie.core.config import get_app_dirs - -app_dirs = get_app_dirs() -from mealie.db.db_setup import create_session -from mealie.services.backups.exports import backup_all - -logger = root_logger.get_logger() - - -def auto_backup(): - for backup in app_dirs.BACKUP_DIR.glob("Auto*.zip"): - backup.unlink() - - templates = [template for template in app_dirs.TEMPLATE_DIR.iterdir()] - session = create_session() - backup_all(session=session, tag="Auto", templates=templates) - logger.info("generating automated backup") - session.close() - logger.info("automated backup generated") diff --git a/poetry.lock b/poetry.lock index 088f1d9fba9c..c664bbca6b78 100644 --- a/poetry.lock +++ b/poetry.lock @@ -390,18 +390,6 @@ dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,< doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer-cli (>=0.0.12,<0.0.13)", "pyyaml (>=5.3.1,<6.0.0)"] test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==21.9b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"] -[[package]] -name = "fastapi-camelcase" -version = "1.0.5" -description = "Package provides an easy way to have camelcase request/response bodies for Pydantic" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pydantic = "*" -pyhumps = "*" - [[package]] name = "filelock" version = "3.6.0" @@ -829,17 +817,6 @@ category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -[[package]] -name = "pathvalidate" -version = "2.5.0" -description = "pathvalidate is a Python library to sanitize/validate a string such as filenames/file-paths/etc." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.extras] -test = ["allpairspy", "click", "faker", "pytest (>=6.0.1)", "pytest-discord (>=0.0.6)", "pytest-md-report (>=0.0.12)"] - [[package]] name = "pillow" version = "8.4.0" @@ -1003,7 +980,7 @@ python-versions = ">=3.5" [[package]] name = "pyhumps" -version = "3.5.0" +version = "3.5.3" description = "🐫 Convert strings (and dictionary keys) between snake case, camel case and pascal case in Python. Inspired by Humps for Node" category = "main" optional = false @@ -1622,7 +1599,7 @@ pgsql = ["psycopg2-binary"] [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "4fba071019a62f5d75e7c9a297a7815b2fed6486bb3616b5029a6fb08001761f" +content-hash = "84c1d9352c058da5cc0f50ca195cbe0897ce64abfbe01d08b9da317b6dd70a70" [metadata.files] aiofiles = [ @@ -1852,9 +1829,6 @@ fastapi = [ {file = "fastapi-0.74.1-py3-none-any.whl", hash = "sha256:b8ec8400623ef0b2ff558ebe06753b349f8e3a5dd38afea650800f2644ddba34"}, {file = "fastapi-0.74.1.tar.gz", hash = "sha256:b58a2c46df14f62ebe6f24a9439927539ba1959b9be55ba0e2f516a683e5b9d4"}, ] -fastapi-camelcase = [ - {file = "fastapi_camelcase-1.0.5.tar.gz", hash = "sha256:2cee005fb1b75649491b9f7cfccc640b12f028eb88084565f7d8cf415192026a"}, -] filelock = [ {file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"}, {file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"}, @@ -2239,10 +2213,6 @@ pathspec = [ {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] -pathvalidate = [ - {file = "pathvalidate-2.5.0-py3-none-any.whl", hash = "sha256:e5b2747ad557363e8f4124f0553d68878b12ecabd77bcca7e7312d5346d20262"}, - {file = "pathvalidate-2.5.0.tar.gz", hash = "sha256:119ba36be7e9a405d704c7b7aea4b871c757c53c9adc0ed64f40be1ed8da2781"}, -] pillow = [ {file = "Pillow-8.4.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d"}, {file = "Pillow-8.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6"}, @@ -2453,8 +2423,8 @@ pygments = [ {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"}, ] pyhumps = [ - {file = "pyhumps-3.5.0-py3-none-any.whl", hash = "sha256:2433eef13d1c258227a0bd5de9660ba17dd6a307e1255d2d20ec9287f8626d96"}, - {file = "pyhumps-3.5.0.tar.gz", hash = "sha256:55e37f16846eaab26057200924cbdadd2152bf0a5d49175a42358464fa881c73"}, + {file = "pyhumps-3.5.3-py3-none-any.whl", hash = "sha256:8d7e9865d6ddb6e64a2e97d951b78b5cc827d3d66cda1297310fc83b2ddf51dc"}, + {file = "pyhumps-3.5.3.tar.gz", hash = "sha256:0ecf7fee84503b45afdd3841ec769b529d32dfaed855e07046ff8babcc0ab831"}, ] pylint = [ {file = "pylint-2.12.2-py3-none-any.whl", hash = "sha256:daabda3f7ed9d1c60f52d563b1b854632fd90035bcf01443e234d3dc794e3b74"}, diff --git a/pyproject.toml b/pyproject.toml index 2f87e128e73b..9a44dc7033f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,13 +25,11 @@ requests = "^2.25.1" PyYAML = "^5.3.1" extruct = "^0.13.0" python-multipart = "^0.0.5" -fastapi-camelcase = "^1.0.5" bcrypt = "^3.2.0" python-jose = "^3.3.0" passlib = "^1.7.4" lxml = "^4.7.1" Pillow = "^8.2.0" -pathvalidate = "^2.4.1" apprise = "^0.9.6" recipe-scrapers = "^13.18.1" psycopg2-binary = {version = "^2.9.1", optional = true} @@ -41,6 +39,7 @@ python-i18n = "^0.3.9" python-ldap = "^3.3.1" pydantic = "^1.9.0" tzdata = "^2021.5" +pyhumps = "^3.5.3" [tool.poetry.dev-dependencies] pylint = "^2.6.0" diff --git a/tests/unit_tests/schema_tests/test_mealie_model.py b/tests/unit_tests/schema_tests/test_mealie_model.py new file mode 100644 index 000000000000..1581903b3b82 --- /dev/null +++ b/tests/unit_tests/schema_tests/test_mealie_model.py @@ -0,0 +1,63 @@ +from mealie.schema._mealie.mealie_model import MealieModel + + +class TestModel(MealieModel): + long_name: str + long_int: int + long_float: float + + +class TestModel2(MealieModel): + long_name: str + long_int: int + long_float: float + another_str: str + + +def test_camelize_variables(): + model = TestModel(long_name="Hello", long_int=1, long_float=1.1) + + as_dict = model.dict(by_alias=True) + + assert as_dict["longName"] == "Hello" + assert as_dict["longInt"] == 1 + assert as_dict["longFloat"] == 1.1 + + +def test_cast_to(): + + model = TestModel(long_name="Hello", long_int=1, long_float=1.1) + + model2 = model.cast(TestModel2, another_str="World") + + assert model2.long_name == "Hello" + assert model2.long_int == 1 + assert model2.long_float == 1.1 + assert model2.another_str == "World" + + +def test_map_to(): + + model = TestModel(long_name="Model1", long_int=100, long_float=1.5) + + model2 = TestModel2(long_name="Model2", long_int=1, long_float=1.1, another_str="World") + + model.map_to(model2) + + assert model2.long_name == "Model1" + assert model2.long_int == 100 + assert model2.long_float == 1.5 + assert model2.another_str == "World" + + +def test_map_from(): + model = TestModel(long_name="Model1", long_int=50, long_float=1.5) + + model2 = TestModel2(long_name="Hello", long_int=1, long_float=1.1, another_str="World") + + model2.map_from(model) + + assert model2.long_name == "Model1" + assert model2.long_int == 50 + assert model2.long_float == 1.5 + assert model2.another_str == "World"