|
|
""" |
|
|
Integrated API Router |
|
|
Combines all services for a comprehensive backend API |
|
|
""" |
|
|
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, HTTPException, BackgroundTasks |
|
|
from fastapi.responses import FileResponse, JSONResponse |
|
|
from typing import Optional, List, Dict, Any |
|
|
from datetime import datetime |
|
|
import logging |
|
|
import uuid |
|
|
import os |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
router = APIRouter(prefix="/api/v2", tags=["Integrated API"]) |
|
|
|
|
|
|
|
|
config_loader = None |
|
|
scheduler_service = None |
|
|
persistence_service = None |
|
|
websocket_service = None |
|
|
|
|
|
|
|
|
def set_services(config, scheduler, persistence, websocket): |
|
|
"""Set service instances""" |
|
|
global config_loader, scheduler_service, persistence_service, websocket_service |
|
|
config_loader = config |
|
|
scheduler_service = scheduler |
|
|
persistence_service = persistence |
|
|
websocket_service = websocket |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.websocket("/ws") |
|
|
async def websocket_endpoint(websocket: WebSocket): |
|
|
"""WebSocket endpoint for real-time updates""" |
|
|
client_id = str(uuid.uuid4()) |
|
|
|
|
|
try: |
|
|
await websocket_service.connection_manager.connect( |
|
|
websocket, |
|
|
client_id, |
|
|
metadata={'connected_at': datetime.now().isoformat()} |
|
|
) |
|
|
|
|
|
|
|
|
await websocket_service.connection_manager.send_personal_message({ |
|
|
'type': 'connected', |
|
|
'client_id': client_id, |
|
|
'message': 'Connected to crypto data tracker' |
|
|
}, client_id) |
|
|
|
|
|
|
|
|
while True: |
|
|
data = await websocket.receive_json() |
|
|
await websocket_service.handle_client_message(websocket, client_id, data) |
|
|
|
|
|
except WebSocketDisconnect: |
|
|
websocket_service.connection_manager.disconnect(client_id) |
|
|
except Exception as e: |
|
|
logger.error(f"WebSocket error for client {client_id}: {e}") |
|
|
websocket_service.connection_manager.disconnect(client_id) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/config/apis") |
|
|
async def get_all_apis(): |
|
|
"""Get all configured APIs""" |
|
|
return { |
|
|
'apis': config_loader.get_all_apis(), |
|
|
'total': len(config_loader.apis) |
|
|
} |
|
|
|
|
|
|
|
|
@router.get("/config/apis/{api_id}") |
|
|
async def get_api(api_id: str): |
|
|
"""Get specific API configuration""" |
|
|
api = config_loader.apis.get(api_id) |
|
|
|
|
|
if not api: |
|
|
raise HTTPException(status_code=404, detail="API not found") |
|
|
|
|
|
return api |
|
|
|
|
|
|
|
|
@router.get("/config/categories") |
|
|
async def get_categories(): |
|
|
"""Get all API categories""" |
|
|
categories = config_loader.get_categories() |
|
|
|
|
|
category_stats = {} |
|
|
for category in categories: |
|
|
apis = config_loader.get_apis_by_category(category) |
|
|
category_stats[category] = { |
|
|
'count': len(apis), |
|
|
'apis': list(apis.keys()) |
|
|
} |
|
|
|
|
|
return { |
|
|
'categories': categories, |
|
|
'stats': category_stats |
|
|
} |
|
|
|
|
|
|
|
|
@router.get("/config/apis/category/{category}") |
|
|
async def get_apis_by_category(category: str): |
|
|
"""Get APIs by category""" |
|
|
apis = config_loader.get_apis_by_category(category) |
|
|
|
|
|
return { |
|
|
'category': category, |
|
|
'apis': apis, |
|
|
'count': len(apis) |
|
|
} |
|
|
|
|
|
|
|
|
@router.post("/config/apis") |
|
|
async def add_custom_api(api_data: Dict[str, Any]): |
|
|
"""Add a custom API""" |
|
|
try: |
|
|
success = config_loader.add_custom_api(api_data) |
|
|
|
|
|
if success: |
|
|
return {'status': 'success', 'message': 'API added successfully'} |
|
|
else: |
|
|
raise HTTPException(status_code=400, detail="Failed to add API") |
|
|
|
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
@router.delete("/config/apis/{api_id}") |
|
|
async def remove_api(api_id: str): |
|
|
"""Remove an API""" |
|
|
success = config_loader.remove_api(api_id) |
|
|
|
|
|
if success: |
|
|
return {'status': 'success', 'message': 'API removed successfully'} |
|
|
else: |
|
|
raise HTTPException(status_code=404, detail="API not found") |
|
|
|
|
|
|
|
|
@router.get("/config/export") |
|
|
async def export_config(): |
|
|
"""Export configuration to JSON""" |
|
|
filepath = f"data/exports/config_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" |
|
|
os.makedirs(os.path.dirname(filepath), exist_ok=True) |
|
|
|
|
|
config_loader.export_config(filepath) |
|
|
|
|
|
return FileResponse( |
|
|
filepath, |
|
|
media_type='application/json', |
|
|
filename=os.path.basename(filepath) |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/schedule/tasks") |
|
|
async def get_all_schedules(): |
|
|
"""Get all scheduled tasks""" |
|
|
return scheduler_service.get_all_task_statuses() |
|
|
|
|
|
|
|
|
@router.get("/schedule/tasks/{api_id}") |
|
|
async def get_schedule(api_id: str): |
|
|
"""Get schedule for specific API""" |
|
|
status = scheduler_service.get_task_status(api_id) |
|
|
|
|
|
if not status: |
|
|
raise HTTPException(status_code=404, detail="Task not found") |
|
|
|
|
|
return status |
|
|
|
|
|
|
|
|
@router.put("/schedule/tasks/{api_id}") |
|
|
async def update_schedule(api_id: str, interval: Optional[int] = None, enabled: Optional[bool] = None): |
|
|
"""Update schedule for an API""" |
|
|
try: |
|
|
scheduler_service.update_task_schedule(api_id, interval, enabled) |
|
|
|
|
|
|
|
|
await websocket_service.notify_schedule_update({ |
|
|
'api_id': api_id, |
|
|
'interval': interval, |
|
|
'enabled': enabled |
|
|
}) |
|
|
|
|
|
return { |
|
|
'status': 'success', |
|
|
'message': 'Schedule updated', |
|
|
'task': scheduler_service.get_task_status(api_id) |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
@router.post("/schedule/tasks/{api_id}/force-update") |
|
|
async def force_update(api_id: str): |
|
|
"""Force immediate update for an API""" |
|
|
try: |
|
|
success = await scheduler_service.force_update(api_id) |
|
|
|
|
|
if success: |
|
|
return { |
|
|
'status': 'success', |
|
|
'message': 'Update completed', |
|
|
'task': scheduler_service.get_task_status(api_id) |
|
|
} |
|
|
else: |
|
|
raise HTTPException(status_code=500, detail="Update failed") |
|
|
|
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
@router.get("/schedule/export") |
|
|
async def export_schedules(): |
|
|
"""Export schedules to JSON""" |
|
|
filepath = f"data/exports/schedules_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" |
|
|
os.makedirs(os.path.dirname(filepath), exist_ok=True) |
|
|
|
|
|
scheduler_service.export_schedules(filepath) |
|
|
|
|
|
return FileResponse( |
|
|
filepath, |
|
|
media_type='application/json', |
|
|
filename=os.path.basename(filepath) |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/data/cached") |
|
|
async def get_all_cached_data(): |
|
|
"""Get all cached data""" |
|
|
return persistence_service.get_all_cached_data() |
|
|
|
|
|
|
|
|
@router.get("/data/cached/{api_id}") |
|
|
async def get_cached_data(api_id: str): |
|
|
"""Get cached data for specific API""" |
|
|
data = persistence_service.get_cached_data(api_id) |
|
|
|
|
|
if not data: |
|
|
raise HTTPException(status_code=404, detail="No cached data found") |
|
|
|
|
|
return data |
|
|
|
|
|
|
|
|
@router.get("/data/history/{api_id}") |
|
|
async def get_history(api_id: str, limit: int = 100): |
|
|
"""Get historical data for an API""" |
|
|
history = persistence_service.get_history(api_id, limit) |
|
|
|
|
|
return { |
|
|
'api_id': api_id, |
|
|
'history': history, |
|
|
'count': len(history) |
|
|
} |
|
|
|
|
|
|
|
|
@router.get("/data/statistics") |
|
|
async def get_data_statistics(): |
|
|
"""Get data storage statistics""" |
|
|
return persistence_service.get_statistics() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/export/json") |
|
|
async def export_to_json( |
|
|
api_ids: Optional[List[str]] = None, |
|
|
include_history: bool = False, |
|
|
background_tasks: BackgroundTasks = None |
|
|
): |
|
|
"""Export data to JSON""" |
|
|
try: |
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') |
|
|
filepath = f"data/exports/data_export_{timestamp}.json" |
|
|
os.makedirs(os.path.dirname(filepath), exist_ok=True) |
|
|
|
|
|
await persistence_service.export_to_json(filepath, api_ids, include_history) |
|
|
|
|
|
return { |
|
|
'status': 'success', |
|
|
'filepath': filepath, |
|
|
'download_url': f"/api/v2/download?file={filepath}" |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
@router.post("/export/csv") |
|
|
async def export_to_csv(api_ids: Optional[List[str]] = None, flatten: bool = True): |
|
|
"""Export data to CSV""" |
|
|
try: |
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') |
|
|
filepath = f"data/exports/data_export_{timestamp}.csv" |
|
|
os.makedirs(os.path.dirname(filepath), exist_ok=True) |
|
|
|
|
|
await persistence_service.export_to_csv(filepath, api_ids, flatten) |
|
|
|
|
|
return { |
|
|
'status': 'success', |
|
|
'filepath': filepath, |
|
|
'download_url': f"/api/v2/download?file={filepath}" |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
@router.post("/export/history/{api_id}") |
|
|
async def export_history(api_id: str): |
|
|
"""Export historical data for an API to CSV""" |
|
|
try: |
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') |
|
|
filepath = f"data/exports/{api_id}_history_{timestamp}.csv" |
|
|
os.makedirs(os.path.dirname(filepath), exist_ok=True) |
|
|
|
|
|
await persistence_service.export_history_to_csv(filepath, api_id) |
|
|
|
|
|
return { |
|
|
'status': 'success', |
|
|
'filepath': filepath, |
|
|
'download_url': f"/api/v2/download?file={filepath}" |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
@router.get("/download") |
|
|
async def download_file(file: str): |
|
|
"""Download exported file""" |
|
|
if not os.path.exists(file): |
|
|
raise HTTPException(status_code=404, detail="File not found") |
|
|
|
|
|
return FileResponse( |
|
|
file, |
|
|
media_type='application/octet-stream', |
|
|
filename=os.path.basename(file) |
|
|
) |
|
|
|
|
|
|
|
|
@router.post("/backup") |
|
|
async def create_backup(): |
|
|
"""Create a backup of all data""" |
|
|
try: |
|
|
backup_file = await persistence_service.backup_all_data() |
|
|
|
|
|
return { |
|
|
'status': 'success', |
|
|
'backup_file': backup_file, |
|
|
'download_url': f"/api/v2/download?file={backup_file}" |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
@router.post("/restore") |
|
|
async def restore_from_backup(backup_file: str): |
|
|
"""Restore data from backup""" |
|
|
try: |
|
|
success = await persistence_service.restore_from_backup(backup_file) |
|
|
|
|
|
if success: |
|
|
return {'status': 'success', 'message': 'Data restored successfully'} |
|
|
else: |
|
|
raise HTTPException(status_code=500, detail="Restore failed") |
|
|
|
|
|
except Exception as e: |
|
|
raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/status") |
|
|
async def get_system_status(): |
|
|
"""Get overall system status""" |
|
|
return { |
|
|
'timestamp': datetime.now().isoformat(), |
|
|
'services': { |
|
|
'config_loader': { |
|
|
'apis_loaded': len(config_loader.apis), |
|
|
'categories': len(config_loader.get_categories()), |
|
|
'schedules': len(config_loader.schedules) |
|
|
}, |
|
|
'scheduler': { |
|
|
'running': scheduler_service.running, |
|
|
'total_tasks': len(scheduler_service.tasks), |
|
|
'realtime_tasks': len(scheduler_service.realtime_tasks), |
|
|
'cache_size': len(scheduler_service.data_cache) |
|
|
}, |
|
|
'persistence': { |
|
|
'cached_apis': len(persistence_service.cache), |
|
|
'apis_with_history': len(persistence_service.history), |
|
|
'total_history_records': sum(len(h) for h in persistence_service.history.values()) |
|
|
}, |
|
|
'websocket': websocket_service.get_stats() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@router.get("/health") |
|
|
async def health_check(): |
|
|
"""Health check endpoint""" |
|
|
return { |
|
|
'status': 'healthy', |
|
|
'timestamp': datetime.now().isoformat(), |
|
|
'services': { |
|
|
'config': config_loader is not None, |
|
|
'scheduler': scheduler_service is not None and scheduler_service.running, |
|
|
'persistence': persistence_service is not None, |
|
|
'websocket': websocket_service is not None |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/cleanup/cache") |
|
|
async def clear_cache(): |
|
|
"""Clear all cached data""" |
|
|
persistence_service.clear_cache() |
|
|
return {'status': 'success', 'message': 'Cache cleared'} |
|
|
|
|
|
|
|
|
@router.post("/cleanup/history") |
|
|
async def clear_history(api_id: Optional[str] = None): |
|
|
"""Clear history""" |
|
|
persistence_service.clear_history(api_id) |
|
|
|
|
|
if api_id: |
|
|
return {'status': 'success', 'message': f'History cleared for {api_id}'} |
|
|
else: |
|
|
return {'status': 'success', 'message': 'All history cleared'} |
|
|
|
|
|
|
|
|
@router.post("/cleanup/old-data") |
|
|
async def cleanup_old_data(days: int = 7): |
|
|
"""Remove data older than specified days""" |
|
|
removed = await persistence_service.cleanup_old_data(days) |
|
|
|
|
|
return { |
|
|
'status': 'success', |
|
|
'message': f'Cleaned up {removed} old records', |
|
|
'removed_count': removed |
|
|
} |
|
|
|