mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-07 16:33:10 -05:00
252
mealie/schema/_mealie/datetime_parse.py
Normal file
252
mealie/schema/_mealie/datetime_parse.py
Normal file
@@ -0,0 +1,252 @@
|
||||
"""
|
||||
From Pydantic V1: https://github.com/pydantic/pydantic/blob/abcf81ec104d2da70894ac0402ae11a7186c5e47/pydantic/datetime_parse.py
|
||||
"""
|
||||
|
||||
import re
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
|
||||
date_expr = r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})"
|
||||
time_expr = (
|
||||
r"(?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
|
||||
r"(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?"
|
||||
r"(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$"
|
||||
)
|
||||
|
||||
date_re = re.compile(f"{date_expr}$")
|
||||
time_re = re.compile(time_expr)
|
||||
datetime_re = re.compile(f"{date_expr}[T ]{time_expr}")
|
||||
|
||||
standard_duration_re = re.compile(
|
||||
r"^"
|
||||
r"(?:(?P<days>-?\d+) (days?, )?)?"
|
||||
r"((?:(?P<hours>-?\d+):)(?=\d+:\d+))?"
|
||||
r"(?:(?P<minutes>-?\d+):)?"
|
||||
r"(?P<seconds>-?\d+)"
|
||||
r"(?:\.(?P<microseconds>\d{1,6})\d{0,6})?"
|
||||
r"$"
|
||||
)
|
||||
|
||||
# Support the sections of ISO 8601 date representation that are accepted by timedelta
|
||||
iso8601_duration_re = re.compile(
|
||||
r"^(?P<sign>[-+]?)"
|
||||
r"P"
|
||||
r"(?:(?P<days>\d+(.\d+)?)D)?"
|
||||
r"(?:T"
|
||||
r"(?:(?P<hours>\d+(.\d+)?)H)?"
|
||||
r"(?:(?P<minutes>\d+(.\d+)?)M)?"
|
||||
r"(?:(?P<seconds>\d+(.\d+)?)S)?"
|
||||
r")?"
|
||||
r"$"
|
||||
)
|
||||
|
||||
EPOCH = datetime(1970, 1, 1)
|
||||
# if greater than this, the number is in ms, if less than or equal it's in seconds
|
||||
# (in seconds this is 11th October 2603, in ms it's 20th August 1970)
|
||||
MS_WATERSHED = int(2e10)
|
||||
# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9
|
||||
MAX_NUMBER = int(3e20)
|
||||
|
||||
|
||||
class DateError(ValueError):
|
||||
def __init__(self, *args: object) -> None:
|
||||
super().__init__("invalid date format")
|
||||
|
||||
|
||||
class TimeError(ValueError):
|
||||
def __init__(self, *args: object) -> None:
|
||||
super().__init__("invalid time format")
|
||||
|
||||
|
||||
class DateTimeError(ValueError):
|
||||
def __init__(self, *args: object) -> None:
|
||||
super().__init__("invalid datetime format")
|
||||
|
||||
|
||||
class DurationError(ValueError):
|
||||
def __init__(self, *args: object) -> None:
|
||||
super().__init__("invalid duration format")
|
||||
|
||||
|
||||
def get_numeric(value: str | bytes | int | float, native_expected_type: str) -> None | int | float:
|
||||
if isinstance(value, int | float):
|
||||
return value
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return None
|
||||
except TypeError as e:
|
||||
raise TypeError(f"invalid type; expected {native_expected_type}, string, bytes, int or float") from e
|
||||
|
||||
|
||||
def from_unix_seconds(seconds: int | float) -> datetime:
|
||||
if seconds > MAX_NUMBER:
|
||||
return datetime.max
|
||||
elif seconds < -MAX_NUMBER:
|
||||
return datetime.min
|
||||
|
||||
while abs(seconds) > MS_WATERSHED:
|
||||
seconds /= 1000
|
||||
dt = EPOCH + timedelta(seconds=seconds)
|
||||
return dt.replace(tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def _parse_timezone(value: str | None, error: type[Exception]) -> None | int | timezone:
|
||||
if value == "Z":
|
||||
return timezone.utc
|
||||
elif value is not None:
|
||||
offset_mins = int(value[-2:]) if len(value) > 3 else 0
|
||||
offset = 60 * int(value[1:3]) + offset_mins
|
||||
if value[0] == "-":
|
||||
offset = -offset
|
||||
try:
|
||||
return timezone(timedelta(minutes=offset))
|
||||
except ValueError as e:
|
||||
raise error() from e
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def parse_date(value: date | str | bytes | int | float) -> date:
|
||||
"""
|
||||
Parse a date/int/float/string and return a datetime.date.
|
||||
|
||||
Raise ValueError if the input is well formatted but not a valid date.
|
||||
Raise ValueError if the input isn't well formatted.
|
||||
"""
|
||||
if isinstance(value, date):
|
||||
if isinstance(value, datetime):
|
||||
return value.date()
|
||||
else:
|
||||
return value
|
||||
|
||||
number = get_numeric(value, "date")
|
||||
if number is not None:
|
||||
return from_unix_seconds(number).date()
|
||||
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode()
|
||||
|
||||
match = date_re.match(value) # type: ignore
|
||||
if match is None:
|
||||
raise DateError()
|
||||
|
||||
kw = {k: int(v) for k, v in match.groupdict().items()}
|
||||
|
||||
try:
|
||||
return date(**kw)
|
||||
except ValueError as e:
|
||||
raise DateError() from e
|
||||
|
||||
|
||||
def parse_time(value: time | str | bytes | int | float) -> time:
|
||||
"""
|
||||
Parse a time/string and return a datetime.time.
|
||||
|
||||
Raise ValueError if the input is well formatted but not a valid time.
|
||||
Raise ValueError if the input isn't well formatted, in particular if it contains an offset.
|
||||
"""
|
||||
if isinstance(value, time):
|
||||
return value
|
||||
|
||||
number = get_numeric(value, "time")
|
||||
if number is not None:
|
||||
if number >= 86400:
|
||||
# doesn't make sense since the time time loop back around to 0
|
||||
raise TimeError()
|
||||
return (datetime.min + timedelta(seconds=number)).time()
|
||||
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode()
|
||||
|
||||
match = time_re.match(value) # type: ignore
|
||||
if match is None:
|
||||
raise TimeError()
|
||||
|
||||
kw = match.groupdict()
|
||||
if kw["microsecond"]:
|
||||
kw["microsecond"] = kw["microsecond"].ljust(6, "0")
|
||||
|
||||
tzinfo = _parse_timezone(kw.pop("tzinfo"), TimeError)
|
||||
kw_: dict[str, None | int | timezone] = {k: int(v) for k, v in kw.items() if v is not None}
|
||||
kw_["tzinfo"] = tzinfo
|
||||
|
||||
try:
|
||||
return time(**kw_) # type: ignore
|
||||
except ValueError as e:
|
||||
raise TimeError() from e
|
||||
|
||||
|
||||
def parse_datetime(value: datetime | str | bytes | int | float) -> datetime:
|
||||
"""
|
||||
Parse a datetime/int/float/string and return a datetime.datetime.
|
||||
|
||||
This function supports time zone offsets. When the input contains one,
|
||||
the output uses a timezone with a fixed offset from UTC.
|
||||
|
||||
Raise ValueError if the input is well formatted but not a valid datetime.
|
||||
Raise ValueError if the input isn't well formatted.
|
||||
"""
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
|
||||
number = get_numeric(value, "datetime")
|
||||
if number is not None:
|
||||
return from_unix_seconds(number)
|
||||
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode()
|
||||
|
||||
match = datetime_re.match(value) # type: ignore
|
||||
if match is None:
|
||||
raise DateTimeError()
|
||||
|
||||
kw = match.groupdict()
|
||||
if kw["microsecond"]:
|
||||
kw["microsecond"] = kw["microsecond"].ljust(6, "0")
|
||||
|
||||
tzinfo = _parse_timezone(kw.pop("tzinfo"), DateTimeError)
|
||||
kw_: dict[str, None | int | timezone] = {k: int(v) for k, v in kw.items() if v is not None}
|
||||
kw_["tzinfo"] = tzinfo
|
||||
|
||||
try:
|
||||
return datetime(**kw_) # type: ignore
|
||||
except ValueError as e:
|
||||
raise DateTimeError() from e
|
||||
|
||||
|
||||
def parse_duration(value: str | bytes | int | float) -> timedelta:
|
||||
"""
|
||||
Parse a duration int/float/string and return a datetime.timedelta.
|
||||
|
||||
The preferred format for durations in Django is '%d %H:%M:%S.%f'.
|
||||
|
||||
Also supports ISO 8601 representation.
|
||||
"""
|
||||
if isinstance(value, timedelta):
|
||||
return value
|
||||
|
||||
if isinstance(value, int | float):
|
||||
# below code requires a string
|
||||
value = f"{value:f}"
|
||||
elif isinstance(value, bytes):
|
||||
value = value.decode()
|
||||
|
||||
try:
|
||||
match = standard_duration_re.match(value) or iso8601_duration_re.match(value)
|
||||
except TypeError as e:
|
||||
raise TypeError("invalid type; expected timedelta, string, bytes, int or float") from e
|
||||
|
||||
if not match:
|
||||
raise DurationError()
|
||||
|
||||
kw = match.groupdict()
|
||||
sign = -1 if kw.pop("sign", "+") == "-" else 1
|
||||
if kw.get("microseconds"):
|
||||
kw["microseconds"] = kw["microseconds"].ljust(6, "0")
|
||||
|
||||
if kw.get("seconds") and kw.get("microseconds") and kw["seconds"].startswith("-"):
|
||||
kw["microseconds"] = "-" + kw["microseconds"]
|
||||
|
||||
kw_ = {k: float(v) for k, v in kw.items() if v is not None}
|
||||
|
||||
return sign * timedelta(**kw_)
|
||||
@@ -5,7 +5,7 @@ from enum import Enum
|
||||
from typing import ClassVar, Protocol, TypeVar
|
||||
|
||||
from humps.main import camelize
|
||||
from pydantic import UUID4, BaseModel
|
||||
from pydantic import UUID4, BaseModel, ConfigDict
|
||||
from sqlalchemy import Select, desc, func, or_, text
|
||||
from sqlalchemy.orm import InstrumentedAttribute, Session
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
@@ -28,10 +28,7 @@ class MealieModel(BaseModel):
|
||||
Searchable properties for the search API.
|
||||
The first property will be used for sorting (order_by)
|
||||
"""
|
||||
|
||||
class Config:
|
||||
alias_generator = camelize
|
||||
allow_population_by_field_name = True
|
||||
model_config = ConfigDict(alias_generator=camelize, populate_by_name=True)
|
||||
|
||||
def cast(self, cls: type[T], **kwargs) -> T:
|
||||
"""
|
||||
@@ -48,8 +45,8 @@ class MealieModel(BaseModel):
|
||||
for method chaining.
|
||||
"""
|
||||
|
||||
for field in self.__fields__:
|
||||
if field in dest.__fields__:
|
||||
for field in self.model_fields:
|
||||
if field in dest.model_fields:
|
||||
setattr(dest, field, getattr(self, field))
|
||||
|
||||
return dest
|
||||
@@ -59,8 +56,8 @@ class MealieModel(BaseModel):
|
||||
Map matching values from another model to the current model.
|
||||
"""
|
||||
|
||||
for field in src.__fields__:
|
||||
if field in self.__fields__:
|
||||
for field in src.model_fields:
|
||||
if field in self.model_fields:
|
||||
setattr(self, field, getattr(src, field))
|
||||
|
||||
def merge(self, src: T, replace_null=False):
|
||||
@@ -68,9 +65,9 @@ class MealieModel(BaseModel):
|
||||
Replace matching values from another instance to the current instance.
|
||||
"""
|
||||
|
||||
for field in src.__fields__:
|
||||
for field in src.model_fields:
|
||||
val = getattr(src, field)
|
||||
if field in self.__fields__ and (val is not None or replace_null):
|
||||
if field in self.model_fields and (val is not None or replace_null):
|
||||
setattr(self, field, val)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -49,7 +49,7 @@ class AdminAboutInfo(AppInfo):
|
||||
api_port: int
|
||||
api_docs: bool
|
||||
db_type: str
|
||||
db_url: str | None
|
||||
db_url: str | None = None
|
||||
default_group: str
|
||||
build_id: str
|
||||
recipe_scraper_version: str
|
||||
|
||||
@@ -19,9 +19,9 @@ class ImportJob(BackupOptions):
|
||||
|
||||
|
||||
class CreateBackup(BaseModel):
|
||||
tag: str | None
|
||||
tag: str | None = None
|
||||
options: BackupOptions
|
||||
templates: list[str] | None
|
||||
templates: list[str] | None = None
|
||||
|
||||
|
||||
class BackupFile(BaseModel):
|
||||
|
||||
@@ -4,11 +4,11 @@ from pydantic.main import BaseModel
|
||||
class ImportBase(BaseModel):
|
||||
name: str
|
||||
status: bool
|
||||
exception: str | None
|
||||
exception: str | None = None
|
||||
|
||||
|
||||
class RecipeImport(ImportBase):
|
||||
slug: str | None
|
||||
slug: str | None = None
|
||||
|
||||
|
||||
class CommentImport(ImportBase):
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from pydantic import validator
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import ConfigDict, Field, field_validator
|
||||
from slugify import slugify
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
@@ -8,14 +10,12 @@ from ..recipe.recipe_category import RecipeCategoryResponse
|
||||
|
||||
class CustomPageBase(MealieModel):
|
||||
name: str
|
||||
slug: str | None
|
||||
slug: Annotated[str | None, Field(validate_default=True)]
|
||||
position: int
|
||||
categories: list[RecipeCategoryResponse] = []
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@validator("slug", always=True, pre=True)
|
||||
@field_validator("slug", mode="before")
|
||||
def validate_slug(slug: str, values):
|
||||
name: str = values["name"]
|
||||
calc_slug: str = slugify(name)
|
||||
@@ -28,6 +28,4 @@ class CustomPageBase(MealieModel):
|
||||
|
||||
class CustomPageOut(CustomPageBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from pydantic import UUID4, validator
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import UUID4, ConfigDict, Field, field_validator
|
||||
from pydantic_core.core_schema import ValidationInfo
|
||||
from slugify import slugify
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
@@ -14,9 +17,9 @@ from ..recipe.recipe_category import CategoryBase, TagBase
|
||||
class CreateCookBook(MealieModel):
|
||||
name: str
|
||||
description: str = ""
|
||||
slug: str | None = None
|
||||
slug: Annotated[str | None, Field(validate_default=True)] = None
|
||||
position: int = 1
|
||||
public: bool = False
|
||||
public: Annotated[bool, Field(validate_default=True)] = False
|
||||
categories: list[CategoryBase] = []
|
||||
tags: list[TagBase] = []
|
||||
tools: list[RecipeTool] = []
|
||||
@@ -24,13 +27,13 @@ class CreateCookBook(MealieModel):
|
||||
require_all_tags: bool = True
|
||||
require_all_tools: bool = True
|
||||
|
||||
@validator("public", always=True, pre=True)
|
||||
def validate_public(public: bool | None, values: dict) -> bool: # type: ignore
|
||||
@field_validator("public", mode="before")
|
||||
def validate_public(public: bool | None) -> bool:
|
||||
return False if public is None else public
|
||||
|
||||
@validator("slug", always=True, pre=True)
|
||||
def validate_slug(slug: str, values): # type: ignore
|
||||
name: str = values["name"]
|
||||
@field_validator("slug", mode="before")
|
||||
def validate_slug(slug: str, info: ValidationInfo):
|
||||
name: str = info.data["name"]
|
||||
calc_slug: str = slugify(name)
|
||||
|
||||
if slug != calc_slug:
|
||||
@@ -50,9 +53,7 @@ class UpdateCookBook(SaveCookBook):
|
||||
class ReadCookBook(UpdateCookBook):
|
||||
group_id: UUID4
|
||||
categories: list[CategoryBase] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
@@ -66,6 +67,4 @@ class CookBookPagination(PaginationBase):
|
||||
class RecipeCookBook(ReadCookBook):
|
||||
group_id: UUID4
|
||||
recipes: list[RecipeSummary]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
from collections.abc import Callable, Mapping
|
||||
from typing import Any
|
||||
|
||||
from pydantic.utils import GetterDict
|
||||
|
||||
|
||||
class CustomGetterDict(GetterDict):
|
||||
transformations: Mapping[str, Callable[[Any], Any]]
|
||||
|
||||
def get(self, key: Any, default: Any = None) -> Any:
|
||||
# Transform extras into key-value dict
|
||||
if key in self.transformations:
|
||||
value = super().get(key, default)
|
||||
return self.transformations[key](value)
|
||||
|
||||
# Keep all other fields as they are
|
||||
else:
|
||||
return super().get(key, default)
|
||||
|
||||
|
||||
class ExtrasGetterDict(CustomGetterDict):
|
||||
transformations = {"extras": lambda value: {x.key_name: x.value for x in value}}
|
||||
|
||||
|
||||
class GroupGetterDict(CustomGetterDict):
|
||||
transformations = {"group": lambda value: value.name}
|
||||
|
||||
|
||||
class UserGetterDict(CustomGetterDict):
|
||||
transformations = {
|
||||
"group": lambda value: value.name,
|
||||
"favorite_recipes": lambda value: [x.slug for x in value],
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
from pydantic import UUID4, NoneStr
|
||||
from pydantic import UUID4, ConfigDict
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
|
||||
@@ -54,9 +54,7 @@ class GroupEventNotifierOptionsSave(GroupEventNotifierOptions):
|
||||
|
||||
class GroupEventNotifierOptionsOut(GroupEventNotifierOptions):
|
||||
id: UUID4
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# =======================================================================
|
||||
@@ -65,7 +63,7 @@ class GroupEventNotifierOptionsOut(GroupEventNotifierOptions):
|
||||
|
||||
class GroupEventNotifierCreate(MealieModel):
|
||||
name: str
|
||||
apprise_url: str
|
||||
apprise_url: str | None = None
|
||||
|
||||
|
||||
class GroupEventNotifierSave(GroupEventNotifierCreate):
|
||||
@@ -76,7 +74,7 @@ class GroupEventNotifierSave(GroupEventNotifierCreate):
|
||||
|
||||
class GroupEventNotifierUpdate(GroupEventNotifierSave):
|
||||
id: UUID4
|
||||
apprise_url: NoneStr = None
|
||||
apprise_url: str | None = None
|
||||
|
||||
|
||||
class GroupEventNotifierOut(MealieModel):
|
||||
@@ -85,9 +83,7 @@ class GroupEventNotifierOut(MealieModel):
|
||||
enabled: bool
|
||||
group_id: UUID4
|
||||
options: GroupEventNotifierOptionsOut
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
@@ -100,6 +96,4 @@ class GroupEventPagination(PaginationBase):
|
||||
|
||||
class GroupEventNotifierPrivate(GroupEventNotifierOut):
|
||||
apprise_url: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import UUID4
|
||||
from pydantic import UUID4, ConfigDict
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
|
||||
@@ -13,6 +13,4 @@ class GroupDataExport(MealieModel):
|
||||
path: str
|
||||
size: str
|
||||
expires: datetime
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import UUID4
|
||||
from pydantic import UUID4, ConfigDict
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
|
||||
@@ -24,6 +24,4 @@ class CreateGroupPreferences(UpdateGroupPreferences):
|
||||
|
||||
class ReadGroupPreferences(CreateGroupPreferences):
|
||||
id: UUID4
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pydantic import validator
|
||||
from pydantic import field_validator
|
||||
|
||||
from mealie.schema._mealie.mealie_model import MealieModel
|
||||
from mealie.schema._mealie.validators import validate_locale
|
||||
@@ -7,8 +7,8 @@ from mealie.schema._mealie.validators import validate_locale
|
||||
class SeederConfig(MealieModel):
|
||||
locale: str
|
||||
|
||||
@validator("locale")
|
||||
def valid_locale(cls, v, values, **kwargs):
|
||||
@field_validator("locale")
|
||||
def valid_locale(cls, v):
|
||||
if not validate_locale(v):
|
||||
raise ValueError("invalid locale")
|
||||
return v
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import UUID4, validator
|
||||
from pydantic import UUID4, ConfigDict, field_validator
|
||||
from sqlalchemy.orm import joinedload, selectinload
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
|
||||
@@ -15,7 +15,6 @@ from mealie.db.models.group import (
|
||||
from mealie.db.models.recipe import IngredientFoodModel, RecipeModel
|
||||
from mealie.schema._mealie import MealieModel
|
||||
from mealie.schema._mealie.types import NoneFloat
|
||||
from mealie.schema.getter_dict import ExtrasGetterDict
|
||||
from mealie.schema.labels.multi_purpose_label import MultiPurposeLabelSummary
|
||||
from mealie.schema.recipe.recipe import RecipeSummary
|
||||
from mealie.schema.recipe.recipe_ingredient import (
|
||||
@@ -38,7 +37,8 @@ class ShoppingListItemRecipeRefCreate(MealieModel):
|
||||
recipe_note: str | None = None
|
||||
"""the original note from the recipe"""
|
||||
|
||||
@validator("recipe_quantity", pre=True)
|
||||
@field_validator("recipe_quantity", mode="before")
|
||||
@classmethod
|
||||
def default_none_to_zero(cls, v):
|
||||
return 0 if v is None else v
|
||||
|
||||
@@ -49,8 +49,7 @@ class ShoppingListItemRecipeRefUpdate(ShoppingListItemRecipeRefCreate):
|
||||
|
||||
|
||||
class ShoppingListItemRecipeRefOut(ShoppingListItemRecipeRefUpdate):
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ShoppingListItemBase(RecipeIngredientBase):
|
||||
@@ -67,6 +66,13 @@ class ShoppingListItemBase(RecipeIngredientBase):
|
||||
is_food: bool = False
|
||||
extras: dict | None = {}
|
||||
|
||||
@field_validator("extras", mode="before")
|
||||
def convert_extras_to_dict(cls, v):
|
||||
if isinstance(v, dict):
|
||||
return v
|
||||
|
||||
return {x.key_name: x.value for x in v} if v else {}
|
||||
|
||||
|
||||
class ShoppingListItemCreate(ShoppingListItemBase):
|
||||
recipe_references: list[ShoppingListItemRecipeRefCreate] = []
|
||||
@@ -85,14 +91,14 @@ class ShoppingListItemUpdateBulk(ShoppingListItemUpdate):
|
||||
class ShoppingListItemOut(ShoppingListItemBase):
|
||||
id: UUID4
|
||||
|
||||
food: IngredientFood | None
|
||||
label: MultiPurposeLabelSummary | None
|
||||
unit: IngredientUnit | None
|
||||
food: IngredientFood | None = None
|
||||
label: MultiPurposeLabelSummary | None = None
|
||||
unit: IngredientUnit | None = None
|
||||
|
||||
recipe_references: list[ShoppingListItemRecipeRefOut] = []
|
||||
|
||||
created_at: datetime | None
|
||||
update_at: datetime | None
|
||||
created_at: datetime | None = None
|
||||
update_at: datetime | None = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
@@ -102,9 +108,7 @@ class ShoppingListItemOut(ShoppingListItemBase):
|
||||
self.label = self.food.label
|
||||
self.label_id = self.label.id
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
getter_dict = ExtrasGetterDict
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
@@ -138,9 +142,7 @@ class ShoppingListMultiPurposeLabelUpdate(ShoppingListMultiPurposeLabelCreate):
|
||||
|
||||
class ShoppingListMultiPurposeLabelOut(ShoppingListMultiPurposeLabelUpdate):
|
||||
label: MultiPurposeLabelSummary
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
@@ -155,8 +157,15 @@ class ShoppingListCreate(MealieModel):
|
||||
name: str | None = None
|
||||
extras: dict | None = {}
|
||||
|
||||
created_at: datetime | None
|
||||
update_at: datetime | None
|
||||
created_at: datetime | None = None
|
||||
update_at: datetime | None = None
|
||||
|
||||
@field_validator("extras", mode="before")
|
||||
def convert_extras_to_dict(cls, v):
|
||||
if isinstance(v, dict):
|
||||
return v
|
||||
|
||||
return {x.key_name: x.value for x in v} if v else {}
|
||||
|
||||
|
||||
class ShoppingListRecipeRefOut(MealieModel):
|
||||
@@ -167,9 +176,7 @@ class ShoppingListRecipeRefOut(MealieModel):
|
||||
"""the number of times this recipe has been added"""
|
||||
|
||||
recipe: RecipeSummary
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
@@ -188,10 +195,7 @@ class ShoppingListSummary(ShoppingListSave):
|
||||
id: UUID4
|
||||
recipe_references: list[ShoppingListRecipeRefOut]
|
||||
label_settings: list[ShoppingListMultiPurposeLabelOut]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
getter_dict = ExtrasGetterDict
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
@@ -222,10 +226,7 @@ class ShoppingListUpdate(ShoppingListSave):
|
||||
class ShoppingListOut(ShoppingListUpdate):
|
||||
recipe_references: list[ShoppingListRecipeRefOut]
|
||||
label_settings: list[ShoppingListMultiPurposeLabelOut]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
getter_dict = ExtrasGetterDict
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import NoneStr
|
||||
from pydantic import ConfigDict
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
|
||||
@@ -19,9 +19,7 @@ class ReadInviteToken(MealieModel):
|
||||
token: str
|
||||
uses_left: int
|
||||
group_id: UUID
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class EmailInvitation(MealieModel):
|
||||
@@ -31,4 +29,4 @@ class EmailInvitation(MealieModel):
|
||||
|
||||
class EmailInitationResponse(MealieModel):
|
||||
success: bool
|
||||
error: NoneStr = None
|
||||
error: str | None = None
|
||||
|
||||
@@ -3,10 +3,10 @@ import enum
|
||||
from uuid import UUID
|
||||
|
||||
from isodate import parse_time
|
||||
from pydantic import UUID4, validator
|
||||
from pydantic.datetime_parse import parse_datetime
|
||||
from pydantic import UUID4, ConfigDict, field_validator
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
from mealie.schema._mealie.datetime_parse import parse_datetime
|
||||
from mealie.schema.response.pagination import PaginationBase
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class CreateWebhook(MealieModel):
|
||||
webhook_type: WebhookType = WebhookType.mealplan
|
||||
scheduled_time: datetime.time
|
||||
|
||||
@validator("scheduled_time", pre=True)
|
||||
@field_validator("scheduled_time", mode="before")
|
||||
@classmethod
|
||||
def validate_scheduled_time(cls, v):
|
||||
"""
|
||||
@@ -55,9 +55,7 @@ class SaveWebhook(CreateWebhook):
|
||||
|
||||
class ReadWebhook(SaveWebhook):
|
||||
id: UUID4
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class WebhookPagination(PaginationBase):
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import ClassVar
|
||||
|
||||
from pydantic import UUID4
|
||||
from pydantic import UUID4, ConfigDict
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
from mealie.schema.response.pagination import PaginationBase
|
||||
@@ -23,9 +23,7 @@ class MultiPurposeLabelUpdate(MultiPurposeLabelSave):
|
||||
|
||||
class MultiPurposeLabelSummary(MultiPurposeLabelUpdate):
|
||||
_searchable_properties: ClassVar[list[str]] = ["name"]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class MultiPurposeLabelPagination(PaginationBase):
|
||||
@@ -33,5 +31,4 @@ class MultiPurposeLabelPagination(PaginationBase):
|
||||
|
||||
|
||||
class MultiPurposeLabelOut(MultiPurposeLabelUpdate):
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -4,6 +4,10 @@ from fastapi.exceptions import HTTPException, RequestValidationError
|
||||
from pydantic import ValidationError
|
||||
|
||||
|
||||
def format_exception(ex: Exception) -> str:
|
||||
return f"{ex.__class__.__name__}: {ex}"
|
||||
|
||||
|
||||
def make_dependable(cls):
|
||||
"""
|
||||
Pydantic BaseModels are very powerful because we get lots of validations and type checking right out of the box.
|
||||
@@ -29,7 +33,7 @@ def make_dependable(cls):
|
||||
except (ValidationError, RequestValidationError) as e:
|
||||
for error in e.errors():
|
||||
error["loc"] = ["query"] + list(error["loc"])
|
||||
raise HTTPException(422, detail=e.errors()) from None
|
||||
raise HTTPException(422, detail=[format_exception(ex) for ex in e.errors()]) from None
|
||||
|
||||
init_cls_and_handle_errors.__signature__ = signature(cls)
|
||||
return init_cls_and_handle_errors
|
||||
|
||||
@@ -11,14 +11,14 @@ def mapper(source: U, dest: T, **_) -> T:
|
||||
Map a source model to a destination model. Only top-level fields are mapped.
|
||||
"""
|
||||
|
||||
for field in source.__fields__:
|
||||
if field in dest.__fields__:
|
||||
for field in source.model_fields:
|
||||
if field in dest.model_fields:
|
||||
setattr(dest, field, getattr(source, field))
|
||||
|
||||
return dest
|
||||
|
||||
|
||||
def cast(source: U, dest: type[T], **kwargs) -> T:
|
||||
create_data = {field: getattr(source, field) for field in source.__fields__ if field in dest.__fields__}
|
||||
create_data = {field: getattr(source, field) for field in source.model_fields if field in dest.model_fields}
|
||||
create_data.update(kwargs or {})
|
||||
return dest(**create_data)
|
||||
|
||||
@@ -1,53 +1,45 @@
|
||||
from datetime import date
|
||||
import datetime
|
||||
|
||||
from pydantic import validator
|
||||
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
|
||||
name: str | None
|
||||
description: str | None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
slug: str | None = None
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class MealDayIn(MealieModel):
|
||||
date: date | None
|
||||
date: datetime.date | None = None
|
||||
meals: list[MealIn]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class MealDayOut(MealDayIn):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class MealPlanIn(MealieModel):
|
||||
group: str
|
||||
start_date: date
|
||||
end_date: date
|
||||
start_date: datetime.date
|
||||
end_date: datetime.date
|
||||
plan_days: list[MealDayIn]
|
||||
|
||||
@validator("end_date")
|
||||
def end_date_after_start_date(v, values, config, field):
|
||||
if "start_date" in values and v < values["start_date"]:
|
||||
@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
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class MealPlanOut(MealPlanIn):
|
||||
id: int
|
||||
shopping_list: int | None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
shopping_list: int | None = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from datetime import date
|
||||
from enum import Enum
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import validator
|
||||
from pydantic import ConfigDict, Field, field_validator
|
||||
from pydantic_core.core_schema import ValidationInfo
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
|
||||
@@ -30,13 +32,13 @@ class CreatePlanEntry(MealieModel):
|
||||
entry_type: PlanEntryType = PlanEntryType.breakfast
|
||||
title: str = ""
|
||||
text: str = ""
|
||||
recipe_id: UUID | None
|
||||
recipe_id: Annotated[UUID | None, Field(validate_default=True)] = None
|
||||
|
||||
@validator("recipe_id", always=True)
|
||||
@field_validator("recipe_id")
|
||||
@classmethod
|
||||
def id_or_title(cls, value, values):
|
||||
if bool(value) is False and bool(values["title"]) is False:
|
||||
raise ValueError(f"`recipe_id={value}` or `title={values['title']}` must be provided")
|
||||
def id_or_title(cls, value, info: ValidationInfo):
|
||||
if bool(value) is False and bool(info.data["title"]) is False:
|
||||
raise ValueError(f"`recipe_id={value}` or `title={info.data['title']}` must be provided")
|
||||
|
||||
return value
|
||||
|
||||
@@ -44,22 +46,18 @@ class CreatePlanEntry(MealieModel):
|
||||
class UpdatePlanEntry(CreatePlanEntry):
|
||||
id: int
|
||||
group_id: UUID
|
||||
user_id: UUID | None
|
||||
user_id: UUID | None = None
|
||||
|
||||
|
||||
class SavePlanEntry(CreatePlanEntry):
|
||||
group_id: UUID
|
||||
user_id: UUID | None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
user_id: UUID | None = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ReadPlanEntry(UpdatePlanEntry):
|
||||
recipe: RecipeSummary | None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
recipe: RecipeSummary | None = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import datetime
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import UUID4
|
||||
from pydantic import UUID4, ConfigDict
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
|
||||
@@ -14,14 +14,11 @@ class Category(MealieModel):
|
||||
id: UUID4
|
||||
name: str
|
||||
slug: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class Tag(Category):
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class PlanRulesDay(str, Enum):
|
||||
@@ -64,9 +61,7 @@ class PlanRulesSave(PlanRulesCreate):
|
||||
|
||||
class PlanRulesOut(PlanRulesSave):
|
||||
id: UUID4
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
from pydantic import ConfigDict
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
from mealie.schema.getter_dict import GroupGetterDict
|
||||
|
||||
|
||||
class ListItem(MealieModel):
|
||||
title: str | None
|
||||
title: str | None = None
|
||||
text: str = ""
|
||||
quantity: int = 1
|
||||
checked: bool = False
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ShoppingListIn(MealieModel):
|
||||
name: str
|
||||
group: str | None
|
||||
group: str | None = None
|
||||
items: list[ListItem]
|
||||
|
||||
|
||||
class ShoppingListOut(ShoppingListIn):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
getter_dict = GroupGetterDict
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from numbers import Number
|
||||
from pathlib import Path
|
||||
from typing import Any, ClassVar
|
||||
from typing import Annotated, Any, ClassVar
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import UUID4, BaseModel, Field, validator
|
||||
from pydantic import UUID4, BaseModel, ConfigDict, Field, field_validator
|
||||
from pydantic_core.core_schema import ValidationInfo
|
||||
from slugify import slugify
|
||||
from sqlalchemy import Select, desc, func, or_, select, text
|
||||
from sqlalchemy.orm import Session, joinedload, selectinload
|
||||
@@ -22,7 +24,6 @@ from ...db.models.recipe import (
|
||||
RecipeInstruction,
|
||||
RecipeModel,
|
||||
)
|
||||
from ..getter_dict import ExtrasGetterDict
|
||||
from .recipe_asset import RecipeAsset
|
||||
from .recipe_comments import RecipeCommentOut
|
||||
from .recipe_notes import RecipeNote
|
||||
@@ -39,9 +40,7 @@ class RecipeTag(MealieModel):
|
||||
slug: str
|
||||
|
||||
_searchable_properties: ClassVar[list[str]] = ["name"]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class RecipeTagPagination(PaginationBase):
|
||||
@@ -80,16 +79,16 @@ class CreateRecipe(MealieModel):
|
||||
|
||||
|
||||
class RecipeSummary(MealieModel):
|
||||
id: UUID4 | None
|
||||
id: UUID4 | None = None
|
||||
_normalize_search: ClassVar[bool] = True
|
||||
|
||||
user_id: UUID4 = Field(default_factory=uuid4)
|
||||
group_id: UUID4 = Field(default_factory=uuid4)
|
||||
user_id: UUID4 = Field(default_factory=uuid4, validate_default=True)
|
||||
group_id: UUID4 = Field(default_factory=uuid4, validate_default=True)
|
||||
|
||||
name: str | None
|
||||
slug: str = ""
|
||||
image: Any | None
|
||||
recipe_yield: str | None
|
||||
name: str | None = None
|
||||
slug: Annotated[str, Field(validate_default=True)] = ""
|
||||
image: Any | None = None
|
||||
recipe_yield: str | None = None
|
||||
|
||||
total_time: str | None = None
|
||||
prep_time: str | None = None
|
||||
@@ -97,21 +96,28 @@ class RecipeSummary(MealieModel):
|
||||
perform_time: str | None = None
|
||||
|
||||
description: str | None = ""
|
||||
recipe_category: list[RecipeCategory] | None = []
|
||||
tags: list[RecipeTag] | None = []
|
||||
recipe_category: Annotated[list[RecipeCategory] | None, Field(validate_default=True)] | None = []
|
||||
tags: Annotated[list[RecipeTag] | None, Field(validate_default=True)] = []
|
||||
tools: list[RecipeTool] = []
|
||||
rating: int | None
|
||||
rating: int | None = None
|
||||
org_url: str | None = Field(None, alias="orgURL")
|
||||
|
||||
date_added: datetime.date | None
|
||||
date_updated: datetime.datetime | None
|
||||
date_added: datetime.date | None = None
|
||||
date_updated: datetime.datetime | None = None
|
||||
|
||||
created_at: datetime.datetime | None
|
||||
update_at: datetime.datetime | None
|
||||
last_made: datetime.datetime | None
|
||||
created_at: datetime.datetime | None = None
|
||||
update_at: datetime.datetime | None = None
|
||||
last_made: datetime.datetime | None = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
@field_validator("recipe_yield", "total_time", "prep_time", "cook_time", "perform_time", mode="before")
|
||||
def clean_strings(val: Any):
|
||||
if val is None:
|
||||
return val
|
||||
if isinstance(val, Number):
|
||||
return str(val)
|
||||
|
||||
return val
|
||||
|
||||
|
||||
class RecipePagination(PaginationBase):
|
||||
@@ -119,9 +125,9 @@ class RecipePagination(PaginationBase):
|
||||
|
||||
|
||||
class Recipe(RecipeSummary):
|
||||
recipe_ingredient: list[RecipeIngredient] = []
|
||||
recipe_ingredient: Annotated[list[RecipeIngredient], Field(validate_default=True)] = []
|
||||
recipe_instructions: list[RecipeStep] | None = []
|
||||
nutrition: Nutrition | None
|
||||
nutrition: Nutrition | None = None
|
||||
|
||||
# Mealie Specific
|
||||
settings: RecipeSettings | None = None
|
||||
@@ -175,13 +181,11 @@ class Recipe(RecipeSummary):
|
||||
|
||||
return self.image_dir_from_id(self.id)
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
getter_dict = ExtrasGetterDict
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def from_orm(cls, obj):
|
||||
recipe = super().from_orm(obj)
|
||||
def model_validate(cls, obj):
|
||||
recipe = super().model_validate(obj)
|
||||
recipe.__post_init__()
|
||||
return recipe
|
||||
|
||||
@@ -198,15 +202,15 @@ class Recipe(RecipeSummary):
|
||||
ingredient.is_food = not ingredient.disable_amount
|
||||
ingredient.display = ingredient._format_display()
|
||||
|
||||
@validator("slug", always=True, pre=True, allow_reuse=True)
|
||||
def validate_slug(slug: str, values): # type: ignore
|
||||
if not values.get("name"):
|
||||
@field_validator("slug", mode="before")
|
||||
def validate_slug(slug: str, info: ValidationInfo):
|
||||
if not info.data.get("name"):
|
||||
return slug
|
||||
|
||||
return slugify(values["name"])
|
||||
return slugify(info.data["name"])
|
||||
|
||||
@validator("recipe_ingredient", always=True, pre=True, allow_reuse=True)
|
||||
def validate_ingredients(recipe_ingredient, values):
|
||||
@field_validator("recipe_ingredient", mode="before")
|
||||
def validate_ingredients(recipe_ingredient):
|
||||
if not recipe_ingredient or not isinstance(recipe_ingredient, list):
|
||||
return recipe_ingredient
|
||||
|
||||
@@ -215,30 +219,37 @@ class Recipe(RecipeSummary):
|
||||
|
||||
return recipe_ingredient
|
||||
|
||||
@validator("tags", always=True, pre=True, allow_reuse=True)
|
||||
def validate_tags(cats: list[Any]): # type: ignore
|
||||
@field_validator("tags", mode="before")
|
||||
def validate_tags(cats: list[Any]):
|
||||
if isinstance(cats, list) and cats and isinstance(cats[0], str):
|
||||
return [RecipeTag(id=uuid4(), name=c, slug=slugify(c)) for c in cats]
|
||||
return cats
|
||||
|
||||
@validator("recipe_category", always=True, pre=True, allow_reuse=True)
|
||||
def validate_categories(cats: list[Any]): # type: ignore
|
||||
@field_validator("recipe_category", mode="before")
|
||||
def validate_categories(cats: list[Any]):
|
||||
if isinstance(cats, list) and cats and isinstance(cats[0], str):
|
||||
return [RecipeCategory(id=uuid4(), name=c, slug=slugify(c)) for c in cats]
|
||||
return cats
|
||||
|
||||
@validator("group_id", always=True, pre=True, allow_reuse=True)
|
||||
@field_validator("group_id", mode="before")
|
||||
def validate_group_id(group_id: Any):
|
||||
if isinstance(group_id, int):
|
||||
return uuid4()
|
||||
return group_id
|
||||
|
||||
@validator("user_id", always=True, pre=True, allow_reuse=True)
|
||||
@field_validator("user_id", mode="before")
|
||||
def validate_user_id(user_id: Any):
|
||||
if isinstance(user_id, int):
|
||||
return uuid4()
|
||||
return user_id
|
||||
|
||||
@field_validator("extras", mode="before")
|
||||
def convert_extras_to_dict(cls, v):
|
||||
if isinstance(v, dict):
|
||||
return v
|
||||
|
||||
return {x.key_name: x.value for x in v} if v else {}
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
return [
|
||||
@@ -332,5 +343,5 @@ class RecipeLastMade(BaseModel):
|
||||
|
||||
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient # noqa: E402
|
||||
|
||||
RecipeSummary.update_forward_refs()
|
||||
Recipe.update_forward_refs()
|
||||
RecipeSummary.model_rebuild()
|
||||
Recipe.model_rebuild()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from pydantic import ConfigDict
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
|
||||
|
||||
class RecipeAsset(MealieModel):
|
||||
name: str
|
||||
icon: str
|
||||
file_name: str | None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
file_name: str | None = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pydantic import UUID4
|
||||
from pydantic import UUID4, ConfigDict
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
|
||||
@@ -17,24 +17,18 @@ class CategorySave(CategoryIn):
|
||||
class CategoryBase(CategoryIn):
|
||||
id: UUID4
|
||||
slug: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class CategoryOut(CategoryBase):
|
||||
slug: str
|
||||
group_id: UUID4
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class RecipeCategoryResponse(CategoryBase):
|
||||
recipes: "list[RecipeSummary]" = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class TagIn(CategoryIn):
|
||||
@@ -52,9 +46,7 @@ class TagBase(CategoryBase):
|
||||
class TagOut(TagSave):
|
||||
id: UUID4
|
||||
slug: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class RecipeTagResponse(RecipeCategoryResponse):
|
||||
@@ -69,5 +61,5 @@ class RecipeTagResponse(RecipeCategoryResponse):
|
||||
|
||||
from mealie.schema.recipe.recipe import RecipeSummary # noqa: E402
|
||||
|
||||
RecipeCategoryResponse.update_forward_refs()
|
||||
RecipeTagResponse.update_forward_refs()
|
||||
RecipeCategoryResponse.model_rebuild()
|
||||
RecipeTagResponse.model_rebuild()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import UUID4
|
||||
from pydantic import UUID4, ConfigDict
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
|
||||
@@ -11,11 +11,9 @@ from mealie.schema.response.pagination import PaginationBase
|
||||
|
||||
class UserBase(MealieModel):
|
||||
id: UUID4
|
||||
username: str | None
|
||||
username: str | None = None
|
||||
admin: bool
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class RecipeCommentCreate(MealieModel):
|
||||
@@ -39,9 +37,7 @@ class RecipeCommentOut(RecipeCommentCreate):
|
||||
update_at: datetime
|
||||
user_id: UUID4
|
||||
user: UserBase
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
|
||||
@@ -6,14 +6,13 @@ from fractions import Fraction
|
||||
from typing import ClassVar
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from pydantic import UUID4, Field, validator
|
||||
from pydantic import UUID4, ConfigDict, Field, field_validator
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
|
||||
from mealie.db.models.recipe import IngredientFoodModel
|
||||
from mealie.schema._mealie import MealieModel
|
||||
from mealie.schema._mealie.types import NoneFloat
|
||||
from mealie.schema.getter_dict import ExtrasGetterDict
|
||||
from mealie.schema.response.pagination import PaginationBase
|
||||
|
||||
INGREDIENT_QTY_PRECISION = 3
|
||||
@@ -37,14 +36,20 @@ class UnitFoodBase(MealieModel):
|
||||
description: str = ""
|
||||
extras: dict | None = {}
|
||||
|
||||
@field_validator("extras", mode="before")
|
||||
def convert_extras_to_dict(cls, v):
|
||||
if isinstance(v, dict):
|
||||
return v
|
||||
|
||||
return {x.key_name: x.value for x in v} if v else {}
|
||||
|
||||
|
||||
class CreateIngredientFoodAlias(MealieModel):
|
||||
name: str
|
||||
|
||||
|
||||
class IngredientFoodAlias(CreateIngredientFoodAlias):
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class CreateIngredientFood(UnitFoodBase):
|
||||
@@ -61,15 +66,12 @@ class IngredientFood(CreateIngredientFood):
|
||||
label: MultiPurposeLabelSummary | None = None
|
||||
aliases: list[IngredientFoodAlias] = []
|
||||
|
||||
created_at: datetime.datetime | None
|
||||
update_at: datetime.datetime | None
|
||||
created_at: datetime.datetime | None = None
|
||||
update_at: datetime.datetime | None = None
|
||||
|
||||
_searchable_properties: ClassVar[list[str]] = ["name_normalized", "plural_name_normalized"]
|
||||
_normalize_search: ClassVar[bool] = True
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
getter_dict = ExtrasGetterDict
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
@@ -85,8 +87,7 @@ class CreateIngredientUnitAlias(MealieModel):
|
||||
|
||||
|
||||
class IngredientUnitAlias(CreateIngredientUnitAlias):
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class CreateIngredientUnit(UnitFoodBase):
|
||||
@@ -105,8 +106,8 @@ class IngredientUnit(CreateIngredientUnit):
|
||||
id: UUID4
|
||||
aliases: list[IngredientUnitAlias] = []
|
||||
|
||||
created_at: datetime.datetime | None
|
||||
update_at: datetime.datetime | None
|
||||
created_at: datetime.datetime | None = None
|
||||
update_at: datetime.datetime | None = None
|
||||
|
||||
_searchable_properties: ClassVar[list[str]] = [
|
||||
"name_normalized",
|
||||
@@ -115,15 +116,13 @@ class IngredientUnit(CreateIngredientUnit):
|
||||
"plural_abbreviation_normalized",
|
||||
]
|
||||
_normalize_search: ClassVar[bool] = True
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class RecipeIngredientBase(MealieModel):
|
||||
quantity: NoneFloat = 1
|
||||
unit: IngredientUnit | CreateIngredientUnit | None
|
||||
food: IngredientFood | CreateIngredientFood | None
|
||||
unit: IngredientUnit | CreateIngredientUnit | None = None
|
||||
food: IngredientFood | CreateIngredientFood | None = None
|
||||
note: str | None = ""
|
||||
|
||||
is_food: bool | None = None
|
||||
@@ -152,14 +151,16 @@ class RecipeIngredientBase(MealieModel):
|
||||
if not self.display:
|
||||
self.display = self._format_display()
|
||||
|
||||
@validator("unit", pre=True)
|
||||
@field_validator("unit", mode="before")
|
||||
@classmethod
|
||||
def validate_unit(cls, v):
|
||||
if isinstance(v, str):
|
||||
return CreateIngredientUnit(name=v)
|
||||
else:
|
||||
return v
|
||||
|
||||
@validator("food", pre=True)
|
||||
@field_validator("food", mode="before")
|
||||
@classmethod
|
||||
def validate_food(cls, v):
|
||||
if isinstance(v, str):
|
||||
return CreateIngredientFood(name=v)
|
||||
@@ -260,19 +261,18 @@ class IngredientUnitPagination(PaginationBase):
|
||||
|
||||
|
||||
class RecipeIngredient(RecipeIngredientBase):
|
||||
title: str | None
|
||||
original_text: str | None
|
||||
title: str | None = None
|
||||
original_text: str | None = None
|
||||
disable_amount: bool = True
|
||||
|
||||
# Ref is used as a way to distinguish between an individual ingredient on the frontend
|
||||
# It is required for the reorder and section titles to function properly because of how
|
||||
# Vue handles reactivity. ref may serve another purpose in the future.
|
||||
reference_id: UUID = Field(default_factory=uuid4)
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@validator("quantity", pre=True)
|
||||
@field_validator("quantity", mode="before")
|
||||
@classmethod
|
||||
def validate_quantity(cls, value) -> NoneFloat:
|
||||
"""
|
||||
Sometimes the frontend UI will provide an empty string as a "null" value because of the default
|
||||
@@ -294,7 +294,7 @@ class IngredientConfidence(MealieModel):
|
||||
quantity: NoneFloat = None
|
||||
food: NoneFloat = None
|
||||
|
||||
@validator("quantity", pre=True)
|
||||
@field_validator("quantity", mode="before")
|
||||
@classmethod
|
||||
def validate_quantity(cls, value, values) -> NoneFloat:
|
||||
if isinstance(value, float):
|
||||
@@ -305,7 +305,7 @@ class IngredientConfidence(MealieModel):
|
||||
|
||||
|
||||
class ParsedIngredient(MealieModel):
|
||||
input: str | None
|
||||
input: str | None = None
|
||||
confidence: IngredientConfidence = IngredientConfidence()
|
||||
ingredient: RecipeIngredient
|
||||
|
||||
@@ -337,4 +337,4 @@ class MergeUnit(MealieModel):
|
||||
|
||||
from mealie.schema.labels.multi_purpose_label import MultiPurposeLabelSummary # noqa: E402
|
||||
|
||||
IngredientFood.update_forward_refs()
|
||||
IngredientFood.model_rebuild()
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class RecipeNote(BaseModel):
|
||||
title: str
|
||||
text: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from pydantic import ConfigDict
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
|
||||
|
||||
class Nutrition(MealieModel):
|
||||
calories: str | None
|
||||
fat_content: str | None
|
||||
protein_content: str | None
|
||||
carbohydrate_content: str | None
|
||||
fiber_content: str | None
|
||||
sodium_content: str | None
|
||||
sugar_content: str | None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
calories: str | None = None
|
||||
fat_content: str | None = None
|
||||
protein_content: str | None = None
|
||||
carbohydrate_content: str | None = None
|
||||
fiber_content: str | None = None
|
||||
sodium_content: str | None = None
|
||||
sugar_content: str | None = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from pydantic import ConfigDict
|
||||
|
||||
from mealie.schema._mealie.mealie_model import MealieModel
|
||||
|
||||
|
||||
@@ -8,11 +10,11 @@ class ScrapeRecipeTest(MealieModel):
|
||||
class ScrapeRecipe(MealieModel):
|
||||
url: str
|
||||
include_tags: bool = False
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"url": "https://myfavoriterecipes.com/recipes",
|
||||
"includeTags": True,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from pydantic import ConfigDict
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
|
||||
|
||||
@@ -9,6 +11,4 @@ class RecipeSettings(MealieModel):
|
||||
disable_comments: bool = True
|
||||
disable_amount: bool = True
|
||||
locked: bool = False
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from pydantic import UUID4, Field
|
||||
from pydantic import UUID4, ConfigDict, Field
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
|
||||
@@ -26,16 +26,12 @@ class RecipeShareTokenSave(RecipeShareTokenCreate):
|
||||
class RecipeShareTokenSummary(RecipeShareTokenSave):
|
||||
id: UUID4
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class RecipeShareToken(RecipeShareTokenSummary):
|
||||
recipe: Recipe
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from pydantic import UUID4, Field
|
||||
from pydantic import UUID4, ConfigDict, Field
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
|
||||
@@ -10,10 +10,8 @@ class IngredientReferences(MealieModel):
|
||||
A list of ingredient references.
|
||||
"""
|
||||
|
||||
reference_id: UUID4 | None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
reference_id: UUID4 | None = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class RecipeStep(MealieModel):
|
||||
@@ -21,6 +19,4 @@ class RecipeStep(MealieModel):
|
||||
title: str | None = ""
|
||||
text: str
|
||||
ingredient_references: list[IngredientReferences] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import UUID4, Field
|
||||
from pydantic import UUID4, ConfigDict, Field
|
||||
|
||||
from mealie.core.config import get_app_dirs
|
||||
from mealie.schema._mealie.mealie_model import MealieModel
|
||||
@@ -32,12 +33,10 @@ class RecipeTimelineEventIn(MealieModel):
|
||||
event_type: TimelineEventType
|
||||
|
||||
message: str | None = Field(None, alias="eventMessage")
|
||||
image: TimelineEventImage | None = TimelineEventImage.does_not_have_image
|
||||
image: Annotated[TimelineEventImage | None, Field(validate_default=True)] = TimelineEventImage.does_not_have_image
|
||||
|
||||
timestamp: datetime = datetime.now()
|
||||
|
||||
class Config:
|
||||
use_enum_values = True
|
||||
model_config = ConfigDict(use_enum_values=True)
|
||||
|
||||
|
||||
class RecipeTimelineEventCreate(RecipeTimelineEventIn):
|
||||
@@ -46,20 +45,16 @@ class RecipeTimelineEventCreate(RecipeTimelineEventIn):
|
||||
|
||||
class RecipeTimelineEventUpdate(MealieModel):
|
||||
subject: str
|
||||
message: str | None = Field(alias="eventMessage")
|
||||
message: str | None = Field(None, alias="eventMessage")
|
||||
image: TimelineEventImage | None = None
|
||||
|
||||
class Config:
|
||||
use_enum_values = True
|
||||
model_config = ConfigDict(use_enum_values=True)
|
||||
|
||||
|
||||
class RecipeTimelineEventOut(RecipeTimelineEventCreate):
|
||||
id: UUID4
|
||||
created_at: datetime
|
||||
update_at: datetime
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def image_dir_from_id(cls, recipe_id: UUID4 | str, timeline_event_id: UUID4 | str) -> Path:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pydantic import UUID4
|
||||
from pydantic import UUID4, ConfigDict
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
|
||||
@@ -19,16 +19,12 @@ class RecipeToolSave(RecipeToolCreate):
|
||||
class RecipeToolOut(RecipeToolCreate):
|
||||
id: UUID4
|
||||
slug: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class RecipeToolResponse(RecipeToolOut):
|
||||
recipes: list["RecipeSummary"] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
@@ -41,4 +37,4 @@ class RecipeToolResponse(RecipeToolOut):
|
||||
|
||||
from .recipe import RecipeSummary # noqa: E402
|
||||
|
||||
RecipeToolResponse.update_forward_refs()
|
||||
RecipeToolResponse.model_rebuild()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
|
||||
@@ -10,8 +10,7 @@ class RecipeSlug(MealieModel):
|
||||
|
||||
|
||||
class SlugResponse(BaseModel):
|
||||
class Config:
|
||||
schema_extra = {"example": "adult-mac-and-cheese"}
|
||||
model_config = ConfigDict(json_schema_extra={"example": "adult-mac-and-cheese"})
|
||||
|
||||
|
||||
class UpdateImageResponse(BaseModel):
|
||||
@@ -23,4 +22,4 @@ class RecipeZipTokenResponse(BaseModel):
|
||||
|
||||
|
||||
class RecipeDuplicate(BaseModel):
|
||||
name: str | None
|
||||
name: str | None = None
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import datetime
|
||||
import enum
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic import ConfigDict, Field
|
||||
from pydantic.types import UUID4
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
@@ -34,9 +34,7 @@ class ReportEntryCreate(MealieModel):
|
||||
|
||||
class ReportEntryOut(ReportEntryCreate):
|
||||
id: UUID4
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ReportCreate(MealieModel):
|
||||
@@ -53,9 +51,7 @@ class ReportSummary(ReportCreate):
|
||||
|
||||
class ReportOut(ReportSummary):
|
||||
entries: list[ReportEntryOut] = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import enum
|
||||
from typing import Any, Generic, TypeVar
|
||||
from typing import Annotated, Any, Generic, TypeVar
|
||||
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
|
||||
|
||||
from humps import camelize
|
||||
from pydantic import UUID4, BaseModel, validator
|
||||
from pydantic.generics import GenericModel
|
||||
from pydantic import UUID4, BaseModel, Field, field_validator
|
||||
from pydantic_core.core_schema import ValidationInfo
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
|
||||
@@ -22,12 +22,12 @@ class OrderByNullPosition(str, enum.Enum):
|
||||
|
||||
|
||||
class RecipeSearchQuery(MealieModel):
|
||||
cookbook: UUID4 | str | None
|
||||
cookbook: UUID4 | str | None = None
|
||||
require_all_categories: bool = False
|
||||
require_all_tags: bool = False
|
||||
require_all_tools: bool = False
|
||||
require_all_foods: bool = False
|
||||
search: str | None
|
||||
search: str | None = None
|
||||
_search_seed: str | None = None
|
||||
|
||||
|
||||
@@ -38,23 +38,23 @@ class PaginationQuery(MealieModel):
|
||||
order_by_null_position: OrderByNullPosition | None = None
|
||||
order_direction: OrderDirection = OrderDirection.desc
|
||||
query_filter: str | None = None
|
||||
pagination_seed: str | None = None
|
||||
pagination_seed: Annotated[str | None, Field(validate_default=True)] = None
|
||||
|
||||
@validator("pagination_seed", always=True, pre=True)
|
||||
def validate_randseed(cls, pagination_seed, values):
|
||||
if values.get("order_by") == "random" and not pagination_seed:
|
||||
@field_validator("pagination_seed", mode="before")
|
||||
def validate_randseed(cls, pagination_seed, info: ValidationInfo):
|
||||
if info.data.get("order_by") == "random" and not pagination_seed:
|
||||
raise ValueError("paginationSeed is required when orderBy is random")
|
||||
return pagination_seed
|
||||
|
||||
|
||||
class PaginationBase(GenericModel, Generic[DataT]):
|
||||
class PaginationBase(BaseModel, Generic[DataT]):
|
||||
page: int = 1
|
||||
per_page: int = 10
|
||||
total: int = 0
|
||||
total_pages: int = 0
|
||||
items: list[DataT]
|
||||
next: str | None
|
||||
previous: str | None
|
||||
next: str | None = None
|
||||
previous: str | None = None
|
||||
|
||||
def _set_next(self, route: str, query_params: dict[str, Any]) -> None:
|
||||
if self.page >= self.total_pages:
|
||||
|
||||
@@ -14,7 +14,7 @@ class ErrorResponse(BaseModel):
|
||||
This method is an helper to create an object and convert to a dictionary
|
||||
in the same call, for use while providing details to a HTTPException
|
||||
"""
|
||||
return cls(message=message, exception=exception).dict()
|
||||
return cls(message=message, exception=exception).model_dump()
|
||||
|
||||
|
||||
class SuccessResponse(BaseModel):
|
||||
@@ -27,7 +27,7 @@ class SuccessResponse(BaseModel):
|
||||
This method is an helper to create an object and convert to a dictionary
|
||||
in the same call, for use while providing details to a HTTPException
|
||||
"""
|
||||
return cls(message=message).dict()
|
||||
return cls(message=message).model_dump()
|
||||
|
||||
|
||||
class FileTokenResponse(MealieModel):
|
||||
@@ -39,4 +39,4 @@ class FileTokenResponse(MealieModel):
|
||||
This method is an helper to create an object and convert to a dictionary
|
||||
in the same call, for use while providing details to a HTTPException
|
||||
"""
|
||||
return cls(file_token=token).dict()
|
||||
return cls(file_token=token).model_dump()
|
||||
|
||||
@@ -2,7 +2,7 @@ import datetime
|
||||
import enum
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic import ConfigDict, Field
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
from mealie.schema.response.pagination import PaginationBase
|
||||
@@ -43,9 +43,7 @@ class ServerTaskCreate(MealieModel):
|
||||
|
||||
class ServerTask(ServerTaskCreate):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ServerTaskPagination(PaginationBase):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from pydantic import UUID4, BaseModel
|
||||
from pydantic.types import constr
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import UUID4, BaseModel, StringConstraints
|
||||
|
||||
from mealie.schema._mealie.mealie_model import MealieModel
|
||||
|
||||
@@ -10,8 +11,8 @@ class Token(BaseModel):
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
user_id: UUID4 | None
|
||||
username: constr(to_lower=True, strip_whitespace=True) | None = None # type: ignore
|
||||
user_id: UUID4 | None = None
|
||||
username: Annotated[str, StringConstraints(to_lower=True, strip_whitespace=True)] | None = None # type: ignore
|
||||
|
||||
|
||||
class UnlockResults(MealieModel):
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
from pydantic import validator
|
||||
from pydantic.types import NoneStr, constr
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import Field, StringConstraints, field_validator
|
||||
from pydantic_core.core_schema import ValidationInfo
|
||||
|
||||
from mealie.schema._mealie import MealieModel
|
||||
from mealie.schema._mealie.validators import validate_locale
|
||||
|
||||
|
||||
class CreateUserRegistration(MealieModel):
|
||||
group: NoneStr = None
|
||||
group_token: NoneStr = None
|
||||
email: constr(to_lower=True, strip_whitespace=True) # type: ignore
|
||||
username: constr(to_lower=True, strip_whitespace=True) # type: ignore
|
||||
group: str | None = None
|
||||
group_token: Annotated[str | None, Field(validate_default=True)] = None
|
||||
email: Annotated[str, StringConstraints(to_lower=True, strip_whitespace=True)] # type: ignore
|
||||
username: Annotated[str, StringConstraints(to_lower=True, strip_whitespace=True)] # type: ignore
|
||||
password: str
|
||||
password_confirm: str
|
||||
advanced: bool = False
|
||||
@@ -18,23 +20,23 @@ class CreateUserRegistration(MealieModel):
|
||||
seed_data: bool = False
|
||||
locale: str = "en-US"
|
||||
|
||||
@validator("locale")
|
||||
def valid_locale(cls, v, values, **kwargs):
|
||||
@field_validator("locale")
|
||||
def valid_locale(cls, v):
|
||||
if not validate_locale(v):
|
||||
raise ValueError("invalid locale")
|
||||
return v
|
||||
|
||||
@validator("password_confirm")
|
||||
@field_validator("password_confirm")
|
||||
@classmethod
|
||||
def passwords_match(cls, value, values):
|
||||
if "password" in values and value != values["password"]:
|
||||
def passwords_match(cls, value, info: ValidationInfo):
|
||||
if "password" in info.data and value != info.data["password"]:
|
||||
raise ValueError("passwords do not match")
|
||||
return value
|
||||
|
||||
@validator("group_token", always=True)
|
||||
@field_validator("group_token")
|
||||
@classmethod
|
||||
def group_or_token(cls, value, values):
|
||||
if not bool(value) and not bool(values["group"]):
|
||||
def group_or_token(cls, value, info: ValidationInfo):
|
||||
if not bool(value) and not bool(info.data["group"]):
|
||||
raise ValueError("group or group_token must be provided")
|
||||
|
||||
return value
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Annotated, Any
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import UUID4, Field, validator
|
||||
from pydantic.types import constr
|
||||
from pydantic import UUID4, ConfigDict, Field, StringConstraints, field_validator
|
||||
from sqlalchemy.orm import joinedload, selectinload
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
|
||||
@@ -18,7 +17,6 @@ from mealie.schema.response.pagination import PaginationBase
|
||||
|
||||
from ...db.models.group import Group
|
||||
from ...db.models.recipe import RecipeModel
|
||||
from ..getter_dict import GroupGetterDict, UserGetterDict
|
||||
from ..recipe import CategoryBase
|
||||
|
||||
DEFAULT_INTEGRATION_ID = "generic"
|
||||
@@ -34,25 +32,19 @@ class LongLiveTokenOut(MealieModel):
|
||||
token: str
|
||||
name: str
|
||||
id: int
|
||||
created_at: datetime | None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
created_at: datetime | None = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class CreateToken(LongLiveTokenIn):
|
||||
user_id: UUID4
|
||||
token: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class DeleteTokenResponse(MealieModel):
|
||||
token_delete: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ChangePassword(MealieModel):
|
||||
@@ -61,31 +53,27 @@ class ChangePassword(MealieModel):
|
||||
|
||||
|
||||
class GroupBase(MealieModel):
|
||||
name: constr(strip_whitespace=True, min_length=1) # type: ignore
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
name: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] # type: ignore
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class UserBase(MealieModel):
|
||||
username: str | None
|
||||
id: UUID4 | None = None
|
||||
username: str | None = None
|
||||
full_name: str | None = None
|
||||
email: constr(to_lower=True, strip_whitespace=True) # type: ignore
|
||||
email: Annotated[str, StringConstraints(to_lower=True, strip_whitespace=True)] # type: ignore
|
||||
auth_method: AuthMethod = AuthMethod.MEALIE
|
||||
admin: bool = False
|
||||
group: str | None
|
||||
group: str | None = None
|
||||
advanced: bool = False
|
||||
favorite_recipes: list[str] | None = []
|
||||
|
||||
can_invite: bool = False
|
||||
can_manage: bool = False
|
||||
can_organize: bool = False
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
getter_dict = GroupGetterDict
|
||||
|
||||
schema_extra = {
|
||||
model_config = ConfigDict(
|
||||
from_attributes=True,
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"username": "ChangeMe",
|
||||
"fullName": "Change Me",
|
||||
@@ -93,7 +81,18 @@ class UserBase(MealieModel):
|
||||
"group": settings.DEFAULT_GROUP,
|
||||
"admin": "false",
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@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 UserIn(UserBase):
|
||||
@@ -105,14 +104,10 @@ class UserOut(UserBase):
|
||||
group: str
|
||||
group_id: UUID4
|
||||
group_slug: str
|
||||
tokens: list[LongLiveTokenOut] | None
|
||||
tokens: list[LongLiveTokenOut] | None = None
|
||||
cache_key: str
|
||||
favorite_recipes: list[str] | None = []
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
getter_dict = UserGetterDict
|
||||
favorite_recipes: Annotated[list[str] | None, Field(validate_default=True)] = []
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@property
|
||||
def is_default_user(self) -> bool:
|
||||
@@ -122,6 +117,10 @@ class UserOut(UserBase):
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
return [joinedload(User.group), joinedload(User.favorite_recipes), joinedload(User.tokens)]
|
||||
|
||||
@field_validator("favorite_recipes", mode="before")
|
||||
def convert_favorite_recipes_to_slugs(cls, v):
|
||||
return [recipe.slug for recipe in v] if v else v
|
||||
|
||||
|
||||
class UserPagination(PaginationBase):
|
||||
items: list[UserOut]
|
||||
@@ -129,10 +128,7 @@ class UserPagination(PaginationBase):
|
||||
|
||||
class UserFavorites(UserBase):
|
||||
favorite_recipes: list[RecipeSummary] = [] # type: ignore
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
getter_dict = GroupGetterDict
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
@@ -149,11 +145,10 @@ class PrivateUser(UserOut):
|
||||
group_id: UUID4
|
||||
login_attemps: int = 0
|
||||
locked_at: datetime | None = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@validator("login_attemps", pre=True)
|
||||
@field_validator("login_attemps", mode="before")
|
||||
@classmethod
|
||||
def none_to_zero(cls, v):
|
||||
return 0 if v is None else v
|
||||
|
||||
@@ -189,11 +184,9 @@ class UpdateGroup(GroupBase):
|
||||
|
||||
|
||||
class GroupInDB(UpdateGroup):
|
||||
users: list[UserOut] | None
|
||||
users: list[UserOut] | None = None
|
||||
preferences: ReadGroupPreferences | None = None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@staticmethod
|
||||
def get_directory(id: UUID4) -> Path:
|
||||
@@ -234,6 +227,4 @@ class GroupPagination(PaginationBase):
|
||||
class LongLiveTokenInDB(CreateToken):
|
||||
id: int
|
||||
user: PrivateUser
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pydantic import UUID4
|
||||
from pydantic import UUID4, ConfigDict
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
|
||||
@@ -33,9 +33,7 @@ class SavePasswordResetToken(MealieModel):
|
||||
|
||||
class PrivatePasswordResetToken(SavePasswordResetToken):
|
||||
user: PrivateUser
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
|
||||
Reference in New Issue
Block a user