mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-14 00:22:16 +00:00
feat: Transform to web architecture with FastAPI backend
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>
This commit is contained in:
committed by
Elisiário Couto
parent
73d6bd32db
commit
91f53b35b1
127
leggend/background/scheduler.py
Normal file
127
leggend/background/scheduler.py
Normal file
@@ -0,0 +1,127 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user