""" 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 copy import json import os import sys # Optional: serve a canned prediction (stratoflights Prediction dump / Tawhiri # JSON) instead of synthesizing one. Point datetimes are shifted so the # trajectory starts at the requested launch_datetime. TRAJ_FILE = os.environ.get("FAKE_TAWHIRI_TRAJECTORY") def _iso(dt: datetime) -> str: return dt.isoformat().replace("+00:00", "Z") def _parse(dt: str) -> datetime: return datetime.fromisoformat(dt.replace("Z", "+00:00")) def build_from_file(params): d = json.load(open(TRAJ_FILE)) res = d.get("result") or d stages = copy.deepcopy(res["prediction"]) try: launch_dt = _parse(params.get("launch_datetime")) except Exception: launch_dt = datetime.now(timezone.utc) t0 = _parse(stages[0]["trajectory"][0]["datetime"]) delta = launch_dt - t0 last = None for stage in stages: for p in stage["trajectory"]: p["datetime"] = _iso(_parse(p["datetime"]) + delta) last = p return { "metadata": { "start_datetime": _iso(launch_dt - timedelta(hours=1)), "complete_datetime": last["datetime"], }, "prediction": stages, "request": res.get("request", {}), } 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) path = parsed.path.rstrip("/") # /api/v2 — legacy e2e endpoint; /api/v1/prediction — what # stratoflights' TawhiriClient (Go predictor URL) actually calls. if not (path.endswith("/api/v2") or path.endswith("/api/v1/prediction")): self.send_error(404) return params = {k: v[0] for k, v in parse_qs(parsed.query).items()} builder = build_from_file if TRAJ_FILE else build_prediction body = json.dumps(builder(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 = int(sys.argv[1]) if len(sys.argv) > 1 else 8001 server = HTTPServer(("127.0.0.1", port), Handler) src = f"file {TRAJ_FILE}" if TRAJ_FILE else "synthetic" print(f"fake-tawhiri listening on http://127.0.0.1:{port}/ ({src})") server.serve_forever()