feat: Upgrade to Pydantic V2 (#3134)

* bumped pydantic
This commit is contained in:
Michael Genson
2024-02-11 10:47:37 -06:00
committed by GitHub
parent 248459671e
commit 7a107584c7
129 changed files with 1138 additions and 833 deletions

View File

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

View 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):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import string
import unicodedata
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from .._helpers import check_char, move_parens_to_end
@@ -11,9 +11,7 @@ class BruteParsedIngredient(BaseModel):
note: str = ""
amount: float = 1.0
unit: str = ""
class Config:
anystr_strip_whitespace = True
model_config = ConfigDict(str_strip_whitespace=True)
def parse_fraction(x):

View File

@@ -2,8 +2,10 @@ import subprocess
import tempfile
from fractions import Fraction
from pathlib import Path
from typing import Annotated
from pydantic import BaseModel, validator
from pydantic import BaseModel, Field, field_validator
from pydantic_core.core_schema import ValidationInfo
from mealie.schema._mealie.types import NoneFloat
@@ -19,7 +21,7 @@ class CRFConfidence(BaseModel):
comment: NoneFloat = None
name: NoneFloat = None
unit: NoneFloat = None
qty: NoneFloat = None
qty: Annotated[NoneFloat, Field(validate_default=True)] = None
class CRFIngredient(BaseModel):
@@ -31,13 +33,13 @@ class CRFIngredient(BaseModel):
unit: str = ""
confidence: CRFConfidence
@validator("qty", always=True, pre=True)
def validate_qty(qty, values): # sourcery skip: merge-nested-ifs
@field_validator("qty", mode="before")
def validate_qty(qty, info: ValidationInfo): # sourcery skip: merge-nested-ifs
if qty is None or qty == "":
# Check if other contains a fraction
try:
if values["other"] is not None and values["other"].find("/") != -1:
return round(float(Fraction(values["other"])), 3)
if info.data["other"] is not None and info.data["other"].find("/") != -1:
return round(float(Fraction(info.data["other"])), 3)
else:
return 1
except Exception:

View File

@@ -228,7 +228,7 @@ class NLPParser(ABCIngredientParser):
confidence=IngredientConfidence(
quantity=crf_model.confidence.qty,
food=crf_model.confidence.name,
**crf_model.confidence.dict(),
**crf_model.confidence.model_dump(),
),
)

View File

@@ -129,7 +129,7 @@ class RecipeService(BaseService):
data: Recipe = self._recipe_creation_factory(
self.user,
name=create_data.name,
additional_attrs=create_data.dict(),
additional_attrs=create_data.model_dump(),
)
if isinstance(create_data, CreateRecipe) or create_data.settings is None:
@@ -175,11 +175,11 @@ class RecipeService(BaseService):
# if the item exists, return the actual data
query = repo.get_one(slug, "slug")
if query:
return query.dict()
return query.model_dump()
# otherwise, create the item
new_item = repo.create(data)
return new_item.dict()
return new_item.model_dump()
def _process_recipe_data(self, key: str, data: list | dict | Any):
if isinstance(data, list):
@@ -250,20 +250,21 @@ class RecipeService(BaseService):
"""Duplicates a recipe and returns the new recipe."""
old_recipe = self._get_recipe(old_slug)
new_recipe = old_recipe.copy(exclude={"id", "name", "slug", "image", "comments"})
new_recipe_data = old_recipe.model_dump(exclude={"id", "name", "slug", "image", "comments"}, round_trip=True)
new_recipe = Recipe.model_validate(new_recipe_data)
# Asset images in steps directly link to the original recipe, so we
# need to update them to references to the assets we copy below
def replace_recipe_step(step: RecipeStep) -> RecipeStep:
new_step = step.copy(exclude={"id", "text"})
new_step.id = uuid4()
new_step.text = step.text.replace(str(old_recipe.id), str(new_recipe.id))
new_id = uuid4()
new_text = step.text.replace(str(old_recipe.id), str(new_recipe.id))
new_step = step.model_copy(update={"id": new_id, "text": new_text})
return new_step
# Copy ingredients to make them independent of the original
def copy_recipe_ingredient(ingredient: RecipeIngredient):
new_ingredient = ingredient.copy(exclude={"reference_id"})
new_ingredient.reference_id = uuid4()
new_reference_id = uuid4()
new_ingredient = ingredient.model_copy(update={"reference_id": new_reference_id})
return new_ingredient
new_name = dup_data.name if dup_data.name else old_recipe.name or ""
@@ -284,7 +285,7 @@ class RecipeService(BaseService):
new_recipe = self._recipe_creation_factory(
self.user,
new_name,
additional_attrs=new_recipe.dict(),
additional_attrs=new_recipe.model_dump(),
)
new_recipe = self.repos.recipes.create(new_recipe)
@@ -350,7 +351,9 @@ class RecipeService(BaseService):
if recipe is None:
raise exceptions.NoEntryFound("Recipe not found.")
new_data = self.repos.recipes.by_group(self.group.id).patch(recipe.slug, patch_data.dict(exclude_unset=True))
new_data = self.repos.recipes.by_group(self.group.id).patch(
recipe.slug, patch_data.model_dump(exclude_unset=True)
)
self.check_assets(new_data, recipe.slug)
return new_data

View File

@@ -92,7 +92,7 @@ class TemplateService(BaseService):
save_path = self.temp.joinpath(f"{recipe.slug}.json")
with open(save_path, "w") as f:
f.write(recipe.json(indent=4, by_alias=True))
f.write(recipe.model_dump_json(indent=4, by_alias=True))
return save_path
@@ -115,7 +115,7 @@ class TemplateService(BaseService):
template_text = f.read()
template = Template(template_text)
rendered_text = template.render(recipe=recipe.dict(by_alias=True))
rendered_text = template.render(recipe=recipe.model_dump(by_alias=True))
save_name = f"{recipe.slug}{j2_path.suffix}"
@@ -140,7 +140,7 @@ class TemplateService(BaseService):
zip_temp = self.temp.joinpath(f"{recipe.slug}.zip")
with ZipFile(zip_temp, "w") as myzip:
myzip.writestr(f"{recipe.slug}.json", recipe.json())
myzip.writestr(f"{recipe.slug}.json", recipe.model_dump_json())
if image_asset.is_file():
myzip.write(image_asset, arcname=image_asset.name)

View File

@@ -29,7 +29,7 @@ class RegistrationService:
password=hash_password(self.registration.password),
full_name=self.registration.username,
advanced=self.registration.advanced,
group=group.name,
group=group,
can_invite=new_group,
can_manage=new_group,
can_organize=new_group,