finbrain-python 0.2.1__tar.gz → 0.2.5__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.
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/.github/workflows/ci.yml +1 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/.github/workflows/release.yml +2 -2
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/CHANGELOG.md +52 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/PKG-INFO +62 -1
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/README.md +59 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/RELEASE.md +1 -1
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/pyproject.toml +9 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/aio/client.py +9 -3
- finbrain_python-0.2.5/src/finbrain/aio/endpoints/_utils.py +12 -0
- finbrain_python-0.2.5/src/finbrain/aio/endpoints/government_contracts.py +48 -0
- finbrain_python-0.2.5/src/finbrain/aio/endpoints/patent_filings.py +48 -0
- finbrain_python-0.2.5/src/finbrain/aio/endpoints/reddit_mentions.py +48 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/screener.py +38 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/client.py +22 -3
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/endpoints/_utils.py +7 -1
- finbrain_python-0.2.5/src/finbrain/endpoints/government_contracts.py +73 -0
- finbrain_python-0.2.5/src/finbrain/endpoints/patent_filings.py +74 -0
- finbrain_python-0.2.5/src/finbrain/endpoints/reddit_mentions.py +73 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/endpoints/screener.py +38 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/exceptions.py +24 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/plotting.py +728 -110
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain_python.egg-info/PKG-INFO +62 -1
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain_python.egg-info/SOURCES.txt +12 -1
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain_python.egg-info/requires.txt +2 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/test_async_client.py +3 -0
- finbrain_python-0.2.5/tests/test_client.py +46 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/test_envelope.py +43 -1
- finbrain_python-0.2.5/tests/test_government_contracts.py +214 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/test_integration.py +127 -0
- finbrain_python-0.2.5/tests/test_patent_filings.py +207 -0
- finbrain_python-0.2.5/tests/test_plotting.py +689 -0
- finbrain_python-0.2.5/tests/test_reddit_mentions.py +162 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/test_screener.py +72 -0
- finbrain_python-0.2.5/tests/test_utils.py +24 -0
- finbrain_python-0.2.1/src/finbrain/aio/endpoints/_utils.py +0 -37
- finbrain_python-0.2.1/tests/test_plotting.py +0 -134
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/.gitattributes +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/.gitignore +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/LICENSE +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/examples/async_example.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/examples/transactions_plotting_example.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/setup.cfg +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/__init__.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/aio/__init__.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/__init__.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/analyst_ratings.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/app_ratings.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/available.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/corporate_lobbying.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/house_trades.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/insider_transactions.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/linkedin_data.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/news.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/options.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/predictions.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/recent.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/senate_trades.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/sentiments.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/endpoints/__init__.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/endpoints/analyst_ratings.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/endpoints/app_ratings.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/endpoints/available.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/endpoints/corporate_lobbying.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/endpoints/house_trades.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/endpoints/insider_transactions.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/endpoints/linkedin_data.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/endpoints/news.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/endpoints/options.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/endpoints/predictions.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/endpoints/recent.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/endpoints/senate_trades.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/endpoints/sentiments.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain/py.typed +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain_python.egg-info/dependency_links.txt +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/src/finbrain_python.egg-info/top_level.txt +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/__init__.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/conftest.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/test_analyst_ratings.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/test_app_ratings.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/test_available.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/test_corporate_lobbying.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/test_house_trades.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/test_insider_transactions.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/test_linkedin_data.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/test_news.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/test_options.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/test_predictions.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/test_recent.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/test_senate_trades.py +0 -0
- {finbrain_python-0.2.1 → finbrain_python-0.2.5}/tests/test_sentiments.py +0 -0
|
@@ -24,14 +24,14 @@ jobs:
|
|
|
24
24
|
|
|
25
25
|
# ─────────── push to TestPyPI first ───────────
|
|
26
26
|
- name: Upload to TestPyPI
|
|
27
|
-
run: twine upload --repository-url https://test.pypi.org/legacy/ dist/*
|
|
27
|
+
run: twine upload --skip-existing --repository-url https://test.pypi.org/legacy/ dist/*
|
|
28
28
|
env:
|
|
29
29
|
TWINE_USERNAME: __token__
|
|
30
30
|
TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
|
31
31
|
|
|
32
32
|
# ─────────── then to production PyPI ───────────
|
|
33
33
|
- name: Upload to PyPI
|
|
34
|
-
run: twine upload dist/*
|
|
34
|
+
run: twine upload --skip-existing dist/*
|
|
35
35
|
env:
|
|
36
36
|
TWINE_USERNAME: __token__
|
|
37
37
|
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -5,6 +5,58 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.5] - 2026-06-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Patent Filings Endpoint**: `fb.patent_filings.ticker("AAPL")` — fetch USPTO granted patents mapped to a ticker, with grant date, title, type, claim counts, citations, assignee, inventors, and CPC classifications (`/patent-filings/{SYMBOL}`)
|
|
13
|
+
- **Patent Filings Screener**: `fb.screener.patent_filings()` — cross-ticker patent filings with summary stats (total patents, tickers, top CPC sections) (`/screener/patent-filings`)
|
|
14
|
+
- **Patent Filings Plotting**: `fb.plot.patent_filings()` — visualize patent grants as bars sized by claim count on a secondary y-axis overlaid on a price chart, with hover details showing title, type, and CPC section
|
|
15
|
+
- **Async Patent Filings**: Full async support via `AsyncPatentFilingsAPI`
|
|
16
|
+
- **Patent Filings Tests**: Unit tests (`tests/test_patent_filings.py`), screener tests, integration tests, and plotting tests
|
|
17
|
+
- **Sync Context Manager**: `FinBrainClient` now supports `with FinBrainClient(...) as fb:` and exposes `close()` to release the underlying HTTP session (parity with `AsyncFinBrainClient`)
|
|
18
|
+
- **Exception Types**: Dedicated `BadGateway` (502), `ServiceUnavailable` (503), and `GatewayTimeout` (504) exceptions for transient gateway errors
|
|
19
|
+
- **Analyst Ratings Plotting**: `fb.plot.analyst_ratings()` — overlay analyst rating actions and price targets on a price chart, with markers grouped and coloured by action category (upgrade/downgrade/initiate/maintain); fills the last remaining ticker-level dataset without a plot method
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **Date parameter handling**: `datetime.datetime` values passed to `date_from`/`date_to` are now truncated to `YYYY-MM-DD` instead of leaking a full ISO timestamp into the API query
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **Retry coverage**: Transient gateway errors (502/503/504) are now retried alongside 500, with exponential back-off
|
|
28
|
+
- **Internal**: Async endpoint date helper re-exports the synchronous `to_datestr` to keep a single source of truth
|
|
29
|
+
- **Internal**: Plotting price-overlay methods now share `_resolve_price_column` and `_to_naive_index` helpers, removing ~250 lines of duplicated validation/timezone boilerplate across the chart methods
|
|
30
|
+
- **Tooling**: Added `mypy` static type checking (validating the published `py.typed` surface) to the dev extra and CI; added a sync-client endpoint-parity test; release uploads now use `--skip-existing` for idempotent re-runs
|
|
31
|
+
|
|
32
|
+
## [0.2.4] - 2026-03-30
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **Government Contracts Endpoint**: `fb.government_contracts.ticker("LMT")` — fetch U.S. government contract awards with award amounts, agencies, NAICS codes, and descriptions (`/government-contracts/{SYMBOL}`)
|
|
37
|
+
- **Government Contracts Screener**: `fb.screener.government_contracts()` — cross-ticker government contracts with summary stats (`/screener/government-contracts`)
|
|
38
|
+
- **Government Contracts Plotting**: `fb.plot.government_contracts()` — visualize contract award amounts as bars on a secondary y-axis overlaid on a price chart, with hover details showing agency, NAICS description, and contract description
|
|
39
|
+
- **Async Government Contracts**: Full async support via `AsyncGovernmentContractsAPI`
|
|
40
|
+
- **Government Contracts Tests**: Unit tests (`tests/test_government_contracts.py`), screener tests, integration tests, and plotting tests
|
|
41
|
+
|
|
42
|
+
## [0.2.3] - 2026-03-17
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **Reddit Mentions Screener**: Moved from `fb.reddit_mentions.screener()` to `fb.screener.reddit_mentions()` for consistency with other screener endpoints
|
|
47
|
+
- **Plotting rename**: `fb.plot.reddit_mentions_screener()` renamed to `fb.plot.reddit_mentions_top()` to better reflect the chart's purpose (top N most mentioned tickers)
|
|
48
|
+
|
|
49
|
+
## [0.2.2] - 2026-03-17
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **Reddit Mentions Endpoint**: `fb.reddit_mentions.ticker("TSLA")` — fetch per-subreddit mention counts with full timestamps (sampled every 4 hours) (`/reddit-mentions/{SYMBOL}`)
|
|
54
|
+
- **Reddit Mentions Screener**: `fb.screener.reddit_mentions()` — cross-ticker Reddit mentions with aggregated totals and per-subreddit breakdowns (`/screener/reddit-mentions`)
|
|
55
|
+
- **Reddit Mentions Plotting**: `fb.plot.reddit_mentions()` — stacked bars per subreddit overlaid on a price chart; `fb.plot.reddit_mentions_top()` — horizontal stacked bar chart of top N most mentioned tickers
|
|
56
|
+
- **Async Reddit Mentions**: Full async support via `AsyncRedditMentionsAPI`
|
|
57
|
+
- **Reddit Mentions Tests**: Unit tests, integration tests, and functional plotting tests
|
|
58
|
+
- **Plotting Test Coverage**: Added functional tests for all 11 plotting methods
|
|
59
|
+
|
|
8
60
|
## [0.2.1] - 2026-03-12
|
|
9
61
|
|
|
10
62
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: finbrain-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: Official Python client for the FinBrain API
|
|
5
5
|
Author-email: Ahmet Salim Bilgin <ahmet@finbrain.tech>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -22,6 +22,8 @@ Requires-Dist: httpx>=0.24; extra == "dev"
|
|
|
22
22
|
Requires-Dist: build; extra == "dev"
|
|
23
23
|
Requires-Dist: twine; extra == "dev"
|
|
24
24
|
Requires-Dist: ruff; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy; extra == "dev"
|
|
26
|
+
Requires-Dist: types-requests; extra == "dev"
|
|
25
27
|
Dynamic: license-file
|
|
26
28
|
|
|
27
29
|
# FinBrain Python SDK <!-- omit in toc -->
|
|
@@ -99,6 +101,26 @@ fb.corporate_lobbying.ticker("AAPL",
|
|
|
99
101
|
date_to="2025-06-30",
|
|
100
102
|
as_dataframe=True)
|
|
101
103
|
|
|
104
|
+
# ---------- reddit mentions ----------
|
|
105
|
+
fb.reddit_mentions.ticker("TSLA",
|
|
106
|
+
date_from="2026-03-01",
|
|
107
|
+
date_to="2026-03-17",
|
|
108
|
+
as_dataframe=True)
|
|
109
|
+
|
|
110
|
+
# ---------- government contracts ----------
|
|
111
|
+
fb.government_contracts.ticker("LMT",
|
|
112
|
+
date_from="2025-01-01",
|
|
113
|
+
date_to="2025-12-31",
|
|
114
|
+
limit=50,
|
|
115
|
+
as_dataframe=True)
|
|
116
|
+
|
|
117
|
+
# ---------- patent filings ----------
|
|
118
|
+
fb.patent_filings.ticker("AAPL",
|
|
119
|
+
date_from="2025-01-01",
|
|
120
|
+
date_to="2025-12-31",
|
|
121
|
+
limit=50,
|
|
122
|
+
as_dataframe=True)
|
|
123
|
+
|
|
102
124
|
# ---------- insider transactions ----------
|
|
103
125
|
fb.insider_transactions.ticker("AMZN", as_dataframe=True)
|
|
104
126
|
|
|
@@ -130,6 +152,9 @@ fb.news.ticker("AMZN", limit=20, as_dataframe=True)
|
|
|
130
152
|
fb.screener.sentiment(market="S&P 500", as_dataframe=True)
|
|
131
153
|
fb.screener.predictions_daily(limit=100, as_dataframe=True)
|
|
132
154
|
fb.screener.insider_trading(limit=50)
|
|
155
|
+
fb.screener.reddit_mentions(limit=100, as_dataframe=True)
|
|
156
|
+
fb.screener.government_contracts(limit=100, as_dataframe=True)
|
|
157
|
+
fb.screener.patent_filings(limit=100, as_dataframe=True)
|
|
133
158
|
|
|
134
159
|
# ---------- recent data ----------
|
|
135
160
|
fb.recent.news(limit=100, as_dataframe=True)
|
|
@@ -244,6 +269,33 @@ fb.plot.corporate_lobbying("AAPL",
|
|
|
244
269
|
price_data=price_df,
|
|
245
270
|
date_from="2024-01-01",
|
|
246
271
|
date_to="2025-06-30")
|
|
272
|
+
|
|
273
|
+
# Plot Reddit mentions (stacked bars per subreddit) on your price chart
|
|
274
|
+
fb.plot.reddit_mentions("TSLA",
|
|
275
|
+
price_data=price_df,
|
|
276
|
+
date_from="2026-03-01",
|
|
277
|
+
date_to="2026-03-17")
|
|
278
|
+
|
|
279
|
+
# Plot patent grants (bars sized by claim count) on your price chart
|
|
280
|
+
fb.plot.patent_filings("AAPL",
|
|
281
|
+
price_data=price_df,
|
|
282
|
+
date_from="2024-01-01",
|
|
283
|
+
date_to="2025-06-30")
|
|
284
|
+
|
|
285
|
+
# Plot analyst ratings & price targets (markers coloured by action) on your price chart
|
|
286
|
+
fb.plot.analyst_ratings("AAPL",
|
|
287
|
+
price_data=price_df,
|
|
288
|
+
date_from="2024-01-01",
|
|
289
|
+
date_to="2025-06-30")
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
# ---------- Reddit Mentions Screener Chart (no price data needed) ----------
|
|
294
|
+
# Stacked horizontal bar chart of top 15 most mentioned tickers
|
|
295
|
+
fb.plot.reddit_mentions_top(market="S&P 500")
|
|
296
|
+
|
|
297
|
+
# Customize the number of tickers shown
|
|
298
|
+
fb.plot.reddit_mentions_top(top_n=10, region="US")
|
|
247
299
|
```
|
|
248
300
|
|
|
249
301
|
**Price Data Requirements:**
|
|
@@ -289,12 +341,18 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
|
|
|
289
341
|
| House trades | `client.house_trades.ticker()` | `/congress/house/{SYMBOL}` |
|
|
290
342
|
| Senate trades | `client.senate_trades.ticker()` | `/congress/senate/{SYMBOL}` |
|
|
291
343
|
| Corporate lobbying | `client.corporate_lobbying.ticker()` | `/lobbying/{SYMBOL}` |
|
|
344
|
+
| Reddit mentions | `client.reddit_mentions.ticker()` | `/reddit-mentions/{SYMBOL}` |
|
|
345
|
+
| Gov. contracts | `client.government_contracts.ticker()` | `/government-contracts/{SYMBOL}` |
|
|
346
|
+
| Patent filings | `client.patent_filings.ticker()` | `/patent-filings/{SYMBOL}` |
|
|
292
347
|
| Insider transactions | `client.insider_transactions.ticker()` | `/insider-trading/{SYMBOL}` |
|
|
293
348
|
| LinkedIn | `client.linkedin_data.ticker()` | `/linkedin/{SYMBOL}` |
|
|
294
349
|
| Options – Put/Call | `client.options.put_call()` | `/put-call-ratio/{SYMBOL}` |
|
|
295
350
|
| Screener | `client.screener.sentiment()` | `/screener/sentiment` |
|
|
296
351
|
| | `client.screener.predictions_daily()` | `/screener/predictions/daily` |
|
|
297
352
|
| | `client.screener.insider_trading()` | `/screener/insider-trading` |
|
|
353
|
+
| | `client.screener.reddit_mentions()` | `/screener/reddit-mentions` |
|
|
354
|
+
| | `client.screener.government_contracts()` | `/screener/government-contracts` |
|
|
355
|
+
| | `client.screener.patent_filings()` | `/screener/patent-filings` |
|
|
298
356
|
| | ... and 8 more screener methods | |
|
|
299
357
|
| Recent | `client.recent.news()` | `/recent/news` |
|
|
300
358
|
| | `client.recent.analyst_ratings()` | `/recent/analyst-ratings` |
|
|
@@ -322,6 +380,9 @@ except BadRequest as exc:
|
|
|
322
380
|
| 405 | `MethodNotAllowed` | HTTP method not supported on endpoint |
|
|
323
381
|
| 429 | `RateLimitError` | Too many requests |
|
|
324
382
|
| 500 | `ServerError` | FinBrain internal error |
|
|
383
|
+
| 502 | `BadGateway` | Invalid response from upstream server |
|
|
384
|
+
| 503 | `ServiceUnavailable` | Service temporarily unavailable |
|
|
385
|
+
| 504 | `GatewayTimeout` | Upstream server timed out |
|
|
325
386
|
|
|
326
387
|
---
|
|
327
388
|
|
|
@@ -73,6 +73,26 @@ fb.corporate_lobbying.ticker("AAPL",
|
|
|
73
73
|
date_to="2025-06-30",
|
|
74
74
|
as_dataframe=True)
|
|
75
75
|
|
|
76
|
+
# ---------- reddit mentions ----------
|
|
77
|
+
fb.reddit_mentions.ticker("TSLA",
|
|
78
|
+
date_from="2026-03-01",
|
|
79
|
+
date_to="2026-03-17",
|
|
80
|
+
as_dataframe=True)
|
|
81
|
+
|
|
82
|
+
# ---------- government contracts ----------
|
|
83
|
+
fb.government_contracts.ticker("LMT",
|
|
84
|
+
date_from="2025-01-01",
|
|
85
|
+
date_to="2025-12-31",
|
|
86
|
+
limit=50,
|
|
87
|
+
as_dataframe=True)
|
|
88
|
+
|
|
89
|
+
# ---------- patent filings ----------
|
|
90
|
+
fb.patent_filings.ticker("AAPL",
|
|
91
|
+
date_from="2025-01-01",
|
|
92
|
+
date_to="2025-12-31",
|
|
93
|
+
limit=50,
|
|
94
|
+
as_dataframe=True)
|
|
95
|
+
|
|
76
96
|
# ---------- insider transactions ----------
|
|
77
97
|
fb.insider_transactions.ticker("AMZN", as_dataframe=True)
|
|
78
98
|
|
|
@@ -104,6 +124,9 @@ fb.news.ticker("AMZN", limit=20, as_dataframe=True)
|
|
|
104
124
|
fb.screener.sentiment(market="S&P 500", as_dataframe=True)
|
|
105
125
|
fb.screener.predictions_daily(limit=100, as_dataframe=True)
|
|
106
126
|
fb.screener.insider_trading(limit=50)
|
|
127
|
+
fb.screener.reddit_mentions(limit=100, as_dataframe=True)
|
|
128
|
+
fb.screener.government_contracts(limit=100, as_dataframe=True)
|
|
129
|
+
fb.screener.patent_filings(limit=100, as_dataframe=True)
|
|
107
130
|
|
|
108
131
|
# ---------- recent data ----------
|
|
109
132
|
fb.recent.news(limit=100, as_dataframe=True)
|
|
@@ -218,6 +241,33 @@ fb.plot.corporate_lobbying("AAPL",
|
|
|
218
241
|
price_data=price_df,
|
|
219
242
|
date_from="2024-01-01",
|
|
220
243
|
date_to="2025-06-30")
|
|
244
|
+
|
|
245
|
+
# Plot Reddit mentions (stacked bars per subreddit) on your price chart
|
|
246
|
+
fb.plot.reddit_mentions("TSLA",
|
|
247
|
+
price_data=price_df,
|
|
248
|
+
date_from="2026-03-01",
|
|
249
|
+
date_to="2026-03-17")
|
|
250
|
+
|
|
251
|
+
# Plot patent grants (bars sized by claim count) on your price chart
|
|
252
|
+
fb.plot.patent_filings("AAPL",
|
|
253
|
+
price_data=price_df,
|
|
254
|
+
date_from="2024-01-01",
|
|
255
|
+
date_to="2025-06-30")
|
|
256
|
+
|
|
257
|
+
# Plot analyst ratings & price targets (markers coloured by action) on your price chart
|
|
258
|
+
fb.plot.analyst_ratings("AAPL",
|
|
259
|
+
price_data=price_df,
|
|
260
|
+
date_from="2024-01-01",
|
|
261
|
+
date_to="2025-06-30")
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
# ---------- Reddit Mentions Screener Chart (no price data needed) ----------
|
|
266
|
+
# Stacked horizontal bar chart of top 15 most mentioned tickers
|
|
267
|
+
fb.plot.reddit_mentions_top(market="S&P 500")
|
|
268
|
+
|
|
269
|
+
# Customize the number of tickers shown
|
|
270
|
+
fb.plot.reddit_mentions_top(top_n=10, region="US")
|
|
221
271
|
```
|
|
222
272
|
|
|
223
273
|
**Price Data Requirements:**
|
|
@@ -263,12 +313,18 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
|
|
|
263
313
|
| House trades | `client.house_trades.ticker()` | `/congress/house/{SYMBOL}` |
|
|
264
314
|
| Senate trades | `client.senate_trades.ticker()` | `/congress/senate/{SYMBOL}` |
|
|
265
315
|
| Corporate lobbying | `client.corporate_lobbying.ticker()` | `/lobbying/{SYMBOL}` |
|
|
316
|
+
| Reddit mentions | `client.reddit_mentions.ticker()` | `/reddit-mentions/{SYMBOL}` |
|
|
317
|
+
| Gov. contracts | `client.government_contracts.ticker()` | `/government-contracts/{SYMBOL}` |
|
|
318
|
+
| Patent filings | `client.patent_filings.ticker()` | `/patent-filings/{SYMBOL}` |
|
|
266
319
|
| Insider transactions | `client.insider_transactions.ticker()` | `/insider-trading/{SYMBOL}` |
|
|
267
320
|
| LinkedIn | `client.linkedin_data.ticker()` | `/linkedin/{SYMBOL}` |
|
|
268
321
|
| Options – Put/Call | `client.options.put_call()` | `/put-call-ratio/{SYMBOL}` |
|
|
269
322
|
| Screener | `client.screener.sentiment()` | `/screener/sentiment` |
|
|
270
323
|
| | `client.screener.predictions_daily()` | `/screener/predictions/daily` |
|
|
271
324
|
| | `client.screener.insider_trading()` | `/screener/insider-trading` |
|
|
325
|
+
| | `client.screener.reddit_mentions()` | `/screener/reddit-mentions` |
|
|
326
|
+
| | `client.screener.government_contracts()` | `/screener/government-contracts` |
|
|
327
|
+
| | `client.screener.patent_filings()` | `/screener/patent-filings` |
|
|
272
328
|
| | ... and 8 more screener methods | |
|
|
273
329
|
| Recent | `client.recent.news()` | `/recent/news` |
|
|
274
330
|
| | `client.recent.analyst_ratings()` | `/recent/analyst-ratings` |
|
|
@@ -296,6 +352,9 @@ except BadRequest as exc:
|
|
|
296
352
|
| 405 | `MethodNotAllowed` | HTTP method not supported on endpoint |
|
|
297
353
|
| 429 | `RateLimitError` | Too many requests |
|
|
298
354
|
| 500 | `ServerError` | FinBrain internal error |
|
|
355
|
+
| 502 | `BadGateway` | Invalid response from upstream server |
|
|
356
|
+
| 503 | `ServiceUnavailable` | Service temporarily unavailable |
|
|
357
|
+
| 504 | `GatewayTimeout` | Upstream server timed out |
|
|
299
358
|
|
|
300
359
|
---
|
|
301
360
|
|
|
@@ -10,7 +10,7 @@ Starting from v0.1.6, we use **`v`-prefixed tags** (e.g., `v0.1.6`, `v0.2.0`, `v
|
|
|
10
10
|
|
|
11
11
|
### 1. Update CHANGELOG.md
|
|
12
12
|
|
|
13
|
-
Edit `CHANGELOG.md` and
|
|
13
|
+
Edit `CHANGELOG.md` and add a new version section at the top, above the previous release:
|
|
14
14
|
|
|
15
15
|
```markdown
|
|
16
16
|
## [0.1.6] - 2025-01-15
|
|
@@ -45,9 +45,18 @@ dev = [
|
|
|
45
45
|
"build",
|
|
46
46
|
"twine",
|
|
47
47
|
"ruff", # linter/formatter (optional)
|
|
48
|
+
"mypy", # static type checker (validates py.typed surface)
|
|
49
|
+
"types-requests", # stubs for requests (mypy import-untyped otherwise)
|
|
48
50
|
]
|
|
49
51
|
|
|
50
52
|
[tool.pytest.ini_options]
|
|
51
53
|
markers = [
|
|
52
54
|
"integration: hits the real FinBrain v2 API (requires FINBRAIN_API_KEY)",
|
|
53
55
|
]
|
|
56
|
+
|
|
57
|
+
[tool.mypy]
|
|
58
|
+
# Code targets >=3.9 but uses `from __future__ import annotations` throughout,
|
|
59
|
+
# so checking against 3.10 (mypy's minimum target) is equivalent here.
|
|
60
|
+
python_version = "3.10"
|
|
61
|
+
files = ["src/finbrain"]
|
|
62
|
+
ignore_missing_imports = true # third-party libs (pandas, plotly, …) ship no stubs here
|
|
@@ -23,10 +23,13 @@ from .endpoints.news import AsyncNewsAPI
|
|
|
23
23
|
from .endpoints.screener import AsyncScreenerAPI
|
|
24
24
|
from .endpoints.recent import AsyncRecentAPI
|
|
25
25
|
from .endpoints.corporate_lobbying import AsyncCorporateLobbyingAPI
|
|
26
|
+
from .endpoints.reddit_mentions import AsyncRedditMentionsAPI
|
|
27
|
+
from .endpoints.government_contracts import AsyncGovernmentContractsAPI
|
|
28
|
+
from .endpoints.patent_filings import AsyncPatentFilingsAPI
|
|
26
29
|
|
|
27
30
|
|
|
28
|
-
# Which status codes merit a retry
|
|
29
|
-
_RETRYABLE_STATUS = {500}
|
|
31
|
+
# Which status codes merit a retry (transient server / gateway errors)
|
|
32
|
+
_RETRYABLE_STATUS = {500, 502, 503, 504}
|
|
30
33
|
# How long to wait between retries (2, 4, 8 … seconds)
|
|
31
34
|
_BACKOFF_BASE = 2
|
|
32
35
|
|
|
@@ -82,6 +85,9 @@ class AsyncFinBrainClient:
|
|
|
82
85
|
self.screener = AsyncScreenerAPI(self)
|
|
83
86
|
self.recent = AsyncRecentAPI(self)
|
|
84
87
|
self.corporate_lobbying = AsyncCorporateLobbyingAPI(self)
|
|
88
|
+
self.reddit_mentions = AsyncRedditMentionsAPI(self)
|
|
89
|
+
self.government_contracts = AsyncGovernmentContractsAPI(self)
|
|
90
|
+
self.patent_filings = AsyncPatentFilingsAPI(self)
|
|
85
91
|
|
|
86
92
|
async def __aenter__(self) -> "AsyncFinBrainClient":
|
|
87
93
|
"""Context manager entry."""
|
|
@@ -160,7 +166,7 @@ class AsyncFinBrainClient:
|
|
|
160
166
|
|
|
161
167
|
# ── Error path ───────────────────────────────────
|
|
162
168
|
if resp.status_code in _RETRYABLE_STATUS and attempt < self.retries:
|
|
163
|
-
#
|
|
169
|
+
# Transient server/gateway error – exponential back-off then retry
|
|
164
170
|
await asyncio.sleep(_BACKOFF_BASE**attempt)
|
|
165
171
|
continue
|
|
166
172
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared utility functions for async endpoint modules.
|
|
3
|
+
|
|
4
|
+
These helpers are identical to the synchronous ones, so they are re-exported
|
|
5
|
+
from :mod:`finbrain.endpoints._utils` to keep a single source of truth.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ...endpoints._utils import to_datestr
|
|
11
|
+
|
|
12
|
+
__all__ = ["to_datestr"]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import datetime as _dt
|
|
4
|
+
from typing import TYPE_CHECKING, Dict, Any, List
|
|
5
|
+
|
|
6
|
+
from ._utils import to_datestr
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..client import AsyncFinBrainClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsyncGovernmentContractsAPI:
|
|
13
|
+
"""Async wrapper for /government-contracts and /screener/government-contracts endpoints."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, client: "AsyncFinBrainClient") -> None:
|
|
16
|
+
self._c = client
|
|
17
|
+
|
|
18
|
+
async def ticker(
|
|
19
|
+
self,
|
|
20
|
+
symbol: str,
|
|
21
|
+
*,
|
|
22
|
+
date_from: _dt.date | str | None = None,
|
|
23
|
+
date_to: _dt.date | str | None = None,
|
|
24
|
+
limit: int | None = None,
|
|
25
|
+
as_dataframe: bool = False,
|
|
26
|
+
) -> Dict[str, Any] | pd.DataFrame:
|
|
27
|
+
"""Fetch government contract awards for a symbol (async)."""
|
|
28
|
+
params: Dict[str, str] = {}
|
|
29
|
+
if date_from:
|
|
30
|
+
params["startDate"] = to_datestr(date_from)
|
|
31
|
+
if date_to:
|
|
32
|
+
params["endDate"] = to_datestr(date_to)
|
|
33
|
+
if limit is not None:
|
|
34
|
+
params["limit"] = str(limit)
|
|
35
|
+
|
|
36
|
+
path = f"government-contracts/{symbol.upper()}"
|
|
37
|
+
|
|
38
|
+
data: Dict[str, Any] = await self._c._request("GET", path, params=params)
|
|
39
|
+
|
|
40
|
+
if as_dataframe:
|
|
41
|
+
rows: List[Dict[str, Any]] = data.get("contracts", [])
|
|
42
|
+
df = pd.DataFrame(rows)
|
|
43
|
+
if not df.empty and "startDate" in df.columns:
|
|
44
|
+
df["startDate"] = pd.to_datetime(df["startDate"])
|
|
45
|
+
df.set_index("startDate", inplace=True)
|
|
46
|
+
return df
|
|
47
|
+
|
|
48
|
+
return data
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import datetime as _dt
|
|
4
|
+
from typing import TYPE_CHECKING, Dict, Any, List
|
|
5
|
+
|
|
6
|
+
from ._utils import to_datestr
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..client import AsyncFinBrainClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsyncPatentFilingsAPI:
|
|
13
|
+
"""Async wrapper for /patent-filings and /screener/patent-filings endpoints."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, client: "AsyncFinBrainClient") -> None:
|
|
16
|
+
self._c = client
|
|
17
|
+
|
|
18
|
+
async def ticker(
|
|
19
|
+
self,
|
|
20
|
+
symbol: str,
|
|
21
|
+
*,
|
|
22
|
+
date_from: _dt.date | str | None = None,
|
|
23
|
+
date_to: _dt.date | str | None = None,
|
|
24
|
+
limit: int | None = None,
|
|
25
|
+
as_dataframe: bool = False,
|
|
26
|
+
) -> Dict[str, Any] | pd.DataFrame:
|
|
27
|
+
"""Fetch USPTO granted patents for a symbol (async)."""
|
|
28
|
+
params: Dict[str, str] = {}
|
|
29
|
+
if date_from:
|
|
30
|
+
params["startDate"] = to_datestr(date_from)
|
|
31
|
+
if date_to:
|
|
32
|
+
params["endDate"] = to_datestr(date_to)
|
|
33
|
+
if limit is not None:
|
|
34
|
+
params["limit"] = str(limit)
|
|
35
|
+
|
|
36
|
+
path = f"patent-filings/{symbol.upper()}"
|
|
37
|
+
|
|
38
|
+
data: Dict[str, Any] = await self._c._request("GET", path, params=params)
|
|
39
|
+
|
|
40
|
+
if as_dataframe:
|
|
41
|
+
rows: List[Dict[str, Any]] = data.get("patents", [])
|
|
42
|
+
df = pd.DataFrame(rows)
|
|
43
|
+
if not df.empty and "patentDate" in df.columns:
|
|
44
|
+
df["patentDate"] = pd.to_datetime(df["patentDate"])
|
|
45
|
+
df.set_index("patentDate", inplace=True)
|
|
46
|
+
return df
|
|
47
|
+
|
|
48
|
+
return data
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import datetime as _dt
|
|
4
|
+
from typing import TYPE_CHECKING, Dict, Any, List
|
|
5
|
+
|
|
6
|
+
from ._utils import to_datestr
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..client import AsyncFinBrainClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsyncRedditMentionsAPI:
|
|
13
|
+
"""Async wrapper for /reddit-mentions and /screener/reddit-mentions endpoints."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, client: "AsyncFinBrainClient") -> None:
|
|
16
|
+
self._c = client
|
|
17
|
+
|
|
18
|
+
async def ticker(
|
|
19
|
+
self,
|
|
20
|
+
symbol: str,
|
|
21
|
+
*,
|
|
22
|
+
date_from: _dt.date | str | None = None,
|
|
23
|
+
date_to: _dt.date | str | None = None,
|
|
24
|
+
limit: int | None = None,
|
|
25
|
+
as_dataframe: bool = False,
|
|
26
|
+
) -> Dict[str, Any] | pd.DataFrame:
|
|
27
|
+
"""Fetch Reddit mention counts for a symbol across subreddits (async)."""
|
|
28
|
+
params: Dict[str, str] = {}
|
|
29
|
+
if date_from:
|
|
30
|
+
params["startDate"] = to_datestr(date_from)
|
|
31
|
+
if date_to:
|
|
32
|
+
params["endDate"] = to_datestr(date_to)
|
|
33
|
+
if limit is not None:
|
|
34
|
+
params["limit"] = str(limit)
|
|
35
|
+
|
|
36
|
+
path = f"reddit-mentions/{symbol.upper()}"
|
|
37
|
+
|
|
38
|
+
data: Dict[str, Any] = await self._c._request("GET", path, params=params)
|
|
39
|
+
|
|
40
|
+
if as_dataframe:
|
|
41
|
+
rows: List[Dict[str, Any]] = data.get("data", [])
|
|
42
|
+
df = pd.DataFrame(rows)
|
|
43
|
+
if not df.empty and "date" in df.columns:
|
|
44
|
+
df["date"] = pd.to_datetime(df["date"])
|
|
45
|
+
df.set_index("date", inplace=True)
|
|
46
|
+
return df
|
|
47
|
+
|
|
48
|
+
return data
|
|
@@ -208,3 +208,41 @@ class AsyncScreenerAPI:
|
|
|
208
208
|
"""Screen monthly (12-month) predictions across tickers."""
|
|
209
209
|
params = self._build_params(limit=limit, market=market, region=region)
|
|
210
210
|
return await self._get("screener/predictions/monthly", params, as_dataframe)
|
|
211
|
+
|
|
212
|
+
# ── reddit mentions ────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
async def reddit_mentions(
|
|
215
|
+
self,
|
|
216
|
+
*,
|
|
217
|
+
limit: int | None = None,
|
|
218
|
+
market: str | None = None,
|
|
219
|
+
region: str | None = None,
|
|
220
|
+
as_dataframe: bool = False,
|
|
221
|
+
) -> List[Dict[str, Any]] | pd.DataFrame:
|
|
222
|
+
"""Screen Reddit mention counts across tickers (async)."""
|
|
223
|
+
params = self._build_params(limit=limit, market=market, region=region)
|
|
224
|
+
return await self._get("screener/reddit-mentions", params, as_dataframe)
|
|
225
|
+
|
|
226
|
+
# ── government contracts ──────────────────────────────────
|
|
227
|
+
|
|
228
|
+
async def government_contracts(
|
|
229
|
+
self,
|
|
230
|
+
*,
|
|
231
|
+
limit: int | None = None,
|
|
232
|
+
as_dataframe: bool = False,
|
|
233
|
+
) -> List[Dict[str, Any]] | pd.DataFrame:
|
|
234
|
+
"""Screen government contracts across all tickers (async)."""
|
|
235
|
+
params = self._build_params(limit=limit)
|
|
236
|
+
return await self._get("screener/government-contracts", params, as_dataframe)
|
|
237
|
+
|
|
238
|
+
# ── patent filings ────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
async def patent_filings(
|
|
241
|
+
self,
|
|
242
|
+
*,
|
|
243
|
+
limit: int | None = None,
|
|
244
|
+
as_dataframe: bool = False,
|
|
245
|
+
) -> List[Dict[str, Any]] | pd.DataFrame:
|
|
246
|
+
"""Screen USPTO patent filings across all tickers (async)."""
|
|
247
|
+
params = self._build_params(limit=limit)
|
|
248
|
+
return await self._get("screener/patent-filings", params, as_dataframe)
|
|
@@ -24,10 +24,13 @@ from .endpoints.news import NewsAPI
|
|
|
24
24
|
from .endpoints.screener import ScreenerAPI
|
|
25
25
|
from .endpoints.recent import RecentAPI
|
|
26
26
|
from .endpoints.corporate_lobbying import CorporateLobbyingAPI
|
|
27
|
+
from .endpoints.reddit_mentions import RedditMentionsAPI
|
|
28
|
+
from .endpoints.government_contracts import GovernmentContractsAPI
|
|
29
|
+
from .endpoints.patent_filings import PatentFilingsAPI
|
|
27
30
|
|
|
28
31
|
|
|
29
|
-
# Which status codes merit a retry
|
|
30
|
-
_RETRYABLE_STATUS = {500}
|
|
32
|
+
# Which status codes merit a retry (transient server / gateway errors)
|
|
33
|
+
_RETRYABLE_STATUS = {500, 502, 503, 504}
|
|
31
34
|
# How long to wait between retries (2, 4, 8 … seconds)
|
|
32
35
|
_BACKOFF_BASE = 2
|
|
33
36
|
|
|
@@ -77,6 +80,22 @@ class FinBrainClient:
|
|
|
77
80
|
self.screener = ScreenerAPI(self)
|
|
78
81
|
self.recent = RecentAPI(self)
|
|
79
82
|
self.corporate_lobbying = CorporateLobbyingAPI(self)
|
|
83
|
+
self.reddit_mentions = RedditMentionsAPI(self)
|
|
84
|
+
self.government_contracts = GovernmentContractsAPI(self)
|
|
85
|
+
self.patent_filings = PatentFilingsAPI(self)
|
|
86
|
+
|
|
87
|
+
# ---------- lifecycle ----------
|
|
88
|
+
def __enter__(self) -> "FinBrainClient":
|
|
89
|
+
"""Context manager entry."""
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
93
|
+
"""Context manager exit — close the underlying HTTP session."""
|
|
94
|
+
self.close()
|
|
95
|
+
|
|
96
|
+
def close(self) -> None:
|
|
97
|
+
"""Close the underlying ``requests.Session``."""
|
|
98
|
+
self.session.close()
|
|
80
99
|
|
|
81
100
|
# ---------- private helpers ----------
|
|
82
101
|
def _request(
|
|
@@ -127,7 +146,7 @@ class FinBrainClient:
|
|
|
127
146
|
|
|
128
147
|
# ── Error path ───────────────────────────────────
|
|
129
148
|
if resp.status_code in _RETRYABLE_STATUS and attempt < self.retries:
|
|
130
|
-
#
|
|
149
|
+
# Transient server/gateway error – exponential back-off then retry
|
|
131
150
|
time.sleep(_BACKOFF_BASE**attempt)
|
|
132
151
|
continue
|
|
133
152
|
|
|
@@ -28,10 +28,16 @@ def to_datestr(value: _dt.date | str) -> str:
|
|
|
28
28
|
|
|
29
29
|
Examples
|
|
30
30
|
--------
|
|
31
|
-
>>> from datetime import date
|
|
31
|
+
>>> from datetime import date, datetime
|
|
32
32
|
>>> to_datestr(date(2025, 1, 15))
|
|
33
33
|
'2025-01-15'
|
|
34
|
+
>>> to_datestr(datetime(2025, 1, 15, 9, 30))
|
|
35
|
+
'2025-01-15'
|
|
34
36
|
>>> to_datestr("2025-01-15")
|
|
35
37
|
'2025-01-15'
|
|
36
38
|
"""
|
|
39
|
+
# ``datetime`` subclasses ``date``; take the date part so the time
|
|
40
|
+
# component never leaks into the ``YYYY-MM-DD`` API parameter.
|
|
41
|
+
if isinstance(value, _dt.datetime):
|
|
42
|
+
return value.date().isoformat()
|
|
37
43
|
return value.isoformat() if isinstance(value, _dt.date) else value
|