71 lines
2.8 KiB
Python
71 lines
2.8 KiB
Python
"""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
|
|
# {"5": n, ..., "1": n} when the source publishes a star breakdown.
|
|
rating_breakdown: Optional[Dict[str, 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")
|