osint-dashboard/alembic/versions/003_news.py
Sirius DevOps 91f436390b
Some checks failed
build-and-deploy / build (push) Failing after 4s
Add news pipeline: hourly scraper + Gemini summarizer in compose
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).
2026-08-24 17:28:46 -04:00

57 lines
1.6 KiB
Python

"""news tables: scraped articles + LLM article_summaries
Revision ID: 003_news
Revises: 002_cameras
Create Date: 2026-08-24
"""
from alembic import op
import sqlalchemy as sa # noqa: F401
# revision identifiers, used by Alembic.
revision = '003_news'
down_revision = '002_cameras'
branch_labels = None
depends_on = None
def upgrade() -> None:
# Idempotent DDL: the news scraper's Scrapy pipeline also issues
# `CREATE TABLE IF NOT EXISTS articles`, so either the scraper or the app
# may create these first depending on container startup order. IF NOT
# EXISTS makes both orders safe — whichever runs first wins, the other
# no-ops. Same table shapes as the upstream newsPipeline services.
op.execute(
"""
CREATE TABLE IF NOT EXISTS articles (
id SERIAL PRIMARY KEY,
title TEXT,
url TEXT UNIQUE,
content TEXT,
domain TEXT,
timestamp TIMESTAMPTZ
)
"""
)
op.execute(
"CREATE INDEX IF NOT EXISTS ix_articles_timestamp ON articles (timestamp)"
)
op.execute(
"""
CREATE TABLE IF NOT EXISTS article_summaries (
id SERIAL PRIMARY KEY,
summary_text TEXT NOT NULL,
batch_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
"""
)
op.execute(
"CREATE INDEX IF NOT EXISTS ix_article_summaries_batch_timestamp "
"ON article_summaries (batch_timestamp)"
)
def downgrade() -> None:
op.execute("DROP TABLE IF EXISTS article_summaries")
op.execute("DROP TABLE IF EXISTS articles")