RSS feed retrieval and storage system with content extraction and image downloading capabilities
Project description
rss-retriever
Fetch articles from RSS feeds, extract their full text and images, and store them on disk.
Built around ports and adapters, so the pieces are usable independently: take the feed adapter without the storage layer, swap in your own storage backend, or drive the whole pipeline from the bundled CLI.
Install
pip install rss-retriever
Optional tracing support (OpenTelemetry SDK, OTLP exporters, and Arize Phoenix):
pip install "rss-retriever[otel]"
The base install deliberately stays light. opentelemetry-api is included because it is a no-op
without an SDK, so instrumented code paths cost nothing when tracing is not configured.
Requires Python 3.12+.
Library usage
from rss_retriever import ContentExtractor, FileSystemStorage, NewsService, RSSFeedAdapter
service = NewsService(
RSSFeedAdapter({"Phys.org": "https://phys.org/rss-feed/"}),
FileSystemStorage("article_storage"),
ContentExtractor(),
)
articles = service.fetch_and_store_articles(limit_per_source=5)
for article in service.get_recent_articles(limit=10):
print(article.source_name, article.title)
Articles already present in storage are skipped before the expensive extraction step, so re-running against the same feeds is cheap.
Bulk imports
save_article rewrites both index files on every call so a crash cannot orphan an article. That is
the right default for a nightly run but makes a large backfill quadratic in corpus size. Wrap bulk
ingestion in batch_writes() to pay the index cost once:
with storage.batch_writes():
for article in many_articles:
storage.save_article(article)
Article payloads are still written immediately; only the indexes are deferred, and they are flushed even if the block raises. On a 5,000-article import this is roughly 12x faster.
Public API
| Object | Role |
|---|---|
NewsService |
Orchestrates fetch, extract, and store |
RSSFeedAdapter |
NewsPort implementation backed by feedparser |
ContentExtractor |
Full-text and image extraction via newspaper4k |
FileSystemStorage |
StoragePort implementation writing to disk |
NewsPort, StoragePort |
Abstract ports for custom implementations |
Article, ArticleImage |
Domain models, with to_dict() / from_dict() |
Custom backends
Implement StoragePort to store somewhere other than the filesystem:
from rss_retriever import StoragePort
class S3Storage(StoragePort):
def save_article(self, article): ...
def get_article(self, article_id): ...
def get_recent_articles(self, limit=50): ...
def get_unread_articles(self, limit=50): ...
def article_exists(self, url) -> bool: ...
CLI
export RSS_RETRIEVER_RSS_FEEDS='{"Phys.org": "https://phys.org/rss-feed/"}'
export RSS_RETRIEVER_STORAGE_DIR=./article_storage
rss-retriever
Configuration
Read from the environment by Config.from_env(). Construct Config directly to bypass the
environment entirely.
| Variable | Default | Meaning |
|---|---|---|
RSS_RETRIEVER_RSS_FEEDS |
{} |
JSON object mapping source name to feed URL |
RSS_RETRIEVER_STORAGE_DIR |
article_storage |
Where articles are written |
RSS_RETRIEVER_ARTICLES_PER_SOURCE |
5 |
Max articles fetched per feed per run |
RSS_RETRIEVER_RECENT_ARTICLES_LIMIT |
10 |
Max articles returned by recent-article queries |
RSS_RETRIEVER_PREVIEW_IMAGE_COUNT |
3 |
Images summarised per article in CLI output |
RSS_RETRIEVER_LOG_LEVEL |
INFO |
Root log level |
RSS_RETRIEVER_REQUEST_TIMEOUT |
10 |
Per-request timeout in seconds |
RSS_RETRIEVER_CHUNK_SIZE |
8192 |
Download chunk size in bytes |
There are no default feeds: a library should not fetch anything the caller did not ask for.
rss_retriever.config.EXAMPLE_FEEDS holds a starting set if you want one.
Storage layout
article_storage/
├── index.json # article_id -> metadata, for recency queries
├── url_index.json # url -> article_id, for deduplication
└── <source>_<url_hash>/
├── content.txt
├── content.html
├── metadata.json
└── images/
Article IDs are <source_slug>_<first 10 hex of md5(url)> and are stable across runs.
Tracing
from rss_retriever.telemetry import setup_telemetry
setup_telemetry(service_name="my-service")
Configured through the standard OTEL_* environment variables. Requires the otel extra. Note
that this installs a global tracer provider and sets OTLP defaults, so call it from an application
entry point rather than from library code.
Development
uv sync --extra dev
uv run pytest -m "not network" # unit tests, no network
uv run pytest # includes live-network tests
uv run ruff check .
uv run mypy rss_retriever/
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rss_retriever-0.1.0.tar.gz.
File metadata
- Download URL: rss_retriever-0.1.0.tar.gz
- Upload date:
- Size: 89.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
685b2fedf7ca8ddbda3096e47a595bc3c90003a7ab39a666a0d131b693c3db78
|
|
| MD5 |
5b67cf9678d8de894917567fc833a714
|
|
| BLAKE2b-256 |
761d1e010ab8f8120de2a742056390a1fd145675f936a3df11d9a88911007c1d
|
File details
Details for the file rss_retriever-0.1.0-py3-none-any.whl.
File metadata
- Download URL: rss_retriever-0.1.0-py3-none-any.whl
- Upload date:
- Size: 21.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7cf646eba6b0a954395f3f9eeea2d40e08d91e28cf27019a6d82d8265007c1dd
|
|
| MD5 |
bdf3c5ee135d533ce84cdd2be0d0d525
|
|
| BLAKE2b-256 |
477b0dc0fe78c122b0b584b02284c113e6aa1bcd6edc455f805dd686e610c992
|