← back to Unclaimed Property Platform
services/search/run_local.py
99 lines
"""Runnable anonymous-search HTTP endpoint on the Python stdlib (no third-party deps, $0).
This is the dependency-free transport over SearchService — it lets the whole anti-enumeration
+ masking path run and be smoke-tested without FastAPI/uvicorn/OpenSearch. Production uses
the FastAPI reference in search_api.py; this proves the core end-to-end over real HTTP.
python -m services.search.run_local # seeds synthetic data, serves :8799
curl 'http://127.0.0.1:8799/v1/search?q=Oneil'
curl 'http://127.0.0.1:8799/healthz'
Env: UPP_PORT (default 8799), UPP_RATE_MAX (default 30), UPP_RATE_WINDOW (default 60).
Client-key = the direct peer IP (trusted for a local direct-connect server). Behind a real
proxy, derive the key from the trusted proxy hop, never a raw X-Forwarded-For header.
"""
from __future__ import annotations
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlparse
from services.common.rate_limit import SlidingWindowRateLimiter
from services.common.sqlite_repo import FileObjectStore, SqliteRepository
from services.ingestion.ingest import FeedDefinition, ingest_authorized_feed
from services.search.service import EmptyQuery, RateLimited, SearchService
_SAMPLE = Path(__file__).resolve().parents[2] / "data" / "sample" / "sample_state_feed.csv"
def build_service() -> SearchService:
import tempfile
tmp = Path(tempfile.mkdtemp(prefix="upp-serve-"))
store = FileObjectStore(tmp / "obj")
(tmp / "obj").mkdir(parents=True, exist_ok=True)
store.write_bytes("incoming/s.csv", _SAMPLE.read_bytes())
repo = SqliteRepository(str(tmp / "serve.db"))
ingest_authorized_feed(FeedDefinition("SAMPLE", "incoming/s.csv"), store, repo)
limiter = SlidingWindowRateLimiter(
max_requests=int(os.environ.get("UPP_RATE_MAX", "30")),
window_seconds=float(os.environ.get("UPP_RATE_WINDOW", "60")),
)
return SearchService(repository=repo, limiter=limiter)
def make_handler(service: SearchService):
class Handler(BaseHTTPRequestHandler):
def _json(self, code: int, body: dict, headers: dict | None = None) -> None:
payload = json.dumps(body).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
for k, v in (headers or {}).items():
self.send_header(k, str(v))
self.end_headers()
self.wfile.write(payload)
def do_GET(self) -> None: # noqa: N802
parsed = urlparse(self.path)
if parsed.path == "/healthz":
self._json(200, {"status": "ok"})
return
if parsed.path != "/v1/search":
self._json(404, {"error": "not found"})
return
qs = parse_qs(parsed.query)
query = (qs.get("q", [""])[0])
client_key = f"ip:{self.client_address[0]}"
try:
results = service.search(query, client_key=client_key)
self._json(200, {"results": results, "count": len(results)})
except EmptyQuery as e:
self._json(400, {"error": str(e)})
except RateLimited as e:
self._json(429, {"error": "rate limited"},
headers={"Retry-After": int(e.retry_after) + 1})
def log_message(self, *args) -> None: # quiet
pass
return Handler
def main() -> int:
port = int(os.environ.get("UPP_PORT", "8799"))
service = build_service()
# Single-threaded: the SQLite connection is bound to this thread. Requests serialize,
# which is fine for a local demo. Production (Postgres + connection pool) is thread-safe.
httpd = HTTPServer(("127.0.0.1", port), make_handler(service))
print(f"serving synthetic unclaimed-property search on http://127.0.0.1:{port}")
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
return 0
if __name__ == "__main__":
raise SystemExit(main())