cosmo/backend/app/services/nasa_sbdb_service.py

185 lines
6.5 KiB
Python

"""
NASA SBDB (Small-Body Database) Close-Approach Data API Service
Fetches close approach events for asteroids and comets
API Docs: https://ssd-api.jpl.nasa.gov/doc/cad.html
"""
import logging
import httpx
from typing import List, Dict, Optional, Any
from datetime import datetime, timedelta
from app.config import settings
logger = logging.getLogger(__name__)
class NasaSbdbService:
"""NASA Small-Body Database Close-Approach Data API client"""
def __init__(self):
self.base_url = "https://ssd-api.jpl.nasa.gov/cad.api"
self.timeout = settings.nasa_api_timeout or 30
async def get_close_approaches(
self,
date_min: Optional[str] = None,
date_max: Optional[str] = None,
dist_max: Optional[str] = "0.2", # Max distance in AU (Earth-Moon distance ~0.0026 AU)
body: Optional[str] = None,
sort: str = "date",
limit: Optional[int] = None,
fullname: bool = False
) -> List[Dict[str, Any]]:
"""
Query NASA SBDB Close-Approach Data API
Args:
date_min: Minimum approach date (YYYY-MM-DD or 'now')
date_max: Maximum approach date (YYYY-MM-DD)
dist_max: Maximum approach distance in AU (default 0.2 AU)
body: Filter by specific body (e.g., 'Earth')
sort: Sort by 'date', 'dist', 'dist-min', etc.
limit: Maximum number of results
fullname: Return full designation names
Returns:
List of close approach events
"""
params = {
"dist-max": dist_max,
"sort": sort,
"fullname": "true" if fullname else "false"
}
if date_min:
params["date-min"] = date_min
if date_max:
params["date-max"] = date_max
if body:
params["body"] = body
if limit:
params["limit"] = str(limit)
logger.info(f"Querying NASA SBDB for close approaches: {params}")
# Use proxy if configured
proxies = settings.proxy_dict
if proxies:
logger.info(f"Using proxy for NASA SBDB API")
try:
async with httpx.AsyncClient(timeout=self.timeout, proxies=proxies) as client:
response = await client.get(self.base_url, params=params)
response.raise_for_status()
data = response.json()
if "data" not in data:
logger.warning("No data field in NASA SBDB response")
return []
# Parse response
fields = data.get("fields", [])
rows = data.get("data", [])
events = []
for row in rows:
event = dict(zip(fields, row))
events.append(event)
logger.info(f"Retrieved {len(events)} close approach events from NASA SBDB")
return events
except httpx.HTTPStatusError as e:
logger.error(f"NASA SBDB API HTTP error: {e.response.status_code} - {e.response.text}")
return []
except httpx.TimeoutException:
logger.error(f"NASA SBDB API timeout after {self.timeout}s")
return []
except Exception as e:
logger.error(f"Error querying NASA SBDB: {e}")
return []
def parse_event_to_celestial_event(self, sbdb_event: Dict[str, Any], approach_body: str = "Earth") -> Dict[str, Any]:
"""
Parse NASA SBDB event data to CelestialEvent format
Args:
sbdb_event: Event data from NASA SBDB API
approach_body: Name of the body being approached (e.g., "Earth", "Mars")
SBDB fields typically include:
- des: Object designation
- orbit_id: Orbit ID
- jd: Julian Date of close approach
- cd: Calendar date (YYYY-MMM-DD HH:MM)
- dist: Nominal approach distance (AU)
- dist_min: Minimum approach distance (AU)
- dist_max: Maximum approach distance (AU)
- v_rel: Relative velocity (km/s)
- v_inf: Velocity at infinity (km/s)
- t_sigma_f: Time uncertainty (formatted string)
- h: Absolute magnitude
- fullname: Full object name (if requested)
"""
try:
# Extract fields
designation = sbdb_event.get("des", "Unknown")
fullname = sbdb_event.get("fullname", designation)
cd = sbdb_event.get("cd", "") # Calendar date string
dist = sbdb_event.get("dist", "") # Nominal distance in AU
dist_min = sbdb_event.get("dist_min", "")
v_rel = sbdb_event.get("v_rel", "")
# Note: NASA API doesn't return the approach body, so we use the parameter
body = approach_body
# Parse date (format: YYYY-MMM-DD HH:MM)
event_time = datetime.strptime(cd, "%Y-%b-%d %H:%M")
# Create title
title = f"{fullname} Close Approach to {body}"
# Create description
desc_parts = [
f"Asteroid/Comet {fullname} will make a close approach to {body}.",
f"Nominal distance: {dist} AU",
]
if dist_min:
desc_parts.append(f"Minimum distance: {dist_min} AU")
if v_rel:
desc_parts.append(f"Relative velocity: {v_rel} km/s")
description = " ".join(desc_parts)
# Store all technical details in JSONB
details = {
"designation": designation,
"orbit_id": sbdb_event.get("orbit_id"),
"julian_date": sbdb_event.get("jd"),
"nominal_dist_au": dist,
"dist_min_au": dist_min,
"dist_max_au": sbdb_event.get("dist_max"),
"relative_velocity_km_s": v_rel,
"v_inf": sbdb_event.get("v_inf"),
"time_sigma": sbdb_event.get("t_sigma_f"),
"absolute_magnitude": sbdb_event.get("h"),
"approach_body": body
}
return {
"body_id": designation, # Will need to map to celestial_bodies.id
"title": title,
"event_type": "approach",
"event_time": event_time,
"description": description,
"details": details,
"source": "nasa_sbdb"
}
except Exception as e:
logger.error(f"Error parsing SBDB event: {e}")
return None
# Singleton instance
nasa_sbdb_service = NasaSbdbService()