Initial commit
This commit is contained in:
commit
5f2db08b43
100 files changed
+16985
No files matched your search
@@ -0,0 +1,186 @@
|
||||
# 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())
|
||||
Reference in new issue
Block a user