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>
127 lines
4.8 KiB
Python
127 lines
4.8 KiB
Python
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
from apscheduler.triggers.cron import CronTrigger
|
|
from loguru import logger
|
|
|
|
from leggend.config import config
|
|
from leggend.services.sync_service import SyncService
|
|
|
|
|
|
class BackgroundScheduler:
|
|
def __init__(self):
|
|
self.scheduler = AsyncIOScheduler()
|
|
self.sync_service = SyncService()
|
|
|
|
def start(self):
|
|
"""Start the scheduler and configure sync jobs based on configuration"""
|
|
schedule_config = config.scheduler_config.get("sync", {})
|
|
|
|
if not schedule_config.get("enabled", True):
|
|
logger.info("Sync scheduling is disabled in configuration")
|
|
self.scheduler.start()
|
|
return
|
|
|
|
# Use custom cron expression if provided, otherwise use hour/minute
|
|
if schedule_config.get("cron"):
|
|
# Parse custom cron expression (e.g., "0 3 * * *" for daily at 3 AM)
|
|
try:
|
|
cron_parts = schedule_config["cron"].split()
|
|
if len(cron_parts) == 5:
|
|
minute, hour, day, month, day_of_week = cron_parts
|
|
trigger = CronTrigger(
|
|
minute=minute,
|
|
hour=hour,
|
|
day=day if day != "*" else None,
|
|
month=month if month != "*" else None,
|
|
day_of_week=day_of_week if day_of_week != "*" else None,
|
|
)
|
|
else:
|
|
logger.error(f"Invalid cron expression: {schedule_config['cron']}")
|
|
return
|
|
except Exception as e:
|
|
logger.error(f"Error parsing cron expression: {e}")
|
|
return
|
|
else:
|
|
# Use hour/minute configuration (default: 3:00 AM daily)
|
|
hour = schedule_config.get("hour", 3)
|
|
minute = schedule_config.get("minute", 0)
|
|
trigger = CronTrigger(hour=hour, minute=minute)
|
|
|
|
self.scheduler.add_job(
|
|
self._run_sync,
|
|
trigger,
|
|
id="daily_sync",
|
|
name="Scheduled sync of all transactions",
|
|
max_instances=1,
|
|
)
|
|
|
|
self.scheduler.start()
|
|
logger.info(f"Background scheduler started with sync job: {trigger}")
|
|
|
|
def shutdown(self):
|
|
if self.scheduler.running:
|
|
self.scheduler.shutdown()
|
|
logger.info("Background scheduler shutdown")
|
|
|
|
def reschedule_sync(self, schedule_config: dict):
|
|
"""Reschedule the sync job with new configuration"""
|
|
if self.scheduler.running:
|
|
try:
|
|
self.scheduler.remove_job("daily_sync")
|
|
logger.info("Removed existing sync job")
|
|
except Exception:
|
|
pass # Job might not exist
|
|
|
|
if not schedule_config.get("enabled", True):
|
|
logger.info("Sync scheduling disabled")
|
|
return
|
|
|
|
# Configure new schedule
|
|
if schedule_config.get("cron"):
|
|
try:
|
|
cron_parts = schedule_config["cron"].split()
|
|
if len(cron_parts) == 5:
|
|
minute, hour, day, month, day_of_week = cron_parts
|
|
trigger = CronTrigger(
|
|
minute=minute,
|
|
hour=hour,
|
|
day=day if day != "*" else None,
|
|
month=month if month != "*" else None,
|
|
day_of_week=day_of_week if day_of_week != "*" else None,
|
|
)
|
|
else:
|
|
logger.error(f"Invalid cron expression: {schedule_config['cron']}")
|
|
return
|
|
except Exception as e:
|
|
logger.error(f"Error parsing cron expression: {e}")
|
|
return
|
|
else:
|
|
hour = schedule_config.get("hour", 3)
|
|
minute = schedule_config.get("minute", 0)
|
|
trigger = CronTrigger(hour=hour, minute=minute)
|
|
|
|
self.scheduler.add_job(
|
|
self._run_sync,
|
|
trigger,
|
|
id="daily_sync",
|
|
name="Scheduled sync of all transactions",
|
|
max_instances=1,
|
|
)
|
|
logger.info(f"Rescheduled sync job with: {trigger}")
|
|
|
|
async def _run_sync(self):
|
|
try:
|
|
logger.info("Starting scheduled sync job")
|
|
await self.sync_service.sync_all_accounts()
|
|
logger.info("Scheduled sync job completed successfully")
|
|
except Exception as e:
|
|
logger.error(f"Scheduled sync job failed: {e}")
|
|
|
|
def get_next_sync_time(self):
|
|
"""Get the next scheduled sync time"""
|
|
job = self.scheduler.get_job("daily_sync")
|
|
if job:
|
|
return job.next_run_time
|
|
return None
|
|
|
|
|
|
scheduler = BackgroundScheduler() |