101 lines
3.5 KiB
Python
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']}")
|