2026-10-04 13:04:59 +02:00
|
|
|
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]]
|
|
|
|
|
|
|
|
|
|
|
2026-10-04 15:04:19 +02:00
|
|
|
class SourceCategoryIn(StrictModel):
|
|
|
|
|
source_url: str
|
|
|
|
|
name: str | None = None
|
|
|
|
|
product_category_id: str | None = None
|
|
|
|
|
product_category_name: str | None = None
|
|
|
|
|
|
|
|
|
|
|
2026-10-04 13:04:59 +02:00
|
|
|
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
|