rss-retriever 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rss_retriever-0.1.0/.github/workflows/publish.yml +63 -0
- rss_retriever-0.1.0/.github/workflows/release-please.yml +22 -0
- rss_retriever-0.1.0/.github/workflows/unit-tests.yml +51 -0
- rss_retriever-0.1.0/.gitignore +29 -0
- rss_retriever-0.1.0/CHANGELOG.md +13 -0
- rss_retriever-0.1.0/CONTRIBUTING.md +65 -0
- rss_retriever-0.1.0/Dockerfile +35 -0
- rss_retriever-0.1.0/Justfile +72 -0
- rss_retriever-0.1.0/LICENSE +21 -0
- rss_retriever-0.1.0/PKG-INFO +195 -0
- rss_retriever-0.1.0/README.md +156 -0
- rss_retriever-0.1.0/pyproject.toml +125 -0
- rss_retriever-0.1.0/rss_retriever/__init__.py +26 -0
- rss_retriever-0.1.0/rss_retriever/__main__.py +7 -0
- rss_retriever-0.1.0/rss_retriever/adapters/__init__.py +1 -0
- rss_retriever-0.1.0/rss_retriever/adapters/content.py +91 -0
- rss_retriever-0.1.0/rss_retriever/adapters/rss.py +163 -0
- rss_retriever-0.1.0/rss_retriever/adapters/storage.py +364 -0
- rss_retriever-0.1.0/rss_retriever/config.py +112 -0
- rss_retriever-0.1.0/rss_retriever/domain/__init__.py +1 -0
- rss_retriever-0.1.0/rss_retriever/domain/article.py +110 -0
- rss_retriever-0.1.0/rss_retriever/domain/ports.py +91 -0
- rss_retriever-0.1.0/rss_retriever/main.py +67 -0
- rss_retriever-0.1.0/rss_retriever/service/__init__.py +1 -0
- rss_retriever-0.1.0/rss_retriever/service/news.py +101 -0
- rss_retriever-0.1.0/rss_retriever/telemetry.py +123 -0
- rss_retriever-0.1.0/tests/conftest.py +80 -0
- rss_retriever-0.1.0/tests/test_config.py +71 -0
- rss_retriever-0.1.0/tests/test_content_extractor.py +132 -0
- rss_retriever-0.1.0/tests/test_news_service.py +126 -0
- rss_retriever-0.1.0/tests/test_rss_adapter.py +108 -0
- rss_retriever-0.1.0/tests/test_storage.py +177 -0
- rss_retriever-0.1.0/uv.lock +1241 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [created]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
target:
|
|
9
|
+
description: Index to publish to
|
|
10
|
+
required: true
|
|
11
|
+
default: testpypi
|
|
12
|
+
type: choice
|
|
13
|
+
options:
|
|
14
|
+
- testpypi
|
|
15
|
+
- pypi
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v7
|
|
22
|
+
|
|
23
|
+
- name: Install uv
|
|
24
|
+
uses: astral-sh/setup-uv@v7
|
|
25
|
+
|
|
26
|
+
- name: Build sdist and wheel
|
|
27
|
+
run: uv build
|
|
28
|
+
|
|
29
|
+
# Catches malformed metadata before it reaches an immutable index.
|
|
30
|
+
- name: Check distribution metadata
|
|
31
|
+
run: uvx twine check dist/*
|
|
32
|
+
|
|
33
|
+
- uses: actions/upload-artifact@v7
|
|
34
|
+
with:
|
|
35
|
+
name: dist
|
|
36
|
+
path: dist/
|
|
37
|
+
|
|
38
|
+
testpypi:
|
|
39
|
+
needs: build
|
|
40
|
+
if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/download-artifact@v8
|
|
44
|
+
with:
|
|
45
|
+
name: dist
|
|
46
|
+
path: dist/
|
|
47
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
48
|
+
with:
|
|
49
|
+
repository-url: https://test.pypi.org/legacy/
|
|
50
|
+
password: ${{ secrets.TEST_PYPI_TOKEN || secrets.PYPI_TOKEN }}
|
|
51
|
+
|
|
52
|
+
pypi:
|
|
53
|
+
needs: build
|
|
54
|
+
if: github.event_name == 'release' || inputs.target == 'pypi'
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/download-artifact@v8
|
|
58
|
+
with:
|
|
59
|
+
name: dist
|
|
60
|
+
path: dist/
|
|
61
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
62
|
+
with:
|
|
63
|
+
password: ${{ secrets.PYPI_TOKEN }}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: release-please
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
pull-requests: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
release-please:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: googleapis/release-please-action@v5
|
|
17
|
+
with:
|
|
18
|
+
release-type: python
|
|
19
|
+
# A PAT, not the default GITHUB_TOKEN: releases created by
|
|
20
|
+
# github-actions[bot] cannot trigger further workflows, so the publish
|
|
21
|
+
# job would never fire on `on: release`.
|
|
22
|
+
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Run Unit Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
unit-tests:
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.12", "3.13"]
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v7
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v7
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
|
|
24
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
25
|
+
run: uv python install ${{ matrix.python-version }}
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: uv sync --extra dev
|
|
29
|
+
|
|
30
|
+
# Network-marked tests hit live sites and would make CI flaky.
|
|
31
|
+
- name: Run tests
|
|
32
|
+
run: uv run pytest -m "not network"
|
|
33
|
+
|
|
34
|
+
lint:
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v7
|
|
38
|
+
|
|
39
|
+
- name: Install uv
|
|
40
|
+
uses: astral-sh/setup-uv@v7
|
|
41
|
+
with:
|
|
42
|
+
enable-cache: true
|
|
43
|
+
|
|
44
|
+
- name: Install dependencies
|
|
45
|
+
run: uv sync --extra dev
|
|
46
|
+
|
|
47
|
+
- name: Ruff
|
|
48
|
+
run: uv run ruff check .
|
|
49
|
+
|
|
50
|
+
- name: mypy
|
|
51
|
+
run: uv run mypy rss_retriever/
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
*.egg
|
|
6
|
+
|
|
7
|
+
# Build artifacts
|
|
8
|
+
build/
|
|
9
|
+
dist/
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
|
|
15
|
+
# Test and tooling caches
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.coverage
|
|
20
|
+
coverage.xml
|
|
21
|
+
htmlcov/
|
|
22
|
+
|
|
23
|
+
# Runtime output
|
|
24
|
+
article_storage/
|
|
25
|
+
|
|
26
|
+
# Editors and OS
|
|
27
|
+
.DS_Store
|
|
28
|
+
.idea/
|
|
29
|
+
.vscode/
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-07-31)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* rss-retriever ([c367e94](https://github.com/OscillateLabsLLC/rss-retriever/commit/c367e9443e50ffc81e7ff8a50fe7ee2253bf0a1d))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **ci:** pin setup-uv to v7 ([d88192d](https://github.com/OscillateLabsLLC/rss-retriever/commit/d88192d3ba1c97f6859b4ac846da6d6b321acc84))
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Contributing to rss-retriever
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in contributing!
|
|
4
|
+
|
|
5
|
+
## Development Setup
|
|
6
|
+
|
|
7
|
+
### Prerequisites
|
|
8
|
+
|
|
9
|
+
- Python 3.12+
|
|
10
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
11
|
+
|
|
12
|
+
### Getting Started
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
git clone https://github.com/OscillateLabsLLC/rss-retriever
|
|
16
|
+
cd rss-retriever
|
|
17
|
+
|
|
18
|
+
# Install dependencies (including dev extras)
|
|
19
|
+
uv sync --extra dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Common Commands
|
|
23
|
+
|
|
24
|
+
This repo uses [just](https://github.com/casey/just) as its task runner. `just --list` shows
|
|
25
|
+
everything available; the `uv` equivalents are given below in case you'd rather not install it.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
just test # unit tests, no network access required
|
|
29
|
+
just test-all # everything, including tests that hit live sites
|
|
30
|
+
just check # lint, format and type-check
|
|
31
|
+
just validate # check + test
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Without `just`:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uv run pytest -m "not network"
|
|
38
|
+
uv run pytest
|
|
39
|
+
uv run ruff check .
|
|
40
|
+
uv run mypy rss_retriever/
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Tests that reach the network are marked with `@pytest.mark.network` so they can be deselected in
|
|
44
|
+
CI and offline development. Prefer fixtures over live requests when adding tests.
|
|
45
|
+
|
|
46
|
+
## Pull Requests
|
|
47
|
+
|
|
48
|
+
1. Create a feature branch: `git checkout -b feat/my-feature`
|
|
49
|
+
2. Make your changes and add tests where applicable
|
|
50
|
+
3. Run `uv run pytest -m "not network"` to ensure everything passes
|
|
51
|
+
4. Commit using [Conventional Commits](https://www.conventionalcommits.org/) (e.g., `feat:`, `fix:`, `docs:`)
|
|
52
|
+
5. Open a pull request
|
|
53
|
+
|
|
54
|
+
**PR Guidelines:**
|
|
55
|
+
|
|
56
|
+
- Keep PRs focused on a single concern
|
|
57
|
+
- Include tests for new functionality
|
|
58
|
+
- Ensure all CI checks pass
|
|
59
|
+
|
|
60
|
+
Releases are automated with release-please: merged conventional commits drive the version bump and
|
|
61
|
+
publish to PyPI, so commit messages matter.
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
By contributing, you agree that your contributions will be licensed under the MIT License.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Use lightweight Python base image
|
|
2
|
+
FROM python:3.12-slim
|
|
3
|
+
|
|
4
|
+
ENV PYTHONUNBUFFERED=1 \
|
|
5
|
+
PYTHONDONTWRITEBYTECODE=1 \
|
|
6
|
+
DEBIAN_FRONTEND=noninteractive \
|
|
7
|
+
UV_PROJECT_ENVIRONMENT=/usr/local \
|
|
8
|
+
RSS_RETRIEVER_STORAGE_DIR=/app/article_storage \
|
|
9
|
+
RSS_RETRIEVER_LOG_LEVEL=INFO \
|
|
10
|
+
RSS_RETRIEVER_ARTICLES_PER_SOURCE=5 \
|
|
11
|
+
RSS_RETRIEVER_RECENT_ARTICLES_LIMIT=10 \
|
|
12
|
+
RSS_RETRIEVER_PREVIEW_IMAGE_COUNT=3 \
|
|
13
|
+
RSS_RETRIEVER_REQUEST_TIMEOUT=10 \
|
|
14
|
+
RSS_RETRIEVER_CHUNK_SIZE=8192
|
|
15
|
+
|
|
16
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
17
|
+
ca-certificates \
|
|
18
|
+
&& apt-get clean \
|
|
19
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
20
|
+
|
|
21
|
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
|
22
|
+
|
|
23
|
+
WORKDIR /app
|
|
24
|
+
|
|
25
|
+
COPY pyproject.toml README.md LICENSE /app/
|
|
26
|
+
COPY rss_retriever /app/rss_retriever
|
|
27
|
+
|
|
28
|
+
# Tracing is opt-in; build with --build-arg EXTRAS='[otel]' to include it.
|
|
29
|
+
ARG EXTRAS=""
|
|
30
|
+
RUN uv pip install --system ".${EXTRAS}"
|
|
31
|
+
|
|
32
|
+
RUN mkdir -p ${RSS_RETRIEVER_STORAGE_DIR} \
|
|
33
|
+
&& chmod -R 755 /app
|
|
34
|
+
|
|
35
|
+
CMD ["python3", "-m", "rss_retriever"]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# List all recipes
|
|
2
|
+
default:
|
|
3
|
+
@just --list
|
|
4
|
+
|
|
5
|
+
# Install dependencies from the lockfile
|
|
6
|
+
install:
|
|
7
|
+
uv sync
|
|
8
|
+
|
|
9
|
+
install-all:
|
|
10
|
+
uv sync --all-extras
|
|
11
|
+
|
|
12
|
+
# Generate lockfile from pyproject.toml
|
|
13
|
+
lock:
|
|
14
|
+
uv lock
|
|
15
|
+
|
|
16
|
+
# Update dependencies and regenerate lockfile
|
|
17
|
+
upgrade:
|
|
18
|
+
uv lock --upgrade
|
|
19
|
+
|
|
20
|
+
# Run Ruff linter
|
|
21
|
+
lint:
|
|
22
|
+
uv run ruff check .
|
|
23
|
+
|
|
24
|
+
# Run Ruff formatter
|
|
25
|
+
fmt:
|
|
26
|
+
uv run ruff format .
|
|
27
|
+
|
|
28
|
+
# Run the type checker
|
|
29
|
+
typecheck:
|
|
30
|
+
uv run mypy rss_retriever/
|
|
31
|
+
|
|
32
|
+
# Run linting, formatting, and type checking
|
|
33
|
+
check: lint fmt typecheck
|
|
34
|
+
|
|
35
|
+
# Run the unit tests (no network required)
|
|
36
|
+
test:
|
|
37
|
+
uv run pytest -m "not network"
|
|
38
|
+
|
|
39
|
+
# Run every test, including those that hit live sites
|
|
40
|
+
test-all:
|
|
41
|
+
uv run pytest
|
|
42
|
+
|
|
43
|
+
# Run both tests and checks
|
|
44
|
+
validate: check test
|
|
45
|
+
|
|
46
|
+
# Run the RSS retriever
|
|
47
|
+
run:
|
|
48
|
+
uv run python -m rss_retriever
|
|
49
|
+
|
|
50
|
+
# Run with console tracing for debugging
|
|
51
|
+
run-debug:
|
|
52
|
+
OTEL_EXPORTER=console uv run python -m rss_retriever
|
|
53
|
+
|
|
54
|
+
# Run Phoenix locally for tracing
|
|
55
|
+
phoenix:
|
|
56
|
+
docker run -p 6006:6006 ghcr.io/arize-ai/phoenix:latest
|
|
57
|
+
|
|
58
|
+
# Build Docker image
|
|
59
|
+
docker-build:
|
|
60
|
+
docker build -t rss-retriever .
|
|
61
|
+
|
|
62
|
+
# Clean up Python cache and build files
|
|
63
|
+
clean:
|
|
64
|
+
find . -type d -name "__pycache__" -exec rm -rf {} +
|
|
65
|
+
find . -type f -name "*.pyc" -delete
|
|
66
|
+
find . -type f -name "*.pyo" -delete
|
|
67
|
+
find . -type f -name "*.pyd" -delete
|
|
68
|
+
find . -type f -name ".coverage" -delete
|
|
69
|
+
find . -type d -name "*.egg-info" -exec rm -rf {} +
|
|
70
|
+
find . -type d -name "*.egg" -exec rm -rf {} +
|
|
71
|
+
find . -type d -name ".pytest_cache" -exec rm -rf {} +
|
|
72
|
+
find . -type d -name ".ruff_cache" -exec rm -rf {} +
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 OscillateLabs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rss-retriever
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: RSS feed retrieval and storage system with content extraction and image downloading capabilities
|
|
5
|
+
Project-URL: Repository, https://github.com/OscillateLabsLLC/rss-retriever
|
|
6
|
+
Project-URL: Issues, https://github.com/OscillateLabsLLC/rss-retriever/issues
|
|
7
|
+
Author-email: Mike Gray <mike@oscillatelabs.net>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: content-extraction,feed,image-download,news,rss
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content :: News/Diary
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Requires-Dist: aiohttp>=3.9.3
|
|
19
|
+
Requires-Dist: feedparser>=6.0.11
|
|
20
|
+
Requires-Dist: lxml[html-clean]>=5.3.1
|
|
21
|
+
Requires-Dist: newspaper4k>=0.9.3
|
|
22
|
+
Requires-Dist: opentelemetry-api>=1.30.0
|
|
23
|
+
Requires-Dist: python-json-logger>=3.3.0
|
|
24
|
+
Requires-Dist: requests>=2.32.3
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
30
|
+
Requires-Dist: types-requests; extra == 'dev'
|
|
31
|
+
Provides-Extra: otel
|
|
32
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.30.0; extra == 'otel'
|
|
33
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.30.0; extra == 'otel'
|
|
34
|
+
Requires-Dist: opentelemetry-instrumentation-aiohttp-client>=0.51b0; extra == 'otel'
|
|
35
|
+
Requires-Dist: opentelemetry-instrumentation-requests>=0.51b0; extra == 'otel'
|
|
36
|
+
Requires-Dist: opentelemetry-instrumentation>=0.51b0; extra == 'otel'
|
|
37
|
+
Requires-Dist: opentelemetry-sdk>=1.30.0; extra == 'otel'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# rss-retriever
|
|
41
|
+
|
|
42
|
+
[](https://github.com/OscillateLabsLLC/.github/blob/main/SUPPORT_STATUS.md)
|
|
43
|
+
[](https://github.com/OscillateLabsLLC/rss-retriever/actions/workflows/unit-tests.yml)
|
|
44
|
+
[](https://pypi.org/project/rss-retriever/)
|
|
45
|
+
|
|
46
|
+
Fetch articles from RSS feeds, extract their full text and images, and store them on disk.
|
|
47
|
+
|
|
48
|
+
Built around ports and adapters, so the pieces are usable independently: take the feed adapter
|
|
49
|
+
without the storage layer, swap in your own storage backend, or drive the whole pipeline from the
|
|
50
|
+
bundled CLI.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install rss-retriever
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Optional tracing support (OpenTelemetry SDK, OTLP exporters, and Arize Phoenix):
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install "rss-retriever[otel]"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The base install deliberately stays light. `opentelemetry-api` is included because it is a no-op
|
|
65
|
+
without an SDK, so instrumented code paths cost nothing when tracing is not configured.
|
|
66
|
+
|
|
67
|
+
Requires Python 3.12+.
|
|
68
|
+
|
|
69
|
+
## Library usage
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from rss_retriever import ContentExtractor, FileSystemStorage, NewsService, RSSFeedAdapter
|
|
73
|
+
|
|
74
|
+
service = NewsService(
|
|
75
|
+
RSSFeedAdapter({"Phys.org": "https://phys.org/rss-feed/"}),
|
|
76
|
+
FileSystemStorage("article_storage"),
|
|
77
|
+
ContentExtractor(),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
articles = service.fetch_and_store_articles(limit_per_source=5)
|
|
81
|
+
for article in service.get_recent_articles(limit=10):
|
|
82
|
+
print(article.source_name, article.title)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Articles already present in storage are skipped before the expensive extraction step, so re-running
|
|
86
|
+
against the same feeds is cheap.
|
|
87
|
+
|
|
88
|
+
### Bulk imports
|
|
89
|
+
|
|
90
|
+
`save_article` rewrites both index files on every call so a crash cannot orphan an article. That is
|
|
91
|
+
the right default for a nightly run but makes a large backfill quadratic in corpus size. Wrap bulk
|
|
92
|
+
ingestion in `batch_writes()` to pay the index cost once:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
with storage.batch_writes():
|
|
96
|
+
for article in many_articles:
|
|
97
|
+
storage.save_article(article)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Article payloads are still written immediately; only the indexes are deferred, and they are flushed
|
|
101
|
+
even if the block raises. On a 5,000-article import this is roughly 12x faster.
|
|
102
|
+
|
|
103
|
+
### Public API
|
|
104
|
+
|
|
105
|
+
| Object | Role |
|
|
106
|
+
| ------------------------- | ----------------------------------------------- |
|
|
107
|
+
| `NewsService` | Orchestrates fetch, extract, and store |
|
|
108
|
+
| `RSSFeedAdapter` | `NewsPort` implementation backed by feedparser |
|
|
109
|
+
| `ContentExtractor` | Full-text and image extraction via newspaper4k |
|
|
110
|
+
| `FileSystemStorage` | `StoragePort` implementation writing to disk |
|
|
111
|
+
| `NewsPort`, `StoragePort` | Abstract ports for custom implementations |
|
|
112
|
+
| `Article`, `ArticleImage` | Domain models, with `to_dict()` / `from_dict()` |
|
|
113
|
+
|
|
114
|
+
### Custom backends
|
|
115
|
+
|
|
116
|
+
Implement `StoragePort` to store somewhere other than the filesystem:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from rss_retriever import StoragePort
|
|
120
|
+
|
|
121
|
+
class S3Storage(StoragePort):
|
|
122
|
+
def save_article(self, article): ...
|
|
123
|
+
def get_article(self, article_id): ...
|
|
124
|
+
def get_recent_articles(self, limit=50): ...
|
|
125
|
+
def get_unread_articles(self, limit=50): ...
|
|
126
|
+
def article_exists(self, url) -> bool: ...
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## CLI
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
export RSS_RETRIEVER_RSS_FEEDS='{"Phys.org": "https://phys.org/rss-feed/"}'
|
|
133
|
+
export RSS_RETRIEVER_STORAGE_DIR=./article_storage
|
|
134
|
+
rss-retriever
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Configuration
|
|
138
|
+
|
|
139
|
+
Read from the environment by `Config.from_env()`. Construct `Config` directly to bypass the
|
|
140
|
+
environment entirely.
|
|
141
|
+
|
|
142
|
+
| Variable | Default | Meaning |
|
|
143
|
+
| ------------------------------------- | ----------------- | ----------------------------------------------- |
|
|
144
|
+
| `RSS_RETRIEVER_RSS_FEEDS` | `{}` | JSON object mapping source name to feed URL |
|
|
145
|
+
| `RSS_RETRIEVER_STORAGE_DIR` | `article_storage` | Where articles are written |
|
|
146
|
+
| `RSS_RETRIEVER_ARTICLES_PER_SOURCE` | `5` | Max articles fetched per feed per run |
|
|
147
|
+
| `RSS_RETRIEVER_RECENT_ARTICLES_LIMIT` | `10` | Max articles returned by recent-article queries |
|
|
148
|
+
| `RSS_RETRIEVER_PREVIEW_IMAGE_COUNT` | `3` | Images summarised per article in CLI output |
|
|
149
|
+
| `RSS_RETRIEVER_LOG_LEVEL` | `INFO` | Root log level |
|
|
150
|
+
| `RSS_RETRIEVER_REQUEST_TIMEOUT` | `10` | Per-request timeout in seconds |
|
|
151
|
+
| `RSS_RETRIEVER_CHUNK_SIZE` | `8192` | Download chunk size in bytes |
|
|
152
|
+
|
|
153
|
+
There are no default feeds: a library should not fetch anything the caller did not ask for.
|
|
154
|
+
`rss_retriever.config.EXAMPLE_FEEDS` holds a starting set if you want one.
|
|
155
|
+
|
|
156
|
+
## Storage layout
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
article_storage/
|
|
160
|
+
├── index.json # article_id -> metadata, for recency queries
|
|
161
|
+
├── url_index.json # url -> article_id, for deduplication
|
|
162
|
+
└── <source>_<url_hash>/
|
|
163
|
+
├── content.txt
|
|
164
|
+
├── content.html
|
|
165
|
+
├── metadata.json
|
|
166
|
+
└── images/
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Article IDs are `<source_slug>_<first 10 hex of md5(url)>` and are stable across runs.
|
|
170
|
+
|
|
171
|
+
## Tracing
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from rss_retriever.telemetry import setup_telemetry
|
|
175
|
+
|
|
176
|
+
setup_telemetry(service_name="my-service")
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Configured through the standard `OTEL_*` environment variables. Requires the `otel` extra. Note
|
|
180
|
+
that this installs a global tracer provider and sets OTLP defaults, so call it from an application
|
|
181
|
+
entry point rather than from library code.
|
|
182
|
+
|
|
183
|
+
## Development
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
uv sync --extra dev
|
|
187
|
+
uv run pytest -m "not network" # unit tests, no network
|
|
188
|
+
uv run pytest # includes live-network tests
|
|
189
|
+
uv run ruff check .
|
|
190
|
+
uv run mypy rss_retriever/
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT
|