"""The record a collector produces for one product page / search result.""" from __future__ import annotations from dataclasses import dataclass, field from decimal import Decimal from typing import Any, Dict, List, Optional SOURCE_TYPES = ("scraped_page", "search_snippet", "brand_official") @dataclass class Listing: site_domain: str source_sku: str source_url: str source_type: str brand_slug: str category: str title: str evidence_text: str confidence: float parser: str family: Optional[str] = None model: Optional[str] = None model_number: Optional[str] = None ram_gb: Optional[Decimal] = None storage_gb: Optional[Decimal] = None colour: Optional[str] = None price: Optional[Decimal] = None mrp: Optional[Decimal] = None availability: Optional[str] = None in_stock: Optional[bool] = None pincode: Optional[str] = None pincode_applied: bool = False rating: Optional[Decimal] = None review_count: Optional[int] = None # Customer reviews the page itself publishes (schema.org Review); stored # in elec.listing_review, not on the listing row. reviews: List[Dict[str, Any]] = field(default_factory=list) gtin: Optional[str] = None image_urls: List[str] = field(default_factory=list) specs_raw: Dict[str, Any] = field(default_factory=dict) specs: Dict[str, Any] = field(default_factory=dict) spec_sources: Dict[str, str] = field(default_factory=dict) search_query: Optional[str] = None content_hash: Optional[str] = None # Not stored on the listing; used for matching. variant_key: Optional[str] = None model_norm: Optional[str] = None processor: Optional[str] = None def validate(self) -> None: """The anti-fabrication contract, checked before anything is written.""" if self.source_type not in SOURCE_TYPES: raise ValueError(f"bad source_type {self.source_type!r}") if not self.source_url.startswith(("http://", "https://")): raise ValueError("listing without a real source URL") if not self.evidence_text.strip(): raise ValueError("listing without evidence text") if self.price is not None: if not (Decimal(500) <= self.price <= Decimal(1000000)): raise ValueError(f"implausible price {self.price}") if self.mrp is not None and self.price is not None and self.mrp < self.price: self.mrp = None if self.pincode_applied and not self.pincode: raise ValueError("pincode_applied without a pincode") if not 0 <= self.confidence <= 1: raise ValueError("confidence out of range")