69 lines
2.3 KiB
Python
69 lines
2.3 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Scheduler loop for the news summarizer — hourly summarize at minute :05.
|
||
|
|
|
||
|
|
Replaces the k8s CronJob (`5 * * * *`) with an in-compose loop. Runs once on
|
||
|
|
boot (catches up on any articles scraped since the last summary), then fires
|
||
|
|
at each :NEWS_SUMMARIZE_MINUTE wall-clock boundary.
|
||
|
|
|
||
|
|
The loop is serial, so a slow LLM pass never overlaps the next run.
|
||
|
|
|
||
|
|
Env (all optional, 12-factor):
|
||
|
|
NEWS_SUMMARIZE_MINUTE minute of the hour to fire (default 5)
|
||
|
|
NEWS_SUMMARIZE_RUN_ON_START "1" to summarize once immediately on boot (default 1)
|
||
|
|
GEMINI_API_KEY required to do real work; unset = idle
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import datetime
|
||
|
|
import logging
|
||
|
|
import os
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
import time
|
||
|
|
|
||
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
||
|
|
logger = logging.getLogger("news.summarizer.scheduler")
|
||
|
|
|
||
|
|
MINUTE = int(os.getenv("NEWS_SUMMARIZE_MINUTE", "5"))
|
||
|
|
RUN_ON_START = os.getenv("NEWS_SUMMARIZE_RUN_ON_START", "1").lower() in ("1", "true", "yes")
|
||
|
|
|
||
|
|
|
||
|
|
def seconds_until_next(minute: int) -> float:
|
||
|
|
"""Seconds until the next occurrence of ``minute`` past the hour (local time)."""
|
||
|
|
now = datetime.datetime.now()
|
||
|
|
nxt = now.replace(minute=minute, second=0, microsecond=0) + datetime.timedelta(hours=1)
|
||
|
|
return (nxt - now).total_seconds()
|
||
|
|
|
||
|
|
|
||
|
|
def run_summarize() -> None:
|
||
|
|
logger.info("summarize starting at %s", datetime.datetime.now().isoformat(timespec="seconds"))
|
||
|
|
try:
|
||
|
|
proc = subprocess.run([sys.executable, "summarizer.py"], cwd="/app")
|
||
|
|
logger.info("summarize finished rc=%s", proc.returncode)
|
||
|
|
except Exception: # noqa: BLE001 — keep the loop alive across failures
|
||
|
|
logger.exception("summarize failed")
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> None:
|
||
|
|
if not os.getenv("GEMINI_API_KEY", "").strip():
|
||
|
|
logger.warning(
|
||
|
|
"GEMINI_API_KEY not set — summarizer will idle (set it in .env and "
|
||
|
|
"recreate the service to enable)"
|
||
|
|
)
|
||
|
|
logger.info(
|
||
|
|
"news summarizer loop starting (minute=%s, run_on_start=%s)",
|
||
|
|
MINUTE, RUN_ON_START,
|
||
|
|
)
|
||
|
|
if RUN_ON_START:
|
||
|
|
run_summarize()
|
||
|
|
while True:
|
||
|
|
delay = seconds_until_next(MINUTE)
|
||
|
|
logger.info("next summarize at :%02d (in %.0fs)", MINUTE, delay)
|
||
|
|
time.sleep(delay)
|
||
|
|
run_summarize()
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(main())
|