188 lines
5.3 KiB
Python
188 lines
5.3 KiB
Python
"""
|
|
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})"
|