BuyingAgent/backend/app/api/schemas.py
Lars 2117ca6b54
Some checks failed
Deploy Development / deploy (push) Successful in 1m34s
Test Suite / pytest-backend (push) Failing after 16s
Test Suite / smoke-dev (push) Successful in 0s
Test Suite / frontend-checks (push) Successful in 33s
Klappe Geizhals-Filtergruppen beim Lernen auf und nimm neue Kategorien ueber den Quellpfad auf.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-04 15:04:19 +02:00

282 lines
6.7 KiB
Python

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