# noqa: E501 """Stdlib HTTP server for the dashboard (spec ยง5). No framework. Exposes the API_CONTRACT endpoints over http.server + a tiny static adapter for the frontend (scraper/dashboard/static/). A thin layer over scraper.dashboard.api: parse HTTP line + query + JSON body, build an api.Request, call api.run, write (status, body) back as JSON. Run: python -m scraper.dashboard.server """ from __future__ import annotations import json import os import sys from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import urlparse, parse_qs from . import api from .api import Request, Services STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static") _CONTENT_TYPES = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json; charset=utf-8", ".svg": "image/svg+xml", ".ico": "image/x-icon", ".png": "image/png", } class DashboardHandler(BaseHTTPRequestHandler): def log_message(self, *args): pass def do_GET(self): return self._dispatch("GET") def do_POST(self): return self._dispatch("POST") def do_PUT(self): return self._dispatch("PUT") def do_DELETE(self): return self._dispatch("DELETE") def _dispatch(self, method): parsed = urlparse(self.path) if not parsed.path.startswith("/api"): return self._serve_static(parsed.path) try: query = parse_qs(parsed.query) flat = {} for k, v in query.items(): flat[k] = v[0] if len(v) == 1 else v body = {} if method in ("POST", "PUT"): body = self._read_json_body() token = self._bearer() req = Request( method=method, path=parsed.path, query=flat, body=body, token=token, services=self.services, user_id=None, ) status, payload = api.run(req) except Exception as exc: status, payload = 500, { "error": {"code": "internal_error", "message": str(exc)}} return self._write_json(status, payload) def _bearer(self): auth = self.headers.get("Authorization", "") if auth.lower().startswith("bearer "): return auth[7:].strip() return None def _read_json_body(self): length = int(self.headers.get("Content-Length") or 0) if length <= 0: return {} raw = self.rfile.read(length) try: data = json.loads(raw.decode("utf-8")) except (ValueError, UnicodeDecodeError): return {} return data if isinstance(data, dict) else {} def _serve_static(self, path): rel = path.lstrip("/") or "index.html" if ".." in rel or rel.startswith("/"): return self._write_raw(404, b"not found", "text/plain") fp = os.path.join(STATIC_DIR, rel) if os.path.isfile(fp): ctype = _CONTENT_TYPES.get( os.path.splitext(fp)[1], "application/octet-stream") with open(fp, "rb") as fh: body = fh.read() return self._write_raw(200, body, ctype) root = os.path.join(STATIC_DIR, "index.html") if os.path.isfile(root): with open(root, "rb") as fh: body = fh.read() return self._write_raw(200, body, "text/html; charset=utf-8") return self._write_raw(404, b"not found", "text/plain") def _write_json(self, status, payload): body = json.dumps(payload, ensure_ascii=False).encode("utf-8") return self._write_raw(status, body, "application/json; charset=utf-8") def _write_raw(self, status, body, ctype): self.send_response(status) self.send_header("Content-Type", ctype) self.send_header("Content-Length", str(len(body))) self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() if status != 204: self.wfile.write(body) def make_handler(services, log_on=False): cls = type("BoundHandler", (DashboardHandler,), {"services": services}) if log_on: cls.log_message = BaseHTTPRequestHandler.log_message return cls def serve(host="127.0.0.1", port=8000, db_path=None, prefs_path="out/prefs.json", auth_path="out/auth.json", serve_static=True, log_on=False): services = Services.default(db_path=db_path, prefs_path=prefs_path, auth_path=auth_path) handler = make_handler(services, log_on=log_on) srv = ThreadingHTTPServer((host, port), handler) sys.stderr.write( "dashboard: http://%s:%d/ (static %s)\n" % (host, port, "on" if serve_static else "off")) try: srv.serve_forever() except KeyboardInterrupt: pass finally: srv.server_close() return 0 def main(argv=None): import argparse p = argparse.ArgumentParser( prog="scraper.dashboard", description="Serve the legislative dashboard (API + static UI).") p.add_argument("--host", default="127.0.0.1") p.add_argument("--port", type=int, default=8000) p.add_argument("--db", default=None, help="SQLite path (default: db/daily_file.sqlite)") p.add_argument("--prefs", default="out/prefs.json", help="preferences JSON file") p.add_argument("--auth", default="out/auth.json", help="local-auth JSON file") p.add_argument("--no-static", dest="static", action="store_false", help="serve API only, no static frontend") p.add_argument("--log", dest="log", action="store_true", help="log HTTP requests to stderr") args = p.parse_args(argv) return serve(host=args.host, port=args.port, db_path=args.db, prefs_path=args.prefs, auth_path=args.auth, serve_static=args.static, log_on=args.log) if __name__ == "__main__": sys.exit(main())