mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 18:41:50 +01:00
feat: add type safe Ad model
This commit is contained in:
committed by
Sebastian Thomschke
parent
1369da1c34
commit
6ede14596d
115
src/kleinanzeigen_bot/model/ad_model.py
Normal file
115
src/kleinanzeigen_bot/model/ad_model.py
Normal file
@@ -0,0 +1,115 @@
|
||||
# SPDX-FileCopyrightText: © Sebastian Thomschke and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime # noqa: TC003 Move import into a type-checking block
|
||||
from typing import Any, Dict, Final, List, Literal
|
||||
|
||||
from pydantic import Field, model_validator, validator
|
||||
|
||||
from kleinanzeigen_bot.utils.misc import parse_datetime, parse_decimal
|
||||
from kleinanzeigen_bot.utils.pydantics import ContextualModel
|
||||
|
||||
MAX_DESCRIPTION_LENGTH:Final[int] = 4000
|
||||
|
||||
|
||||
def _iso_datetime_field() -> Any:
|
||||
return Field(
|
||||
default = None,
|
||||
description = "ISO-8601 timestamp with optional timezone (e.g. 2024-12-25T00:00:00 or 2024-12-25T00:00:00Z)",
|
||||
json_schema_extra = {
|
||||
"anyOf": [
|
||||
{"type": "null"},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": (
|
||||
r"^\d{4}-\d{2}-\d{2}T" # date + 'T'
|
||||
r"\d{2}:\d{2}:\d{2}" # hh:mm:ss
|
||||
r"(?:\.\d{1,6})?" # optional .micro
|
||||
r"(?:Z|[+-]\d{2}:\d{2})?$" # optional Z or ±HH:MM
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class ContactPartial(ContextualModel):
|
||||
name:str | None = None
|
||||
street:str | None = None
|
||||
zipcode:int | str | None = None
|
||||
location:str | None = None
|
||||
|
||||
phone:str | None = None
|
||||
|
||||
|
||||
class AdPartial(ContextualModel):
|
||||
active:bool = True
|
||||
type:Literal["OFFER", "WANTED"] = "OFFER"
|
||||
title:str = Field(..., min_length = 10)
|
||||
description:str
|
||||
description_prefix:str | None = None
|
||||
description_suffix:str | None = None
|
||||
category:str
|
||||
special_attributes:Dict[str, str] | None = Field(default = None)
|
||||
price:int | None = None
|
||||
price_type:Literal["FIXED", "NEGOTIABLE", "GIVE_AWAY", "NOT_APPLICABLE"] = "NEGOTIABLE"
|
||||
shipping_type:Literal["PICKUP", "SHIPPING", "NOT_APPLICABLE"] = "SHIPPING"
|
||||
shipping_costs:float | None = None
|
||||
shipping_options:List[str] | None = Field(default = None)
|
||||
sell_directly:bool | None = False
|
||||
images:List[str] | None = Field(default = None)
|
||||
contact:ContactPartial | None = None
|
||||
republication_interval:int = 7
|
||||
|
||||
id:int | None = None
|
||||
created_on:datetime | None = _iso_datetime_field()
|
||||
updated_on:datetime | None = _iso_datetime_field()
|
||||
content_hash:str | None = None
|
||||
|
||||
@validator("created_on", "updated_on", pre = True)
|
||||
@classmethod
|
||||
def _parse_dates(cls, v:Any) -> Any:
|
||||
return parse_datetime(v)
|
||||
|
||||
@validator("shipping_costs", pre = True)
|
||||
@classmethod
|
||||
def _parse_shipping_costs(cls, v:float | int | str) -> Any:
|
||||
if v:
|
||||
return round(parse_decimal(v), 2)
|
||||
return None
|
||||
|
||||
@validator("description")
|
||||
@classmethod
|
||||
def _validate_description_length(cls, v:str) -> str:
|
||||
if len(v) > MAX_DESCRIPTION_LENGTH:
|
||||
raise ValueError(f"description length exceeds {MAX_DESCRIPTION_LENGTH} characters")
|
||||
return v
|
||||
|
||||
@model_validator(mode = "before")
|
||||
@classmethod
|
||||
def _validate_price_and_price_type(cls, values:Dict[str, Any]) -> Dict[str, Any]:
|
||||
price_type = values.get("price_type")
|
||||
price = values.get("price")
|
||||
if price_type == "GIVE_AWAY" and price is not None:
|
||||
raise ValueError("price must not be specified when price_type is GIVE_AWAY")
|
||||
if price_type == "FIXED" and price is None:
|
||||
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
|
||||
|
||||
|
||||
class Contact(ContactPartial):
|
||||
name:str # pyright: ignore[reportGeneralTypeIssues, reportIncompatibleVariableOverride]
|
||||
zipcode:int | str # pyright: ignore[reportGeneralTypeIssues, reportIncompatibleVariableOverride]
|
||||
|
||||
|
||||
class Ad(AdPartial):
|
||||
contact:Contact # pyright: ignore[reportGeneralTypeIssues, reportIncompatibleVariableOverride]
|
||||
Reference in New Issue
Block a user