Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Upgrade to Pydantic V2 #3134

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a7d7932
bumped pydantic
michael-genson Feb 8, 2024
f77c674
ran bump-pydantic to auto-update models
michael-genson Feb 8, 2024
aa44756
fixed most deprecated model methods
michael-genson Feb 8, 2024
c28ec77
added pydantic base settings
michael-genson Feb 8, 2024
fcc4f32
partially fixed validators
michael-genson Feb 8, 2024
94b2d9b
fix always=True
michael-genson Feb 8, 2024
0150ef5
removed allow_reuse flags
michael-genson Feb 9, 2024
d2bc957
fixed missing type annotations
michael-genson Feb 9, 2024
f4d7139
ported old is_classvar from v1
michael-genson Feb 9, 2024
91a61a3
replace from_orm with model_validate
michael-genson Feb 9, 2024
bef518c
fixed default Nones and removed some getter dicts
michael-genson Feb 9, 2024
00bac88
potential fix for extras getterdict
michael-genson Feb 9, 2024
ef3e28c
small fixes
michael-genson Feb 9, 2024
41994b2
fix for extras
michael-genson Feb 9, 2024
bfe1bba
fixed group/group.name conversion
michael-genson Feb 9, 2024
6fa71ff
relax pydantic settings
michael-genson Feb 9, 2024
2ee5700
fixed broken postgres url
michael-genson Feb 9, 2024
79ce9d0
model fix
michael-genson Feb 9, 2024
37060ea
possible fix for auto_init
michael-genson Feb 9, 2024
bc0785a
ruff fixes
michael-genson Feb 9, 2024
ac44559
fixed deprecated .json
michael-genson Feb 10, 2024
c084ae8
probably actually fixed auto_init
michael-genson Feb 10, 2024
818fcc3
fixed bad enum serialization
michael-genson Feb 10, 2024
cea765c
pretty sure pydantic used to do this for us
michael-genson Feb 10, 2024
f8fed04
fixed cookbook slug vs id check
michael-genson Feb 10, 2024
0803ca2
restored old parse_date functionality from pydantic v1
michael-genson Feb 10, 2024
b3ee25f
fix exception serialization
michael-genson Feb 10, 2024
d9013c8
fixed deprecated copy params
michael-genson Feb 10, 2024
9ae3f85
Merge remote-tracking branch 'upstream/mealie-next' into feat/upgrade…
michael-genson Feb 10, 2024
118c310
fix for missing event message
michael-genson Feb 10, 2024
0fe4fdd
fix old types
michael-genson Feb 10, 2024
aa2426b
more ruff fixes
michael-genson Feb 10, 2024
62015ae
Merge branch 'mealie-next' into feat/upgrade-pydantic-v2
michael-genson Feb 10, 2024
eb19784
Merge remote-tracking branch 'upstream/mealie-next' into feat/upgrade…
michael-genson Feb 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mealie/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 12 additions & 7 deletions mealie/core/settings/db_providers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from abc import ABC, abstractmethod
from pathlib import Path

from pydantic import BaseModel, BaseSettings, PostgresDsn
from pydantic import BaseModel, PostgresDsn
from pydantic_settings import BaseSettings, SettingsConfigDict


class AbstractDBProvider(ABC):
Expand Down Expand Up @@ -38,15 +39,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(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostgresDsn changes, pretty sure this is right

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=self.POSTGRES_PASSWORD,
host=host,
path=f"{self.POSTGRES_DB or ''}",
)
)

@property
Expand Down
42 changes: 21 additions & 21 deletions mealie/core/settings/settings.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -100,12 +102,12 @@ def DB_URL_PUBLIC(self) -> str | None:
# ===============================================
# 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
Expand All @@ -122,11 +124,11 @@ def SMTP_ENABLE(self) -> bool:

@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:
Expand All @@ -143,15 +145,15 @@ def validate_smtp(
# 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"
Expand All @@ -173,9 +175,7 @@ def LDAP_ENABLED(self) -> bool:
# 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:
Expand Down
6 changes: 2 additions & 4 deletions mealie/core/settings/themes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseSettings
from pydantic_settings import BaseSettings, SettingsConfigDict


class Theme(BaseSettings):
Expand All @@ -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")
16 changes: 8 additions & 8 deletions mealie/db/models/_model_utils/auto_init.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

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

Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -164,7 +164,7 @@ def wrapper(self: SqlAlchemyBase, *args, **kwargs): # 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:
Expand Down
5 changes: 4 additions & 1 deletion mealie/db/models/_model_utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@ def accepts_kwargs(func: Callable) -> bool:
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)

Expand Down
7 changes: 4 additions & 3 deletions mealie/db/models/group/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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
Expand Down Expand Up @@ -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",
Expand All @@ -91,6 +91,7 @@ class Config:
"mealplans",
"data_exports",
}
)

@auto_init()
def __init__(self, **_) -> None:
Expand Down
5 changes: 2 additions & 3 deletions mealie/db/models/group/report.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down
17 changes: 5 additions & 12 deletions mealie/db/models/group/shopping_list.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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()
Expand Down
7 changes: 4 additions & 3 deletions mealie/db/models/recipe/instruction.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pydantic import ConfigDict
from sqlalchemy import ForeignKey, Integer, String, orm
from sqlalchemy.orm import Mapped, mapped_column

Expand Down Expand Up @@ -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:
Expand Down
11 changes: 6 additions & 5 deletions mealie/db/models/recipe/recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

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
Expand Down Expand Up @@ -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",
Expand All @@ -146,7 +146,8 @@ class Config:
"settings",
"comments",
"timeline_events",
}
},
)

@validates("name")
def validate_name(self, _, name):
Expand Down
7 changes: 4 additions & 3 deletions mealie/db/models/users/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
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
Expand Down Expand Up @@ -84,16 +85,16 @@ 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",
"can_invite",
"can_organize",
"group",
}
)

@hybrid_property
def group_slug(self) -> str:
Expand Down
Loading
Loading