""" Predefined Scheduled Tasks All registered tasks for scheduled execution """ import logging from datetime import datetime, timedelta from typing import Dict, Any, List, Optional from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.dialects.postgresql import insert from app.jobs.registry import task_registry from app.models.db.celestial_body import CelestialBody from app.models.db.position import Position from app.services.horizons import HorizonsService logger = logging.getLogger(__name__) @task_registry.register( name="sync_solar_system_positions", description="同步太阳系天体位置数据,从NASA Horizons API获取指定天体的位置数据并保存到数据库", category="data_sync", parameters=[ { "name": "body_ids", "type": "array", "description": "要同步的天体ID列表,例如['10', '199', '299']。如果不指定,则同步所有活跃的太阳系天体", "required": False, "default": None }, { "name": "days", "type": "integer", "description": "同步天数,从今天开始向未来延伸的天数", "required": False, "default": 7 }, { "name": "source", "type": "string", "description": "数据源标记,用于标识数据来源", "required": False, "default": "nasa_horizons_cron" } ] ) async def sync_solar_system_positions( db: AsyncSession, logger: logging.Logger, params: Dict[str, Any] ) -> Dict[str, Any]: """ Sync solar system body positions from NASA Horizons Args: db: Database session logger: Logger instance params: Task parameters - body_ids: List of body IDs to sync (optional, defaults to all active) - days: Number of days to sync (default: 7) - source: Source tag for the data (default: "nasa_horizons_cron") Returns: Summary of sync operation """ body_ids = params.get("body_ids") days = params.get("days", 7) source = params.get("source", "nasa_horizons_cron") logger.info(f"Starting solar system position sync: days={days}, source={source}") # Get list of bodies to sync if body_ids: # Use specified body IDs result = await db.execute( select(CelestialBody).where( CelestialBody.id.in_(body_ids), CelestialBody.is_active == True ) ) bodies = result.scalars().all() logger.info(f"Syncing {len(bodies)} specified bodies") else: # Get all active solar system bodies # Typically solar system bodies include planets, dwarf planets, and major satellites result = await db.execute( select(CelestialBody).where( CelestialBody.is_active == True, CelestialBody.system_id == 1, CelestialBody.type.in_(['planet', 'dwarf_planet', 'satellite']) ) ) bodies = result.scalars().all() logger.info(f"Syncing all {len(bodies)} active solar system bodies") if not bodies: logger.warning("No bodies found to sync") return { "success": True, "bodies_synced": 0, "total_positions": 0, "message": "No bodies found" } # Initialize services horizons = HorizonsService() # Sync positions for each body total_positions = 0 synced_bodies = [] failed_bodies = [] start_time = datetime.utcnow() end_time = start_time + timedelta(days=days) for body in bodies: # Use savepoint for this body's operations async with db.begin_nested(): # Creates a SAVEPOINT try: logger.debug(f"Fetching positions for {body.name} ({body.id})") # Fetch positions from NASA Horizons positions = await horizons.get_body_positions( body_id=body.id, start_time=start_time, end_time=end_time, step="1d" # Daily positions ) # Save positions to database (upsert logic) count = 0 for pos in positions: # Use PostgreSQL's INSERT ... ON CONFLICT to handle duplicates stmt = insert(Position).values( body_id=body.id, time=pos.time, x=pos.x, y=pos.y, z=pos.z, vx=getattr(pos, 'vx', None), vy=getattr(pos, 'vy', None), vz=getattr(pos, 'vz', None), source=source ) # On conflict (body_id, time), update the existing record stmt = stmt.on_conflict_do_update( index_elements=['body_id', 'time'], set_={ 'x': pos.x, 'y': pos.y, 'z': pos.z, 'vx': getattr(pos, 'vx', None), 'vy': getattr(pos, 'vy', None), 'vz': getattr(pos, 'vz', None), 'source': source } ) await db.execute(stmt) count += 1 # Savepoint will auto-commit if no exception total_positions += count synced_bodies.append(body.name) logger.debug(f"Saved {count} positions for {body.name}") except Exception as e: # Savepoint will auto-rollback on exception logger.error(f"Failed to sync {body.name}: {str(e)}") failed_bodies.append({"body": body.name, "error": str(e)}) # Continue to next body # Summary result = { "success": len(failed_bodies) == 0, "bodies_synced": len(synced_bodies), "total_positions": total_positions, "synced_bodies": synced_bodies, "failed_bodies": failed_bodies, "time_range": f"{start_time.date()} to {end_time.date()}", "source": source } logger.info(f"Sync completed: {len(synced_bodies)} bodies, {total_positions} positions") return result @task_registry.register( name="sync_celestial_events", description="同步天体事件数据(预留功能,暂未实现)", category="data_sync", parameters=[ { "name": "event_types", "type": "array", "description": "事件类型列表,如['eclipse', 'conjunction', 'opposition']", "required": False, "default": None }, { "name": "days_ahead", "type": "integer", "description": "向未来查询的天数", "required": False, "default": 30 } ] ) async def sync_celestial_events( db: AsyncSession, logger: logging.Logger, params: Dict[str, Any] ) -> Dict[str, Any]: """ Sync celestial events (PLACEHOLDER - NOT IMPLEMENTED YET) This is a reserved task for future implementation. It will sync astronomical events like eclipses, conjunctions, oppositions, etc. Args: db: Database session logger: Logger instance params: Task parameters - event_types: Types of events to sync - days_ahead: Number of days ahead to query Returns: Summary of sync operation """ logger.warning("sync_celestial_events is not implemented yet") return { "success": False, "message": "This task is reserved for future implementation", "events_synced": 0 }