""" 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"), db: AsyncSession = Depends(get_db) ): """ 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 = await horizons_service.search_body_by_name(name, db) 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, db) 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}