feat: Nous Portal news summarizer, ticker flags, map pins #8
2 changed files with 86 additions and 2 deletions
64
alembic/versions/005_news_items.py
Normal file
64
alembic/versions/005_news_items.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
"""news_items table + article_summaries.model
|
||||
|
||||
Revision ID: 005_news_items
|
||||
Revises: 004_camera_enum
|
||||
Create Date: 2026-08-28
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "005_news_items"
|
||||
down_revision = "004_camera_enum"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Idempotent DDL: ingest (summarizer ensure_tables) may create the same
|
||||
# shapes first depending on container startup order. IF NOT EXISTS makes
|
||||
# both orders safe — whichever runs first wins, the other no-ops.
|
||||
# One statement per op.execute: asyncpg rejects multi-command prepared
|
||||
# statements (same style as 003_news).
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TABLE article_summaries
|
||||
ADD COLUMN IF NOT EXISTS model TEXT
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS news_items (
|
||||
id SERIAL PRIMARY KEY,
|
||||
summary_id INTEGER REFERENCES article_summaries(id) ON DELETE CASCADE,
|
||||
kind TEXT NOT NULL,
|
||||
headline TEXT NOT NULL,
|
||||
importance TEXT NOT NULL,
|
||||
location_name TEXT,
|
||||
lat DOUBLE PRECISION,
|
||||
lon DOUBLE PRECISION,
|
||||
location_confidence TEXT,
|
||||
category TEXT,
|
||||
url TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS ix_news_items_kind_created
|
||||
ON news_items (kind, created_at DESC)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS ix_news_items_map_bbox
|
||||
ON news_items (lon, lat)
|
||||
WHERE kind = 'map' AND lat IS NOT NULL AND lon IS NOT NULL
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP TABLE IF EXISTS news_items")
|
||||
op.execute("ALTER TABLE article_summaries DROP COLUMN IF EXISTS model")
|
||||
|
|
@ -185,8 +185,8 @@ Index("ix_fires_bbox", fires.c.longitude, fires.c.latitude)
|
|||
|
||||
|
||||
# ── News pipeline (scraper + summarizer) ──────────────────────────────────
|
||||
# Written by the vendored news-scraper (Scrapy) / news-summarizer (Gemini)
|
||||
# services; schema must match the idempotent alembic migration 003_news.
|
||||
# Written by the vendored news-scraper (Scrapy) / news-summarizer services;
|
||||
# schema must match the idempotent alembic migrations 003_news + 005_news_items.
|
||||
|
||||
articles = Table(
|
||||
"articles",
|
||||
|
|
@ -209,6 +209,26 @@ article_summaries = Table(
|
|||
Column("summary_text", Text, nullable=False),
|
||||
Column("batch_timestamp", DateTime(timezone=True),
|
||||
server_default=func.now(), nullable=False),
|
||||
Column("model", Text), # LLM id used for this batch; nullable for old rows
|
||||
)
|
||||
|
||||
Index("ix_article_summaries_batch_timestamp", article_summaries.c.batch_timestamp)
|
||||
|
||||
|
||||
news_items = Table(
|
||||
"news_items",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("summary_id", Integer),
|
||||
Column("kind", Text, nullable=False),
|
||||
Column("headline", Text, nullable=False),
|
||||
Column("importance", Text, nullable=False),
|
||||
Column("location_name", Text),
|
||||
Column("lat", Float),
|
||||
Column("lon", Float),
|
||||
Column("location_confidence", Text),
|
||||
Column("category", Text),
|
||||
Column("url", Text),
|
||||
Column("created_at", DateTime(timezone=True), server_default=func.now(), nullable=False),
|
||||
)
|
||||
Index("ix_news_items_kind_created", news_items.c.kind, news_items.c.created_at)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue