feat: Add Households to Mealie (#3970)

This commit is contained in:
Michael Genson
2024-08-22 10:14:32 -05:00
committed by GitHub
parent 0c29cef17d
commit eb170cc7e5
315 changed files with 6975 additions and 3577 deletions

View File

@@ -3,11 +3,11 @@ from .datetime_parse import DateError, DateTimeError, DurationError, TimeError
from .mealie_model import HasUUID, MealieModel, SearchType
__all__ = [
"HasUUID",
"MealieModel",
"SearchType",
"DateError",
"DateTimeError",
"DurationError",
"TimeError",
"HasUUID",
"MealieModel",
"SearchType",
]

View File

@@ -7,7 +7,7 @@ from enum import Enum
from typing import ClassVar, Protocol, TypeVar
from humps.main import camelize
from pydantic import UUID4, BaseModel, ConfigDict, model_validator
from pydantic import UUID4, AliasChoices, BaseModel, ConfigDict, Field, model_validator
from sqlalchemy import Select, desc, func, or_, text
from sqlalchemy.orm import InstrumentedAttribute, Session
from sqlalchemy.orm.interfaces import LoaderOption
@@ -20,6 +20,26 @@ T = TypeVar("T", bound=BaseModel)
HOUR_ONLY_TZ_PATTERN = re.compile(r"[+-]\d{2}$")
def UpdatedAtField(*args, **kwargs):
"""
Wrapper for Pydantic's Field, which sets default values for our update_at aliases
Since the database stores this value as update_at, we want to accept this as a possible value
"""
kwargs.pop("alias", None)
kwargs.pop("alias_generator", None)
# ensure the alias is not overwritten by the generator, if there is one
# https://docs.pydantic.dev/latest/concepts/alias/#alias-priority
kwargs["alias_priority"] = 2
kwargs["validation_alias"] = AliasChoices("update_at", "updateAt", "updated_at", "updatedAt")
kwargs["serialization_alias"] = "updatedAt"
return Field(*args, **kwargs)
class SearchType(Enum):
fuzzy = "fuzzy"
tokenized = "tokenized"
@@ -77,7 +97,7 @@ class MealieModel(BaseModel):
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 = {field: getattr(self, field) for field in self.model_fields if field in cls.model_fields}
create_data.update(kwargs or {})
return cls(**create_data)

View File

@@ -20,6 +20,12 @@ __all__ = [
"MaintenanceLogs",
"MaintenanceStorageDetails",
"MaintenanceSummary",
"ChowdownURL",
"MigrationFile",
"MigrationImport",
"Migrations",
"CustomPageBase",
"CustomPageOut",
"CommentImport",
"CustomPageImport",
"GroupImport",
@@ -28,11 +34,11 @@ __all__ = [
"RecipeImport",
"SettingsImport",
"UserImport",
"EmailReady",
"EmailSuccess",
"EmailTest",
"CustomPageBase",
"CustomPageOut",
"AllBackups",
"BackupFile",
"BackupOptions",
"CreateBackup",
"ImportJob",
"AdminAboutInfo",
"AppInfo",
"AppStartupInfo",
@@ -40,13 +46,7 @@ __all__ = [
"AppTheme",
"CheckAppConfig",
"OIDCInfo",
"ChowdownURL",
"MigrationFile",
"MigrationImport",
"Migrations",
"AllBackups",
"BackupFile",
"BackupOptions",
"CreateBackup",
"ImportJob",
"EmailReady",
"EmailSuccess",
"EmailTest",
]

View File

@@ -4,6 +4,7 @@ from mealie.schema._mealie import MealieModel
class AppStatistics(MealieModel):
total_recipes: int
total_users: int
total_households: int
total_groups: int
uncategorized_recipes: int
untagged_recipes: int
@@ -15,6 +16,7 @@ class AppInfo(MealieModel):
demo_status: bool
allow_signup: bool
default_group_slug: str | None = None
default_household_slug: str | None = None
enable_oidc: bool
oidc_redirect: bool
oidc_provider_name: str
@@ -58,6 +60,7 @@ class AdminAboutInfo(AppInfo):
db_type: str
db_url: str | None = None
default_group: str
default_household: str
build_id: str
recipe_scraper_version: str

View File

@@ -10,7 +10,7 @@ from mealie.schema._mealie import MealieModel
from mealie.schema.recipe.recipe import RecipeSummary, RecipeTool
from mealie.schema.response.pagination import PaginationBase
from ...db.models.group import CookBook
from ...db.models.household import CookBook
from ..recipe.recipe_category import CategoryBase, TagBase
@@ -44,6 +44,7 @@ class CreateCookBook(MealieModel):
class SaveCookBook(CreateCookBook):
group_id: UUID4
household_id: UUID4
class UpdateCookBook(SaveCookBook):
@@ -52,6 +53,7 @@ class UpdateCookBook(SaveCookBook):
class ReadCookBook(UpdateCookBook):
group_id: UUID4
household_id: UUID4
categories: list[CategoryBase] = []
model_config = ConfigDict(from_attributes=True)
@@ -66,5 +68,6 @@ class CookBookPagination(PaginationBase):
class RecipeCookBook(ReadCookBook):
group_id: UUID4
household_id: UUID4
recipes: list[RecipeSummary]
model_config = ConfigDict(from_attributes=True)

View File

@@ -1,158 +1,19 @@
# This file is auto-generated by gen_schema_exports.py
from .group import GroupAdminUpdate
from .group_events import (
GroupEventNotifierCreate,
GroupEventNotifierOptions,
GroupEventNotifierOptionsOut,
GroupEventNotifierOptionsSave,
GroupEventNotifierOut,
GroupEventNotifierPrivate,
GroupEventNotifierSave,
GroupEventNotifierUpdate,
GroupEventPagination,
)
from .group_exports import GroupDataExport
from .group_migration import DataMigrationCreate, SupportedMigrations
from .group_permissions import SetPermissions
from .group_preferences import (
CreateGroupPreferences,
ReadGroupPreferences,
UpdateGroupPreferences,
)
from .group_recipe_action import (
CreateGroupRecipeAction,
GroupRecipeActionOut,
GroupRecipeActionPagination,
GroupRecipeActionType,
SaveGroupRecipeAction,
)
from .group_preferences import CreateGroupPreferences, ReadGroupPreferences, UpdateGroupPreferences
from .group_seeder import SeederConfig
from .group_shopping_list import (
ShoppingListAddRecipeParams,
ShoppingListCreate,
ShoppingListItemBase,
ShoppingListItemCreate,
ShoppingListItemOut,
ShoppingListItemPagination,
ShoppingListItemRecipeRefCreate,
ShoppingListItemRecipeRefOut,
ShoppingListItemRecipeRefUpdate,
ShoppingListItemsCollectionOut,
ShoppingListItemUpdate,
ShoppingListItemUpdateBulk,
ShoppingListMultiPurposeLabelCreate,
ShoppingListMultiPurposeLabelOut,
ShoppingListMultiPurposeLabelUpdate,
ShoppingListOut,
ShoppingListPagination,
ShoppingListRecipeRefOut,
ShoppingListRemoveRecipeParams,
ShoppingListSave,
ShoppingListSummary,
ShoppingListUpdate,
)
from .group_statistics import GroupStatistics, GroupStorage
from .invite_token import (
CreateInviteToken,
EmailInitationResponse,
EmailInvitation,
ReadInviteToken,
SaveInviteToken,
)
from .webhook import (
CreateWebhook,
ReadWebhook,
SaveWebhook,
WebhookPagination,
WebhookType,
)
from .group_statistics import GroupStorage
__all__ = [
"GroupEventNotifierCreate",
"GroupEventNotifierOptions",
"GroupEventNotifierOptionsOut",
"GroupEventNotifierOptionsSave",
"GroupEventNotifierOut",
"GroupEventNotifierPrivate",
"GroupEventNotifierSave",
"GroupEventNotifierUpdate",
"GroupEventPagination",
"CreateGroupRecipeAction",
"GroupRecipeActionOut",
"GroupRecipeActionPagination",
"GroupRecipeActionType",
"SaveGroupRecipeAction",
"CreateWebhook",
"ReadWebhook",
"SaveWebhook",
"WebhookPagination",
"WebhookType",
"GroupDataExport",
"CreateGroupPreferences",
"ReadGroupPreferences",
"UpdateGroupPreferences",
"GroupStatistics",
"GroupStorage",
"DataMigrationCreate",
"SupportedMigrations",
"SeederConfig",
"CreateInviteToken",
"EmailInitationResponse",
"EmailInvitation",
"ReadInviteToken",
"SaveInviteToken",
"SetPermissions",
"ShoppingListAddRecipeParams",
"ShoppingListCreate",
"ShoppingListItemBase",
"ShoppingListItemCreate",
"ShoppingListItemOut",
"ShoppingListItemPagination",
"ShoppingListItemRecipeRefCreate",
"ShoppingListItemRecipeRefOut",
"ShoppingListItemRecipeRefUpdate",
"ShoppingListItemUpdate",
"ShoppingListItemUpdateBulk",
"ShoppingListItemsCollectionOut",
"ShoppingListMultiPurposeLabelCreate",
"ShoppingListMultiPurposeLabelOut",
"ShoppingListMultiPurposeLabelUpdate",
"ShoppingListOut",
"ShoppingListPagination",
"ShoppingListRecipeRefOut",
"ShoppingListRemoveRecipeParams",
"ShoppingListSave",
"ShoppingListSummary",
"ShoppingListUpdate",
"GroupAdminUpdate",
"CreateWebhook",
"ReadWebhook",
"SaveWebhook",
"WebhookPagination",
"WebhookType",
"GroupAdminUpdate",
"CreateGroupPreferences",
"ReadGroupPreferences",
"UpdateGroupPreferences",
"SetPermissions",
"DataMigrationCreate",
"SupportedMigrations",
"SeederConfig",
"GroupDataExport",
"CreateInviteToken",
"EmailInitationResponse",
"EmailInvitation",
"ReadInviteToken",
"SaveInviteToken",
"GroupStatistics",
"GroupStorage",
"GroupEventNotifierCreate",
"GroupEventNotifierOptions",
"GroupEventNotifierOptionsOut",
"GroupEventNotifierOptionsSave",
"GroupEventNotifierOut",
"GroupEventNotifierPrivate",
"GroupEventNotifierSave",
"GroupEventNotifierUpdate",
"GroupEventPagination",
]

View File

@@ -7,15 +7,6 @@ from mealie.schema._mealie import MealieModel
class UpdateGroupPreferences(MealieModel):
private_group: bool = True
first_day_of_week: int = 0
# Recipe Defaults
recipe_public: bool = True
recipe_show_nutrition: bool = False
recipe_show_assets: bool = False
recipe_landscape_view: bool = False
recipe_disable_comments: bool = False
recipe_disable_amount: bool = True
class CreateGroupPreferences(UpdateGroupPreferences):

View File

@@ -2,14 +2,6 @@ from mealie.pkgs.stats import fs_stats
from mealie.schema._mealie import MealieModel
class GroupStatistics(MealieModel):
total_recipes: int
total_users: int
total_categories: int
total_tags: int
total_tools: int
class GroupStorage(MealieModel):
used_storage_bytes: int
used_storage_str: str

View File

@@ -0,0 +1,126 @@
# This file is auto-generated by gen_schema_exports.py
from .group_events import (
GroupEventNotifierCreate,
GroupEventNotifierOptions,
GroupEventNotifierOptionsOut,
GroupEventNotifierOptionsSave,
GroupEventNotifierOut,
GroupEventNotifierPrivate,
GroupEventNotifierSave,
GroupEventNotifierUpdate,
GroupEventPagination,
)
from .group_recipe_action import (
CreateGroupRecipeAction,
GroupRecipeActionOut,
GroupRecipeActionPagination,
GroupRecipeActionType,
SaveGroupRecipeAction,
)
from .group_shopping_list import (
ShoppingListAddRecipeParams,
ShoppingListCreate,
ShoppingListItemBase,
ShoppingListItemCreate,
ShoppingListItemOut,
ShoppingListItemPagination,
ShoppingListItemRecipeRefCreate,
ShoppingListItemRecipeRefOut,
ShoppingListItemRecipeRefUpdate,
ShoppingListItemsCollectionOut,
ShoppingListItemUpdate,
ShoppingListItemUpdateBulk,
ShoppingListMultiPurposeLabelCreate,
ShoppingListMultiPurposeLabelOut,
ShoppingListMultiPurposeLabelUpdate,
ShoppingListOut,
ShoppingListPagination,
ShoppingListRecipeRefOut,
ShoppingListRemoveRecipeParams,
ShoppingListSave,
ShoppingListSummary,
ShoppingListUpdate,
)
from .household import (
HouseholdCreate,
HouseholdInDB,
HouseholdPagination,
HouseholdSave,
HouseholdSummary,
HouseholdUserSummary,
UpdateHousehold,
UpdateHouseholdAdmin,
)
from .household_permissions import SetPermissions
from .household_preferences import (
CreateHouseholdPreferences,
ReadHouseholdPreferences,
SaveHouseholdPreferences,
UpdateHouseholdPreferences,
)
from .household_statistics import HouseholdStatistics
from .invite_token import CreateInviteToken, EmailInitationResponse, EmailInvitation, ReadInviteToken, SaveInviteToken
from .webhook import CreateWebhook, ReadWebhook, SaveWebhook, WebhookPagination, WebhookType
__all__ = [
"GroupEventNotifierCreate",
"GroupEventNotifierOptions",
"GroupEventNotifierOptionsOut",
"GroupEventNotifierOptionsSave",
"GroupEventNotifierOut",
"GroupEventNotifierPrivate",
"GroupEventNotifierSave",
"GroupEventNotifierUpdate",
"GroupEventPagination",
"CreateGroupRecipeAction",
"GroupRecipeActionOut",
"GroupRecipeActionPagination",
"GroupRecipeActionType",
"SaveGroupRecipeAction",
"CreateWebhook",
"ReadWebhook",
"SaveWebhook",
"WebhookPagination",
"WebhookType",
"CreateHouseholdPreferences",
"ReadHouseholdPreferences",
"SaveHouseholdPreferences",
"UpdateHouseholdPreferences",
"HouseholdCreate",
"HouseholdInDB",
"HouseholdPagination",
"HouseholdSave",
"HouseholdSummary",
"HouseholdUserSummary",
"UpdateHousehold",
"UpdateHouseholdAdmin",
"HouseholdStatistics",
"CreateInviteToken",
"EmailInitationResponse",
"EmailInvitation",
"ReadInviteToken",
"SaveInviteToken",
"ShoppingListAddRecipeParams",
"ShoppingListCreate",
"ShoppingListItemBase",
"ShoppingListItemCreate",
"ShoppingListItemOut",
"ShoppingListItemPagination",
"ShoppingListItemRecipeRefCreate",
"ShoppingListItemRecipeRefOut",
"ShoppingListItemRecipeRefUpdate",
"ShoppingListItemUpdate",
"ShoppingListItemUpdateBulk",
"ShoppingListItemsCollectionOut",
"ShoppingListMultiPurposeLabelCreate",
"ShoppingListMultiPurposeLabelOut",
"ShoppingListMultiPurposeLabelUpdate",
"ShoppingListOut",
"ShoppingListPagination",
"ShoppingListRecipeRefOut",
"ShoppingListRemoveRecipeParams",
"ShoppingListSave",
"ShoppingListSummary",
"ShoppingListUpdate",
"SetPermissions",
]

View File

@@ -2,7 +2,7 @@ from pydantic import UUID4, ConfigDict
from sqlalchemy.orm import joinedload
from sqlalchemy.orm.interfaces import LoaderOption
from mealie.db.models.group import GroupEventNotifierModel
from mealie.db.models.household import GroupEventNotifierModel
from mealie.schema._mealie import MealieModel
from mealie.schema.response.pagination import PaginationBase
@@ -69,6 +69,7 @@ class GroupEventNotifierCreate(MealieModel):
class GroupEventNotifierSave(GroupEventNotifierCreate):
enabled: bool = True
group_id: UUID4
household_id: UUID4
options: GroupEventNotifierOptions = GroupEventNotifierOptions()
@@ -82,6 +83,7 @@ class GroupEventNotifierOut(MealieModel):
name: str
enabled: bool
group_id: UUID4
household_id: UUID4
options: GroupEventNotifierOptionsOut
model_config = ConfigDict(from_attributes=True)

View File

@@ -21,6 +21,7 @@ class CreateGroupRecipeAction(MealieModel):
class SaveGroupRecipeAction(CreateGroupRecipeAction):
group_id: UUID4
household_id: UUID4
class GroupRecipeActionOut(SaveGroupRecipeAction):

View File

@@ -7,7 +7,7 @@ from pydantic import UUID4, ConfigDict, field_validator, model_validator
from sqlalchemy.orm import joinedload, selectinload
from sqlalchemy.orm.interfaces import LoaderOption
from mealie.db.models.group import (
from mealie.db.models.household import (
ShoppingList,
ShoppingListItem,
ShoppingListMultiPurposeLabel,
@@ -15,6 +15,7 @@ from mealie.db.models.group import (
)
from mealie.db.models.recipe import IngredientFoodModel, RecipeModel
from mealie.schema._mealie import MealieModel
from mealie.schema._mealie.mealie_model import UpdatedAtField
from mealie.schema._mealie.types import NoneFloat
from mealie.schema.labels.multi_purpose_label import MultiPurposeLabelSummary
from mealie.schema.recipe.recipe import RecipeSummary
@@ -104,6 +105,8 @@ class ShoppingListItemUpdateBulk(ShoppingListItemUpdate):
class ShoppingListItemOut(ShoppingListItemBase):
id: UUID4
group_id: UUID4
household_id: UUID4
food: IngredientFood | None = None
label: MultiPurposeLabelSummary | None = None
@@ -112,7 +115,7 @@ class ShoppingListItemOut(ShoppingListItemBase):
recipe_references: list[ShoppingListItemRecipeRefOut] = []
created_at: datetime | None = None
update_at: datetime | None = None
updated_at: datetime | None = UpdatedAtField(None)
@model_validator(mode="after")
def populate_missing_label(self):
@@ -134,6 +137,7 @@ class ShoppingListItemOut(ShoppingListItemBase):
joinedload(ShoppingListItem.label),
joinedload(ShoppingListItem.unit),
selectinload(ShoppingListItem.recipe_references),
joinedload(ShoppingListItem.shopping_list).joinedload(ShoppingList.user),
]
@@ -173,7 +177,7 @@ class ShoppingListCreate(MealieModel):
extras: dict | None = {}
created_at: datetime | None = None
update_at: datetime | None = None
updated_at: datetime | None = UpdatedAtField(None)
@field_validator("extras", mode="before")
def convert_extras_to_dict(cls, v):
@@ -209,6 +213,7 @@ class ShoppingListSave(ShoppingListCreate):
class ShoppingListSummary(ShoppingListSave):
id: UUID4
household_id: UUID4
recipe_references: list[ShoppingListRecipeRefOut]
label_settings: list[ShoppingListMultiPurposeLabelOut]
model_config = ConfigDict(from_attributes=True)
@@ -227,6 +232,7 @@ class ShoppingListSummary(ShoppingListSave):
.joinedload(ShoppingListRecipeReference.recipe)
.joinedload(RecipeModel.tools),
selectinload(ShoppingList.label_settings).joinedload(ShoppingListMultiPurposeLabel.label),
joinedload(ShoppingList.user),
]
@@ -240,6 +246,7 @@ class ShoppingListUpdate(ShoppingListSave):
class ShoppingListOut(ShoppingListUpdate):
household_id: UUID4
recipe_references: list[ShoppingListRecipeRefOut] = []
label_settings: list[ShoppingListMultiPurposeLabelOut] = []
model_config = ConfigDict(from_attributes=True)
@@ -272,6 +279,7 @@ class ShoppingListOut(ShoppingListUpdate):
.joinedload(ShoppingListRecipeReference.recipe)
.joinedload(RecipeModel.tools),
selectinload(ShoppingList.label_settings).joinedload(ShoppingListMultiPurposeLabel.label),
joinedload(ShoppingList.user),
]

View File

@@ -0,0 +1,75 @@
from typing import Annotated
from pydantic import UUID4, ConfigDict, StringConstraints, field_validator
from sqlalchemy.orm import joinedload, selectinload
from sqlalchemy.orm.interfaces import LoaderOption
from mealie.db.models.household.household import Household
from mealie.db.models.users.users import User
from mealie.schema._mealie.mealie_model import MealieModel
from mealie.schema.household.webhook import ReadWebhook
from mealie.schema.response.pagination import PaginationBase
from .household_preferences import ReadHouseholdPreferences, UpdateHouseholdPreferences
class HouseholdCreate(MealieModel):
group_id: UUID4 | None = None
name: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
model_config = ConfigDict(from_attributes=True)
class HouseholdSave(HouseholdCreate):
group_id: UUID4
class UpdateHousehold(HouseholdSave):
id: UUID4
slug: str
class UpdateHouseholdAdmin(HouseholdSave):
id: UUID4
preferences: UpdateHouseholdPreferences | None = None
class HouseholdSummary(UpdateHousehold):
preferences: ReadHouseholdPreferences | None = None
model_config = ConfigDict(from_attributes=True)
class HouseholdUserSummary(MealieModel):
id: UUID4
full_name: str
model_config = ConfigDict(from_attributes=True)
class HouseholdInDB(HouseholdSummary):
group: str
users: list[HouseholdUserSummary] | None = None
webhooks: list[ReadWebhook] = []
model_config = ConfigDict(from_attributes=True)
@classmethod
def loader_options(cls) -> list[LoaderOption]:
return [
joinedload(Household.group),
joinedload(Household.webhooks),
joinedload(Household.preferences),
selectinload(Household.users).joinedload(User.group),
selectinload(Household.users).joinedload(User.tokens),
]
@field_validator("group", mode="before")
def convert_group_to_name(cls, v):
if not v or isinstance(v, str):
return v
try:
return v.name
except AttributeError:
return v
class HouseholdPagination(PaginationBase):
items: list[HouseholdInDB]

View File

@@ -0,0 +1,28 @@
from pydantic import UUID4, ConfigDict
from mealie.schema._mealie import MealieModel
class UpdateHouseholdPreferences(MealieModel):
private_household: bool = True
first_day_of_week: int = 0
# Recipe Defaults
recipe_public: bool = True
recipe_show_nutrition: bool = False
recipe_show_assets: bool = False
recipe_landscape_view: bool = False
recipe_disable_comments: bool = False
recipe_disable_amount: bool = True
class CreateHouseholdPreferences(UpdateHouseholdPreferences): ...
class SaveHouseholdPreferences(UpdateHouseholdPreferences):
household_id: UUID4
class ReadHouseholdPreferences(CreateHouseholdPreferences):
id: UUID4
model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,9 @@
from mealie.schema._mealie.mealie_model import MealieModel
class HouseholdStatistics(MealieModel):
total_recipes: int
total_users: int
total_categories: int
total_tags: int
total_tools: int

View File

@@ -12,6 +12,7 @@ class CreateInviteToken(MealieModel):
class SaveInviteToken(MealieModel):
uses_left: int
group_id: UUID
household_id: UUID
token: str
@@ -19,6 +20,7 @@ class ReadInviteToken(MealieModel):
token: str
uses_left: int
group_id: UUID
household_id: UUID
model_config = ConfigDict(from_attributes=True)

View File

@@ -1,6 +1,5 @@
import datetime
import enum
from uuid import UUID
from isodate import parse_time
from pydantic import UUID4, ConfigDict, field_validator
@@ -50,7 +49,8 @@ class CreateWebhook(MealieModel):
class SaveWebhook(CreateWebhook):
group_id: UUID
group_id: UUID4
household_id: UUID4
class ReadWebhook(SaveWebhook):

View File

@@ -1,5 +1,4 @@
# This file is auto-generated by gen_schema_exports.py
from .meal import MealDayIn, MealDayOut, MealIn, MealPlanIn, MealPlanOut
from .new_meal import (
CreatePlanEntry,
CreateRandomEntry,
@@ -22,14 +21,6 @@ from .plan_rules import (
from .shopping_list import ListItem, ShoppingListIn, ShoppingListOut
__all__ = [
"Category",
"PlanRulesCreate",
"PlanRulesDay",
"PlanRulesOut",
"PlanRulesPagination",
"PlanRulesSave",
"PlanRulesType",
"Tag",
"ListItem",
"ShoppingListIn",
"ShoppingListOut",
@@ -40,9 +31,12 @@ __all__ = [
"ReadPlanEntry",
"SavePlanEntry",
"UpdatePlanEntry",
"MealDayIn",
"MealDayOut",
"MealIn",
"MealPlanIn",
"MealPlanOut",
"Category",
"PlanRulesCreate",
"PlanRulesDay",
"PlanRulesOut",
"PlanRulesPagination",
"PlanRulesSave",
"PlanRulesType",
"Tag",
]

View File

@@ -1,45 +0,0 @@
import datetime
from pydantic import ConfigDict, field_validator
from pydantic_core.core_schema import ValidationInfo
from mealie.schema._mealie import MealieModel
class MealIn(MealieModel):
slug: str | None = None
name: str | None = None
description: str | None = None
model_config = ConfigDict(from_attributes=True)
class MealDayIn(MealieModel):
date: datetime.date | None = None
meals: list[MealIn]
model_config = ConfigDict(from_attributes=True)
class MealDayOut(MealDayIn):
id: int
model_config = ConfigDict(from_attributes=True)
class MealPlanIn(MealieModel):
group: str
start_date: datetime.date
end_date: datetime.date
plan_days: list[MealDayIn]
@field_validator("end_date")
def end_date_after_start_date(v, info: ValidationInfo):
if "start_date" in info.data and v < info.data["start_date"]:
raise ValueError("EndDate should be greater than StartDate")
return v
model_config = ConfigDict(from_attributes=True)
class MealPlanOut(MealPlanIn):
id: int
shopping_list: int | None = None
model_config = ConfigDict(from_attributes=True)

View File

@@ -8,7 +8,7 @@ from pydantic_core.core_schema import ValidationInfo
from sqlalchemy.orm import selectinload
from sqlalchemy.orm.interfaces import LoaderOption
from mealie.db.models.group import GroupMealPlan
from mealie.db.models.household import GroupMealPlan
from mealie.db.models.recipe import RecipeModel
from mealie.schema._mealie import MealieModel
from mealie.schema.recipe.recipe import RecipeSummary
@@ -56,6 +56,7 @@ class SavePlanEntry(CreatePlanEntry):
class ReadPlanEntry(UpdatePlanEntry):
household_id: UUID
recipe: RecipeSummary | None = None
model_config = ConfigDict(from_attributes=True)
@@ -65,6 +66,7 @@ class ReadPlanEntry(UpdatePlanEntry):
selectinload(GroupMealPlan.recipe).joinedload(RecipeModel.recipe_category),
selectinload(GroupMealPlan.recipe).joinedload(RecipeModel.tags),
selectinload(GroupMealPlan.recipe).joinedload(RecipeModel.tools),
selectinload(GroupMealPlan.user),
]

View File

@@ -5,7 +5,7 @@ from pydantic import UUID4, ConfigDict
from sqlalchemy.orm import joinedload
from sqlalchemy.orm.interfaces import LoaderOption
from mealie.db.models.group import GroupMealPlanRules
from mealie.db.models.household import GroupMealPlanRules
from mealie.schema._mealie import MealieModel
from mealie.schema.response.pagination import PaginationBase
@@ -57,6 +57,7 @@ class PlanRulesCreate(MealieModel):
class PlanRulesSave(PlanRulesCreate):
group_id: UUID4
household_id: UUID4
class PlanRulesOut(PlanRulesSave):

View File

@@ -0,0 +1,7 @@
# This file is auto-generated by gen_schema_exports.py
from .recipe_ingredient import OpenAIIngredient, OpenAIIngredients
__all__ = [
"OpenAIIngredient",
"OpenAIIngredients",
]

View File

@@ -88,10 +88,9 @@ from .recipe_tool import RecipeToolCreate, RecipeToolOut, RecipeToolResponse, Re
from .request_helpers import RecipeDuplicate, RecipeSlug, RecipeZipTokenResponse, SlugResponse, UpdateImageResponse
__all__ = [
"RecipeToolCreate",
"RecipeToolOut",
"RecipeToolResponse",
"RecipeToolSave",
"IngredientReferences",
"RecipeStep",
"RecipeNote",
"CategoryBase",
"CategoryIn",
"CategoryOut",
@@ -102,41 +101,19 @@ __all__ = [
"TagIn",
"TagOut",
"TagSave",
"AssignCategories",
"AssignSettings",
"AssignTags",
"DeleteRecipes",
"ExportBase",
"ExportRecipes",
"ExportTypes",
"RecipeAsset",
"RecipeTimelineEventCreate",
"RecipeTimelineEventIn",
"RecipeTimelineEventOut",
"RecipeTimelineEventPagination",
"RecipeTimelineEventUpdate",
"TimelineEventImage",
"TimelineEventType",
"Nutrition",
"RecipeShareToken",
"RecipeShareTokenCreate",
"RecipeShareTokenSave",
"RecipeShareTokenSummary",
"ScrapeRecipe",
"ScrapeRecipeTest",
"RecipeCommentCreate",
"RecipeCommentOut",
"RecipeCommentPagination",
"RecipeCommentSave",
"RecipeCommentUpdate",
"UserBase",
"RecipeImageTypes",
"CreateRecipe",
"CreateRecipeBulk",
"CreateRecipeByUrlBulk",
"Recipe",
"RecipeCategory",
"RecipeCategoryPagination",
"RecipeLastMade",
"RecipePagination",
"RecipeSummary",
"RecipeTag",
"RecipeTagPagination",
"RecipeTool",
"RecipeToolPagination",
"IngredientReferences",
"RecipeStep",
"CreateIngredientFood",
"CreateIngredientFoodAlias",
"CreateIngredientUnit",
@@ -159,20 +136,43 @@ __all__ = [
"SaveIngredientFood",
"SaveIngredientUnit",
"UnitFoodBase",
"RecipeAsset",
"RecipeTimelineEventCreate",
"RecipeTimelineEventIn",
"RecipeTimelineEventOut",
"RecipeTimelineEventPagination",
"RecipeTimelineEventUpdate",
"TimelineEventImage",
"TimelineEventType",
"RecipeCommentCreate",
"RecipeCommentOut",
"RecipeCommentPagination",
"RecipeCommentSave",
"RecipeCommentUpdate",
"UserBase",
"RecipeSettings",
"CreateRecipe",
"CreateRecipeBulk",
"CreateRecipeByUrlBulk",
"Recipe",
"RecipeCategory",
"RecipeCategoryPagination",
"RecipeLastMade",
"RecipePagination",
"RecipeSummary",
"RecipeTag",
"RecipeTagPagination",
"RecipeTool",
"RecipeToolPagination",
"ScrapeRecipe",
"ScrapeRecipeTest",
"AssignCategories",
"AssignSettings",
"AssignTags",
"DeleteRecipes",
"ExportBase",
"ExportRecipes",
"ExportTypes",
"RecipeToolCreate",
"RecipeToolOut",
"RecipeToolResponse",
"RecipeToolSave",
"RecipeImageTypes",
"RecipeDuplicate",
"RecipeSlug",
"RecipeZipTokenResponse",
"SlugResponse",
"UpdateImageResponse",
"Nutrition",
"RecipeSettings",
"RecipeNote",
]

View File

@@ -15,6 +15,7 @@ from sqlalchemy.orm.interfaces import LoaderOption
from mealie.core.config import get_app_dirs
from mealie.schema._mealie import MealieModel, SearchType
from mealie.schema._mealie.mealie_model import UpdatedAtField
from mealie.schema.response.pagination import PaginationBase
from ...db.models.recipe import (
@@ -83,6 +84,7 @@ class RecipeSummary(MealieModel):
_normalize_search: ClassVar[bool] = True
user_id: UUID4 = Field(default_factory=uuid4, validate_default=True)
household_id: UUID4 = Field(default_factory=uuid4, validate_default=True)
group_id: UUID4 = Field(default_factory=uuid4, validate_default=True)
name: str | None = None
@@ -106,7 +108,7 @@ class RecipeSummary(MealieModel):
date_updated: datetime.datetime | None = None
created_at: datetime.datetime | None = None
update_at: datetime.datetime | None = None
updated_at: datetime.datetime | None = UpdatedAtField(None)
last_made: datetime.datetime | None = None
model_config = ConfigDict(from_attributes=True)
@@ -230,6 +232,12 @@ class Recipe(RecipeSummary):
return uuid4()
return group_id
@field_validator("household_id", mode="before")
def validate_household_id(household_id: Any):
if isinstance(household_id, int):
return uuid4()
return household_id
@field_validator("user_id", mode="before")
def validate_user_id(user_id: Any):
if isinstance(user_id, int):

View File

@@ -5,7 +5,9 @@ from sqlalchemy.orm import joinedload
from sqlalchemy.orm.interfaces import LoaderOption
from mealie.db.models.recipe import RecipeComment
from mealie.db.models.recipe.recipe import RecipeModel
from mealie.schema._mealie import MealieModel
from mealie.schema._mealie.mealie_model import UpdatedAtField
from mealie.schema.response.pagination import PaginationBase
@@ -34,14 +36,17 @@ class RecipeCommentOut(RecipeCommentCreate):
id: UUID4
recipe_id: UUID4
created_at: datetime
update_at: datetime
updated_at: datetime = UpdatedAtField(...)
user_id: UUID4
user: UserBase
model_config = ConfigDict(from_attributes=True)
@classmethod
def loader_options(cls) -> list[LoaderOption]:
return [joinedload(RecipeComment.user)]
return [
joinedload(RecipeComment.user),
joinedload(RecipeComment.recipe).joinedload(RecipeModel.user),
]
class RecipeCommentPagination(PaginationBase):

View File

@@ -12,6 +12,7 @@ from sqlalchemy.orm.interfaces import LoaderOption
from mealie.db.models.recipe import IngredientFoodModel
from mealie.schema._mealie import MealieModel
from mealie.schema._mealie.mealie_model import UpdatedAtField
from mealie.schema._mealie.types import NoneFloat
from mealie.schema.response.pagination import PaginationBase
@@ -78,7 +79,7 @@ class IngredientFood(CreateIngredientFood):
aliases: list[IngredientFoodAlias] = []
created_at: datetime.datetime | None = None
update_at: datetime.datetime | None = None
updated_at: datetime.datetime | None = UpdatedAtField(None)
_searchable_properties: ClassVar[list[str]] = [
"name_normalized",
@@ -124,7 +125,7 @@ class IngredientUnit(CreateIngredientUnit):
aliases: list[IngredientUnitAlias] = []
created_at: datetime.datetime | None = None
update_at: datetime.datetime | None = None
updated_at: datetime.datetime | None = UpdatedAtField(None)
_searchable_properties: ClassVar[list[str]] = [
"name_normalized",

View File

@@ -4,9 +4,13 @@ from pathlib import Path
from typing import Annotated
from pydantic import UUID4, ConfigDict, Field
from sqlalchemy.orm import joinedload
from sqlalchemy.orm.interfaces import LoaderOption
from mealie.core.config import get_app_dirs
from mealie.schema._mealie.mealie_model import MealieModel
from mealie.db.models.recipe.recipe_timeline import RecipeTimelineEvent
from mealie.schema._mealie import MealieModel
from mealie.schema._mealie.mealie_model import UpdatedAtField
from mealie.schema.recipe.recipe import Recipe
from mealie.schema.response.pagination import PaginationBase
@@ -52,10 +56,20 @@ class RecipeTimelineEventUpdate(MealieModel):
class RecipeTimelineEventOut(RecipeTimelineEventCreate):
id: UUID4
group_id: UUID4
household_id: UUID4
created_at: datetime
update_at: datetime
updated_at: datetime = UpdatedAtField(...)
model_config = ConfigDict(from_attributes=True)
@classmethod
def loader_options(cls) -> list[LoaderOption]:
return [
joinedload(RecipeTimelineEvent.recipe),
joinedload(RecipeTimelineEvent.user),
]
@classmethod
def image_dir_from_id(cls, recipe_id: UUID4 | str, timeline_event_id: UUID4 | str) -> Path:
return Recipe.timeline_image_dir_from_id(recipe_id, timeline_event_id)

View File

@@ -11,13 +11,13 @@ __all__ = [
"QueryFilterComponent",
"RelationalKeyword",
"RelationalOperator",
"SearchFilter",
"ValidationResponse",
"OrderByNullPosition",
"OrderDirection",
"PaginationBase",
"PaginationQuery",
"RecipeSearchQuery",
"SearchFilter",
"ErrorResponse",
"FileTokenResponse",
"SuccessResponse",

View File

@@ -10,6 +10,7 @@ from dateutil import parser as date_parser
from dateutil.parser import ParserError
from humps import decamelize
from sqlalchemy import ColumnElement, Select, and_, inspect, or_
from sqlalchemy.ext.associationproxy import AssociationProxyInstance
from sqlalchemy.orm import InstrumentedAttribute, Mapper
from sqlalchemy.sql import sqltypes
@@ -255,7 +256,9 @@ class QueryFilter:
Works with shallow attributes (e.g. "slug" from `RecipeModel`)
and arbitrarily deep ones (e.g. "recipe.group.preferences" on `RecipeTimelineEvent`).
"""
mapper: Mapper
model_attr: InstrumentedAttribute | None = None
attribute_chain = attr_string.split(".")
if not attribute_chain:
raise ValueError("invalid query string: attribute name cannot be empty")
@@ -265,16 +268,29 @@ class QueryFilter:
try:
model_attr = getattr(current_model, attribute_link)
# proxied attributes can't be joined to the query directly, so we need to inspect the proxy
# and get the actual model and its attribute
if isinstance(model_attr, AssociationProxyInstance):
proxied_attribute_link = model_attr.target_collection
next_attribute_link = model_attr.value_attr
model_attr = getattr(current_model, proxied_attribute_link)
if query is not None:
query = query.join(model_attr, isouter=True)
mapper = inspect(current_model)
relationship = mapper.relationships[proxied_attribute_link]
current_model = relationship.mapper.class_
model_attr = getattr(current_model, next_attribute_link)
# at the end of the chain there are no more relationships to inspect
if i == len(attribute_chain) - 1:
break
if query is not None:
query = query.join(
model_attr, isouter=True
) # we use outer joins to not unintentionally filter out values
query = query.join(model_attr, isouter=True)
mapper: Mapper = inspect(current_model)
mapper = inspect(current_model)
relationship = mapper.relationships[attribute_link]
current_model = relationship.mapper.class_

View File

@@ -6,8 +6,10 @@ from .user import (
CreateToken,
DeleteTokenResponse,
GroupBase,
GroupHouseholdSummary,
GroupInDB,
GroupPagination,
GroupSummary,
LongLiveTokenIn,
LongLiveTokenInDB,
LongLiveTokenOut,
@@ -21,6 +23,7 @@ from .user import (
UserRatingOut,
UserRatings,
UserRatingSummary,
UserRatingUpdate,
UserSummary,
UserSummaryPagination,
)
@@ -34,25 +37,27 @@ from .user_passwords import (
)
__all__ = [
"CreateUserRegistration",
"CredentialsRequest",
"CredentialsRequestForm",
"OIDCRequest",
"Token",
"TokenData",
"UnlockResults",
"ForgotPassword",
"PasswordResetToken",
"PrivatePasswordResetToken",
"ResetPassword",
"SavePasswordResetToken",
"ValidateResetToken",
"CredentialsRequest",
"CredentialsRequestForm",
"OIDCRequest",
"Token",
"TokenData",
"UnlockResults",
"CreateUserRegistration",
"ChangePassword",
"CreateToken",
"DeleteTokenResponse",
"GroupBase",
"GroupHouseholdSummary",
"GroupInDB",
"GroupPagination",
"GroupSummary",
"LongLiveTokenIn",
"LongLiveTokenInDB",
"LongLiveTokenOut",
@@ -65,6 +70,7 @@ __all__ = [
"UserRatingCreate",
"UserRatingOut",
"UserRatingSummary",
"UserRatingUpdate",
"UserRatings",
"UserSummary",
"UserSummaryPagination",

View File

@@ -9,6 +9,7 @@ from mealie.schema._mealie.validators import validate_locale
class CreateUserRegistration(MealieModel):
group: str | None = None
household: str | None = None
group_token: Annotated[str | None, Field(validate_default=True)] = None
email: Annotated[str, StringConstraints(to_lower=True, strip_whitespace=True)]
username: Annotated[str, StringConstraints(to_lower=True, strip_whitespace=True)]

View File

@@ -9,10 +9,10 @@ from sqlalchemy.orm.interfaces import LoaderOption
from mealie.core.config import get_app_dirs, get_app_settings
from mealie.db.models.users import User
from mealie.db.models.users.users import AuthMethod
from mealie.db.models.users.users import AuthMethod, LongLiveToken
from mealie.schema._mealie import MealieModel
from mealie.schema.group.group_preferences import ReadGroupPreferences
from mealie.schema.group.webhook import CreateWebhook, ReadWebhook
from mealie.schema.household.webhook import CreateWebhook, ReadWebhook
from mealie.schema.response.pagination import PaginationBase
from ...db.models.group import Group
@@ -35,6 +35,10 @@ class LongLiveTokenOut(MealieModel):
created_at: datetime | None = None
model_config = ConfigDict(from_attributes=True)
@classmethod
def loader_options(cls) -> list[LoaderOption]:
return [joinedload(LongLiveToken.user)]
class CreateToken(LongLiveTokenIn):
user_id: UUID4
@@ -53,7 +57,7 @@ class ChangePassword(MealieModel):
class GroupBase(MealieModel):
name: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] # type: ignore
name: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
model_config = ConfigDict(from_attributes=True)
@@ -93,10 +97,11 @@ class UserBase(MealieModel):
id: UUID4 | None = None
username: str | None = None
full_name: str | None = None
email: Annotated[str, StringConstraints(to_lower=True, strip_whitespace=True)] # type: ignore
email: Annotated[str, StringConstraints(to_lower=True, strip_whitespace=True)]
auth_method: AuthMethod = AuthMethod.MEALIE
admin: bool = False
group: str | None = None
household: str | None = None
advanced: bool = False
can_invite: bool = False
@@ -110,6 +115,7 @@ class UserBase(MealieModel):
"fullName": "Change Me",
"email": "changeme@example.com",
"group": settings.DEFAULT_GROUP,
"household": settings.DEFAULT_HOUSEHOLD,
"admin": "false",
}
},
@@ -125,6 +131,16 @@ class UserBase(MealieModel):
except AttributeError:
return v
@field_validator("household", mode="before")
def convert_household_to_name(cls, v):
if not v or isinstance(v, str):
return v
try:
return v.name
except AttributeError:
return v
class UserIn(UserBase):
password: str
@@ -135,6 +151,9 @@ class UserOut(UserBase):
group: str
group_id: UUID4
group_slug: str
household: str
household_id: UUID4
household_slug: str
tokens: list[LongLiveTokenOut] | None = None
cache_key: str
model_config = ConfigDict(from_attributes=True)
@@ -145,7 +164,7 @@ class UserOut(UserBase):
@classmethod
def loader_options(cls) -> list[LoaderOption]:
return [joinedload(User.group), joinedload(User.tokens)]
return [joinedload(User.group), joinedload(User.household), joinedload(User.tokens)]
class UserSummary(MealieModel):
@@ -164,7 +183,6 @@ class UserSummaryPagination(PaginationBase):
class PrivateUser(UserOut):
password: str
group_id: UUID4
login_attemps: int = 0
locked_at: datetime | None = None
model_config = ConfigDict(from_attributes=True)
@@ -193,7 +211,7 @@ class PrivateUser(UserOut):
@classmethod
def loader_options(cls) -> list[LoaderOption]:
return [joinedload(User.group), joinedload(User.tokens)]
return [joinedload(User.group), joinedload(User.household), joinedload(User.tokens)]
class UpdateGroup(GroupBase):
@@ -205,7 +223,14 @@ class UpdateGroup(GroupBase):
webhooks: list[CreateWebhook] = []
class GroupHouseholdSummary(MealieModel):
id: UUID4
name: str
model_config = ConfigDict(from_attributes=True)
class GroupInDB(UpdateGroup):
households: list[GroupHouseholdSummary] | None = None
users: list[UserSummary] | None = None
preferences: ReadGroupPreferences | None = None
webhooks: list[ReadWebhook] = []
@@ -238,6 +263,7 @@ class GroupInDB(UpdateGroup):
joinedload(Group.categories),
joinedload(Group.webhooks),
joinedload(Group.preferences),
joinedload(Group.households),
selectinload(Group.users).joinedload(User.group),
selectinload(Group.users).joinedload(User.tokens),
]

View File

@@ -39,5 +39,6 @@ class PrivatePasswordResetToken(SavePasswordResetToken):
def loader_options(cls) -> list[LoaderOption]:
return [
selectinload(PasswordResetModel.user).joinedload(User.group),
selectinload(PasswordResetModel.user).joinedload(User.household),
selectinload(PasswordResetModel.user).joinedload(User.tokens),
]