finbrain-python 0.2.4__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.4 → finbrain_python-0.2.5}/.github/workflows/ci.yml +1 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/.github/workflows/release.yml +2 -2
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/CHANGELOG.md +24 -0
- {finbrain_python-0.2.4/src/finbrain_python.egg-info → finbrain_python-0.2.5}/PKG-INFO +28 -1
- finbrain_python-0.2.4/PKG-INFO → finbrain_python-0.2.5/README.md +25 -26
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/RELEASE.md +1 -1
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/pyproject.toml +9 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/client.py +5 -3
- finbrain_python-0.2.5/src/finbrain/aio/endpoints/_utils.py +12 -0
- finbrain_python-0.2.5/src/finbrain/aio/endpoints/patent_filings.py +48 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/screener.py +12 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/client.py +18 -3
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/_utils.py +7 -1
- finbrain_python-0.2.5/src/finbrain/endpoints/patent_filings.py +74 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/screener.py +12 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/exceptions.py +24 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/plotting.py +381 -164
- finbrain_python-0.2.4/README.md → finbrain_python-0.2.5/src/finbrain_python.egg-info/PKG-INFO +53 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain_python.egg-info/SOURCES.txt +6 -1
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain_python.egg-info/requires.txt +2 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_async_client.py +1 -0
- finbrain_python-0.2.5/tests/test_client.py +46 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_envelope.py +43 -1
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_integration.py +41 -0
- finbrain_python-0.2.5/tests/test_patent_filings.py +207 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_plotting.py +95 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_screener.py +36 -0
- finbrain_python-0.2.5/tests/test_utils.py +24 -0
- finbrain_python-0.2.4/src/finbrain/aio/endpoints/_utils.py +0 -37
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/.gitattributes +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/.gitignore +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/LICENSE +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/examples/async_example.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/examples/transactions_plotting_example.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/setup.cfg +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/__init__.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/__init__.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/__init__.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/analyst_ratings.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/app_ratings.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/available.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/corporate_lobbying.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/government_contracts.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/house_trades.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/insider_transactions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/linkedin_data.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/news.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/options.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/predictions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/recent.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/reddit_mentions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/senate_trades.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/sentiments.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/__init__.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/analyst_ratings.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/app_ratings.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/available.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/corporate_lobbying.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/government_contracts.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/house_trades.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/insider_transactions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/linkedin_data.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/news.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/options.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/predictions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/recent.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/reddit_mentions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/senate_trades.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/sentiments.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/py.typed +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain_python.egg-info/dependency_links.txt +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain_python.egg-info/top_level.txt +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/__init__.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/conftest.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_analyst_ratings.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_app_ratings.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_available.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_corporate_lobbying.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_government_contracts.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_house_trades.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_insider_transactions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_linkedin_data.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_news.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_options.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_predictions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_recent.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_reddit_mentions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_senate_trades.py +0 -0
- {finbrain_python-0.2.4 → 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,30 @@ 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
|
+
|
|
8
32
|
## [0.2.4] - 2026-03-30
|
|
9
33
|
|
|
10
34
|
### 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 -->
|
|
@@ -112,6 +114,13 @@ fb.government_contracts.ticker("LMT",
|
|
|
112
114
|
limit=50,
|
|
113
115
|
as_dataframe=True)
|
|
114
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
|
+
|
|
115
124
|
# ---------- insider transactions ----------
|
|
116
125
|
fb.insider_transactions.ticker("AMZN", as_dataframe=True)
|
|
117
126
|
|
|
@@ -145,6 +154,7 @@ fb.screener.predictions_daily(limit=100, as_dataframe=True)
|
|
|
145
154
|
fb.screener.insider_trading(limit=50)
|
|
146
155
|
fb.screener.reddit_mentions(limit=100, as_dataframe=True)
|
|
147
156
|
fb.screener.government_contracts(limit=100, as_dataframe=True)
|
|
157
|
+
fb.screener.patent_filings(limit=100, as_dataframe=True)
|
|
148
158
|
|
|
149
159
|
# ---------- recent data ----------
|
|
150
160
|
fb.recent.news(limit=100, as_dataframe=True)
|
|
@@ -265,6 +275,18 @@ fb.plot.reddit_mentions("TSLA",
|
|
|
265
275
|
price_data=price_df,
|
|
266
276
|
date_from="2026-03-01",
|
|
267
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")
|
|
268
290
|
```
|
|
269
291
|
|
|
270
292
|
```python
|
|
@@ -321,6 +343,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
|
|
|
321
343
|
| Corporate lobbying | `client.corporate_lobbying.ticker()` | `/lobbying/{SYMBOL}` |
|
|
322
344
|
| Reddit mentions | `client.reddit_mentions.ticker()` | `/reddit-mentions/{SYMBOL}` |
|
|
323
345
|
| Gov. contracts | `client.government_contracts.ticker()` | `/government-contracts/{SYMBOL}` |
|
|
346
|
+
| Patent filings | `client.patent_filings.ticker()` | `/patent-filings/{SYMBOL}` |
|
|
324
347
|
| Insider transactions | `client.insider_transactions.ticker()` | `/insider-trading/{SYMBOL}` |
|
|
325
348
|
| LinkedIn | `client.linkedin_data.ticker()` | `/linkedin/{SYMBOL}` |
|
|
326
349
|
| Options – Put/Call | `client.options.put_call()` | `/put-call-ratio/{SYMBOL}` |
|
|
@@ -329,6 +352,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
|
|
|
329
352
|
| | `client.screener.insider_trading()` | `/screener/insider-trading` |
|
|
330
353
|
| | `client.screener.reddit_mentions()` | `/screener/reddit-mentions` |
|
|
331
354
|
| | `client.screener.government_contracts()` | `/screener/government-contracts` |
|
|
355
|
+
| | `client.screener.patent_filings()` | `/screener/patent-filings` |
|
|
332
356
|
| | ... and 8 more screener methods | |
|
|
333
357
|
| Recent | `client.recent.news()` | `/recent/news` |
|
|
334
358
|
| | `client.recent.analyst_ratings()` | `/recent/analyst-ratings` |
|
|
@@ -356,6 +380,9 @@ except BadRequest as exc:
|
|
|
356
380
|
| 405 | `MethodNotAllowed` | HTTP method not supported on endpoint |
|
|
357
381
|
| 429 | `RateLimitError` | Too many requests |
|
|
358
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 |
|
|
359
386
|
|
|
360
387
|
---
|
|
361
388
|
|
|
@@ -1,29 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: finbrain-python
|
|
3
|
-
Version: 0.2.4
|
|
4
|
-
Summary: Official Python client for the FinBrain API
|
|
5
|
-
Author-email: Ahmet Salim Bilgin <ahmet@finbrain.tech>
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Requires-Python: >=3.9
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Requires-Dist: requests>=2.31
|
|
11
|
-
Requires-Dist: typer[all]>=0.12
|
|
12
|
-
Requires-Dist: pandas>=2.2
|
|
13
|
-
Requires-Dist: plotly>=6.0
|
|
14
|
-
Requires-Dist: numpy>=1.22.4
|
|
15
|
-
Provides-Extra: async
|
|
16
|
-
Requires-Dist: httpx>=0.24; extra == "async"
|
|
17
|
-
Provides-Extra: dev
|
|
18
|
-
Requires-Dist: pytest; extra == "dev"
|
|
19
|
-
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
20
|
-
Requires-Dist: responses; extra == "dev"
|
|
21
|
-
Requires-Dist: httpx>=0.24; extra == "dev"
|
|
22
|
-
Requires-Dist: build; extra == "dev"
|
|
23
|
-
Requires-Dist: twine; extra == "dev"
|
|
24
|
-
Requires-Dist: ruff; extra == "dev"
|
|
25
|
-
Dynamic: license-file
|
|
26
|
-
|
|
27
1
|
# FinBrain Python SDK <!-- omit in toc -->
|
|
28
2
|
|
|
29
3
|
[](https://pypi.org/project/finbrain-python/)
|
|
@@ -112,6 +86,13 @@ fb.government_contracts.ticker("LMT",
|
|
|
112
86
|
limit=50,
|
|
113
87
|
as_dataframe=True)
|
|
114
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
|
+
|
|
115
96
|
# ---------- insider transactions ----------
|
|
116
97
|
fb.insider_transactions.ticker("AMZN", as_dataframe=True)
|
|
117
98
|
|
|
@@ -145,6 +126,7 @@ fb.screener.predictions_daily(limit=100, as_dataframe=True)
|
|
|
145
126
|
fb.screener.insider_trading(limit=50)
|
|
146
127
|
fb.screener.reddit_mentions(limit=100, as_dataframe=True)
|
|
147
128
|
fb.screener.government_contracts(limit=100, as_dataframe=True)
|
|
129
|
+
fb.screener.patent_filings(limit=100, as_dataframe=True)
|
|
148
130
|
|
|
149
131
|
# ---------- recent data ----------
|
|
150
132
|
fb.recent.news(limit=100, as_dataframe=True)
|
|
@@ -265,6 +247,18 @@ fb.plot.reddit_mentions("TSLA",
|
|
|
265
247
|
price_data=price_df,
|
|
266
248
|
date_from="2026-03-01",
|
|
267
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")
|
|
268
262
|
```
|
|
269
263
|
|
|
270
264
|
```python
|
|
@@ -321,6 +315,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
|
|
|
321
315
|
| Corporate lobbying | `client.corporate_lobbying.ticker()` | `/lobbying/{SYMBOL}` |
|
|
322
316
|
| Reddit mentions | `client.reddit_mentions.ticker()` | `/reddit-mentions/{SYMBOL}` |
|
|
323
317
|
| Gov. contracts | `client.government_contracts.ticker()` | `/government-contracts/{SYMBOL}` |
|
|
318
|
+
| Patent filings | `client.patent_filings.ticker()` | `/patent-filings/{SYMBOL}` |
|
|
324
319
|
| Insider transactions | `client.insider_transactions.ticker()` | `/insider-trading/{SYMBOL}` |
|
|
325
320
|
| LinkedIn | `client.linkedin_data.ticker()` | `/linkedin/{SYMBOL}` |
|
|
326
321
|
| Options – Put/Call | `client.options.put_call()` | `/put-call-ratio/{SYMBOL}` |
|
|
@@ -329,6 +324,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
|
|
|
329
324
|
| | `client.screener.insider_trading()` | `/screener/insider-trading` |
|
|
330
325
|
| | `client.screener.reddit_mentions()` | `/screener/reddit-mentions` |
|
|
331
326
|
| | `client.screener.government_contracts()` | `/screener/government-contracts` |
|
|
327
|
+
| | `client.screener.patent_filings()` | `/screener/patent-filings` |
|
|
332
328
|
| | ... and 8 more screener methods | |
|
|
333
329
|
| Recent | `client.recent.news()` | `/recent/news` |
|
|
334
330
|
| | `client.recent.analyst_ratings()` | `/recent/analyst-ratings` |
|
|
@@ -356,6 +352,9 @@ except BadRequest as exc:
|
|
|
356
352
|
| 405 | `MethodNotAllowed` | HTTP method not supported on endpoint |
|
|
357
353
|
| 429 | `RateLimitError` | Too many requests |
|
|
358
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 |
|
|
359
358
|
|
|
360
359
|
---
|
|
361
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
|
|
@@ -25,10 +25,11 @@ from .endpoints.recent import AsyncRecentAPI
|
|
|
25
25
|
from .endpoints.corporate_lobbying import AsyncCorporateLobbyingAPI
|
|
26
26
|
from .endpoints.reddit_mentions import AsyncRedditMentionsAPI
|
|
27
27
|
from .endpoints.government_contracts import AsyncGovernmentContractsAPI
|
|
28
|
+
from .endpoints.patent_filings import AsyncPatentFilingsAPI
|
|
28
29
|
|
|
29
30
|
|
|
30
|
-
# Which status codes merit a retry
|
|
31
|
-
_RETRYABLE_STATUS = {500}
|
|
31
|
+
# Which status codes merit a retry (transient server / gateway errors)
|
|
32
|
+
_RETRYABLE_STATUS = {500, 502, 503, 504}
|
|
32
33
|
# How long to wait between retries (2, 4, 8 … seconds)
|
|
33
34
|
_BACKOFF_BASE = 2
|
|
34
35
|
|
|
@@ -86,6 +87,7 @@ class AsyncFinBrainClient:
|
|
|
86
87
|
self.corporate_lobbying = AsyncCorporateLobbyingAPI(self)
|
|
87
88
|
self.reddit_mentions = AsyncRedditMentionsAPI(self)
|
|
88
89
|
self.government_contracts = AsyncGovernmentContractsAPI(self)
|
|
90
|
+
self.patent_filings = AsyncPatentFilingsAPI(self)
|
|
89
91
|
|
|
90
92
|
async def __aenter__(self) -> "AsyncFinBrainClient":
|
|
91
93
|
"""Context manager entry."""
|
|
@@ -164,7 +166,7 @@ class AsyncFinBrainClient:
|
|
|
164
166
|
|
|
165
167
|
# ── Error path ───────────────────────────────────
|
|
166
168
|
if resp.status_code in _RETRYABLE_STATUS and attempt < self.retries:
|
|
167
|
-
#
|
|
169
|
+
# Transient server/gateway error – exponential back-off then retry
|
|
168
170
|
await asyncio.sleep(_BACKOFF_BASE**attempt)
|
|
169
171
|
continue
|
|
170
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 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
|
|
@@ -234,3 +234,15 @@ class AsyncScreenerAPI:
|
|
|
234
234
|
"""Screen government contracts across all tickers (async)."""
|
|
235
235
|
params = self._build_params(limit=limit)
|
|
236
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)
|
|
@@ -26,10 +26,11 @@ from .endpoints.recent import RecentAPI
|
|
|
26
26
|
from .endpoints.corporate_lobbying import CorporateLobbyingAPI
|
|
27
27
|
from .endpoints.reddit_mentions import RedditMentionsAPI
|
|
28
28
|
from .endpoints.government_contracts import GovernmentContractsAPI
|
|
29
|
+
from .endpoints.patent_filings import PatentFilingsAPI
|
|
29
30
|
|
|
30
31
|
|
|
31
|
-
# Which status codes merit a retry
|
|
32
|
-
_RETRYABLE_STATUS = {500}
|
|
32
|
+
# Which status codes merit a retry (transient server / gateway errors)
|
|
33
|
+
_RETRYABLE_STATUS = {500, 502, 503, 504}
|
|
33
34
|
# How long to wait between retries (2, 4, 8 … seconds)
|
|
34
35
|
_BACKOFF_BASE = 2
|
|
35
36
|
|
|
@@ -81,6 +82,20 @@ class FinBrainClient:
|
|
|
81
82
|
self.corporate_lobbying = CorporateLobbyingAPI(self)
|
|
82
83
|
self.reddit_mentions = RedditMentionsAPI(self)
|
|
83
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()
|
|
84
99
|
|
|
85
100
|
# ---------- private helpers ----------
|
|
86
101
|
def _request(
|
|
@@ -131,7 +146,7 @@ class FinBrainClient:
|
|
|
131
146
|
|
|
132
147
|
# ── Error path ───────────────────────────────────
|
|
133
148
|
if resp.status_code in _RETRYABLE_STATUS and attempt < self.retries:
|
|
134
|
-
#
|
|
149
|
+
# Transient server/gateway error – exponential back-off then retry
|
|
135
150
|
time.sleep(_BACKOFF_BASE**attempt)
|
|
136
151
|
continue
|
|
137
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
|
|
@@ -0,0 +1,74 @@
|
|
|
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: # imported only by type-checkers
|
|
9
|
+
from ..client import FinBrainClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PatentFilingsAPI:
|
|
13
|
+
"""
|
|
14
|
+
Endpoints
|
|
15
|
+
---------
|
|
16
|
+
``/patent-filings/<TICKER>`` - USPTO granted patents mapped to a ticker.
|
|
17
|
+
``/screener/patent-filings`` - cross-ticker patent filings screener.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# ------------------------------------------------------------------ #
|
|
21
|
+
def __init__(self, client: "FinBrainClient") -> None:
|
|
22
|
+
self._c = client # reference to the parent client
|
|
23
|
+
|
|
24
|
+
# ------------------------------------------------------------------ #
|
|
25
|
+
def ticker(
|
|
26
|
+
self,
|
|
27
|
+
symbol: str,
|
|
28
|
+
*,
|
|
29
|
+
date_from: _dt.date | str | None = None,
|
|
30
|
+
date_to: _dt.date | str | None = None,
|
|
31
|
+
limit: int | None = None,
|
|
32
|
+
as_dataframe: bool = False,
|
|
33
|
+
) -> Dict[str, Any] | pd.DataFrame:
|
|
34
|
+
"""
|
|
35
|
+
Fetch USPTO granted patents for *symbol*.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
symbol :
|
|
40
|
+
Ticker symbol; auto-upper-cased.
|
|
41
|
+
date_from, date_to :
|
|
42
|
+
Optional ISO dates (``YYYY-MM-DD``) bounding the returned rows by
|
|
43
|
+
grant date (``patentDate``).
|
|
44
|
+
limit :
|
|
45
|
+
Maximum number of records to return (1-500).
|
|
46
|
+
as_dataframe :
|
|
47
|
+
If *True*, return a **pandas.DataFrame** indexed by ``patentDate``;
|
|
48
|
+
otherwise return the raw JSON dict.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
dict | pandas.DataFrame
|
|
53
|
+
"""
|
|
54
|
+
params: Dict[str, str] = {}
|
|
55
|
+
if date_from:
|
|
56
|
+
params["startDate"] = to_datestr(date_from)
|
|
57
|
+
if date_to:
|
|
58
|
+
params["endDate"] = to_datestr(date_to)
|
|
59
|
+
if limit is not None:
|
|
60
|
+
params["limit"] = str(limit)
|
|
61
|
+
|
|
62
|
+
path = f"patent-filings/{symbol.upper()}"
|
|
63
|
+
|
|
64
|
+
data: Dict[str, Any] = self._c._request("GET", path, params=params)
|
|
65
|
+
|
|
66
|
+
if as_dataframe:
|
|
67
|
+
rows: List[Dict[str, Any]] = data.get("patents", [])
|
|
68
|
+
df = pd.DataFrame(rows)
|
|
69
|
+
if not df.empty and "patentDate" in df.columns:
|
|
70
|
+
df["patentDate"] = pd.to_datetime(df["patentDate"])
|
|
71
|
+
df.set_index("patentDate", inplace=True)
|
|
72
|
+
return df
|
|
73
|
+
|
|
74
|
+
return data
|
|
@@ -239,3 +239,15 @@ class ScreenerAPI:
|
|
|
239
239
|
"""Screen government contracts across all tickers."""
|
|
240
240
|
params = self._build_params(limit=limit)
|
|
241
241
|
return self._get("screener/government-contracts", params, as_dataframe)
|
|
242
|
+
|
|
243
|
+
# ── patent filings ────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
def patent_filings(
|
|
246
|
+
self,
|
|
247
|
+
*,
|
|
248
|
+
limit: int | None = None,
|
|
249
|
+
as_dataframe: bool = False,
|
|
250
|
+
) -> List[Dict[str, Any]] | pd.DataFrame:
|
|
251
|
+
"""Screen USPTO patent filings across all tickers."""
|
|
252
|
+
params = self._build_params(limit=limit)
|
|
253
|
+
return self._get("screener/patent-filings", params, as_dataframe)
|
|
@@ -14,6 +14,9 @@ Docs-based mapping
|
|
|
14
14
|
405 Method Not Allowed → MethodNotAllowed
|
|
15
15
|
429 Rate Limit Exceeded → RateLimitError
|
|
16
16
|
500 Internal Server Error → ServerError
|
|
17
|
+
502 Bad Gateway → BadGateway
|
|
18
|
+
503 Service Unavailable → ServiceUnavailable
|
|
19
|
+
504 Gateway Timeout → GatewayTimeout
|
|
17
20
|
"""
|
|
18
21
|
|
|
19
22
|
from __future__ import annotations
|
|
@@ -29,6 +32,9 @@ __all__ = [
|
|
|
29
32
|
"MethodNotAllowed",
|
|
30
33
|
"RateLimitError",
|
|
31
34
|
"ServerError",
|
|
35
|
+
"BadGateway",
|
|
36
|
+
"ServiceUnavailable",
|
|
37
|
+
"GatewayTimeout",
|
|
32
38
|
#
|
|
33
39
|
"InvalidResponse",
|
|
34
40
|
"http_error_to_exception",
|
|
@@ -96,6 +102,18 @@ class ServerError(FinBrainError):
|
|
|
96
102
|
"""500 - Internal error on FinBrain's side. Retrying later may help."""
|
|
97
103
|
|
|
98
104
|
|
|
105
|
+
class BadGateway(FinBrainError):
|
|
106
|
+
"""502 - Invalid response from an upstream server. Usually transient."""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ServiceUnavailable(FinBrainError):
|
|
110
|
+
"""503 - Service temporarily unavailable (overloaded or in maintenance)."""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class GatewayTimeout(FinBrainError):
|
|
114
|
+
"""504 - Upstream server did not respond in time. Usually transient."""
|
|
115
|
+
|
|
116
|
+
|
|
99
117
|
# ─────────────────────────────────────────────────────────────
|
|
100
118
|
# Transport / decoding guard
|
|
101
119
|
# ─────────────────────────────────────────────────────────────
|
|
@@ -167,6 +185,12 @@ def http_error_to_exception(resp) -> FinBrainError: # expects requests.Response
|
|
|
167
185
|
return RateLimitError(message, **kwargs)
|
|
168
186
|
if status == 500:
|
|
169
187
|
return ServerError(message, **kwargs)
|
|
188
|
+
if status == 502:
|
|
189
|
+
return BadGateway(message, **kwargs)
|
|
190
|
+
if status == 503:
|
|
191
|
+
return ServiceUnavailable(message, **kwargs)
|
|
192
|
+
if status == 504:
|
|
193
|
+
return GatewayTimeout(message, **kwargs)
|
|
170
194
|
|
|
171
195
|
# Fallback for undocumented codes (future-proofing)
|
|
172
196
|
return FinBrainError(message, **kwargs)
|