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.
Files changed (44) hide show
  1. pyfinews-0.1.0/.gitignore +33 -0
  2. pyfinews-0.1.0/CHANGELOG.md +21 -0
  3. pyfinews-0.1.0/CONTRIBUTING.md +92 -0
  4. pyfinews-0.1.0/LICENSE +21 -0
  5. pyfinews-0.1.0/PKG-INFO +255 -0
  6. pyfinews-0.1.0/README.md +227 -0
  7. pyfinews-0.1.0/pyproject.toml +43 -0
  8. pyfinews-0.1.0/scripts/get_news.py +67 -0
  9. pyfinews-0.1.0/src/pyfinews/__init__.py +25 -0
  10. pyfinews-0.1.0/src/pyfinews/article_text.py +124 -0
  11. pyfinews-0.1.0/src/pyfinews/client.py +231 -0
  12. pyfinews-0.1.0/src/pyfinews/deduplication.py +34 -0
  13. pyfinews-0.1.0/src/pyfinews/exceptions.py +13 -0
  14. pyfinews-0.1.0/src/pyfinews/filters.py +31 -0
  15. pyfinews-0.1.0/src/pyfinews/models.py +85 -0
  16. pyfinews-0.1.0/src/pyfinews/progress.py +42 -0
  17. pyfinews-0.1.0/src/pyfinews/providers/__init__.py +5 -0
  18. pyfinews-0.1.0/src/pyfinews/providers/google_news.py +167 -0
  19. pyfinews-0.1.0/src/pyfinews/publishers/__init__.py +5 -0
  20. pyfinews-0.1.0/src/pyfinews/publishers/default_publishers.json +284 -0
  21. pyfinews-0.1.0/src/pyfinews/publishers/loader.py +127 -0
  22. pyfinews-0.1.0/src/pyfinews/queries/__init__.py +6 -0
  23. pyfinews-0.1.0/src/pyfinews/queries/financial.py +166 -0
  24. pyfinews-0.1.0/src/pyfinews/queries/google_news.py +25 -0
  25. pyfinews-0.1.0/src/pyfinews/taxonomy/__init__.py +5 -0
  26. pyfinews-0.1.0/src/pyfinews/taxonomy/default_taxonomy.json +194 -0
  27. pyfinews-0.1.0/src/pyfinews/taxonomy/loader.py +101 -0
  28. pyfinews-0.1.0/src/pyfinews/url_resolution.py +112 -0
  29. pyfinews-0.1.0/tests/providers/__init__.py +0 -0
  30. pyfinews-0.1.0/tests/providers/test_google_news.py +80 -0
  31. pyfinews-0.1.0/tests/publishers/__init__.py +0 -0
  32. pyfinews-0.1.0/tests/publishers/test_loader.py +69 -0
  33. pyfinews-0.1.0/tests/queries/__init__.py +0 -0
  34. pyfinews-0.1.0/tests/queries/test_financial.py +80 -0
  35. pyfinews-0.1.0/tests/queries/test_google_news.py +19 -0
  36. pyfinews-0.1.0/tests/taxonomy/__init__.py +0 -0
  37. pyfinews-0.1.0/tests/taxonomy/test_loader.py +72 -0
  38. pyfinews-0.1.0/tests/test_article_text.py +131 -0
  39. pyfinews-0.1.0/tests/test_client.py +188 -0
  40. pyfinews-0.1.0/tests/test_deduplication.py +24 -0
  41. pyfinews-0.1.0/tests/test_filters.py +25 -0
  42. pyfinews-0.1.0/tests/test_models.py +80 -0
  43. pyfinews-0.1.0/tests/test_url_resolution.py +79 -0
  44. 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.
@@ -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
+ ```
@@ -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
+ ```