cosmo/backend/app/api/star_system.py

269 lines
8.2 KiB
Python

"""
Star System Management API routes
Handles CRUD operations for star systems (Solar System and exoplanet systems)
"""
import logging
from fastapi import APIRouter, HTTPException, Depends, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Optional
from app.database import get_db
from app.models.star_system import (
StarSystemCreate,
StarSystemUpdate,
StarSystemResponse,
StarSystemWithBodies,
StarSystemListResponse,
StarSystemStatistics
)
from app.services.star_system_service import star_system_service
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/star-systems", tags=["star-systems"])
@router.get("", response_model=StarSystemListResponse)
async def get_star_systems(
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(100, ge=1, le=1000, description="Maximum records to return"),
exclude_solar: bool = Query(False, description="Exclude Solar System from results"),
search: Optional[str] = Query(None, description="Search by name (English or Chinese)"),
db: AsyncSession = Depends(get_db)
):
"""
Get all star systems with pagination and optional filtering
Args:
skip: Number of records to skip (for pagination)
limit: Maximum number of records to return
exclude_solar: If True, exclude Solar System (id=1) from results
search: Search keyword to filter by name or host star name
db: Database session
Returns:
List of star systems with total count
"""
systems = await star_system_service.get_all(
db=db,
skip=skip,
limit=limit,
exclude_solar=exclude_solar,
search=search
)
# Get total count for pagination
from sqlalchemy import select, func, or_
from app.models.db.star_system import StarSystem
from app.models.db.celestial_body import CelestialBody
count_query = select(func.count(StarSystem.id))
if exclude_solar:
count_query = count_query.where(StarSystem.id != 1)
if search:
search_pattern = f"%{search}%"
count_query = count_query.where(
or_(
StarSystem.name.ilike(search_pattern),
StarSystem.name_zh.ilike(search_pattern),
StarSystem.host_star_name.ilike(search_pattern)
)
)
result = await db.execute(count_query)
total = result.scalar()
# Get star counts for all systems
system_ids = [s.id for s in systems]
star_counts_query = select(
CelestialBody.system_id,
func.count(CelestialBody.id).label('star_count')
).where(
CelestialBody.system_id.in_(system_ids),
CelestialBody.type == 'star'
).group_by(CelestialBody.system_id)
star_counts_result = await db.execute(star_counts_query)
star_counts_dict = {row.system_id: row.star_count for row in star_counts_result.all()}
# Build response with star_count
systems_response = []
for system in systems:
system_dict = StarSystemResponse.from_orm(system).dict()
system_dict['star_count'] = star_counts_dict.get(system.id, 1) # Default to 1 if no stars found
systems_response.append(StarSystemResponse(**system_dict))
return StarSystemListResponse(
total=total,
systems=systems_response
)
@router.get("/statistics", response_model=StarSystemStatistics)
async def get_statistics(db: AsyncSession = Depends(get_db)):
"""
Get star system statistics
Returns:
- Total star systems count
- Exoplanet systems count
- Total planets count (Solar System + exoplanets)
- Nearest star systems (top 10)
"""
stats = await star_system_service.get_statistics(db)
return StarSystemStatistics(**stats)
@router.get("/{system_id}", response_model=StarSystemResponse)
async def get_star_system(
system_id: int,
db: AsyncSession = Depends(get_db)
):
"""
Get a single star system by ID
Args:
system_id: Star system ID (1 = Solar System, 2+ = Exoplanet systems)
"""
system = await star_system_service.get_by_id(db, system_id)
if not system:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Star system with ID {system_id} not found"
)
return StarSystemResponse.from_orm(system)
@router.get("/{system_id}/bodies", response_model=StarSystemWithBodies)
async def get_star_system_with_bodies(
system_id: int,
db: AsyncSession = Depends(get_db)
):
"""
Get a star system with all its celestial bodies
Args:
system_id: Star system ID
Returns:
Star system details along with list of all celestial bodies
(stars, planets, dwarf planets, satellites, probes, comets, etc.)
"""
result = await star_system_service.get_with_bodies(db, system_id)
if not result:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Star system with ID {system_id} not found"
)
# Convert ORM objects to dicts
system_dict = StarSystemResponse.from_orm(result["system"]).dict()
bodies_list = [
{
"id": body.id,
"name": body.name,
"name_zh": body.name_zh,
"type": body.type,
"description": body.description,
"details": body.details,
"is_active": body.is_active,
"extra_data": body.extra_data,
}
for body in result["bodies"]
]
return StarSystemWithBodies(
**system_dict,
bodies=bodies_list,
body_count=result["body_count"]
)
@router.post("", status_code=status.HTTP_201_CREATED, response_model=StarSystemResponse)
async def create_star_system(
system_data: StarSystemCreate,
db: AsyncSession = Depends(get_db)
):
"""
Create a new star system
Note: This is an admin operation. Use with caution.
"""
# Check if name already exists
existing = await star_system_service.get_by_name(db, system_data.name)
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Star system with name '{system_data.name}' already exists"
)
new_system = await star_system_service.create(db, system_data.dict())
return StarSystemResponse.from_orm(new_system)
@router.put("/{system_id}", response_model=StarSystemResponse)
async def update_star_system(
system_id: int,
system_data: StarSystemUpdate,
db: AsyncSession = Depends(get_db)
):
"""
Update a star system
Args:
system_id: Star system ID to update
system_data: Fields to update (only non-null fields will be updated)
"""
# Filter out None values
update_data = {k: v for k, v in system_data.dict().items() if v is not None}
if not update_data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No fields to update"
)
updated_system = await star_system_service.update(db, system_id, update_data)
if not updated_system:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Star system with ID {system_id} not found"
)
return StarSystemResponse.from_orm(updated_system)
@router.delete("/{system_id}")
async def delete_star_system(
system_id: int,
db: AsyncSession = Depends(get_db)
):
"""
Delete a star system and all its celestial bodies
WARNING: This will cascade delete all celestial bodies in this system!
Cannot delete Solar System (id=1).
"""
if system_id == 1:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Cannot delete Solar System"
)
try:
deleted = await star_system_service.delete_system(db, system_id)
if not deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Star system with ID {system_id} not found"
)
return {
"message": f"Star system {system_id} and all its bodies deleted successfully"
}
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=str(e)
)