triggers and execution queue; subagents

This commit is contained in:
2026-03-04 21:27:41 -04:00
parent a50955558e
commit 8ff277c8c6
28 changed files with 3683 additions and 14 deletions

View File

@@ -0,0 +1,187 @@
"""
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})"