Some checks failed
build-and-deploy / build (push) Failing after 4s
Vendor the newsPipeline scraper + summarizer into the repo and wire them into
docker-compose against the EXISTING osint-db (no second Postgres), replacing
the upstream k8s CronJobs with in-compose wall-clock loops (:00 scrape, :05
summarize).
- news/scraper: vendored Scrapy project (257 RSS feeds) + hourly loop
scheduler (run_news_scraper.py)
- news/summerizer: vendored Gemini map-reduce summarizer, cleaned:
* fix broken google-genai response handling (_extract_text, defensive)
* fix malformed INSERT/GRANT query in save_summary_to_db
* OSINT-neutral default MAP_PROMPT; futures/markets language gated behind
INCLUDE_FUTURES=0 (yfinance lazy-imported)
* env-configurable model, batch size, lookback window
+ hourly loop scheduler (run_news_summarizer.py, :05)
- alembic 003_news: idempotent articles + article_summaries tables
- API: GET /api/news and GET /api/news/summaries (+ models, schemas)
- tests/test_api_news.py: 5 DB-backed contract tests (all pass vs real PG)
- docs/news.md + .env.example updates
Both services run under the `ingest` compose profile (matching the
ingester/camera-scraper pattern) and build arm64 on the Pi via the existing
Forgejo CI workflow. telebot left out of scope (reserved env only).
68 lines
2.3 KiB
Python
68 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())
|