""" APScheduler-based batch scheduler for the Overlay headline collector. Integrated into FastAPI's lifespan via start_scheduler() / stop_scheduler(). """ import logging logger = logging.getLogger(__name__) _scheduler = None def _get_scheduler(): global _scheduler if _scheduler is None: try: from apscheduler.schedulers.asyncio import AsyncIOScheduler _scheduler = AsyncIOScheduler(timezone="UTC") except ImportError: logger.warning("apscheduler not installed; overlay scheduling disabled") return None return _scheduler async def _run_collect_job() -> None: """Scheduled job: collect Yahoo RSS headlines.""" from app.core.database import AsyncSessionLocal from app.services.overlay.overlay_pipeline import OverlayPipeline pipeline = OverlayPipeline() async with AsyncSessionLocal() as db: try: counts = await pipeline.collect_all(db) logger.info(f"[Scheduler] collect_all completed: {counts}") except Exception as e: logger.error(f"[Scheduler] collect_all failed: {e}") def start_scheduler() -> None: """Start the APScheduler. Call from FastAPI lifespan startup.""" from app.core.config import settings if not getattr(settings, "OVERLAY_ENABLED", True): logger.info("Overlay disabled; scheduler not started") return sched = _get_scheduler() if sched is None: return try: from apscheduler.triggers.cron import CronTrigger # Yahoo RSS collect: 23:30 UTC daily on weekdays (≈ 18:30 ET) sched.add_job( _run_collect_job, trigger=CronTrigger(day_of_week="mon-fri", hour=23, minute=30, timezone="UTC"), id="overlay_collect", replace_existing=True, misfire_grace_time=600, ) sched.start() logger.info("Overlay scheduler started (collect @ 23:30 UTC, weekdays)") except Exception as e: logger.error(f"Overlay scheduler start failed: {e}") def stop_scheduler() -> None: sched = _get_scheduler() if sched and sched.running: sched.shutdown(wait=False) logger.info("Overlay scheduler stopped")