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).
50 lines
1.9 KiB
Python
50 lines
1.9 KiB
Python
import scrapy
|
|
from scrapy.spiders import XMLFeedSpider
|
|
from urllib.parse import urlparse
|
|
import datetime
|
|
import re
|
|
|
|
class NewsRSSSpider(XMLFeedSpider):
|
|
name = "articles"
|
|
iterator = 'xml'
|
|
itertag = 'item' # Standard RSS tag for an article
|
|
namespaces = [
|
|
('dc', 'http://purl.org/dc/elements/1.1/'),
|
|
('content', 'http://purl.org/rss/1.0/modules/content/'),
|
|
('media', 'http://search.yahoo.com/mrss/')
|
|
]
|
|
|
|
def __init__(self, filename='urls.txt', *args, **kwargs):
|
|
super(NewsRSSSpider, self).__init__(*args, **kwargs)
|
|
with open(filename, 'r') as f:
|
|
# We filter for RSS feeds only here
|
|
self.start_urls = [line.strip() for line in f if '/rss' in line or '/feed' in line]
|
|
|
|
def parse_node(self, response, node):
|
|
"""This runs for every <item> found in the RSS XML"""
|
|
title = node.xpath('title/text()').get()
|
|
link = node.xpath('link/text()').get()
|
|
pub_date = node.xpath('pubDate/text()').get()
|
|
|
|
# We now yield a Request to the actual article to get the full text
|
|
# Since these are RSS links, they are usually 'clean' HTML
|
|
if link:
|
|
yield scrapy.Request(link, callback=self.parse_article, meta={'title': title, 'date': pub_date})
|
|
|
|
def parse_article(self, response):
|
|
|
|
title = response.meta.get('title')
|
|
|
|
# Greedy search for the main text body
|
|
article_text = response.xpath('//article//p/text() | //main//p/text() | //div[contains(@class, "body")]//p/text()').getall()
|
|
pure_text = " ".join(article_text)
|
|
pure_text = re.sub(r'\s+', ' ', pure_text).strip()
|
|
|
|
if len(pure_text) > 300:
|
|
yield {
|
|
'title': title,
|
|
'url': response.url,
|
|
'text': pure_text,
|
|
'domain': urlparse(response.url).netloc,
|
|
'timestamp': datetime.datetime.now().isoformat()
|
|
}
|