181 lines
5.7 KiB
Python
181 lines
5.7 KiB
Python
"""Get details of upcoming space launches."""
|
|
|
|
import json
|
|
import os
|
|
import typing
|
|
from datetime import datetime
|
|
|
|
import requests
|
|
|
|
from .types import StrDict
|
|
from .utils import filename_timestamp, get_most_recent_file
|
|
|
|
Launch = dict[str, typing.Any]
|
|
Summary = dict[str, typing.Any]
|
|
|
|
ttl = 60 * 60 * 2 # two hours
|
|
|
|
|
|
def next_launch_api_data(rocket_dir: str, limit: int = 200) -> StrDict | None:
|
|
"""Get the next upcoming launches from the API."""
|
|
now = datetime.now()
|
|
filename = os.path.join(rocket_dir, now.strftime("%Y-%m-%d_%H:%M:%S.json"))
|
|
url = "https://ll.thespacedevs.com/2.2.0/launch/upcoming/"
|
|
|
|
params: dict[str, str | int] = {"limit": limit}
|
|
r = requests.get(url, params=params)
|
|
try:
|
|
data: StrDict = r.json()
|
|
except requests.exceptions.JSONDecodeError:
|
|
return None
|
|
open(filename, "w").write(r.text)
|
|
return data
|
|
|
|
|
|
def next_launch_api(rocket_dir: str, limit: int = 200) -> list[Summary] | None:
|
|
"""Get the next upcoming launches from the API."""
|
|
data = next_launch_api_data(rocket_dir, limit)
|
|
if not data:
|
|
return None
|
|
return [summarize_launch(launch) for launch in data["results"]]
|
|
|
|
|
|
def format_time(time_str: str, net_precision: str) -> tuple[str, str | None]:
|
|
"""Format time based on precision."""
|
|
dt = datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
include_time = False
|
|
# Format the date based on precision
|
|
time_format: str | None
|
|
field2 = None
|
|
|
|
match net_precision:
|
|
case "Year":
|
|
time_format = "%Y"
|
|
case "Month":
|
|
time_format = "%b %Y"
|
|
case "Week":
|
|
time_format = "%d %b %Y"
|
|
field2 = "(Week of)"
|
|
case "Day" | "Hour" | "Minute":
|
|
time_format = "%d %b %Y"
|
|
include_time = net_precision in {"Hour", "Minute"}
|
|
case "Second":
|
|
time_format = "%d %b %Y"
|
|
include_time = True
|
|
case _ if net_precision and net_precision.startswith("Quarter "):
|
|
time_format = f"Q{net_precision[-1]} %Y"
|
|
case _:
|
|
time_format = None
|
|
|
|
if not time_format:
|
|
return (repr(time_str), repr(net_precision))
|
|
|
|
assert time_format
|
|
|
|
formatted = dt.strftime(time_format)
|
|
|
|
if include_time:
|
|
return (formatted, dt.strftime("%H:%M"))
|
|
else:
|
|
return (formatted, field2)
|
|
|
|
|
|
launch_providers = {
|
|
"Indian Space Research Organization": "ISRO",
|
|
"United Launch Alliance": "ULA",
|
|
"Payload Aerospace S.L.": "Payload Aerospace",
|
|
"Russian Federal Space Agency (ROSCOSMOS)": "ROSCOSMOS",
|
|
"China Aerospace Science and Technology Corporation": "CASC",
|
|
}
|
|
|
|
|
|
def get_nested(data: dict[str, typing.Any], keys: list[str]) -> typing.Any | None:
|
|
"""
|
|
Safely get a nested value from a dictionary.
|
|
|
|
Args:
|
|
data (Dict[str, Any]): The dictionary to search.
|
|
keys (List[str]): A list of keys for the nested lookup.
|
|
|
|
Returns:
|
|
Optional[Any]: The retrieved value, or None if any key is missing.
|
|
"""
|
|
for key in keys:
|
|
if data is None or key not in data:
|
|
return None
|
|
data = data[key]
|
|
return data
|
|
|
|
|
|
def summarize_launch(launch: Launch) -> Summary:
|
|
"""Summarize rocket launch."""
|
|
try:
|
|
launch_provider = launch["launch_service_provider"]["name"]
|
|
launch_provider_abbrev = launch_providers.get(launch_provider)
|
|
except (TypeError, IndexError):
|
|
launch_provider = None
|
|
launch_provider_abbrev = None
|
|
|
|
net_precision = typing.cast(str, get_nested(launch, ["net_precision", "name"]))
|
|
t0_date, t0_time = format_time(launch["net"], net_precision)
|
|
|
|
return {
|
|
"name": launch.get("name"),
|
|
"slug": launch["slug"],
|
|
"status": launch.get("status"),
|
|
"net": launch.get("net"),
|
|
"net_precision": net_precision,
|
|
"t0_date": t0_date,
|
|
"t0_time": t0_time,
|
|
"window_start": launch.get("window_start"),
|
|
"window_end": launch.get("window_end"),
|
|
"launch_provider": launch_provider,
|
|
"launch_provider_abbrev": launch_provider_abbrev,
|
|
"launch_provider_type": get_nested(launch, ["launch_service_provider", "type"]),
|
|
"rocket": launch["rocket"]["configuration"],
|
|
"mission": launch.get("mission"),
|
|
"mission_name": get_nested(launch, ["mission", "name"]),
|
|
"pad_name": launch["pad"]["name"],
|
|
"pad_wikipedia_url": launch["pad"]["wiki_url"],
|
|
"location": launch["pad"]["location"]["name"],
|
|
"country_code": launch["pad"]["country_code"],
|
|
"orbit": get_nested(launch, ["mission", "orbit"]),
|
|
}
|
|
|
|
|
|
def load_cached_launches(rocket_dir: str) -> StrDict | None:
|
|
"""Read the most recent cache of launches."""
|
|
filename = get_most_recent_file(rocket_dir, "json")
|
|
return typing.cast(StrDict, json.load(open(filename))) if filename else None
|
|
|
|
|
|
def read_cached_launches(rocket_dir: str) -> list[Summary]:
|
|
"""Read cached launches."""
|
|
data = load_cached_launches(rocket_dir)
|
|
return [summarize_launch(launch) for launch in data["results"]] if data else []
|
|
|
|
|
|
def get_launches(
|
|
rocket_dir: str, limit: int = 200, refresh: bool = False
|
|
) -> list[Summary] | None:
|
|
"""Get rocket launches with caching."""
|
|
now = datetime.now()
|
|
existing = [
|
|
x for x in (filename_timestamp(f, "json") for f in os.listdir(rocket_dir)) if x
|
|
]
|
|
|
|
existing.sort(reverse=True)
|
|
|
|
if refresh or not existing or (now - existing[0][0]).seconds > ttl:
|
|
try:
|
|
return next_launch_api(rocket_dir, limit=limit)
|
|
except Exception:
|
|
pass # fallback to cached version
|
|
|
|
f = existing[0][1]
|
|
|
|
filename = os.path.join(rocket_dir, f)
|
|
data = json.load(open(filename))
|
|
return [summarize_launch(launch) for launch in data["results"]]
|