import json import os import sys from datetime import datetime, timezone from typing import Any import dateutil import dateutil.parser import requests data_dir = "/home/edward/lib/data" spacex_dir = os.path.join(data_dir, "spacex") now = datetime.now() here = dateutil.tz.tzlocal() do_refresh = len(sys.argv) > 1 and sys.argv[1] == "--refresh" Launch = dict[str, str | datetime | list[Any]] def filename_timestamp(filename: str) -> tuple[datetime, str] | None: """Get datetime from filename.""" try: ts = datetime.strptime(filename, "%Y-%m-%d_%H:%M:%S.json") except ValueError: return None return (ts, filename) def next_spacex_launch_api(limit: int) -> list[Launch]: """Get the next upcoming launches from the API.""" filename = os.path.join(spacex_dir, now.strftime("%Y-%m-%d_%H:%M:%S.json")) url = "https://api.spacexdata.com/v4/launches/upcoming" params: dict[str, str | int] = dict(tbd="false", limit=limit) r = requests.get(url, params=params) open(filename, "w").write(r.text) launch_list = r.json() if "error" in launch_list: print(r.url) print(launch_list) raise ValueError return [ parse_get_next_spacex_launch(launch) for launch in launch_list if launch["date_precision"] not in ("month", "quarter", "half") ] def get_spacex_payloads() -> dict[str, str]: """Load SpaceX payloads or refresh if missing.""" filename = os.path.join(spacex_dir, "payloads.json") if not os.path.exists(filename): refresh_payloads() data = json.load(open(filename)) # print(json.dumps(data, indent=2)) for p in data: if "type" in p: continue print(json.dumps(p, indent=2)) return {p["id"]: p["type"] for p in data} def get_spacex_rockets() -> dict[str, str]: """Load mapping of rocket ID to rocket name.""" filename = os.path.join(spacex_dir, "rockets.json") return {r["id"]: r["name"] for r in json.load(open(filename))} def refresh_payloads() -> None: """Download list of payloads and save.""" url = "https://api.spacexdata.com/v4/payloads" filename = os.path.join(spacex_dir, "payloads.json") r = requests.get(url) open(filename, "w").write(r.text) rocket_map = get_spacex_rockets() payloads = get_spacex_payloads() def parse_get_next_spacex_launch(launch: dict[str, Any]) -> Launch: global payloads date_utc = dateutil.parser.parse(launch["date_utc"]) date_utc.replace(tzinfo=timezone.utc) # TODO: payload need_refresh = any(p not in payloads for p in launch["payloads"]) if need_refresh: refresh_payloads() payloads = get_spacex_payloads() assert all(p in payloads for p in launch["payloads"]) return { "rocket": rocket_map[launch["rocket"]], "name": launch["name"], "when": date_utc.astimezone(here), "date_precision": launch["date_precision"], "payloads": [payloads.get(p, "[unknown payload]") for p in launch["payloads"]], } def get_most_recent_existing(existing): for _, f in existing: filename = os.path.join(spacex_dir, f) launch_list = json.load(open(filename)) if "error" in launch_list: print(launch_list) continue return launch_list def get_next_spacex_launch(limit: int) -> list[Launch]: filename = os.path.join(data_dir, "next_spacex_launch.json") spacex_dir = os.path.join(data_dir, "spacex") existing = [x for x in (filename_timestamp(f) for f in os.listdir(spacex_dir)) if x] existing.sort(reverse=True) if do_refresh or not existing or (now - existing[0][0]).seconds > 3600: # one hour try: return next_spacex_launch_api(limit=limit) except ValueError: print("*** SpaceX next launch error ***") for _, f in existing: filename = os.path.join(spacex_dir, f) launch_list = json.load(open(filename)) if "error" in launch_list: print(launch_list) continue return [ parse_get_next_spacex_launch(launch) for launch in launch_list if launch["date_precision"] not in ("month", "quarter", "half") ]