""" APScheduler integration for cron-style triggers. Provides scheduling of periodic triggers (e.g., sync exchange state hourly, recompute indicators every 5 minutes, daily portfolio reports). """ import logging from typing import Optional from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger as APSCronTrigger from apscheduler.triggers.interval import IntervalTrigger from .queue import TriggerQueue from .types import Priority, Trigger logger = logging.getLogger(__name__) class TriggerScheduler: """ Scheduler for periodic trigger execution. Wraps APScheduler to enqueue triggers at scheduled times. """ def __init__(self, trigger_queue: TriggerQueue): """ Initialize scheduler. Args: trigger_queue: TriggerQueue to enqueue triggers into """ self.trigger_queue = trigger_queue self.scheduler = AsyncIOScheduler() self._job_counter = 0 def start(self) -> None: """Start the scheduler""" self.scheduler.start() logger.info("TriggerScheduler started") def shutdown(self, wait: bool = True) -> None: """ Shut down the scheduler. Args: wait: If True, wait for running jobs to complete """ self.scheduler.shutdown(wait=wait) logger.info("TriggerScheduler shut down") def schedule_interval( self, trigger: Trigger, seconds: Optional[int] = None, minutes: Optional[int] = None, hours: Optional[int] = None, priority: Optional[Priority] = None, ) -> str: """ Schedule a trigger to run at regular intervals. Args: trigger: Trigger to execute seconds: Interval in seconds minutes: Interval in minutes hours: Interval in hours priority: Priority override for execution Returns: Job ID (can be used to remove job later) Example: # Run every 5 minutes scheduler.schedule_interval( IndicatorUpdateTrigger("rsi_14"), minutes=5 ) """ job_id = f"interval_{self._job_counter}" self._job_counter += 1 async def job_func(): await self.trigger_queue.enqueue(trigger, priority) self.scheduler.add_job( job_func, trigger=IntervalTrigger(seconds=seconds, minutes=minutes, hours=hours), id=job_id, name=f"Interval: {trigger.name}", ) logger.info( f"Scheduled interval job: {job_id}, trigger={trigger.name}, " f"interval=(s={seconds}, m={minutes}, h={hours})" ) return job_id def schedule_cron( self, trigger: Trigger, minute: Optional[str] = None, hour: Optional[str] = None, day: Optional[str] = None, month: Optional[str] = None, day_of_week: Optional[str] = None, priority: Optional[Priority] = None, ) -> str: """ Schedule a trigger to run on a cron schedule. Args: trigger: Trigger to execute minute: Minute expression (0-59, *, */5, etc.) hour: Hour expression (0-23, *, etc.) day: Day of month expression (1-31, *, etc.) month: Month expression (1-12, *, etc.) day_of_week: Day of week expression (0-6, mon-sun, *, etc.) priority: Priority override for execution Returns: Job ID (can be used to remove job later) Example: # Run at 9:00 AM every weekday scheduler.schedule_cron( SyncExchangeStateTrigger(), hour="9", minute="0", day_of_week="mon-fri" ) """ job_id = f"cron_{self._job_counter}" self._job_counter += 1 async def job_func(): await self.trigger_queue.enqueue(trigger, priority) self.scheduler.add_job( job_func, trigger=APSCronTrigger( minute=minute, hour=hour, day=day, month=month, day_of_week=day_of_week, ), id=job_id, name=f"Cron: {trigger.name}", ) logger.info( f"Scheduled cron job: {job_id}, trigger={trigger.name}, " f"schedule=(m={minute}, h={hour}, d={day}, dow={day_of_week})" ) return job_id def remove_job(self, job_id: str) -> bool: """ Remove a scheduled job. Args: job_id: Job ID returned from schedule_* methods Returns: True if job was removed, False if not found """ try: self.scheduler.remove_job(job_id) logger.info(f"Removed scheduled job: {job_id}") return True except Exception as e: logger.warning(f"Could not remove job {job_id}: {e}") return False def get_jobs(self) -> list: """Get list of all scheduled jobs""" return self.scheduler.get_jobs() def __repr__(self) -> str: job_count = len(self.scheduler.get_jobs()) running = self.scheduler.running return f"TriggerScheduler(running={running}, jobs={job_count})"