mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-07 10:15:03 -04:00
Merge branch 'mealie-next' into feat--send-reset-email-from-admin-dashboard
This commit is contained in:
commit
99db24cdec
@ -112,7 +112,7 @@ async def system_startup():
|
||||
logger.info("-----SYSTEM STARTUP----- \n")
|
||||
logger.info("------APP SETTINGS------")
|
||||
logger.info(
|
||||
settings.json(
|
||||
settings.model_dump_json(
|
||||
indent=4,
|
||||
exclude={
|
||||
"SECRET",
|
||||
|
@ -1,7 +1,9 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from urllib import parse as urlparse
|
||||
|
||||
from pydantic import BaseModel, BaseSettings, PostgresDsn
|
||||
from pydantic import BaseModel, PostgresDsn
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class AbstractDBProvider(ABC):
|
||||
@ -38,15 +40,19 @@ class PostgresProvider(AbstractDBProvider, BaseSettings):
|
||||
POSTGRES_PORT: str = "5432"
|
||||
POSTGRES_DB: str = "mealie"
|
||||
|
||||
model_config = SettingsConfigDict(arbitrary_types_allowed=True, extra="allow")
|
||||
|
||||
@property
|
||||
def db_url(self) -> str:
|
||||
host = f"{self.POSTGRES_SERVER}:{self.POSTGRES_PORT}"
|
||||
return PostgresDsn.build(
|
||||
scheme="postgresql",
|
||||
user=self.POSTGRES_USER,
|
||||
password=self.POSTGRES_PASSWORD,
|
||||
host=host,
|
||||
path=f"/{self.POSTGRES_DB or ''}",
|
||||
return str(
|
||||
PostgresDsn.build(
|
||||
scheme="postgresql",
|
||||
username=self.POSTGRES_USER,
|
||||
password=urlparse.quote_plus(self.POSTGRES_PASSWORD),
|
||||
host=host,
|
||||
path=f"{self.POSTGRES_DB or ''}",
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -1,7 +1,8 @@
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseSettings, NoneStr, validator
|
||||
from pydantic import field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
from mealie.core.settings.themes import Theme
|
||||
|
||||
@ -55,7 +56,8 @@ class AppSettings(BaseSettings):
|
||||
SECURITY_USER_LOCKOUT_TIME: int = 24
|
||||
"time in hours"
|
||||
|
||||
@validator("BASE_URL")
|
||||
@field_validator("BASE_URL")
|
||||
@classmethod
|
||||
def remove_trailing_slash(cls, v: str) -> str:
|
||||
if v and v[-1] == "/":
|
||||
return v[:-1]
|
||||
@ -100,12 +102,12 @@ class AppSettings(BaseSettings):
|
||||
# ===============================================
|
||||
# Email Configuration
|
||||
|
||||
SMTP_HOST: str | None
|
||||
SMTP_HOST: str | None = None
|
||||
SMTP_PORT: str | None = "587"
|
||||
SMTP_FROM_NAME: str | None = "Mealie"
|
||||
SMTP_FROM_EMAIL: str | None
|
||||
SMTP_USER: str | None
|
||||
SMTP_PASSWORD: str | None
|
||||
SMTP_FROM_EMAIL: str | None = None
|
||||
SMTP_USER: str | None = None
|
||||
SMTP_PASSWORD: str | None = None
|
||||
SMTP_AUTH_STRATEGY: str | None = "TLS" # Options: 'TLS', 'SSL', 'NONE'
|
||||
|
||||
@property
|
||||
@ -122,11 +124,11 @@ class AppSettings(BaseSettings):
|
||||
|
||||
@staticmethod
|
||||
def validate_smtp(
|
||||
host: str | None,
|
||||
port: str | None,
|
||||
from_name: str | None,
|
||||
from_email: str | None,
|
||||
strategy: str | None,
|
||||
host: str | None = None,
|
||||
port: str | None = None,
|
||||
from_name: str | None = None,
|
||||
from_email: str | None = None,
|
||||
strategy: str | None = None,
|
||||
user: str | None = None,
|
||||
password: str | None = None,
|
||||
) -> bool:
|
||||
@ -143,15 +145,15 @@ class AppSettings(BaseSettings):
|
||||
# LDAP Configuration
|
||||
|
||||
LDAP_AUTH_ENABLED: bool = False
|
||||
LDAP_SERVER_URL: NoneStr = None
|
||||
LDAP_SERVER_URL: str | None = None
|
||||
LDAP_TLS_INSECURE: bool = False
|
||||
LDAP_TLS_CACERTFILE: NoneStr = None
|
||||
LDAP_TLS_CACERTFILE: str | None = None
|
||||
LDAP_ENABLE_STARTTLS: bool = False
|
||||
LDAP_BASE_DN: NoneStr = None
|
||||
LDAP_QUERY_BIND: NoneStr = None
|
||||
LDAP_QUERY_PASSWORD: NoneStr = None
|
||||
LDAP_USER_FILTER: NoneStr = None
|
||||
LDAP_ADMIN_FILTER: NoneStr = None
|
||||
LDAP_BASE_DN: str | None = None
|
||||
LDAP_QUERY_BIND: str | None = None
|
||||
LDAP_QUERY_PASSWORD: str | None = None
|
||||
LDAP_USER_FILTER: str | None = None
|
||||
LDAP_ADMIN_FILTER: str | None = None
|
||||
LDAP_ID_ATTRIBUTE: str = "uid"
|
||||
LDAP_MAIL_ATTRIBUTE: str = "mail"
|
||||
LDAP_NAME_ATTRIBUTE: str = "name"
|
||||
@ -173,9 +175,7 @@ class AppSettings(BaseSettings):
|
||||
# Testing Config
|
||||
|
||||
TESTING: bool = False
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
model_config = SettingsConfigDict(arbitrary_types_allowed=True, extra="allow")
|
||||
|
||||
|
||||
def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, env_encoding="utf-8") -> AppSettings:
|
||||
|
@ -1,4 +1,4 @@
|
||||
from pydantic import BaseSettings
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Theme(BaseSettings):
|
||||
@ -17,6 +17,4 @@ class Theme(BaseSettings):
|
||||
dark_info: str = "#1976D2"
|
||||
dark_warning: str = "#FF6D00"
|
||||
dark_error: str = "#EF5350"
|
||||
|
||||
class Config:
|
||||
env_prefix = "theme_"
|
||||
model_config = SettingsConfigDict(env_prefix="theme_", extra="allow")
|
||||
|
@ -1,7 +1,7 @@
|
||||
from functools import wraps
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field, NoneStr
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import MANYTOMANY, MANYTOONE, ONETOMANY, Session
|
||||
from sqlalchemy.orm.mapper import Mapper
|
||||
@ -21,7 +21,7 @@ class AutoInitConfig(BaseModel):
|
||||
Config class for `auto_init` decorator.
|
||||
"""
|
||||
|
||||
get_attr: NoneStr = None
|
||||
get_attr: str | None = None
|
||||
exclude: set = Field(default_factory=_default_exclusion)
|
||||
# auto_create: bool = False
|
||||
|
||||
@ -31,16 +31,16 @@ def _get_config(relation_cls: type[SqlAlchemyBase]) -> AutoInitConfig:
|
||||
Returns the config for the given class.
|
||||
"""
|
||||
cfg = AutoInitConfig()
|
||||
cfgKeys = cfg.dict().keys()
|
||||
cfgKeys = cfg.model_dump().keys()
|
||||
# Get the config for the class
|
||||
try:
|
||||
class_config: AutoInitConfig = relation_cls.Config
|
||||
class_config: ConfigDict = relation_cls.model_config
|
||||
except AttributeError:
|
||||
return cfg
|
||||
# Map all matching attributes in Config to all AutoInitConfig attributes
|
||||
for attr in dir(class_config):
|
||||
for attr in class_config:
|
||||
if attr in cfgKeys:
|
||||
setattr(cfg, attr, getattr(class_config, attr))
|
||||
setattr(cfg, attr, class_config[attr])
|
||||
|
||||
return cfg
|
||||
|
||||
@ -97,7 +97,7 @@ def handle_one_to_many_list(
|
||||
|
||||
updated_elems.append(existing_elem)
|
||||
|
||||
new_elems = [safe_call(relation_cls, elem, session=session) for elem in elems_to_create]
|
||||
new_elems = [safe_call(relation_cls, elem.copy(), session=session) for elem in elems_to_create]
|
||||
return new_elems + updated_elems
|
||||
|
||||
|
||||
@ -164,7 +164,7 @@ def auto_init(): # sourcery no-metrics
|
||||
setattr(self, key, instances)
|
||||
|
||||
elif relation_dir == ONETOMANY:
|
||||
instance = safe_call(relation_cls, val, session=session)
|
||||
instance = safe_call(relation_cls, val.copy() if val else None, session=session)
|
||||
setattr(self, key, instance)
|
||||
|
||||
elif relation_dir == MANYTOONE and not use_list:
|
||||
|
@ -29,12 +29,15 @@ def get_valid_call(func: Callable, args_dict) -> dict:
|
||||
return {k: v for k, v in args_dict.items() if k in valid_args}
|
||||
|
||||
|
||||
def safe_call(func, dict_args: dict, **kwargs) -> Any:
|
||||
def safe_call(func, dict_args: dict | None, **kwargs) -> Any:
|
||||
"""
|
||||
Safely calls the supplied function with the supplied dictionary of arguments.
|
||||
by removing any invalid arguments.
|
||||
"""
|
||||
|
||||
if dict_args is None:
|
||||
dict_args = {}
|
||||
|
||||
if kwargs:
|
||||
dict_args.update(kwargs)
|
||||
|
||||
|
@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.orm.session import Session
|
||||
@ -79,9 +80,8 @@ class Group(SqlAlchemyBase, BaseMixins):
|
||||
ingredient_foods: Mapped[list["IngredientFoodModel"]] = orm.relationship("IngredientFoodModel", **common_args)
|
||||
tools: Mapped[list["Tool"]] = orm.relationship("Tool", **common_args)
|
||||
tags: Mapped[list["Tag"]] = orm.relationship("Tag", **common_args)
|
||||
|
||||
class Config:
|
||||
exclude = {
|
||||
model_config = ConfigDict(
|
||||
exclude={
|
||||
"users",
|
||||
"webhooks",
|
||||
"shopping_lists",
|
||||
@ -91,6 +91,7 @@ class Group(SqlAlchemyBase, BaseMixins):
|
||||
"mealplans",
|
||||
"data_exports",
|
||||
}
|
||||
)
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
|
@ -1,6 +1,7 @@
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy import ForeignKey, orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.sql.sqltypes import Boolean, DateTime, String
|
||||
@ -47,9 +48,7 @@ class ReportModel(SqlAlchemyBase, BaseMixins):
|
||||
# Relationships
|
||||
group_id: Mapped[GUID] = mapped_column(GUID, ForeignKey("groups.id"), nullable=False, index=True)
|
||||
group: Mapped["Group"] = orm.relationship("Group", back_populates="group_reports", single_parent=True)
|
||||
|
||||
class Config:
|
||||
exclude = ["entries"]
|
||||
model_config = ConfigDict(exclude=["entries"])
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
|
@ -1,5 +1,6 @@
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy import Boolean, Float, ForeignKey, Integer, String, UniqueConstraint, orm
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
@ -69,9 +70,7 @@ class ShoppingListItem(SqlAlchemyBase, BaseMixins):
|
||||
recipe_references: Mapped[list[ShoppingListItemRecipeReference]] = orm.relationship(
|
||||
ShoppingListItemRecipeReference, cascade="all, delete, delete-orphan"
|
||||
)
|
||||
|
||||
class Config:
|
||||
exclude = {"id", "label", "food", "unit"}
|
||||
model_config = ConfigDict(exclude={"id", "label", "food", "unit"})
|
||||
|
||||
@api_extras
|
||||
@auto_init()
|
||||
@ -91,9 +90,7 @@ class ShoppingListRecipeReference(BaseMixins, SqlAlchemyBase):
|
||||
)
|
||||
|
||||
recipe_quantity: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
|
||||
class Config:
|
||||
exclude = {"id", "recipe"}
|
||||
model_config = ConfigDict(exclude={"id", "recipe"})
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
@ -112,9 +109,7 @@ class ShoppingListMultiPurposeLabel(SqlAlchemyBase, BaseMixins):
|
||||
"MultiPurposeLabel", back_populates="shopping_lists_label_settings"
|
||||
)
|
||||
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
|
||||
class Config:
|
||||
exclude = {"label"}
|
||||
model_config = ConfigDict(exclude={"label"})
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, **_) -> None:
|
||||
@ -146,9 +141,7 @@ class ShoppingList(SqlAlchemyBase, BaseMixins):
|
||||
collection_class=ordering_list("position"),
|
||||
)
|
||||
extras: Mapped[list[ShoppingListExtras]] = orm.relationship("ShoppingListExtras", cascade="all, delete-orphan")
|
||||
|
||||
class Config:
|
||||
exclude = {"id", "list_items"}
|
||||
model_config = ConfigDict(exclude={"id", "list_items"})
|
||||
|
||||
@api_extras
|
||||
@auto_init()
|
||||
|
@ -1,3 +1,4 @@
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy import ForeignKey, Integer, String, orm
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
@ -28,12 +29,12 @@ class RecipeInstruction(SqlAlchemyBase):
|
||||
ingredient_references: Mapped[list[RecipeIngredientRefLink]] = orm.relationship(
|
||||
RecipeIngredientRefLink, cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
class Config:
|
||||
exclude = {
|
||||
model_config = ConfigDict(
|
||||
exclude={
|
||||
"id",
|
||||
"ingredient_references",
|
||||
}
|
||||
)
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, ingredient_references, session, **_) -> None:
|
||||
|
@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
from sqlalchemy.orm import Mapped, mapped_column, validates
|
||||
@ -134,10 +135,9 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
# Automatically updated by sqlalchemy event, do not write to this manually
|
||||
name_normalized: Mapped[str] = mapped_column(sa.String, nullable=False, index=True)
|
||||
description_normalized: Mapped[str | None] = mapped_column(sa.String, index=True)
|
||||
|
||||
class Config:
|
||||
get_attr = "slug"
|
||||
exclude = {
|
||||
model_config = ConfigDict(
|
||||
get_attr="slug",
|
||||
exclude={
|
||||
"assets",
|
||||
"notes",
|
||||
"nutrition",
|
||||
@ -146,7 +146,8 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||
"settings",
|
||||
"comments",
|
||||
"timeline_events",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@validates("name")
|
||||
def validate_name(self, _, name):
|
||||
|
@ -2,6 +2,7 @@ import enum
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from pydantic import ConfigDict
|
||||
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, Integer, String, orm
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
@ -84,9 +85,8 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
favorite_recipes: Mapped[list["RecipeModel"]] = orm.relationship(
|
||||
"RecipeModel", secondary=users_to_favorites, back_populates="favorited_by"
|
||||
)
|
||||
|
||||
class Config:
|
||||
exclude = {
|
||||
model_config = ConfigDict(
|
||||
exclude={
|
||||
"password",
|
||||
"admin",
|
||||
"can_manage",
|
||||
@ -94,6 +94,7 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
"can_organize",
|
||||
"group",
|
||||
}
|
||||
)
|
||||
|
||||
@hybrid_property
|
||||
def group_slug(self) -> str:
|
||||
|
@ -33,12 +33,12 @@
|
||||
"generic-deleted": "{name} er blevet slettet"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "year|years",
|
||||
"day": "day|days",
|
||||
"hour": "hour|hours",
|
||||
"minute": "minute|minutes",
|
||||
"second": "second|seconds",
|
||||
"millisecond": "millisecond|milliseconds",
|
||||
"microsecond": "microsecond|microseconds"
|
||||
"year": "år|år",
|
||||
"day": "dag|dage",
|
||||
"hour": "time|timer",
|
||||
"minute": "minut|minutter",
|
||||
"second": "sekund|sekunder",
|
||||
"millisecond": "millisekund|millisekunder",
|
||||
"microsecond": "mikrosekund|mikrosekunder"
|
||||
}
|
||||
}
|
||||
|
@ -33,12 +33,12 @@
|
||||
"generic-deleted": "{name} törölve lett"
|
||||
},
|
||||
"datetime": {
|
||||
"year": "év|év",
|
||||
"day": "nap/nap",
|
||||
"hour": "óra|óra",
|
||||
"minute": "perc/perc",
|
||||
"second": "másodperc|másodperc",
|
||||
"millisecond": "ezredmásodperc|ezredmásodperc",
|
||||
"microsecond": "mikroszekundum|mikroszekundum"
|
||||
"year": "év",
|
||||
"day": "nap",
|
||||
"hour": "óra",
|
||||
"minute": "perc",
|
||||
"second": "másodperc",
|
||||
"millisecond": "ezredmásodperc",
|
||||
"microsecond": "mikroszekundum"
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
except AttributeError:
|
||||
self.logger.info(f'Attempted to sort by unknown sort property "{order_by}"; ignoring')
|
||||
result = self.session.execute(q.offset(start).limit(limit)).unique().scalars().all()
|
||||
return [eff_schema.from_orm(x) for x in result]
|
||||
return [eff_schema.model_validate(x) for x in result]
|
||||
|
||||
def multi_query(
|
||||
self,
|
||||
@ -129,7 +129,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
|
||||
q = q.offset(start).limit(limit)
|
||||
result = self.session.execute(q).unique().scalars().all()
|
||||
return [eff_schema.from_orm(x) for x in result]
|
||||
return [eff_schema.model_validate(x) for x in result]
|
||||
|
||||
def _query_one(self, match_value: str | int | UUID4, match_key: str | None = None) -> Model:
|
||||
"""
|
||||
@ -161,11 +161,11 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
if not result:
|
||||
return None
|
||||
|
||||
return eff_schema.from_orm(result)
|
||||
return eff_schema.model_validate(result)
|
||||
|
||||
def create(self, data: Schema | BaseModel | dict) -> Schema:
|
||||
try:
|
||||
data = data if isinstance(data, dict) else data.dict()
|
||||
data = data if isinstance(data, dict) else data.model_dump()
|
||||
new_document = self.model(session=self.session, **data)
|
||||
self.session.add(new_document)
|
||||
self.session.commit()
|
||||
@ -175,12 +175,12 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
|
||||
self.session.refresh(new_document)
|
||||
|
||||
return self.schema.from_orm(new_document)
|
||||
return self.schema.model_validate(new_document)
|
||||
|
||||
def create_many(self, data: Iterable[Schema | dict]) -> list[Schema]:
|
||||
new_documents = []
|
||||
for document in data:
|
||||
document = document if isinstance(document, dict) else document.dict()
|
||||
document = document if isinstance(document, dict) else document.model_dump()
|
||||
new_document = self.model(session=self.session, **document)
|
||||
new_documents.append(new_document)
|
||||
|
||||
@ -190,7 +190,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
for created_document in new_documents:
|
||||
self.session.refresh(created_document)
|
||||
|
||||
return [self.schema.from_orm(x) for x in new_documents]
|
||||
return [self.schema.model_validate(x) for x in new_documents]
|
||||
|
||||
def update(self, match_value: str | int | UUID4, new_data: dict | BaseModel) -> Schema:
|
||||
"""Update a database entry.
|
||||
@ -202,18 +202,18 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
Returns:
|
||||
dict: Returns a dictionary representation of the database entry
|
||||
"""
|
||||
new_data = new_data if isinstance(new_data, dict) else new_data.dict()
|
||||
new_data = new_data if isinstance(new_data, dict) else new_data.model_dump()
|
||||
|
||||
entry = self._query_one(match_value=match_value)
|
||||
entry.update(session=self.session, **new_data)
|
||||
|
||||
self.session.commit()
|
||||
return self.schema.from_orm(entry)
|
||||
return self.schema.model_validate(entry)
|
||||
|
||||
def update_many(self, data: Iterable[Schema | dict]) -> list[Schema]:
|
||||
document_data_by_id: dict[str, dict] = {}
|
||||
for document in data:
|
||||
document_data = document if isinstance(document, dict) else document.dict()
|
||||
document_data = document if isinstance(document, dict) else document.model_dump()
|
||||
document_data_by_id[document_data["id"]] = document_data
|
||||
|
||||
documents_to_update_query = self._query().filter(self.model.id.in_(list(document_data_by_id.keys())))
|
||||
@ -226,14 +226,14 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
updated_documents.append(document_to_update)
|
||||
|
||||
self.session.commit()
|
||||
return [self.schema.from_orm(x) for x in updated_documents]
|
||||
return [self.schema.model_validate(x) for x in updated_documents]
|
||||
|
||||
def patch(self, match_value: str | int | UUID4, new_data: dict | BaseModel) -> Schema:
|
||||
new_data = new_data if isinstance(new_data, dict) else new_data.dict()
|
||||
new_data = new_data if isinstance(new_data, dict) else new_data.model_dump()
|
||||
|
||||
entry = self._query_one(match_value=match_value)
|
||||
|
||||
entry_as_dict = self.schema.from_orm(entry).dict()
|
||||
entry_as_dict = self.schema.model_validate(entry).model_dump()
|
||||
entry_as_dict.update(new_data)
|
||||
|
||||
return self.update(match_value, entry_as_dict)
|
||||
@ -242,7 +242,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
match_key = match_key or self.primary_key
|
||||
|
||||
result = self._query_one(value, match_key)
|
||||
results_as_model = self.schema.from_orm(result)
|
||||
results_as_model = self.schema.model_validate(result)
|
||||
|
||||
try:
|
||||
self.session.delete(result)
|
||||
@ -256,7 +256,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
def delete_many(self, values: Iterable) -> Schema:
|
||||
query = self._query().filter(self.model.id.in_(values)) # type: ignore
|
||||
results = self.session.execute(query).unique().scalars().all()
|
||||
results_as_model = [self.schema.from_orm(result) for result in results]
|
||||
results_as_model = [self.schema.model_validate(result) for result in results]
|
||||
|
||||
try:
|
||||
# we create a delete statement for each row
|
||||
@ -295,7 +295,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
return self.session.scalar(q)
|
||||
else:
|
||||
q = self._query(override_schema=eff_schema).filter(attribute_name == attr_match)
|
||||
return [eff_schema.from_orm(x) for x in self.session.execute(q).scalars().all()]
|
||||
return [eff_schema.model_validate(x) for x in self.session.execute(q).scalars().all()]
|
||||
|
||||
def page_all(self, pagination: PaginationQuery, override=None, search: str | None = None) -> PaginationBase[Schema]:
|
||||
"""
|
||||
@ -309,7 +309,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
"""
|
||||
eff_schema = override or self.schema
|
||||
# Copy this, because calling methods (e.g. tests) might rely on it not getting mutated
|
||||
pagination_result = pagination.copy()
|
||||
pagination_result = pagination.model_copy()
|
||||
q = self._query(override_schema=eff_schema, with_options=False)
|
||||
|
||||
fltr = self._filter_builder()
|
||||
@ -336,7 +336,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
||||
per_page=pagination_result.per_page,
|
||||
total=count,
|
||||
total_pages=total_pages,
|
||||
items=[eff_schema.from_orm(s) for s in data],
|
||||
items=[eff_schema.model_validate(s) for s in data],
|
||||
)
|
||||
|
||||
def add_pagination_to_query(self, query: Select, pagination: PaginationQuery) -> tuple[Select, int, int]:
|
||||
|
@ -23,7 +23,7 @@ from .repository_generic import RepositoryGeneric
|
||||
class RepositoryGroup(RepositoryGeneric[GroupInDB, Group]):
|
||||
def create(self, data: GroupBase | dict) -> GroupInDB:
|
||||
if isinstance(data, GroupBase):
|
||||
data = data.dict()
|
||||
data = data.model_dump()
|
||||
|
||||
max_attempts = 10
|
||||
original_name = cast(str, data["name"])
|
||||
@ -61,7 +61,7 @@ class RepositoryGroup(RepositoryGeneric[GroupInDB, Group]):
|
||||
dbgroup = self.session.execute(select(self.model).filter_by(name=name)).scalars().one_or_none()
|
||||
if dbgroup is None:
|
||||
return None
|
||||
return self.schema.from_orm(dbgroup)
|
||||
return self.schema.model_validate(dbgroup)
|
||||
|
||||
def get_by_slug_or_id(self, slug_or_id: str | UUID) -> GroupInDB | None:
|
||||
if isinstance(slug_or_id, str):
|
||||
|
@ -28,4 +28,4 @@ class RepositoryMealPlanRules(RepositoryGeneric[PlanRulesOut, GroupMealPlanRules
|
||||
|
||||
rules = self.session.execute(stmt).scalars().all()
|
||||
|
||||
return [self.schema.from_orm(x) for x in rules]
|
||||
return [self.schema.model_validate(x) for x in rules]
|
||||
|
@ -17,4 +17,4 @@ class RepositoryMeals(RepositoryGeneric[ReadPlanEntry, GroupMealPlan]):
|
||||
today = date.today()
|
||||
stmt = select(GroupMealPlan).filter(GroupMealPlan.date == today, GroupMealPlan.group_id == group_id)
|
||||
plans = self.session.execute(stmt).scalars().all()
|
||||
return [self.schema.from_orm(x) for x in plans]
|
||||
return [self.schema.model_validate(x) for x in plans]
|
||||
|
@ -58,7 +58,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
.offset(start)
|
||||
.limit(limit)
|
||||
)
|
||||
return [eff_schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
return [eff_schema.model_validate(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
|
||||
stmt = (
|
||||
select(self.model)
|
||||
@ -67,7 +67,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
.offset(start)
|
||||
.limit(limit)
|
||||
)
|
||||
return [eff_schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
return [eff_schema.model_validate(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
|
||||
def update_image(self, slug: str, _: str | None = None) -> int:
|
||||
entry: RecipeModel = self._query_one(match_value=slug)
|
||||
@ -160,7 +160,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
search: str | None = None,
|
||||
) -> RecipePagination:
|
||||
# Copy this, because calling methods (e.g. tests) might rely on it not getting mutated
|
||||
pagination_result = pagination.copy()
|
||||
pagination_result = pagination.model_copy()
|
||||
q = select(self.model)
|
||||
|
||||
args = [
|
||||
@ -216,7 +216,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
self.session.rollback()
|
||||
raise e
|
||||
|
||||
items = [RecipeSummary.from_orm(item) for item in data]
|
||||
items = [RecipeSummary.model_validate(item) for item in data]
|
||||
return RecipePagination(
|
||||
page=pagination_result.page,
|
||||
per_page=pagination_result.per_page,
|
||||
@ -236,7 +236,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
.join(RecipeModel.recipe_category)
|
||||
.filter(RecipeModel.recipe_category.any(Category.id.in_(ids)))
|
||||
)
|
||||
return [RecipeSummary.from_orm(x) for x in self.session.execute(stmt).unique().scalars().all()]
|
||||
return [RecipeSummary.model_validate(x) for x in self.session.execute(stmt).unique().scalars().all()]
|
||||
|
||||
def _build_recipe_filter(
|
||||
self,
|
||||
@ -298,7 +298,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
require_all_tools=require_all_tools,
|
||||
)
|
||||
stmt = select(RecipeModel).filter(*fltr)
|
||||
return [self.schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
return [self.schema.model_validate(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
|
||||
def get_random_by_categories_and_tags(
|
||||
self, categories: list[RecipeCategory], tags: list[RecipeTag]
|
||||
@ -316,7 +316,7 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
stmt = (
|
||||
select(RecipeModel).filter(and_(*filters)).order_by(func.random()).limit(1) # Postgres and SQLite specific
|
||||
)
|
||||
return [self.schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
return [self.schema.model_validate(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
|
||||
def get_random(self, limit=1) -> list[Recipe]:
|
||||
stmt = (
|
||||
@ -325,14 +325,14 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
||||
.order_by(func.random()) # Postgres and SQLite specific
|
||||
.limit(limit)
|
||||
)
|
||||
return [self.schema.from_orm(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
return [self.schema.model_validate(x) for x in self.session.execute(stmt).scalars().all()]
|
||||
|
||||
def get_by_slug(self, group_id: UUID4, slug: str, limit=1) -> Recipe | None:
|
||||
stmt = select(RecipeModel).filter(RecipeModel.group_id == group_id, RecipeModel.slug == slug)
|
||||
dbrecipe = self.session.execute(stmt).scalars().one_or_none()
|
||||
if dbrecipe is None:
|
||||
return None
|
||||
return self.schema.from_orm(dbrecipe)
|
||||
return self.schema.model_validate(dbrecipe)
|
||||
|
||||
def all_ids(self, group_id: UUID4) -> Sequence[UUID4]:
|
||||
stmt = select(RecipeModel.id).filter(RecipeModel.group_id == group_id)
|
||||
|
@ -18,7 +18,7 @@ class RepositoryUsers(RepositoryGeneric[PrivateUser, User]):
|
||||
def update_password(self, id, password: str):
|
||||
entry = self._query_one(match_value=id)
|
||||
if settings.IS_DEMO:
|
||||
user_to_update = self.schema.from_orm(entry)
|
||||
user_to_update = self.schema.model_validate(entry)
|
||||
if user_to_update.is_default_user:
|
||||
# do not update the default user in demo mode
|
||||
return user_to_update
|
||||
@ -26,7 +26,7 @@ class RepositoryUsers(RepositoryGeneric[PrivateUser, User]):
|
||||
entry.update_password(password)
|
||||
self.session.commit()
|
||||
|
||||
return self.schema.from_orm(entry)
|
||||
return self.schema.model_validate(entry)
|
||||
|
||||
def create(self, user: PrivateUser | dict): # type: ignore
|
||||
new_user = super().create(user)
|
||||
@ -66,9 +66,9 @@ class RepositoryUsers(RepositoryGeneric[PrivateUser, User]):
|
||||
def get_by_username(self, username: str) -> PrivateUser | None:
|
||||
stmt = select(User).filter(User.username == username)
|
||||
dbuser = self.session.execute(stmt).scalars().one_or_none()
|
||||
return None if dbuser is None else self.schema.from_orm(dbuser)
|
||||
return None if dbuser is None else self.schema.model_validate(dbuser)
|
||||
|
||||
def get_locked_users(self) -> list[PrivateUser]:
|
||||
stmt = select(User).filter(User.locked_at != None) # noqa E711
|
||||
results = self.session.execute(stmt).scalars().all()
|
||||
return [self.schema.from_orm(x) for x in results]
|
||||
return [self.schema.model_validate(x) for x in results]
|
||||
|
@ -2,7 +2,7 @@ from abc import ABC
|
||||
from logging import Logger
|
||||
|
||||
from fastapi import Depends
|
||||
from pydantic import UUID4
|
||||
from pydantic import UUID4, ConfigDict
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from mealie.core.config import get_app_dirs, get_app_settings
|
||||
@ -25,10 +25,10 @@ class _BaseController(ABC):
|
||||
session: Session = Depends(generate_session)
|
||||
translator: Translator = Depends(local_provider)
|
||||
|
||||
_repos: AllRepositories | None
|
||||
_logger: Logger | None
|
||||
_settings: AppSettings | None
|
||||
_folders: AppDirectories | None
|
||||
_repos: AllRepositories | None = None
|
||||
_logger: Logger | None = None
|
||||
_settings: AppSettings | None = None
|
||||
_folders: AppDirectories | None = None
|
||||
|
||||
@property
|
||||
def t(self):
|
||||
@ -58,8 +58,7 @@ class _BaseController(ABC):
|
||||
self._folders = get_app_dirs()
|
||||
return self._folders
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
|
||||
class BasePublicController(_BaseController):
|
||||
|
@ -6,11 +6,10 @@ See their repository for details -> https://github.com/dmontagu/fastapi-utils
|
||||
|
||||
import inspect
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TypeVar, cast, get_type_hints
|
||||
from typing import Any, ClassVar, ForwardRef, TypeVar, cast, get_origin, get_type_hints
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.routing import APIRoute
|
||||
from pydantic.typing import is_classvar
|
||||
from starlette.routing import Route, WebSocketRoute
|
||||
|
||||
T = TypeVar("T")
|
||||
@ -47,6 +46,25 @@ def _cbv(router: APIRouter, cls: type[T], *urls: str, instance: Any | None = Non
|
||||
return cls
|
||||
|
||||
|
||||
# copied from Pydantic V1 Source: https://github.com/pydantic/pydantic/blob/1c91c8627b541b22354b9ed56b9ef1bb21ac6fbd/pydantic/v1/typing.py
|
||||
def _check_classvar(v: type[Any] | None) -> bool:
|
||||
if v is None:
|
||||
return False
|
||||
|
||||
return v.__class__ == ClassVar.__class__ and getattr(v, "_name", None) == "ClassVar"
|
||||
|
||||
|
||||
# copied from Pydantic V1 Source: https://github.com/pydantic/pydantic/blob/1c91c8627b541b22354b9ed56b9ef1bb21ac6fbd/pydantic/v1/typing.py
|
||||
def _is_classvar(ann_type: type[Any]) -> bool:
|
||||
if _check_classvar(ann_type) or _check_classvar(get_origin(ann_type)):
|
||||
return True
|
||||
|
||||
if ann_type.__class__ == ForwardRef and ann_type.__forward_arg__.startswith("ClassVar["): # type: ignore
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _init_cbv(cls: type[Any], instance: Any | None = None) -> None:
|
||||
"""
|
||||
Idempotently modifies the provided `cls`, performing the following modifications:
|
||||
@ -67,7 +85,7 @@ def _init_cbv(cls: type[Any], instance: Any | None = None) -> None:
|
||||
|
||||
dependency_names: list[str] = []
|
||||
for name, hint in get_type_hints(cls).items():
|
||||
if is_classvar(hint):
|
||||
if _is_classvar(hint):
|
||||
continue
|
||||
|
||||
if name.startswith("_"):
|
||||
|
@ -108,7 +108,7 @@ class HttpRepo(Generic[C, R, U]):
|
||||
)
|
||||
|
||||
try:
|
||||
item = self.repo.patch(item_id, data.dict(exclude_unset=True, exclude_defaults=True))
|
||||
item = self.repo.patch(item_id, data.model_dump(exclude_unset=True, exclude_defaults=True))
|
||||
except Exception as ex:
|
||||
self.handle_exception(ex)
|
||||
|
||||
|
@ -43,7 +43,7 @@ class AdminUserManagementRoutes(BaseAdminController):
|
||||
override=GroupInDB,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=GroupInDB, status_code=status.HTTP_201_CREATED)
|
||||
|
@ -37,7 +37,7 @@ class AdminUserManagementRoutes(BaseAdminController):
|
||||
override=UserOut,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=UserOut, status_code=201)
|
||||
|
@ -18,7 +18,7 @@ class AdminServerTasksController(BaseAdminController):
|
||||
override=ServerTask,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("/server-tasks", response_model=ServerTask, status_code=201)
|
||||
|
@ -53,4 +53,4 @@ def get_app_theme(resp: Response):
|
||||
settings = get_app_settings()
|
||||
|
||||
resp.headers["Cache-Control"] = "public, max-age=604800"
|
||||
return AppTheme(**settings.theme.dict())
|
||||
return AppTheme(**settings.theme.model_dump())
|
||||
|
@ -48,7 +48,7 @@ class MealieAuthToken(BaseModel):
|
||||
|
||||
@classmethod
|
||||
def respond(cls, token: str, token_type: str = "bearer") -> dict:
|
||||
return cls(access_token=token, token_type=token_type).dict()
|
||||
return cls(access_token=token, token_type=token_type).model_dump()
|
||||
|
||||
|
||||
@public_router.post("/token")
|
||||
|
@ -47,7 +47,7 @@ class RecipeCommentRoutes(BaseUserController):
|
||||
override=RecipeCommentOut,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=RecipeCommentOut, status_code=201)
|
||||
|
@ -1,3 +1,5 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import UUID4
|
||||
|
||||
@ -36,12 +38,19 @@ class PublicCookbooksController(BasePublicExploreController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all", group_slug=self.group.slug), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all", group_slug=self.group.slug), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.get("/{item_id}", response_model=RecipeCookBook)
|
||||
def get_one(self, item_id: UUID4 | str) -> RecipeCookBook:
|
||||
match_attr = "slug" if isinstance(item_id, str) else "id"
|
||||
if isinstance(item_id, UUID):
|
||||
match_attr = "id"
|
||||
else:
|
||||
try:
|
||||
UUID(item_id)
|
||||
match_attr = "id"
|
||||
except ValueError:
|
||||
match_attr = "slug"
|
||||
cookbook = self.cookbooks.get_one(item_id, match_attr)
|
||||
|
||||
if not cookbook or not cookbook.public:
|
||||
|
@ -26,7 +26,7 @@ class PublicFoodsController(BasePublicExploreController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all", group_slug=self.group.slug), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all", group_slug=self.group.slug), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.get("/{item_id}", response_model=IngredientFood)
|
||||
|
@ -31,7 +31,9 @@ class PublicCategoriesController(BasePublicExploreController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(categories_router.url_path_for("get_all", group_slug=self.group.slug), q.dict())
|
||||
response.set_pagination_guides(
|
||||
categories_router.url_path_for("get_all", group_slug=self.group.slug), q.model_dump()
|
||||
)
|
||||
return response
|
||||
|
||||
@categories_router.get("/{item_id}", response_model=CategoryOut)
|
||||
@ -59,7 +61,7 @@ class PublicTagsController(BasePublicExploreController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(tags_router.url_path_for("get_all", group_slug=self.group.slug), q.dict())
|
||||
response.set_pagination_guides(tags_router.url_path_for("get_all", group_slug=self.group.slug), q.model_dump())
|
||||
return response
|
||||
|
||||
@tags_router.get("/{item_id}", response_model=TagOut)
|
||||
@ -87,7 +89,7 @@ class PublicToolsController(BasePublicExploreController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(tools_router.url_path_for("get_all", group_slug=self.group.slug), q.dict())
|
||||
response.set_pagination_guides(tools_router.url_path_for("get_all", group_slug=self.group.slug), q.model_dump())
|
||||
return response
|
||||
|
||||
@tools_router.get("/{item_id}", response_model=RecipeToolOut)
|
||||
|
@ -1,3 +1,5 @@
|
||||
from uuid import UUID
|
||||
|
||||
import orjson
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from pydantic import UUID4
|
||||
@ -37,7 +39,14 @@ class PublicRecipesController(BasePublicExploreController):
|
||||
) -> PaginationBase[RecipeSummary]:
|
||||
cookbook_data: ReadCookBook | None = None
|
||||
if search_query.cookbook:
|
||||
cb_match_attr = "slug" if isinstance(search_query.cookbook, str) else "id"
|
||||
if isinstance(search_query.cookbook, UUID):
|
||||
cb_match_attr = "id"
|
||||
else:
|
||||
try:
|
||||
UUID(search_query.cookbook)
|
||||
cb_match_attr = "id"
|
||||
except ValueError:
|
||||
cb_match_attr = "slug"
|
||||
cookbook_data = self.cookbooks.get_one(search_query.cookbook, cb_match_attr)
|
||||
|
||||
if cookbook_data is None or not cookbook_data.public:
|
||||
@ -64,13 +73,13 @@ class PublicRecipesController(BasePublicExploreController):
|
||||
)
|
||||
|
||||
# merge default pagination with the request's query params
|
||||
query_params = q.dict() | {**request.query_params}
|
||||
query_params = q.model_dump() | {**request.query_params}
|
||||
pagination_response.set_pagination_guides(
|
||||
router.url_path_for("get_all", group_slug=self.group.slug),
|
||||
{k: v for k, v in query_params.items() if v is not None},
|
||||
)
|
||||
|
||||
json_compatible_response = orjson.dumps(pagination_response.dict(by_alias=True))
|
||||
json_compatible_response = orjson.dumps(pagination_response.model_dump(by_alias=True))
|
||||
|
||||
# Response is returned directly, to avoid validation and improve performance
|
||||
return JSONBytes(content=json_compatible_response)
|
||||
|
@ -1,4 +1,5 @@
|
||||
from functools import cached_property
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import UUID4
|
||||
@ -48,7 +49,7 @@ class GroupCookbookController(BaseCrudController):
|
||||
override=ReadCookBook,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=ReadCookBook, status_code=201)
|
||||
@ -85,7 +86,15 @@ class GroupCookbookController(BaseCrudController):
|
||||
|
||||
@router.get("/{item_id}", response_model=RecipeCookBook)
|
||||
def get_one(self, item_id: UUID4 | str):
|
||||
match_attr = "slug" if isinstance(item_id, str) else "id"
|
||||
if isinstance(item_id, UUID):
|
||||
match_attr = "id"
|
||||
else:
|
||||
try:
|
||||
UUID(item_id)
|
||||
match_attr = "id"
|
||||
except ValueError:
|
||||
match_attr = "slug"
|
||||
|
||||
cookbook = self.repo.get_one(item_id, match_attr)
|
||||
|
||||
if cookbook is None:
|
||||
|
@ -58,7 +58,7 @@ class GroupEventsNotifierController(BaseUserController):
|
||||
override=GroupEventNotifierOut,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=GroupEventNotifierOut, status_code=201)
|
||||
|
@ -48,7 +48,7 @@ class MultiPurposeLabelsController(BaseUserController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=MultiPurposeLabelOut)
|
||||
|
@ -31,7 +31,7 @@ class GroupMealplanConfigController(BaseUserController):
|
||||
override=PlanRulesOut,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=PlanRulesOut, status_code=201)
|
||||
|
@ -105,7 +105,7 @@ class ShoppingListItemController(BaseCrudController):
|
||||
@item_router.get("", response_model=ShoppingListItemPagination)
|
||||
def get_all(self, q: PaginationQuery = Depends()):
|
||||
response = self.repo.page_all(pagination=q, override=ShoppingListItemOut)
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@item_router.post("/create-bulk", response_model=ShoppingListItemsCollectionOut, status_code=201)
|
||||
@ -174,7 +174,7 @@ class ShoppingListController(BaseCrudController):
|
||||
override=ShoppingListSummary,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=ShoppingListOut, status_code=201)
|
||||
|
@ -32,7 +32,7 @@ class ReadWebhookController(BaseUserController):
|
||||
override=ReadWebhook,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=ReadWebhook, status_code=201)
|
||||
|
@ -1,7 +1,7 @@
|
||||
from functools import cached_property
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import UUID4, BaseModel
|
||||
from pydantic import UUID4, BaseModel, ConfigDict
|
||||
|
||||
from mealie.routes._base import BaseCrudController, controller
|
||||
from mealie.routes._base.mixins import HttpRepo
|
||||
@ -20,9 +20,7 @@ class CategorySummary(BaseModel):
|
||||
id: UUID4
|
||||
slug: str
|
||||
name: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@controller(router)
|
||||
@ -46,7 +44,7 @@ class RecipeCategoryController(BaseCrudController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", status_code=201)
|
||||
@ -71,7 +69,7 @@ class RecipeCategoryController(BaseCrudController):
|
||||
def get_one(self, item_id: UUID4):
|
||||
"""Returns a list of recipes associated with the provided category."""
|
||||
category_obj = self.mixins.get_one(item_id)
|
||||
category_obj = CategorySummary.from_orm(category_obj)
|
||||
category_obj = CategorySummary.model_validate(category_obj)
|
||||
return category_obj
|
||||
|
||||
@router.put("/{item_id}", response_model=CategorySummary)
|
||||
@ -119,7 +117,7 @@ class RecipeCategoryController(BaseCrudController):
|
||||
def get_one_by_slug(self, category_slug: str):
|
||||
"""Returns a category object with the associated recieps relating to the category"""
|
||||
category: RecipeCategory = self.mixins.get_one(category_slug, "slug")
|
||||
return RecipeCategoryResponse.construct(
|
||||
return RecipeCategoryResponse.model_construct(
|
||||
id=category.id,
|
||||
slug=category.slug,
|
||||
name=category.name,
|
||||
|
@ -35,7 +35,7 @@ class TagController(BaseCrudController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.get("/empty")
|
||||
|
@ -32,7 +32,7 @@ class RecipeToolController(BaseUserController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=RecipeTool, status_code=201)
|
||||
|
@ -1,5 +1,6 @@
|
||||
from functools import cached_property
|
||||
from shutil import copyfileobj
|
||||
from uuid import UUID
|
||||
from zipfile import ZipFile
|
||||
|
||||
import orjson
|
||||
@ -125,7 +126,7 @@ class RecipeExportController(BaseRecipeController):
|
||||
recipe: Recipe = self.mixins.get_one(slug)
|
||||
image_asset = recipe.image_dir.joinpath(RecipeImageTypes.original.value)
|
||||
with ZipFile(temp_path, "w") as myzip:
|
||||
myzip.writestr(f"{slug}.json", recipe.json())
|
||||
myzip.writestr(f"{slug}.json", recipe.model_dump_json())
|
||||
|
||||
if image_asset.is_file():
|
||||
myzip.write(image_asset, arcname=image_asset.name)
|
||||
@ -244,7 +245,14 @@ class RecipeController(BaseRecipeController):
|
||||
):
|
||||
cookbook_data: ReadCookBook | None = None
|
||||
if search_query.cookbook:
|
||||
cb_match_attr = "slug" if isinstance(search_query.cookbook, str) else "id"
|
||||
if isinstance(search_query.cookbook, UUID):
|
||||
cb_match_attr = "id"
|
||||
else:
|
||||
try:
|
||||
UUID(search_query.cookbook)
|
||||
cb_match_attr = "id"
|
||||
except ValueError:
|
||||
cb_match_attr = "slug"
|
||||
cookbook_data = self.cookbooks_repo.get_one(search_query.cookbook, cb_match_attr)
|
||||
|
||||
if cookbook_data is None:
|
||||
@ -265,13 +273,13 @@ class RecipeController(BaseRecipeController):
|
||||
)
|
||||
|
||||
# merge default pagination with the request's query params
|
||||
query_params = q.dict() | {**request.query_params}
|
||||
query_params = q.model_dump() | {**request.query_params}
|
||||
pagination_response.set_pagination_guides(
|
||||
router.url_path_for("get_all"),
|
||||
{k: v for k, v in query_params.items() if v is not None},
|
||||
)
|
||||
|
||||
json_compatible_response = orjson.dumps(pagination_response.dict(by_alias=True))
|
||||
json_compatible_response = orjson.dumps(pagination_response.model_dump(by_alias=True))
|
||||
|
||||
# Response is returned directly, to avoid validation and improve performance
|
||||
return JSONBytes(content=json_compatible_response)
|
||||
|
@ -49,7 +49,7 @@ class RecipeTimelineEventsController(BaseCrudController):
|
||||
override=RecipeTimelineEventOut,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(events_router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(events_router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@events_router.post("", response_model=RecipeTimelineEventOut, status_code=201)
|
||||
|
@ -30,7 +30,7 @@ class RecipeSharedController(BaseUserController):
|
||||
|
||||
@router.post("", response_model=RecipeShareToken, status_code=201)
|
||||
def create_one(self, data: RecipeShareTokenCreate) -> RecipeShareToken:
|
||||
save_data = RecipeShareTokenSave(**data.dict(), group_id=self.group_id)
|
||||
save_data = RecipeShareTokenSave(**data.model_dump(), group_id=self.group_id)
|
||||
return self.mixins.create_one(save_data)
|
||||
|
||||
@router.get("/{item_id}", response_model=RecipeShareToken)
|
||||
|
@ -52,7 +52,7 @@ class IngredientFoodsController(BaseUserController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=IngredientFood, status_code=201)
|
||||
|
@ -52,7 +52,7 @@ class IngredientUnitsController(BaseUserController):
|
||||
search=search,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@router.post("", response_model=IngredientUnit, status_code=201)
|
||||
|
@ -29,7 +29,7 @@ class AdminUserController(BaseAdminController):
|
||||
override=UserOut,
|
||||
)
|
||||
|
||||
response.set_pagination_guides(admin_router.url_path_for("get_all"), q.dict())
|
||||
response.set_pagination_guides(admin_router.url_path_for("get_all"), q.model_dump())
|
||||
return response
|
||||
|
||||
@admin_router.post("", response_model=UserOut, status_code=201)
|
||||
@ -103,7 +103,7 @@ class UserController(BaseUserController):
|
||||
)
|
||||
|
||||
try:
|
||||
self.repos.users.update(item_id, new_data.dict())
|
||||
self.repos.users.update(item_id, new_data.model_dump())
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
|
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, model_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,26 +91,25 @@ 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
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
created_at: datetime | None = None
|
||||
update_at: datetime | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def post_validate(self):
|
||||
# if we're missing a label, but the food has a label, use that as the label
|
||||
if (not self.label) and (self.food and self.food.label):
|
||||
self.label = self.food.label
|
||||
self.label_id = self.label.id
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
getter_dict = ExtrasGetterDict
|
||||
return self
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def loader_options(cls) -> list[LoaderOption]:
|
||||
@ -138,9 +143,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 +158,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 +177,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 +196,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 +227,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, model_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,21 +181,10 @@ 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)
|
||||
recipe.__post_init__()
|
||||
return recipe
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.__post_init__()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
@model_validator(mode="after")
|
||||
def post_validate(self):
|
||||
# the ingredient disable_amount property is unreliable,
|
||||
# so we set it here and recalculate the display property
|
||||
disable_amount = self.settings.disable_amount if self.settings else True
|
||||
@ -198,15 +193,17 @@ 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"):
|
||||
return self
|
||||
|
||||
@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 +212,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 +336,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, model_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
|
||||
@ -32,19 +31,35 @@ def display_fraction(fraction: Fraction):
|
||||
|
||||
|
||||
class UnitFoodBase(MealieModel):
|
||||
id: UUID4 | None = None
|
||||
name: str
|
||||
plural_name: str | None = None
|
||||
description: str = ""
|
||||
extras: dict | None = {}
|
||||
|
||||
@field_validator("id", mode="before")
|
||||
def convert_empty_id_to_none(cls, v):
|
||||
# sometimes the frontend will give us an empty string instead of null, so we convert it to None,
|
||||
# otherwise Pydantic will try to convert it to a UUID and fail
|
||||
if not v:
|
||||
v = None
|
||||
|
||||
return v
|
||||
|
||||
@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 +76,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 +97,7 @@ class CreateIngredientUnitAlias(MealieModel):
|
||||
|
||||
|
||||
class IngredientUnitAlias(CreateIngredientUnitAlias):
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class CreateIngredientUnit(UnitFoodBase):
|
||||
@ -105,8 +116,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 +126,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
|
||||
@ -135,9 +144,8 @@ class RecipeIngredientBase(MealieModel):
|
||||
Automatically calculated after the object is created, unless overwritten
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def post_validate(self):
|
||||
# calculate missing is_food and disable_amount values
|
||||
# we can't do this in a validator since they depend on each other
|
||||
if self.is_food is None and self.disable_amount is not None:
|
||||
@ -152,14 +160,18 @@ class RecipeIngredientBase(MealieModel):
|
||||
if not self.display:
|
||||
self.display = self._format_display()
|
||||
|
||||
@validator("unit", pre=True)
|
||||
return self
|
||||
|
||||
@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 +272,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 +305,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 +316,7 @@ class IngredientConfidence(MealieModel):
|
||||
|
||||
|
||||
class ParsedIngredient(MealieModel):
|
||||
input: str | None
|
||||
input: str | None = None
|
||||
confidence: IngredientConfidence = IngredientConfidence()
|
||||
ingredient: RecipeIngredient
|
||||
|
||||
@ -337,4 +348,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]:
|
||||
|
@ -43,7 +43,7 @@ class BackupContents:
|
||||
|
||||
|
||||
class BackupFile:
|
||||
temp_dir: Path | None
|
||||
temp_dir: Path | None = None
|
||||
|
||||
def __init__(self, file: Path) -> None:
|
||||
self.zip = file
|
||||
|
@ -24,7 +24,7 @@ class EmailTemplate(BaseModel):
|
||||
def render_html(self, template: Path) -> str:
|
||||
tmpl = Template(template.read_text())
|
||||
|
||||
return tmpl.render(data=self.dict())
|
||||
return tmpl.render(data=self.model_dump())
|
||||
|
||||
|
||||
class EmailService(BaseService):
|
||||
|
@ -23,8 +23,8 @@ from .publisher import ApprisePublisher, PublisherLike, WebhookPublisher
|
||||
|
||||
|
||||
class EventListenerBase(ABC):
|
||||
_session: Session | None
|
||||
_repos: AllRepositories | None
|
||||
_session: Session | None = None
|
||||
_repos: AllRepositories | None = None
|
||||
|
||||
def __init__(self, group_id: UUID4, publisher: PublisherLike) -> None:
|
||||
self.group_id = group_id
|
||||
|
@ -38,9 +38,9 @@ class EventSource:
|
||||
|
||||
|
||||
class EventBusService:
|
||||
bg: BackgroundTasks | None
|
||||
session: Session | None
|
||||
group_id: UUID4 | None
|
||||
bg: BackgroundTasks | None = None
|
||||
session: Session | None = None
|
||||
group_id: UUID4 | None = None
|
||||
|
||||
def __init__(
|
||||
self, bg: BackgroundTasks | None = None, session: Session | None = None, group_id: UUID4 | None = None
|
||||
|
@ -3,7 +3,7 @@ from datetime import date, datetime
|
||||
from enum import Enum, auto
|
||||
from typing import Any
|
||||
|
||||
from pydantic import UUID4
|
||||
from pydantic import UUID4, field_validator
|
||||
|
||||
from ...schema._mealie.mealie_model import MealieModel
|
||||
|
||||
@ -85,79 +85,79 @@ class EventDocumentDataBase(MealieModel):
|
||||
|
||||
|
||||
class EventMealplanCreatedData(EventDocumentDataBase):
|
||||
document_type = EventDocumentType.mealplan
|
||||
operation = EventOperation.create
|
||||
document_type: EventDocumentType = EventDocumentType.mealplan
|
||||
operation: EventOperation = EventOperation.create
|
||||
mealplan_id: int
|
||||
date: date
|
||||
recipe_id: UUID4 | None
|
||||
recipe_name: str | None
|
||||
recipe_slug: str | None
|
||||
recipe_id: UUID4 | None = None
|
||||
recipe_name: str | None = None
|
||||
recipe_slug: str | None = None
|
||||
|
||||
|
||||
class EventUserSignupData(EventDocumentDataBase):
|
||||
document_type = EventDocumentType.user
|
||||
operation = EventOperation.create
|
||||
document_type: EventDocumentType = EventDocumentType.user
|
||||
operation: EventOperation = EventOperation.create
|
||||
username: str
|
||||
email: str
|
||||
|
||||
|
||||
class EventCategoryData(EventDocumentDataBase):
|
||||
document_type = EventDocumentType.category
|
||||
document_type: EventDocumentType = EventDocumentType.category
|
||||
category_id: UUID4
|
||||
|
||||
|
||||
class EventCookbookData(EventDocumentDataBase):
|
||||
document_type = EventDocumentType.cookbook
|
||||
document_type: EventDocumentType = EventDocumentType.cookbook
|
||||
cookbook_id: UUID4
|
||||
|
||||
|
||||
class EventCookbookBulkData(EventDocumentDataBase):
|
||||
document_type = EventDocumentType.cookbook
|
||||
document_type: EventDocumentType = EventDocumentType.cookbook
|
||||
cookbook_ids: list[UUID4]
|
||||
|
||||
|
||||
class EventShoppingListData(EventDocumentDataBase):
|
||||
document_type = EventDocumentType.shopping_list
|
||||
document_type: EventDocumentType = EventDocumentType.shopping_list
|
||||
shopping_list_id: UUID4
|
||||
|
||||
|
||||
class EventShoppingListItemData(EventDocumentDataBase):
|
||||
document_type = EventDocumentType.shopping_list_item
|
||||
document_type: EventDocumentType = EventDocumentType.shopping_list_item
|
||||
shopping_list_id: UUID4
|
||||
shopping_list_item_id: UUID4
|
||||
|
||||
|
||||
class EventShoppingListItemBulkData(EventDocumentDataBase):
|
||||
document_type = EventDocumentType.shopping_list_item
|
||||
document_type: EventDocumentType = EventDocumentType.shopping_list_item
|
||||
shopping_list_id: UUID4
|
||||
shopping_list_item_ids: list[UUID4]
|
||||
|
||||
|
||||
class EventRecipeData(EventDocumentDataBase):
|
||||
document_type = EventDocumentType.recipe
|
||||
document_type: EventDocumentType = EventDocumentType.recipe
|
||||
recipe_slug: str
|
||||
|
||||
|
||||
class EventRecipeBulkReportData(EventDocumentDataBase):
|
||||
document_type = EventDocumentType.recipe_bulk_report
|
||||
document_type: EventDocumentType = EventDocumentType.recipe_bulk_report
|
||||
report_id: UUID4
|
||||
|
||||
|
||||
class EventRecipeTimelineEventData(EventDocumentDataBase):
|
||||
document_type = EventDocumentType.recipe_timeline_event
|
||||
document_type: EventDocumentType = EventDocumentType.recipe_timeline_event
|
||||
recipe_slug: str
|
||||
recipe_timeline_event_id: UUID4
|
||||
|
||||
|
||||
class EventTagData(EventDocumentDataBase):
|
||||
document_type = EventDocumentType.tag
|
||||
document_type: EventDocumentType = EventDocumentType.tag
|
||||
tag_id: UUID4
|
||||
|
||||
|
||||
class EventWebhookData(EventDocumentDataBase):
|
||||
webhook_start_dt: datetime
|
||||
webhook_end_dt: datetime
|
||||
webhook_body: Any
|
||||
webhook_body: Any = None
|
||||
|
||||
|
||||
class EventBusMessage(MealieModel):
|
||||
@ -169,6 +169,11 @@ class EventBusMessage(MealieModel):
|
||||
title = event_type.name.replace("_", " ").title()
|
||||
return cls(title=title, body=body)
|
||||
|
||||
@field_validator("body")
|
||||
def populate_body(v):
|
||||
# if the body is empty, apprise won't send the notification
|
||||
return v or "generic"
|
||||
|
||||
|
||||
class Event(MealieModel):
|
||||
message: EventBusMessage
|
||||
@ -177,8 +182,8 @@ class Event(MealieModel):
|
||||
document_data: EventDocumentDataBase
|
||||
|
||||
# set at instantiation
|
||||
event_id: UUID4 | None
|
||||
timestamp: datetime | None
|
||||
event_id: UUID4 | None = None
|
||||
timestamp: datetime | None = None
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
@ -27,7 +27,7 @@ class ExportedItem:
|
||||
|
||||
|
||||
class ABCExporter(BaseService):
|
||||
write_dir_to_zip: Callable[[Path, str, set[str] | None], None] | None
|
||||
write_dir_to_zip: Callable[[Path, str, set[str] | None], None] | None = None
|
||||
|
||||
def __init__(self, db: AllRepositories, group_id: UUID) -> None:
|
||||
self.logger = get_logger()
|
||||
@ -63,7 +63,7 @@ class ABCExporter(BaseService):
|
||||
self.logger.error("Failed to export item. no item found")
|
||||
continue
|
||||
|
||||
zip.writestr(f"{self.destination_dir}/{item.name}/{item.name}.json", item.model.json())
|
||||
zip.writestr(f"{self.destination_dir}/{item.name}/{item.name}.json", item.model.model_dump_json())
|
||||
|
||||
self._post_export_hook(item.model)
|
||||
|
||||
|
@ -18,7 +18,7 @@ from .utils.migration_helpers import MigrationReaders, glob_walker, import_image
|
||||
class NextcloudDir:
|
||||
name: str
|
||||
recipe: dict
|
||||
image: Path | None
|
||||
image: Path | None = None
|
||||
|
||||
@property
|
||||
def slug(self):
|
||||
|
@ -45,7 +45,7 @@ class DatabaseMigrationHelpers:
|
||||
)
|
||||
)
|
||||
|
||||
items_out.append(item_model.dict())
|
||||
items_out.append(item_model.model_dump())
|
||||
return items_out
|
||||
|
||||
def get_or_set_category(self, categories: Iterable[str]) -> list[RecipeCategory]:
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user