pyfinews 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.
- pyfinews-0.1.0/.gitignore +33 -0
- pyfinews-0.1.0/CHANGELOG.md +21 -0
- pyfinews-0.1.0/CONTRIBUTING.md +92 -0
- pyfinews-0.1.0/LICENSE +21 -0
- pyfinews-0.1.0/PKG-INFO +255 -0
- pyfinews-0.1.0/README.md +227 -0
- pyfinews-0.1.0/pyproject.toml +43 -0
- pyfinews-0.1.0/scripts/get_news.py +67 -0
- pyfinews-0.1.0/src/pyfinews/__init__.py +25 -0
- pyfinews-0.1.0/src/pyfinews/article_text.py +124 -0
- pyfinews-0.1.0/src/pyfinews/client.py +231 -0
- pyfinews-0.1.0/src/pyfinews/deduplication.py +34 -0
- pyfinews-0.1.0/src/pyfinews/exceptions.py +13 -0
- pyfinews-0.1.0/src/pyfinews/filters.py +31 -0
- pyfinews-0.1.0/src/pyfinews/models.py +85 -0
- pyfinews-0.1.0/src/pyfinews/progress.py +42 -0
- pyfinews-0.1.0/src/pyfinews/providers/__init__.py +5 -0
- pyfinews-0.1.0/src/pyfinews/providers/google_news.py +167 -0
- pyfinews-0.1.0/src/pyfinews/publishers/__init__.py +5 -0
- pyfinews-0.1.0/src/pyfinews/publishers/default_publishers.json +284 -0
- pyfinews-0.1.0/src/pyfinews/publishers/loader.py +127 -0
- pyfinews-0.1.0/src/pyfinews/queries/__init__.py +6 -0
- pyfinews-0.1.0/src/pyfinews/queries/financial.py +166 -0
- pyfinews-0.1.0/src/pyfinews/queries/google_news.py +25 -0
- pyfinews-0.1.0/src/pyfinews/taxonomy/__init__.py +5 -0
- pyfinews-0.1.0/src/pyfinews/taxonomy/default_taxonomy.json +194 -0
- pyfinews-0.1.0/src/pyfinews/taxonomy/loader.py +101 -0
- pyfinews-0.1.0/src/pyfinews/url_resolution.py +112 -0
- pyfinews-0.1.0/tests/providers/__init__.py +0 -0
- pyfinews-0.1.0/tests/providers/test_google_news.py +80 -0
- pyfinews-0.1.0/tests/publishers/__init__.py +0 -0
- pyfinews-0.1.0/tests/publishers/test_loader.py +69 -0
- pyfinews-0.1.0/tests/queries/__init__.py +0 -0
- pyfinews-0.1.0/tests/queries/test_financial.py +80 -0
- pyfinews-0.1.0/tests/queries/test_google_news.py +19 -0
- pyfinews-0.1.0/tests/taxonomy/__init__.py +0 -0
- pyfinews-0.1.0/tests/taxonomy/test_loader.py +72 -0
- pyfinews-0.1.0/tests/test_article_text.py +131 -0
- pyfinews-0.1.0/tests/test_client.py +188 -0
- pyfinews-0.1.0/tests/test_deduplication.py +24 -0
- pyfinews-0.1.0/tests/test_filters.py +25 -0
- pyfinews-0.1.0/tests/test_models.py +80 -0
- pyfinews-0.1.0/tests/test_url_resolution.py +79 -0
- pyfinews-0.1.0/uv.lock +864 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
|
|
2
|
+
# Python bytecode and caches
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
.ruff_cache/
|
|
7
|
+
.mypy_cache/
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
|
|
13
|
+
# Build artifacts
|
|
14
|
+
build/
|
|
15
|
+
dist/
|
|
16
|
+
*.egg-info/
|
|
17
|
+
|
|
18
|
+
# Generated data
|
|
19
|
+
/data/
|
|
20
|
+
|
|
21
|
+
# Local environment variables and secrets
|
|
22
|
+
.env
|
|
23
|
+
.env.*
|
|
24
|
+
!.env.example
|
|
25
|
+
|
|
26
|
+
# Logs
|
|
27
|
+
*.log
|
|
28
|
+
|
|
29
|
+
# Editor and operating-system artifacts
|
|
30
|
+
.vscode/
|
|
31
|
+
.idea/
|
|
32
|
+
.DS_Store
|
|
33
|
+
Thumbs.db
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `pyfinews` are documented in this file.
|
|
4
|
+
|
|
5
|
+
This project uses a simplified version of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version numbers follow the principles of [Semantic Versioning](https://semver.org/), with the understanding that `0.y.z` releases are initial development releases and the public API may change.
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-10-20
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Initial public release of `pyfinews`.
|
|
11
|
+
- Financial news headline discovery through Google News RSS.
|
|
12
|
+
- A high-level client API for searching news.
|
|
13
|
+
- Query-building, article models, filtering, and deduplication functionality.
|
|
14
|
+
- Configurable taxonomy and publisher data used by financial news queries.
|
|
15
|
+
- Optional article-page text extraction functionality, where the relevant optional dependency is installed.
|
|
16
|
+
|
|
17
|
+
### Notes
|
|
18
|
+
- This is an initial development release. The public API may change in future `0.y.z` releases.
|
|
19
|
+
- See the README and package documentation for installation instructions, supported options, and usage examples.
|
|
20
|
+
|
|
21
|
+
[0.1.0]: https://github.com/sam-333/pyfinews/releases/tag/v0.1.0
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Contributing to pyfinews
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing to `pyfinews`! Contributions such as bug reports, documentation improvements, tests, and code changes are welcome.
|
|
4
|
+
|
|
5
|
+
## Before you start
|
|
6
|
+
|
|
7
|
+
- Search existing issues and pull requests to see whether the problem or proposal has already been discussed.
|
|
8
|
+
- For a substantial change or a new feature, open an issue first so the approach can be discussed before significant work is undertaken.
|
|
9
|
+
- For a bug report, include a clear description, steps to reproduce the problem, expected behavior, actual behavior, and relevant error output. Remove API tokens, credentials, and other sensitive information from logs.
|
|
10
|
+
|
|
11
|
+
## Development environment
|
|
12
|
+
|
|
13
|
+
`pyfinews` uses a `src/` layout and `uv` for project and dependency management. Python 3.12 or newer is required.
|
|
14
|
+
|
|
15
|
+
1. Fork the repository on GitHub and clone your fork.
|
|
16
|
+
2. Change into the repository directory.
|
|
17
|
+
3. Create/synchronise the development environment:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv sync --extra dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
If you are working on optional article-text extraction and the project defines the `article-text` extra, install it as well:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv sync --extra dev --extra article-text
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
4. Create a branch for your change:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git switch -c descriptive-branch-name
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Check `pyproject.toml` and the README for the project's current dependency groups and optional extras.
|
|
36
|
+
|
|
37
|
+
## Run checks before submitting
|
|
38
|
+
|
|
39
|
+
Run the test suite:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv run pytest
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Run Ruff's linter:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv run ruff check .
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Check formatting:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv run ruff format --check .
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If you change code, add or update tests that cover the behavior. Prefer deterministic tests that do not depend on live Google News responses or other external services. When relevant, test failure cases as well as successful behavior.
|
|
58
|
+
|
|
59
|
+
If a check fails, investigate and fix the underlying issue rather than suppressing the finding without explanation.
|
|
60
|
+
|
|
61
|
+
## Make your changes
|
|
62
|
+
|
|
63
|
+
- Keep changes focused on one issue or feature where practical.
|
|
64
|
+
- Follow the existing project structure, naming conventions, type-annotation style, and error-handling patterns.
|
|
65
|
+
- Update documentation when changing user-visible behavior, public APIs, installation requirements, or configuration.
|
|
66
|
+
- Update `CHANGELOG.md` under an `Unreleased` section for notable user-facing changes. Release maintainers will move those entries into a versioned section when preparing a release.
|
|
67
|
+
- Do not commit virtual environments, build artifacts, caches, credentials, tokens, or private data.
|
|
68
|
+
- Do not change the published version number as part of an ordinary pull request unless the maintainers specifically request it.
|
|
69
|
+
|
|
70
|
+
## Submit a pull request
|
|
71
|
+
|
|
72
|
+
1. Commit your changes with a concise, descriptive commit message.
|
|
73
|
+
2. Push your branch to your fork.
|
|
74
|
+
3. Open a pull request against the main development branch of the upstream repository.
|
|
75
|
+
4. Explain the problem or motivation, summarize the changes, and mention the tests/checks you ran.
|
|
76
|
+
5. Link related issues and call out any breaking changes or follow-up work.
|
|
77
|
+
|
|
78
|
+
A maintainer may request changes before merging. Please keep discussion constructive and be prepared to revise the contribution based on review feedback.
|
|
79
|
+
|
|
80
|
+
## Public API and compatibility
|
|
81
|
+
|
|
82
|
+
The project is in the `0.y.z` initial-development series. This means the public API is not yet guaranteed to be stable, and maintainers may make breaking changes before `1.0.0`. Even so, contributors should avoid unnecessary breaking changes and should clearly document any change that affects existing users.
|
|
83
|
+
|
|
84
|
+
## Releases
|
|
85
|
+
|
|
86
|
+
Releases are managed by project maintainers, not by ordinary pull requests. Maintainers are responsible for reviewing the changes, running release checks, updating the changelog and version, building the distribution artifacts, and publishing an approved release to PyPI.
|
|
87
|
+
|
|
88
|
+
Please do not upload distributions to PyPI or publish a release on behalf of the project unless you have explicit maintainer authorization.
|
|
89
|
+
|
|
90
|
+
## Questions
|
|
91
|
+
|
|
92
|
+
If you are unsure how best to contribute, open an issue for discussion before starting a large change.
|
pyfinews-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pyfinews contributors
|
|
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.
|
pyfinews-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pyfinews
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight Python package for discovering financial news headlines through Google News RSS.
|
|
5
|
+
Author: pyfinews contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: finance,financial-news,google-news,quantitative-research,rss
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Requires-Dist: feedparser<7.0,>=6.0.11
|
|
18
|
+
Provides-Extra: article-text
|
|
19
|
+
Requires-Dist: trafilatura<3.0,>=2.0; extra == 'article-text'
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
24
|
+
Provides-Extra: url-resolution
|
|
25
|
+
Requires-Dist: googlenewsdecoder>=0.1.7; extra == 'url-resolution'
|
|
26
|
+
Requires-Dist: selectolax<1.0; extra == 'url-resolution'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# pyfinews
|
|
30
|
+
|
|
31
|
+
`pyfinews` is a lightweight Python library for discovering financial-news headlines through Google News RSS. It provides a small client API, configurable financial-domain taxonomy, publisher-priority query planning, date filtering, and normalized article models.
|
|
32
|
+
|
|
33
|
+
The RSS provider uses Python's standard library for HTTP transport and `feedparser` for RSS/Atom parsing. Optional article downloading and main-text extraction are handled by Trafilatura. `feedparser` is installed automatically as a runtime dependency.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- General financial-news discovery across configurable domains.
|
|
38
|
+
- Topic-specific discovery for tickers, companies, sectors, macro topics, and search phrases.
|
|
39
|
+
- Configurable RSS request budget through `query_limit`.
|
|
40
|
+
- Publisher selection ordered by ascending `priority` (priority `1` is highest).
|
|
41
|
+
- Multiple domain terms combined into a single OR expression per publisher/domain combination.
|
|
42
|
+
- Configurable publisher and domain JSON files.
|
|
43
|
+
- Lookback filtering and newest-first sorting.
|
|
44
|
+
- No article deduplication: repeated items returned by Google News or overlapping queries are preserved.
|
|
45
|
+
- Testable transport and query-planning layers.
|
|
46
|
+
- Optional Google News RSS URL resolution to publisher URLs.
|
|
47
|
+
- Optional article main-text extraction with Trafilatura.
|
|
48
|
+
|
|
49
|
+
## Requirements
|
|
50
|
+
|
|
51
|
+
- Python 3.12 or newer.
|
|
52
|
+
- `uv` is recommended for environment and dependency management.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
Install from a local checkout:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv sync --extra dev
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Install from a package index if published:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv add pyfinews
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Optional article-text extraction
|
|
69
|
+
|
|
70
|
+
Article text extraction is an optional dependency so headline discovery does not need to install the extraction stack. From a source checkout, install it with:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv sync --extra article-text
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Or, when installing the published package, use the `article-text` extra. Then:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from pyfinews import NewsClient
|
|
80
|
+
|
|
81
|
+
with NewsClient() as news:
|
|
82
|
+
articles = news.search('Apple earnings', max_articles=10, resolve_urls=True)
|
|
83
|
+
results = news.extract_articles_text(articles)
|
|
84
|
+
|
|
85
|
+
for article, result in zip(articles, results):
|
|
86
|
+
if result.status == "success":
|
|
87
|
+
print(article.title)
|
|
88
|
+
print(result.text)
|
|
89
|
+
else:
|
|
90
|
+
print(article.title, result.status, result.error)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`extract_article_text(article)` handles one `NewsArticle` or an HTTP(S) URL. `extract_articles_text(articles)` processes a list sequentially and preserves input order and duplicates. Each `ArticleTextResult` contains `url`, `text`, `status`, and `error`. `ArticleTextStatus` is a public string enum with `SUCCESS`, `FETCH_FAILED`, and `EXTRACTION_FAILED`; an individual page failure does not stop the batch. Trafilatura handles both publisher-page downloading and main-text extraction. Article extraction does not bypass paywalls, logins, or publisher access controls.
|
|
94
|
+
|
|
95
|
+
## Optional Google News URL resolution
|
|
96
|
+
|
|
97
|
+
Google News RSS entries commonly link to Google News URLs rather than directly to publisher pages. URL resolution is an optional, separate step; the normal RSS ingestion flow remains unchanged. You can resolve results directly through `NewsClient` using `resolve_urls=True` on `get()` or `search()`, or call `resolve_article_urls()` on existing results. Install the resolver extra from a source checkout:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
uv sync --extra url-resolution
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
After exporting RSS results to a JSON array (or an object with an `articles` array), resolve them with:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
uv run --extra url-resolution python scripts/resolve_urls.py data/articles.json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The script writes `data/articles_resolved.json` by default. It preserves the original `url` field, adds `rss_url`, `resolved_url`, `url_resolution_status`, and `url_resolution_error`, and continues processing when individual URLs fail. The original RSS URL remains available for provenance. The `NewsArticle.resolved_url` field is optional and appended to the model to preserve existing positional arguments. `ArticleTextExtractor.extract(article)` uses `resolved_url` when present and otherwise falls back to `url`. URL resolution does not bypass publisher paywalls, access controls, or site availability problems.
|
|
110
|
+
|
|
111
|
+
## Quick start
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from pyfinews import NewsClient
|
|
115
|
+
|
|
116
|
+
with NewsClient() as news:
|
|
117
|
+
# Broad financial-news discovery
|
|
118
|
+
market_news = news.get(hours=24, query_limit=30, max_articles=100)
|
|
119
|
+
|
|
120
|
+
# Topic-specific discovery
|
|
121
|
+
apple_news = news.get(
|
|
122
|
+
topic='AAPL OR "Apple Inc"',
|
|
123
|
+
hours=72,
|
|
124
|
+
query_limit=10,
|
|
125
|
+
max_articles=50,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# A single custom Google News query
|
|
129
|
+
custom_news = news.search('semiconductor supply chain', hours=48, max_articles=20)
|
|
130
|
+
|
|
131
|
+
for article in market_news:
|
|
132
|
+
print(article.published_at, article.source, article.title)
|
|
133
|
+
print(article.url)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## API overview
|
|
137
|
+
|
|
138
|
+
### `NewsClient(...)`
|
|
139
|
+
|
|
140
|
+
- `timeout=10.0`: HTTP timeout in seconds.
|
|
141
|
+
- `query_limit=30`: default maximum number of RSS requests made by each `get()` call.
|
|
142
|
+
- `taxonomy=None`: optional list of domain configuration dictionaries.
|
|
143
|
+
- `publishers=None`: optional list of publisher configuration dictionaries.
|
|
144
|
+
- `provider=None`: optional provider implementation, useful for testing or customization.
|
|
145
|
+
|
|
146
|
+
### `news.get(...)`
|
|
147
|
+
|
|
148
|
+
- `topic=None`: optional ticker, company, sector, macro topic, or query phrase. If omitted, uses the financial taxonomy.
|
|
149
|
+
- `hours=24`: lookback window in hours. Set to `None` to disable local lookback filtering and omit the freshness operator.
|
|
150
|
+
- `max_articles=20`: maximum number of articles returned; pass `None` for no client-side maximum.
|
|
151
|
+
- `query_limit=None`: per-call maximum number of RSS requests; defaults to the client's configured budget.
|
|
152
|
+
|
|
153
|
+
`max_articles` controls the maximum number of returned articles; `max_articles=None` disables the result limit (the number returned still depends on what Google News supplies). `query_limit` controls the number of RSS requests, not the number of articles. Each attempted query consumes one request from the budget. A small budget favors higher-priority publishers and may not cover every domain.
|
|
154
|
+
|
|
155
|
+
### `news.search(query, ...)`
|
|
156
|
+
|
|
157
|
+
Runs one arbitrary query without the financial taxonomy or publisher query planner. It accepts `hours` and `max_articles`, but not `query_limit` because it makes one RSS request.
|
|
158
|
+
|
|
159
|
+
### `NewsArticle`
|
|
160
|
+
|
|
161
|
+
Each result is an immutable `NewsArticle` with `title`, `url`, optional `source`, `published_at`, `retrieved_at`, `summary`, `query`, `resolved_url`, `url_resolution_status`, and `url_resolution_error` fields. Datetimes are normalized to UTC. `url` retains the URL supplied by the feed; `resolved_url` optionally stores the publisher URL.
|
|
162
|
+
|
|
163
|
+
## Query planning
|
|
164
|
+
|
|
165
|
+
For market-wide discovery, the planner:
|
|
166
|
+
|
|
167
|
+
1. Excludes domains and publishers marked `enabled: false`.
|
|
168
|
+
2. Sorts enabled publishers by ascending numeric `priority`, with publisher ID as a deterministic tie-breaker.
|
|
169
|
+
3. Combines all configured terms within each domain into one quoted OR expression.
|
|
170
|
+
4. Generates publisher/domain queries, iterating publishers by priority and domains in taxonomy order.
|
|
171
|
+
5. Appends broad domain-only queries after publisher-targeted combinations.
|
|
172
|
+
6. Removes duplicate query strings and returns no more than `query_limit` queries.
|
|
173
|
+
|
|
174
|
+
For topic-specific discovery, the planner starts with the base topic query, followed by publisher-targeted variants in priority order. Publisher names are query hints, not strict source filters. The `aliases` and `regions` fields are retained as metadata; they do not currently affect query selection.
|
|
175
|
+
|
|
176
|
+
## Configuration
|
|
177
|
+
|
|
178
|
+
Default JSON files ship with the package:
|
|
179
|
+
|
|
180
|
+
- `pyfinews.taxonomy/default_taxonomy.json`: financial domains and query terms.
|
|
181
|
+
- `pyfinews.publishers/default_publishers.json`: publisher metadata.
|
|
182
|
+
|
|
183
|
+
Example publisher entry:
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{
|
|
187
|
+
"id": "reuters",
|
|
188
|
+
"name": "Reuters",
|
|
189
|
+
"aliases": ["Reuters News"],
|
|
190
|
+
"regions": ["global", "asia"],
|
|
191
|
+
"enabled": true,
|
|
192
|
+
"priority": 1
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Load the defaults or a custom configuration file:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from pyfinews import load_publishers, load_taxonomy
|
|
200
|
+
|
|
201
|
+
domains = load_taxonomy()
|
|
202
|
+
publishers = load_publishers()
|
|
203
|
+
custom_domains = load_taxonomy("config/taxonomy.json")
|
|
204
|
+
custom_publishers = load_publishers("config/publishers.json")
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
See [Configuration](docs/configuration.md) for the schema and validation rules.
|
|
208
|
+
|
|
209
|
+
## Development and tests
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
uv sync --extra dev
|
|
213
|
+
uv run pytest
|
|
214
|
+
uv run ruff check .
|
|
215
|
+
uv run python -m compileall -q src tests
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
An optional live RSS check is available at `scripts/validate_rss.py`. It makes external network requests, so it is separate from the unit test suite:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
uv run python scripts/validate_rss.py
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Important behavior and limitations
|
|
225
|
+
|
|
226
|
+
- Google News RSS is not a stable, officially documented search API; behavior and availability may change.
|
|
227
|
+
- Freshness operators are approximate; local date filtering is also applied when `hours` is specified.
|
|
228
|
+
- Publisher names in queries do not guarantee that all results originate from that publisher.
|
|
229
|
+
- Summary text and timestamps depend on the RSS entries returned by Google News.
|
|
230
|
+
- Headline discovery retrieves RSS-provided metadata. Optional article-text extraction attempts to retrieve publisher pages, but does not bypass paywalls, logins, or publisher access controls; some pages may not yield text.
|
|
231
|
+
|
|
232
|
+
## Documentation
|
|
233
|
+
|
|
234
|
+
- [Architecture](docs/architecture.md)
|
|
235
|
+
- [Configuration](docs/configuration.md)
|
|
236
|
+
- [API reference](docs/api.md)
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
MIT. See [LICENSE](LICENSE).
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
### Progress updates for URL resolution and text extraction
|
|
244
|
+
|
|
245
|
+
`NewsClient.get()` and `NewsClient.search()` print item-level progress updates to stderr by default when URL resolution or article-text extraction is enabled. Set `show_progress=False` to suppress these updates. Headlines are deduplicated by normalized title before `max_articles` is applied; URLs are not part of the deduplication key.
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
articles = client.get(
|
|
249
|
+
topic="AAPL",
|
|
250
|
+
max_articles=20,
|
|
251
|
+
resolve_urls=True,
|
|
252
|
+
extract_text=True,
|
|
253
|
+
show_progress=True,
|
|
254
|
+
)
|
|
255
|
+
```
|
pyfinews-0.1.0/README.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# pyfinews
|
|
2
|
+
|
|
3
|
+
`pyfinews` is a lightweight Python library for discovering financial-news headlines through Google News RSS. It provides a small client API, configurable financial-domain taxonomy, publisher-priority query planning, date filtering, and normalized article models.
|
|
4
|
+
|
|
5
|
+
The RSS provider uses Python's standard library for HTTP transport and `feedparser` for RSS/Atom parsing. Optional article downloading and main-text extraction are handled by Trafilatura. `feedparser` is installed automatically as a runtime dependency.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- General financial-news discovery across configurable domains.
|
|
10
|
+
- Topic-specific discovery for tickers, companies, sectors, macro topics, and search phrases.
|
|
11
|
+
- Configurable RSS request budget through `query_limit`.
|
|
12
|
+
- Publisher selection ordered by ascending `priority` (priority `1` is highest).
|
|
13
|
+
- Multiple domain terms combined into a single OR expression per publisher/domain combination.
|
|
14
|
+
- Configurable publisher and domain JSON files.
|
|
15
|
+
- Lookback filtering and newest-first sorting.
|
|
16
|
+
- No article deduplication: repeated items returned by Google News or overlapping queries are preserved.
|
|
17
|
+
- Testable transport and query-planning layers.
|
|
18
|
+
- Optional Google News RSS URL resolution to publisher URLs.
|
|
19
|
+
- Optional article main-text extraction with Trafilatura.
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- Python 3.12 or newer.
|
|
24
|
+
- `uv` is recommended for environment and dependency management.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Install from a local checkout:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv sync --extra dev
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Install from a package index if published:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uv add pyfinews
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Optional article-text extraction
|
|
41
|
+
|
|
42
|
+
Article text extraction is an optional dependency so headline discovery does not need to install the extraction stack. From a source checkout, install it with:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv sync --extra article-text
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or, when installing the published package, use the `article-text` extra. Then:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from pyfinews import NewsClient
|
|
52
|
+
|
|
53
|
+
with NewsClient() as news:
|
|
54
|
+
articles = news.search('Apple earnings', max_articles=10, resolve_urls=True)
|
|
55
|
+
results = news.extract_articles_text(articles)
|
|
56
|
+
|
|
57
|
+
for article, result in zip(articles, results):
|
|
58
|
+
if result.status == "success":
|
|
59
|
+
print(article.title)
|
|
60
|
+
print(result.text)
|
|
61
|
+
else:
|
|
62
|
+
print(article.title, result.status, result.error)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`extract_article_text(article)` handles one `NewsArticle` or an HTTP(S) URL. `extract_articles_text(articles)` processes a list sequentially and preserves input order and duplicates. Each `ArticleTextResult` contains `url`, `text`, `status`, and `error`. `ArticleTextStatus` is a public string enum with `SUCCESS`, `FETCH_FAILED`, and `EXTRACTION_FAILED`; an individual page failure does not stop the batch. Trafilatura handles both publisher-page downloading and main-text extraction. Article extraction does not bypass paywalls, logins, or publisher access controls.
|
|
66
|
+
|
|
67
|
+
## Optional Google News URL resolution
|
|
68
|
+
|
|
69
|
+
Google News RSS entries commonly link to Google News URLs rather than directly to publisher pages. URL resolution is an optional, separate step; the normal RSS ingestion flow remains unchanged. You can resolve results directly through `NewsClient` using `resolve_urls=True` on `get()` or `search()`, or call `resolve_article_urls()` on existing results. Install the resolver extra from a source checkout:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
uv sync --extra url-resolution
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
After exporting RSS results to a JSON array (or an object with an `articles` array), resolve them with:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
uv run --extra url-resolution python scripts/resolve_urls.py data/articles.json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The script writes `data/articles_resolved.json` by default. It preserves the original `url` field, adds `rss_url`, `resolved_url`, `url_resolution_status`, and `url_resolution_error`, and continues processing when individual URLs fail. The original RSS URL remains available for provenance. The `NewsArticle.resolved_url` field is optional and appended to the model to preserve existing positional arguments. `ArticleTextExtractor.extract(article)` uses `resolved_url` when present and otherwise falls back to `url`. URL resolution does not bypass publisher paywalls, access controls, or site availability problems.
|
|
82
|
+
|
|
83
|
+
## Quick start
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from pyfinews import NewsClient
|
|
87
|
+
|
|
88
|
+
with NewsClient() as news:
|
|
89
|
+
# Broad financial-news discovery
|
|
90
|
+
market_news = news.get(hours=24, query_limit=30, max_articles=100)
|
|
91
|
+
|
|
92
|
+
# Topic-specific discovery
|
|
93
|
+
apple_news = news.get(
|
|
94
|
+
topic='AAPL OR "Apple Inc"',
|
|
95
|
+
hours=72,
|
|
96
|
+
query_limit=10,
|
|
97
|
+
max_articles=50,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# A single custom Google News query
|
|
101
|
+
custom_news = news.search('semiconductor supply chain', hours=48, max_articles=20)
|
|
102
|
+
|
|
103
|
+
for article in market_news:
|
|
104
|
+
print(article.published_at, article.source, article.title)
|
|
105
|
+
print(article.url)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## API overview
|
|
109
|
+
|
|
110
|
+
### `NewsClient(...)`
|
|
111
|
+
|
|
112
|
+
- `timeout=10.0`: HTTP timeout in seconds.
|
|
113
|
+
- `query_limit=30`: default maximum number of RSS requests made by each `get()` call.
|
|
114
|
+
- `taxonomy=None`: optional list of domain configuration dictionaries.
|
|
115
|
+
- `publishers=None`: optional list of publisher configuration dictionaries.
|
|
116
|
+
- `provider=None`: optional provider implementation, useful for testing or customization.
|
|
117
|
+
|
|
118
|
+
### `news.get(...)`
|
|
119
|
+
|
|
120
|
+
- `topic=None`: optional ticker, company, sector, macro topic, or query phrase. If omitted, uses the financial taxonomy.
|
|
121
|
+
- `hours=24`: lookback window in hours. Set to `None` to disable local lookback filtering and omit the freshness operator.
|
|
122
|
+
- `max_articles=20`: maximum number of articles returned; pass `None` for no client-side maximum.
|
|
123
|
+
- `query_limit=None`: per-call maximum number of RSS requests; defaults to the client's configured budget.
|
|
124
|
+
|
|
125
|
+
`max_articles` controls the maximum number of returned articles; `max_articles=None` disables the result limit (the number returned still depends on what Google News supplies). `query_limit` controls the number of RSS requests, not the number of articles. Each attempted query consumes one request from the budget. A small budget favors higher-priority publishers and may not cover every domain.
|
|
126
|
+
|
|
127
|
+
### `news.search(query, ...)`
|
|
128
|
+
|
|
129
|
+
Runs one arbitrary query without the financial taxonomy or publisher query planner. It accepts `hours` and `max_articles`, but not `query_limit` because it makes one RSS request.
|
|
130
|
+
|
|
131
|
+
### `NewsArticle`
|
|
132
|
+
|
|
133
|
+
Each result is an immutable `NewsArticle` with `title`, `url`, optional `source`, `published_at`, `retrieved_at`, `summary`, `query`, `resolved_url`, `url_resolution_status`, and `url_resolution_error` fields. Datetimes are normalized to UTC. `url` retains the URL supplied by the feed; `resolved_url` optionally stores the publisher URL.
|
|
134
|
+
|
|
135
|
+
## Query planning
|
|
136
|
+
|
|
137
|
+
For market-wide discovery, the planner:
|
|
138
|
+
|
|
139
|
+
1. Excludes domains and publishers marked `enabled: false`.
|
|
140
|
+
2. Sorts enabled publishers by ascending numeric `priority`, with publisher ID as a deterministic tie-breaker.
|
|
141
|
+
3. Combines all configured terms within each domain into one quoted OR expression.
|
|
142
|
+
4. Generates publisher/domain queries, iterating publishers by priority and domains in taxonomy order.
|
|
143
|
+
5. Appends broad domain-only queries after publisher-targeted combinations.
|
|
144
|
+
6. Removes duplicate query strings and returns no more than `query_limit` queries.
|
|
145
|
+
|
|
146
|
+
For topic-specific discovery, the planner starts with the base topic query, followed by publisher-targeted variants in priority order. Publisher names are query hints, not strict source filters. The `aliases` and `regions` fields are retained as metadata; they do not currently affect query selection.
|
|
147
|
+
|
|
148
|
+
## Configuration
|
|
149
|
+
|
|
150
|
+
Default JSON files ship with the package:
|
|
151
|
+
|
|
152
|
+
- `pyfinews.taxonomy/default_taxonomy.json`: financial domains and query terms.
|
|
153
|
+
- `pyfinews.publishers/default_publishers.json`: publisher metadata.
|
|
154
|
+
|
|
155
|
+
Example publisher entry:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"id": "reuters",
|
|
160
|
+
"name": "Reuters",
|
|
161
|
+
"aliases": ["Reuters News"],
|
|
162
|
+
"regions": ["global", "asia"],
|
|
163
|
+
"enabled": true,
|
|
164
|
+
"priority": 1
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Load the defaults or a custom configuration file:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from pyfinews import load_publishers, load_taxonomy
|
|
172
|
+
|
|
173
|
+
domains = load_taxonomy()
|
|
174
|
+
publishers = load_publishers()
|
|
175
|
+
custom_domains = load_taxonomy("config/taxonomy.json")
|
|
176
|
+
custom_publishers = load_publishers("config/publishers.json")
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
See [Configuration](docs/configuration.md) for the schema and validation rules.
|
|
180
|
+
|
|
181
|
+
## Development and tests
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
uv sync --extra dev
|
|
185
|
+
uv run pytest
|
|
186
|
+
uv run ruff check .
|
|
187
|
+
uv run python -m compileall -q src tests
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
An optional live RSS check is available at `scripts/validate_rss.py`. It makes external network requests, so it is separate from the unit test suite:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
uv run python scripts/validate_rss.py
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Important behavior and limitations
|
|
197
|
+
|
|
198
|
+
- Google News RSS is not a stable, officially documented search API; behavior and availability may change.
|
|
199
|
+
- Freshness operators are approximate; local date filtering is also applied when `hours` is specified.
|
|
200
|
+
- Publisher names in queries do not guarantee that all results originate from that publisher.
|
|
201
|
+
- Summary text and timestamps depend on the RSS entries returned by Google News.
|
|
202
|
+
- Headline discovery retrieves RSS-provided metadata. Optional article-text extraction attempts to retrieve publisher pages, but does not bypass paywalls, logins, or publisher access controls; some pages may not yield text.
|
|
203
|
+
|
|
204
|
+
## Documentation
|
|
205
|
+
|
|
206
|
+
- [Architecture](docs/architecture.md)
|
|
207
|
+
- [Configuration](docs/configuration.md)
|
|
208
|
+
- [API reference](docs/api.md)
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT. See [LICENSE](LICENSE).
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
### Progress updates for URL resolution and text extraction
|
|
216
|
+
|
|
217
|
+
`NewsClient.get()` and `NewsClient.search()` print item-level progress updates to stderr by default when URL resolution or article-text extraction is enabled. Set `show_progress=False` to suppress these updates. Headlines are deduplicated by normalized title before `max_articles` is applied; URLs are not part of the deduplication key.
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
articles = client.get(
|
|
221
|
+
topic="AAPL",
|
|
222
|
+
max_articles=20,
|
|
223
|
+
resolve_urls=True,
|
|
224
|
+
extract_text=True,
|
|
225
|
+
show_progress=True,
|
|
226
|
+
)
|
|
227
|
+
```
|