116 lines
3.5 KiB
Python
116 lines
3.5 KiB
Python
"""
|
|
API routes for celestial data
|
|
"""
|
|
from datetime import datetime
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from typing import Optional
|
|
import logging
|
|
|
|
from app.models.celestial import (
|
|
CelestialDataResponse,
|
|
BodyInfo,
|
|
CELESTIAL_BODIES,
|
|
)
|
|
from app.services.horizons import horizons_service
|
|
from app.services.cache import cache_service
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/celestial", tags=["celestial"])
|
|
|
|
|
|
@router.get("/positions", response_model=CelestialDataResponse)
|
|
async def get_celestial_positions(
|
|
start_time: Optional[str] = Query(
|
|
None,
|
|
description="Start time in ISO 8601 format (e.g., 2025-01-01T00:00:00Z)",
|
|
),
|
|
end_time: Optional[str] = Query(
|
|
None,
|
|
description="End time in ISO 8601 format",
|
|
),
|
|
step: str = Query(
|
|
"1d",
|
|
description="Time step (e.g., '1d' for 1 day, '12h' for 12 hours)",
|
|
),
|
|
):
|
|
"""
|
|
Get positions of all celestial bodies for a time range
|
|
|
|
If only start_time is provided, returns a single snapshot.
|
|
If both start_time and end_time are provided, returns positions at intervals defined by step.
|
|
"""
|
|
try:
|
|
# Parse time strings
|
|
start_dt = None if start_time is None else datetime.fromisoformat(start_time.replace("Z", "+00:00"))
|
|
end_dt = None if end_time is None else datetime.fromisoformat(end_time.replace("Z", "+00:00"))
|
|
|
|
# Check cache first
|
|
cached_data = cache_service.get(start_dt, end_dt, step)
|
|
if cached_data is not None:
|
|
return CelestialDataResponse(bodies=cached_data)
|
|
|
|
# Query Horizons
|
|
logger.info(f"Fetching celestial data from Horizons: start={start_dt}, end={end_dt}, step={step}")
|
|
bodies = horizons_service.get_all_bodies(start_dt, end_dt, step)
|
|
|
|
# Cache the result
|
|
cache_service.set(bodies, start_dt, end_dt, step)
|
|
|
|
return CelestialDataResponse(bodies=bodies)
|
|
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=f"Invalid time format: {str(e)}")
|
|
except Exception as e:
|
|
logger.error(f"Error fetching celestial positions: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to fetch data: {str(e)}")
|
|
|
|
|
|
@router.get("/info/{body_id}", response_model=BodyInfo)
|
|
async def get_body_info(body_id: str):
|
|
"""
|
|
Get detailed information about a specific celestial body
|
|
|
|
Args:
|
|
body_id: JPL Horizons ID (e.g., '-31' for Voyager 1, '399' for Earth)
|
|
"""
|
|
if body_id not in CELESTIAL_BODIES:
|
|
raise HTTPException(status_code=404, detail=f"Body {body_id} not found")
|
|
|
|
info = CELESTIAL_BODIES[body_id]
|
|
return BodyInfo(
|
|
id=body_id,
|
|
name=info["name"],
|
|
type=info["type"],
|
|
description=info["description"],
|
|
launch_date=info.get("launch_date"),
|
|
status=info.get("status"),
|
|
)
|
|
|
|
|
|
@router.get("/list")
|
|
async def list_bodies():
|
|
"""
|
|
Get a list of all available celestial bodies
|
|
"""
|
|
bodies_list = []
|
|
for body_id, info in CELESTIAL_BODIES.items():
|
|
bodies_list.append(
|
|
{
|
|
"id": body_id,
|
|
"name": info["name"],
|
|
"type": info["type"],
|
|
"description": info["description"],
|
|
}
|
|
)
|
|
return {"bodies": bodies_list}
|
|
|
|
|
|
@router.post("/cache/clear")
|
|
async def clear_cache():
|
|
"""
|
|
Clear the data cache (admin endpoint)
|
|
"""
|
|
cache_service.clear()
|
|
return {"message": "Cache cleared successfully"}
|