""" Tiny stand-in for the Tawhiri prediction service. Responds to Tawhiri v2's GET query-string endpoint with a synthetic trajectory so stratoflights has something to forward back to leaflet_svelte during e2e tests. Start: python3 /tmp/fake_tawhiri.py Then tell stratoflights to use it: TAWHIRI_BASE_URL=http://localhost:8001/api/v2/ ... python3 manage.py runserver """ from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import parse_qs, urlparse from datetime import datetime, timedelta, timezone import json def _iso(dt: datetime) -> str: return dt.isoformat().replace("+00:00", "Z") def build_prediction(params): try: launch_dt = datetime.fromisoformat( params.get("launch_datetime", "2026-05-01T12:00:00Z").replace("Z", "+00:00"), ) except Exception: launch_dt = datetime.now(timezone.utc) launch_lat = float(params.get("launch_latitude", 62.0)) launch_lng = float(params.get("launch_longitude", 129.0)) launch_alt = float(params.get("launch_altitude", 0.0)) burst_alt = float(params.get("burst_altitude", 30000.0)) ascent_rate = float(params.get("ascent_rate", 5.0)) descent_rate = float(params.get("descent_rate", 5.0)) ascent_duration_s = int((burst_alt - launch_alt) / max(0.1, ascent_rate)) descent_duration_s = int(burst_alt / max(0.1, descent_rate)) step = 30 ascent = [] for t in range(0, ascent_duration_s + 1, step): ascent.append({ "altitude": launch_alt + t * ascent_rate, "datetime": _iso(launch_dt + timedelta(seconds=t)), "latitude": launch_lat + t * 0.00002, "longitude": launch_lng + t * 0.00005, }) descent = [] burst_dt = launch_dt + timedelta(seconds=ascent_duration_s) burst_lat = launch_lat + ascent_duration_s * 0.00002 burst_lng = launch_lng + ascent_duration_s * 0.00005 for t in range(0, descent_duration_s + 1, step): alt = max(0.0, burst_alt - t * descent_rate) descent.append({ "altitude": alt, "datetime": _iso(burst_dt + timedelta(seconds=t)), "latitude": burst_lat + t * 0.00001, "longitude": burst_lng + t * 0.00003, }) return { "metadata": { "start_datetime": _iso(launch_dt - timedelta(hours=1)), "complete_datetime": _iso(burst_dt + timedelta(seconds=descent_duration_s)), }, "prediction": [ {"stage": "ascent", "trajectory": ascent}, {"stage": "descent", "trajectory": descent}, ], "request": { "dataset": "fake", "launch_latitude": launch_lat, "launch_longitude": launch_lng, "launch_altitude": launch_alt, }, } class Handler(BaseHTTPRequestHandler): def do_GET(self): parsed = urlparse(self.path) if not parsed.path.rstrip("/").endswith("/api/v2"): self.send_error(404) return params = {k: v[0] for k, v in parse_qs(parsed.query).items()} body = json.dumps(build_prediction(params)).encode() self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(body) def log_message(self, format, *args): # Quiet default access log; keep stderr clean. pass if __name__ == "__main__": port = 8001 server = HTTPServer(("127.0.0.1", port), Handler) print(f"fake-tawhiri listening on http://127.0.0.1:{port}/api/v2/") server.serve_forever()