mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-14 00:22:16 +00:00
This major update transforms leggen from CLI-only to a web-ready architecture while maintaining full CLI compatibility. New Features: - FastAPI backend service (leggend) with comprehensive REST API - Background job scheduler with configurable cron (replaces Ofelia) - All CLI commands refactored to use API endpoints - Docker configuration updated for new services - API client with health checks and error handling API Endpoints: - /api/v1/banks/* - Bank connections and institutions - /api/v1/accounts/* - Account management and balances - /api/v1/transactions/* - Transaction retrieval with filtering - /api/v1/sync/* - Manual sync and scheduler configuration - /api/v1/notifications/* - Notification settings management CLI Enhancements: - New --api-url option and LEGGEND_API_URL environment variable - Enhanced sync command with --wait and --force options - Improved transactions command with --full and --limit options - Automatic fallback and health checking Breaking Changes: - compose.yml structure updated (leggend service added) - Ofelia scheduler removed (internal scheduler used instead) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
199 lines
7.2 KiB
Python
199 lines
7.2 KiB
Python
from typing import Optional
|
|
from fastapi import APIRouter, HTTPException, BackgroundTasks
|
|
from loguru import logger
|
|
|
|
from leggend.api.models.common import APIResponse
|
|
from leggend.api.models.sync import SyncRequest, SyncStatus, SyncResult, SchedulerConfig
|
|
from leggend.services.sync_service import SyncService
|
|
from leggend.background.scheduler import scheduler
|
|
from leggend.config import config
|
|
|
|
router = APIRouter()
|
|
sync_service = SyncService()
|
|
|
|
|
|
@router.get("/sync/status", response_model=APIResponse)
|
|
async def get_sync_status() -> APIResponse:
|
|
"""Get current sync status"""
|
|
try:
|
|
status = await sync_service.get_sync_status()
|
|
|
|
# Add scheduler information
|
|
next_sync_time = scheduler.get_next_sync_time()
|
|
if next_sync_time:
|
|
status.next_sync = next_sync_time
|
|
|
|
return APIResponse(
|
|
success=True,
|
|
data=status,
|
|
message="Sync status retrieved successfully"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get sync status: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to get sync status: {str(e)}")
|
|
|
|
|
|
@router.post("/sync", response_model=APIResponse)
|
|
async def trigger_sync(
|
|
background_tasks: BackgroundTasks,
|
|
sync_request: Optional[SyncRequest] = None
|
|
) -> APIResponse:
|
|
"""Trigger a manual sync operation"""
|
|
try:
|
|
# Check if sync is already running
|
|
status = await sync_service.get_sync_status()
|
|
if status.is_running and not (sync_request and sync_request.force):
|
|
return APIResponse(
|
|
success=False,
|
|
message="Sync is already running. Use 'force: true' to override."
|
|
)
|
|
|
|
# Determine what to sync
|
|
if sync_request and sync_request.account_ids:
|
|
# Sync specific accounts in background
|
|
background_tasks.add_task(
|
|
sync_service.sync_specific_accounts,
|
|
sync_request.account_ids,
|
|
sync_request.force if sync_request else False
|
|
)
|
|
message = f"Started sync for {len(sync_request.account_ids)} specific accounts"
|
|
else:
|
|
# Sync all accounts in background
|
|
background_tasks.add_task(
|
|
sync_service.sync_all_accounts,
|
|
sync_request.force if sync_request else False
|
|
)
|
|
message = "Started sync for all accounts"
|
|
|
|
return APIResponse(
|
|
success=True,
|
|
data={"sync_started": True, "force": sync_request.force if sync_request else False},
|
|
message=message
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to trigger sync: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to trigger sync: {str(e)}")
|
|
|
|
|
|
@router.post("/sync/now", response_model=APIResponse)
|
|
async def sync_now(sync_request: Optional[SyncRequest] = None) -> APIResponse:
|
|
"""Run sync synchronously and return results (slower, for testing)"""
|
|
try:
|
|
if sync_request and sync_request.account_ids:
|
|
result = await sync_service.sync_specific_accounts(
|
|
sync_request.account_ids,
|
|
sync_request.force
|
|
)
|
|
else:
|
|
result = await sync_service.sync_all_accounts(
|
|
sync_request.force if sync_request else False
|
|
)
|
|
|
|
return APIResponse(
|
|
success=result.success,
|
|
data=result,
|
|
message="Sync completed" if result.success else f"Sync failed with {len(result.errors)} errors"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to run sync: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to run sync: {str(e)}")
|
|
|
|
|
|
@router.get("/sync/scheduler", response_model=APIResponse)
|
|
async def get_scheduler_config() -> APIResponse:
|
|
"""Get current scheduler configuration"""
|
|
try:
|
|
scheduler_config = config.scheduler_config
|
|
next_sync_time = scheduler.get_next_sync_time()
|
|
|
|
response_data = {
|
|
**scheduler_config,
|
|
"next_scheduled_sync": next_sync_time.isoformat() if next_sync_time else None,
|
|
"is_running": scheduler.scheduler.running if hasattr(scheduler, 'scheduler') else False
|
|
}
|
|
|
|
return APIResponse(
|
|
success=True,
|
|
data=response_data,
|
|
message="Scheduler configuration retrieved successfully"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get scheduler config: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to get scheduler config: {str(e)}")
|
|
|
|
|
|
@router.put("/sync/scheduler", response_model=APIResponse)
|
|
async def update_scheduler_config(scheduler_config: SchedulerConfig) -> APIResponse:
|
|
"""Update scheduler configuration"""
|
|
try:
|
|
# Validate cron expression if provided
|
|
if scheduler_config.cron:
|
|
try:
|
|
cron_parts = scheduler_config.cron.split()
|
|
if len(cron_parts) != 5:
|
|
raise ValueError("Cron expression must have 5 parts: minute hour day month day_of_week")
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=f"Invalid cron expression: {str(e)}")
|
|
|
|
# Update configuration
|
|
schedule_data = scheduler_config.dict(exclude_none=True)
|
|
config.update_section("scheduler", {"sync": schedule_data})
|
|
|
|
# Reschedule the job
|
|
scheduler.reschedule_sync(schedule_data)
|
|
|
|
return APIResponse(
|
|
success=True,
|
|
data=schedule_data,
|
|
message="Scheduler configuration updated successfully"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to update scheduler config: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to update scheduler config: {str(e)}")
|
|
|
|
|
|
@router.post("/sync/scheduler/start", response_model=APIResponse)
|
|
async def start_scheduler() -> APIResponse:
|
|
"""Start the background scheduler"""
|
|
try:
|
|
if not scheduler.scheduler.running:
|
|
scheduler.start()
|
|
return APIResponse(
|
|
success=True,
|
|
message="Scheduler started successfully"
|
|
)
|
|
else:
|
|
return APIResponse(
|
|
success=True,
|
|
message="Scheduler is already running"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to start scheduler: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to start scheduler: {str(e)}")
|
|
|
|
|
|
@router.post("/sync/scheduler/stop", response_model=APIResponse)
|
|
async def stop_scheduler() -> APIResponse:
|
|
"""Stop the background scheduler"""
|
|
try:
|
|
if scheduler.scheduler.running:
|
|
scheduler.shutdown()
|
|
return APIResponse(
|
|
success=True,
|
|
message="Scheduler stopped successfully"
|
|
)
|
|
else:
|
|
return APIResponse(
|
|
success=True,
|
|
message="Scheduler is already stopped"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to stop scheduler: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to stop scheduler: {str(e)}") |