Electronics Catalog: API, MCP server, frontend and deployment
Verified catalogue of mobiles and laptops sold in India, collected from real retail listings (FastAPI backend, React frontend, Postgres/pgvector). - REST API under /api/elec (read-only catalogue; admin endpoints need login) - MCP server (FastMCP) at /mcp/ with list_categories, search_products, get_product and price_history tools - Real ratings and reviews read from product pages and search results - Production Dockerfile (requirements-api.txt, no PyTorch) and .env.production.example; remote database only via an explicit ELEC_ALLOW_REMOTE_DB host/name allowlist - docs/API.md: endpoint and MCP reference with live examples Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
68
backend/app/electronics/models.py
Normal file
68
backend/app/electronics/models.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user