cosmo/backend/app/api/celestial_body.py

220 lines
6.8 KiB
Python

"""
Celestial Body Management API routes
Handles CRUD operations for celestial bodies (planets, dwarf planets, satellites, probes, etc.)
"""
import logging
from fastapi import APIRouter, HTTPException, Depends, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel
from typing import Optional, Dict, Any
from app.database import get_db
from app.models.celestial import BodyInfo
from app.services.horizons import horizons_service
from app.services.db_service import celestial_body_service, resource_service
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/celestial", tags=["celestial-body"])
# Pydantic models for CRUD
class CelestialBodyCreate(BaseModel):
id: str
name: str
name_zh: Optional[str] = None
type: str
description: Optional[str] = None
is_active: bool = True
extra_data: Optional[Dict[str, Any]] = None
class CelestialBodyUpdate(BaseModel):
name: Optional[str] = None
name_zh: Optional[str] = None
type: Optional[str] = None
description: Optional[str] = None
is_active: Optional[bool] = None
extra_data: Optional[Dict[str, Any]] = None
@router.post("", status_code=status.HTTP_201_CREATED)
async def create_celestial_body(
body_data: CelestialBodyCreate,
db: AsyncSession = Depends(get_db)
):
"""Create a new celestial body"""
# Check if exists
existing = await celestial_body_service.get_body_by_id(body_data.id, db)
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Body with ID {body_data.id} already exists"
)
new_body = await celestial_body_service.create_body(body_data.dict(), db)
return new_body
@router.get("/search")
async def search_celestial_body(
name: str = Query(..., description="Body name or ID to search in NASA Horizons")
):
"""
Search for a celestial body in NASA Horizons database by name or ID
Returns body information if found, including suggested ID and full name
"""
logger.info(f"Searching for celestial body: {name}")
try:
result = horizons_service.search_body_by_name(name)
if result["success"]:
logger.info(f"Found body: {result['full_name']}")
return {
"success": True,
"data": {
"id": result["id"],
"name": result["name"],
"full_name": result["full_name"],
}
}
else:
logger.warning(f"Search failed: {result['error']}")
return {
"success": False,
"error": result["error"]
}
except Exception as e:
logger.error(f"Search error: {e}")
raise HTTPException(
status_code=500,
detail=f"Search failed: {str(e)}"
)
@router.get("/{body_id}/nasa-data")
async def get_celestial_nasa_data(
body_id: str,
db: AsyncSession = Depends(get_db)
):
"""
Get raw text data from NASA Horizons for a celestial body
(Hacker terminal style output)
"""
# Check if body exists
body = await celestial_body_service.get_body_by_id(body_id, db)
if not body:
raise HTTPException(status_code=404, detail="Celestial body not found")
try:
# Fetch raw text from Horizons using the body_id
# Note: body.id corresponds to JPL Horizons ID
raw_text = await horizons_service.get_object_data_raw(body.id)
return {"id": body.id, "name": body.name, "raw_data": raw_text}
except Exception as e:
logger.error(f"Failed to fetch raw data for {body_id}: {e}")
raise HTTPException(status_code=500, detail=f"Failed to fetch NASA data: {str(e)}")
@router.put("/{body_id}")
async def update_celestial_body(
body_id: str,
body_data: CelestialBodyUpdate,
db: AsyncSession = Depends(get_db)
):
"""Update a celestial body"""
# Filter out None values
update_data = {k: v for k, v in body_data.dict().items() if v is not None}
updated = await celestial_body_service.update_body(body_id, update_data, db)
if not updated:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Body {body_id} not found"
)
return updated
@router.delete("/{body_id}")
async def delete_celestial_body(
body_id: str,
db: AsyncSession = Depends(get_db)
):
"""Delete a celestial body"""
deleted = await celestial_body_service.delete_body(body_id, db)
if not deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Body {body_id} not found"
)
return {"message": "Body deleted successfully"}
@router.get("/info/{body_id}", response_model=BodyInfo)
async def get_body_info(body_id: str, db: AsyncSession = Depends(get_db)):
"""
Get detailed information about a specific celestial body
Args:
body_id: JPL Horizons ID (e.g., '-31' for Voyager 1, '399' for Earth)
"""
body = await celestial_body_service.get_body_by_id(body_id, db)
if not body:
raise HTTPException(status_code=404, detail=f"Body {body_id} not found")
# Extract extra_data fields
extra_data = body.extra_data or {}
return BodyInfo(
id=body.id,
name=body.name,
type=body.type,
description=body.description,
launch_date=extra_data.get("launch_date"),
status=extra_data.get("status"),
)
@router.get("/list")
async def list_bodies(
body_type: Optional[str] = Query(None, description="Filter by body type"),
db: AsyncSession = Depends(get_db)
):
"""
Get a list of all available celestial bodies
"""
bodies = await celestial_body_service.get_all_bodies(db, body_type)
bodies_list = []
for body in bodies:
# Get resources for this body
resources = await resource_service.get_resources_by_body(body.id, None, db)
# Group resources by type
resources_by_type = {}
for resource in resources:
if resource.resource_type not in resources_by_type:
resources_by_type[resource.resource_type] = []
resources_by_type[resource.resource_type].append({
"id": resource.id,
"file_path": resource.file_path,
"file_size": resource.file_size,
"mime_type": resource.mime_type,
})
bodies_list.append(
{
"id": body.id,
"name": body.name,
"name_zh": body.name_zh,
"type": body.type,
"description": body.description,
"is_active": body.is_active,
"resources": resources_by_type,
"has_resources": len(resources) > 0,
}
)
return {"bodies": bodies_list}