from datetime import datetime from decimal import Decimal from typing import Any from uuid import UUID from pydantic import BaseModel, ConfigDict, Field, field_serializer class StrictModel(BaseModel): model_config = ConfigDict(extra="forbid") class AttributeDefinitionIn(StrictModel): key: str value_type: str canonical_unit: str | None = None minimum: Decimal | None = None maximum: Decimal | None = None description: str | None = None class AttributeDefinitionOut(StrictModel): key: str value_type: str canonical_unit: str | None minimum: Decimal | None maximum: Decimal | None description: str | None class ProductIn(StrictModel): id: UUID | None = None category: str brand: str name: str model: str | None = None class ProductOut(StrictModel): id: UUID category: str brand: str name: str model: str | None created_at: datetime class VariantIn(StrictModel): id: UUID | None = None name: str manufacturer_sku: str | None = None identifiers: dict[str, Any] = Field(default_factory=dict) class AttributeIn(StrictModel): id: UUID | None = None attribute: str raw_value: Any normalized_value: Any | None = None unit: str | None = None normalization_status: str normalizer_version: str | None = None confidence: Decimal source_id: str source_reference: str observed_at: datetime normalization_error: str | None = None class AttributeOut(StrictModel): id: UUID variant_id: UUID attribute: str raw_value: Any normalized_value: Any | None unit: str | None normalization_status: str normalizer_version: str confidence: Decimal source_id: str source_reference: str observed_at: datetime ingested_at: datetime normalization_error: str | None class VariantOut(StrictModel): id: UUID product_id: UUID name: str manufacturer_sku: str | None identifiers: dict[str, Any] created_at: datetime attributes: list[AttributeOut] class OfferIn(StrictModel): id: UUID | None = None source_id: str external_id: str seller: str url: str | None = None market: str currency: str condition: str class OfferOut(StrictModel): id: UUID variant_id: UUID source_id: str external_id: str seller: str url: str | None market: str currency: str condition: str created_at: datetime class PriceIn(StrictModel): id: UUID | None = None source_id: str external_observation_id: str observed_at: datetime item_price: Decimal shipping_price: Decimal | None = None currency: str availability: str raw_payload: Any confidence: Decimal @field_serializer("item_price", "shipping_price") def serialize_money(self, value: Decimal | None) -> str | None: return None if value is None else format(value, "f") class PriceOut(StrictModel): id: UUID offer_id: UUID source_id: str external_observation_id: str observed_at: datetime ingested_at: datetime item_price: Decimal shipping_price: Decimal | None currency: str availability: str raw_payload: Any confidence: Decimal @field_serializer("item_price", "shipping_price") def serialize_money(self, value: Decimal | None) -> str | None: return None if value is None else format(Decimal(value).quantize(Decimal("0.01")), "f") class EvaluateIn(StrictModel): profile_id: str profile_version: int offer_id: UUID as_of: datetime | None = None class BuyingProjectIn(StrictModel): id: UUID | None = None name: str product_category_id: str | None = None market: str | None = None currency: str | None = None target_budget: Decimal | None = None template_id: str | None = None template_version: int | None = None profile_id: str | None = None profile_version: int | None = None workspace_id: str | None = None shortlist_size: int = 5 schedule_interval_minutes: int | None = None criteria: dict[str, Any] class BuyingProjectPatch(StrictModel): name: str | None = None target_budget: Decimal | None = None shortlist_size: int | None = None status: str | None = None class ProjectCriteriaIn(StrictModel): items: list[dict[str, Any]] class SourceCategoryIn(StrictModel): source_url: str name: str | None = None product_category_id: str | None = None product_category_name: str | None = None class CriterionDefinitionIn(StrictModel): key: str value_type: str unit: str | None = None semantic: str | None = None category: str | None = None description: str | None = None origin: str | None = None source: str | None = None product_category_id: str | None = None label: str | None = None scoring_attribute_key: str | None = None class DiscoverySearchIn(StrictModel): buying_project_id: UUID | None = None source_id: str source_category_id: UUID schedule_interval_minutes: int = 180 broad_result_threshold: int = 300 class DiscoverySearchPatch(StrictModel): schedule_interval_minutes: int | None = None status: str | None = None class SourceAttributeMappingIn(StrictModel): source_id: str source_category_id: UUID | None = None raw_name: str criterion_definition_id: UUID status: str | None = None class SourceAttributeMappingPatch(StrictModel): raw_name: str | None = None criterion_definition_id: UUID | None = None status: str | None = None class CriterionMappingIn(StrictModel): criterion_definition_id: UUID | None = None criterion_key: str | None = None source_id: str source_category_id: UUID external_filter_key: str source_value: str | None = None encoding: dict[str, Any] = Field(default_factory=lambda: {"url_param": "xf"}) status: str | None = None value_type: str | None = None unit: str | None = None semantic: str | None = None description: str | None = None operator: str | None = None probe_value: Any = None class CriterionMappingPatch(StrictModel): source_value: str | None = None encoding: dict[str, Any] | None = None status: str | None = None probe_value: Any = None operator: str | None = None class Page(StrictModel): items: list[Any] limit: int offset: int total: int def page(items: list[Any], limit: int, offset: int, total: int) -> dict[str, Any]: return {"items": items, "limit": limit, "offset": offset, "total": total} def parse_page(limit: int = 50, offset: int = 0) -> tuple[int, int]: from app.errors import AppError if limit < 1 or limit > 200 or offset < 0: raise AppError(422, "PAGINATION", "limit must be 1-200 and offset >= 0") return limit, offset