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:
61
backend/app/electronics/net/breaker.py
Normal file
61
backend/app/electronics/net/breaker.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Per-host circuit breaker.
|
||||
|
||||
One 403, 429, 503 or CAPTCHA page opens the breaker for that host for
|
||||
ELEC_BREAKER_COOLDOWN_HOURS. While it is open the host is not requested at all
|
||||
and its products are collected from web search results instead. There is no
|
||||
retry-with-a-different-identity: a block is an answer.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable, Dict, Optional, Tuple
|
||||
|
||||
from app.infrastructure.settings import ELEC_BREAKER_COOLDOWN_HOURS
|
||||
|
||||
|
||||
class CircuitBreaker:
|
||||
def __init__(
|
||||
self,
|
||||
cooldown_seconds: float = ELEC_BREAKER_COOLDOWN_HOURS * 3600,
|
||||
on_trip: Optional[Callable[[str, str, float], None]] = None,
|
||||
clock: Callable[[], float] = time.time,
|
||||
) -> None:
|
||||
self.cooldown = cooldown_seconds
|
||||
self.on_trip = on_trip
|
||||
self._clock = clock
|
||||
self._open: Dict[str, Tuple[float, str]] = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
@staticmethod
|
||||
def _key(host: str) -> str:
|
||||
host = host.lower()
|
||||
return host[4:] if host.startswith("www.") else host
|
||||
|
||||
def preload(self, host: str, until_epoch: float, reason: str) -> None:
|
||||
"""Restore a breaker that was opened in an earlier run (elec.site)."""
|
||||
if until_epoch > self._clock():
|
||||
with self._lock:
|
||||
self._open[self._key(host)] = (until_epoch, reason)
|
||||
|
||||
def trip(self, host: str, reason: str) -> None:
|
||||
until = self._clock() + self.cooldown
|
||||
with self._lock:
|
||||
self._open[self._key(host)] = (until, reason)
|
||||
if self.on_trip:
|
||||
self.on_trip(self._key(host), reason, until)
|
||||
|
||||
def is_open(self, host: str) -> bool:
|
||||
key = self._key(host)
|
||||
with self._lock:
|
||||
entry = self._open.get(key)
|
||||
if not entry:
|
||||
return False
|
||||
if entry[0] <= self._clock():
|
||||
del self._open[key]
|
||||
return False
|
||||
return True
|
||||
|
||||
def reason(self, host: str) -> Optional[str]:
|
||||
entry = self._open.get(self._key(host))
|
||||
return entry[1] if entry else None
|
||||
Reference in New Issue
Block a user