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']}")