diff --git a/src/kleinanzeigen_bot/model/ad_model.py b/src/kleinanzeigen_bot/model/ad_model.py index b2b32eb..1b9594b 100644 --- a/src/kleinanzeigen_bot/model/ad_model.py +++ b/src/kleinanzeigen_bot/model/ad_model.py @@ -5,9 +5,9 @@ from __future__ import annotations import hashlib, json # isort: skip from datetime import datetime # noqa: TC003 Move import into a type-checking block -from typing import Any, Dict, Final, List, Literal, Mapping, Sequence +from typing import Annotated, Any, Dict, Final, List, Literal, Mapping, Sequence -from pydantic import Field, model_validator, validator +from pydantic import AfterValidator, Field, field_validator, model_validator from typing_extensions import Self from kleinanzeigen_bot.model.config_model import AdDefaults # noqa: TC001 Move application import into a type-checking block @@ -52,6 +52,15 @@ class ContactPartial(ContextualModel): phone:str | None = _OPTIONAL() +def _validate_shipping_option_item(v:str) -> str: + if not v.strip(): + raise ValueError("must be non-empty and non-blank") + return v + + +ShippingOption = Annotated[str, AfterValidator(_validate_shipping_option_item)] + + class AdPartial(ContextualModel): active:bool | None = _OPTIONAL() type:Literal["OFFER", "WANTED"] | None = _OPTIONAL() @@ -65,7 +74,7 @@ class AdPartial(ContextualModel): price_type:Literal["FIXED", "NEGOTIABLE", "GIVE_AWAY", "NOT_APPLICABLE"] | None = _OPTIONAL() shipping_type:Literal["PICKUP", "SHIPPING", "NOT_APPLICABLE"] | None = _OPTIONAL() shipping_costs:float | None = _OPTIONAL() - shipping_options:List[str] | None = _OPTIONAL() + shipping_options:List[ShippingOption] | None = _OPTIONAL() sell_directly:bool | None = _OPTIONAL() images:List[str] | None = _OPTIONAL() contact:ContactPartial | None = _OPTIONAL() @@ -76,19 +85,19 @@ class AdPartial(ContextualModel): updated_on:datetime | None = _ISO_DATETIME() content_hash:str | None = _OPTIONAL() - @validator("created_on", "updated_on", pre = True) + @field_validator("created_on", "updated_on", mode = "before") @classmethod def _parse_dates(cls, v:Any) -> Any: return parse_datetime(v) - @validator("shipping_costs", pre = True) + @field_validator("shipping_costs", mode = "before") @classmethod def _parse_shipping_costs(cls, v:float | int | str) -> Any: if v: return round(parse_decimal(v), 2) return None - @validator("description") + @field_validator("description") @classmethod def _validate_description_length(cls, v:str) -> str: if len(v) > MAX_DESCRIPTION_LENGTH: @@ -106,13 +115,6 @@ class AdPartial(ContextualModel): raise ValueError("price is required when price_type is FIXED") return values - @validator("shipping_options", each_item = True) - @classmethod - def _validate_shipping_option(cls, v:str) -> str: - if not v.strip(): - raise ValueError("shipping_options entries must be non-empty") - return v - def update_content_hash(self) -> Self: """Calculate and updates the content_hash value for user-modifiable fields of the ad.""" diff --git a/src/kleinanzeigen_bot/model/config_model.py b/src/kleinanzeigen_bot/model/config_model.py index f86cc05..d069983 100644 --- a/src/kleinanzeigen_bot/model/config_model.py +++ b/src/kleinanzeigen_bot/model/config_model.py @@ -4,9 +4,9 @@ from __future__ import annotations import copy -from typing import Any, List, Literal +from typing import Annotated, Any, List, Literal -from pydantic import Field, model_validator, validator +from pydantic import AfterValidator, Field, model_validator from typing_extensions import deprecated from kleinanzeigen_bot.utils import dicts @@ -102,8 +102,17 @@ class CaptchaConfig(ContextualModel): restart_delay:str = "6h" +def _validate_glob_pattern(v:str) -> str: + if not v.strip(): + raise ValueError("must be a non-empty, non-blank glob pattern") + return v + + +GlobPattern = Annotated[str, AfterValidator(_validate_glob_pattern)] + + class Config(ContextualModel): - ad_files:List[str] = Field( + ad_files:List[GlobPattern] = Field( default_factory = lambda: ["./**/ad_*.{json,yml,yaml}"], min_items = 1, description = """ @@ -137,10 +146,3 @@ Example: return Config.model_validate( dicts.apply_defaults(copy.deepcopy(values), defaults = self.model_dump()) ) - - @validator("ad_files", each_item = True) - @classmethod - def _non_empty_glob_pattern(cls, v:str) -> str: - if not v.strip(): - raise ValueError("ad_files entries must be non-empty glob patterns") - return v