osint-dashboard/news/scraper/newsScraper/pipelines.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

92 lines
2.7 KiB
Python

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
import logging
import psycopg2
import os
from scrapy.exceptions import DropItem
class PostgresPipeline:
def __init__(self, db_config):
# 1. Store the config
self.db_config = db_config
# 2. Initialize the set here so it exists when process_item is called
self.seen_urls = set()
@classmethod
def from_crawler(cls, crawler):
db_config = {
'host': crawler.settings.get('DB_HOST'),
'database': crawler.settings.get('DB_NAME'),
'user': crawler.settings.get('DB_USER'),
'password': crawler.settings.get('DB_PASSWORD'),
}
return cls(db_config=db_config)
def open_spider(self, spider):
# Connect using environment variables
self.connection = psycopg2.connect(
host=os.getenv('DB_HOST'),
database=os.getenv('DB_NAME'),
user=os.getenv('DB_USER'),
password=os.getenv('DB_PASSWORD'),
port=os.getenv('DB_PORT', '5432')
)
self.cur = self.connection.cursor()
# Create table if it doesn't exist
self.cur.execute("""
CREATE TABLE IF NOT EXISTS articles (
id SERIAL PRIMARY KEY,
title TEXT,
url TEXT UNIQUE,
content TEXT,
domain TEXT,
timestamp TIMESTAMPTZ
)
""")
self.connection.commit()
def process_item(self, item, spider):
if item ['url'] in self.seen_urls:
raise DropItem()
try:
self.cur.execute("""
INSERT INTO articles (title, url, content, domain, timestamp)
VALUES (%s, %s, %s, %s, %s)
ON CONFLICT (url) DO NOTHING
""", (
item['title'],
item['url'],
item['text'],
item['domain'],
item['timestamp']
))
if self.cur.rowcount == 0:
e = DropItem("Duplicate URL (database conflict)")
e.log_level = logging.DEBUG
raise e
self.connection.commit()
return item
except Exception as e:
spider.logger.error(f"Error saving to Postgres: {e}")
self.connection.rollback()
raise
def close_spider(self, spider):
self.cur.close()
self.connection.close()
from itemadapter import ItemAdapter
class NewsscraperPipeline:
def process_item(self, item, spider):
return item