chore: refactor base schema (#1098)

* remove dead backup code

* implmenet own base model

* refactor to use MealieModel instead of CamelModel

* cleanup deps
This commit is contained in:
Hayden 2022-03-25 10:56:49 -08:00 committed by GitHub
parent bcd98cba2f
commit 11b4d2389a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 253 additions and 623 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,2 @@
from .mealie_model import *
from .types import *

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 = ""

View file

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

View file

@ -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

View file

@ -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 = ""

View file

@ -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] = []

View file

@ -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]

View file

@ -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

View file

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

View file

@ -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]

View file

@ -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] = []

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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]

View file

@ -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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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,
}

View file

@ -1,4 +1,3 @@
from .auto_backup import *
from .purge_group_exports import *
from .purge_password_reset import *
from .purge_registration import *

View file

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

38
poetry.lock generated
View file

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

View file

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

View file

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