import logging from typing import Callable, Optional, Dict, Any from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.interval import IntervalTrigger from zoneinfo import ZoneInfo logger = logging.getLogger(__name__) class EventManagement: """ Generic event scheduler wrapper around APScheduler's AsyncIOScheduler. Features: - start/stop lifecycle - register timed events with either cron or interval style - remove events by id """ def __init__(self, timezone: str = "Europe/Zurich"): self._timezone = ZoneInfo(timezone) self._scheduler: Optional[AsyncIOScheduler] = None @property def scheduler(self) -> AsyncIOScheduler: if self._scheduler is None: self._scheduler = AsyncIOScheduler(timezone=self._timezone) return self._scheduler def start(self) -> None: if not self.scheduler.running: self.scheduler.start() logger.info("EventManagement scheduler started") def stop(self) -> None: if self._scheduler and self._scheduler.running: try: self._scheduler.shutdown(wait=False) logger.info("EventManagement scheduler stopped") except Exception as exc: logger.error(f"Error stopping scheduler: {exc}") def register_cron( self, job_id: str, func: Callable, *, cron_kwargs: Optional[Dict[str, Any]] = None, replace_existing: bool = True, coalesce: bool = True, max_instances: int = 1, misfire_grace_time: int = 1800, **kwargs: Any, ) -> None: """ Register a job using CronTrigger. Provide cron fields as keyword args, e.g.: cron_kwargs={"minute": "0,20,40"} """ trigger = CronTrigger(timezone=self._timezone, **(cron_kwargs or {})) self.scheduler.add_job( func, trigger, id=job_id, replace_existing=replace_existing, coalesce=coalesce, max_instances=max_instances, misfire_grace_time=misfire_grace_time, **kwargs, ) logger.info(f"Registered cron job '{job_id}' with args {cron_kwargs}") def register_interval( self, job_id: str, func: Callable, *, seconds: Optional[int] = None, minutes: Optional[int] = None, hours: Optional[int] = None, replace_existing: bool = True, coalesce: bool = True, max_instances: int = 1, misfire_grace_time: int = 1800, **kwargs: Any, ) -> None: """ Register a job using IntervalTrigger. """ trigger = IntervalTrigger( seconds=seconds, minutes=minutes, hours=hours, timezone=self._timezone ) self.scheduler.add_job( func, trigger, id=job_id, replace_existing=replace_existing, coalesce=coalesce, max_instances=max_instances, misfire_grace_time=misfire_grace_time, **kwargs, ) logger.info( f"Registered interval job '{job_id}' (h={hours}, m={minutes}, s={seconds})" ) def remove(self, job_id: str) -> None: try: self.scheduler.remove_job(job_id) logger.info(f"Removed job '{job_id}'") except Exception as exc: logger.warning(f"Could not remove job '{job_id}': {exc}") # Singleton instance for easy import and reuse eventManager = EventManagement()