Files
argus-nexus/backend/services/launches.py
T
2026-03-09 22:07:19 +01:00

101 lines
3.5 KiB
Python

import httpx
import asyncio
from datetime import datetime, timezone
# Launch Library 2 API — completely free, no API key required
UPCOMING_URL = "https://ll.thespacedevs.com/2.2.0/launch/upcoming/?limit=25&format=json"
RECENT_URL = "https://ll.thespacedevs.com/2.2.0/launch/previous/?limit=10&format=json"
def _parse_launch(item: dict) -> dict | None:
"""Parse a single launch object from the LL2 API response."""
pad = item.get("pad") or {}
location = pad.get("location") or {}
try:
lat = float(pad.get("latitude", ""))
lon = float(pad.get("longitude", ""))
except (ValueError, TypeError):
return None
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
return None
provider = (item.get("launch_service_provider") or {}).get("name", "Unknown")
rocket = ((item.get("rocket") or {}).get("configuration") or {}).get("name", "Unknown")
mission = item.get("mission") or {}
status = (item.get("status") or {}).get("name", "Unknown")
return {
"id": item.get("id", ""),
"name": item.get("name", "Unknown Launch"),
"status": status,
"net": item.get("net", ""),
"provider": provider,
"country": location.get("country_code", ""),
"pad": pad.get("name", "Unknown Pad"),
"lat": lat,
"lon": lon,
"image": item.get("image", ""),
"rocket": rocket,
"mission": mission.get("name") or "",
"mission_description": mission.get("description") or "",
"type": "launch",
}
async def fetch_launches() -> list:
"""
Fetches real rocket launch data from Launch Library 2.
Returns combined upcoming + recent launches, sorted by date.
"""
try:
async with httpx.AsyncClient() as client:
upcoming_resp, recent_resp = await asyncio.gather(
client.get(UPCOMING_URL, timeout=15.0),
client.get(RECENT_URL, timeout=15.0),
)
upcoming = []
if upcoming_resp.status_code == 200:
for item in upcoming_resp.json().get("results", []):
parsed = _parse_launch(item)
if parsed:
upcoming.append(parsed)
else:
print(f"[LAUNCHES] Upcoming API returned {upcoming_resp.status_code}")
recent = []
if recent_resp.status_code == 200:
for item in recent_resp.json().get("results", []):
parsed = _parse_launch(item)
if parsed:
recent.append(parsed)
else:
print(f"[LAUNCHES] Recent API returned {recent_resp.status_code}")
combined = upcoming + recent
# Sort by NET date (earliest first), unknown dates go last
def sort_key(launch):
try:
return datetime.fromisoformat(launch["net"].replace("Z", "+00:00"))
except Exception:
return datetime.max.replace(tzinfo=timezone.utc)
combined.sort(key=sort_key)
print(f"[LAUNCHES] {len(upcoming)} upcoming, {len(recent)} recent launches")
return combined
except Exception as e:
print(f"[LAUNCHES] Fetch error: {e}")
return []
if __name__ == "__main__":
result = asyncio.run(fetch_launches())
print(f"Total launches: {len(result)}")
for launch in result[:5]:
print(f" {launch['net'][:16]} {launch['status']:15s} {launch['name']}")