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.
Files changed (89) hide show
  1. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/.github/workflows/ci.yml +1 -0
  2. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/.github/workflows/release.yml +2 -2
  3. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/CHANGELOG.md +24 -0
  4. {finbrain_python-0.2.4/src/finbrain_python.egg-info → finbrain_python-0.2.5}/PKG-INFO +28 -1
  5. finbrain_python-0.2.4/PKG-INFO → finbrain_python-0.2.5/README.md +25 -26
  6. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/RELEASE.md +1 -1
  7. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/pyproject.toml +9 -0
  8. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/client.py +5 -3
  9. finbrain_python-0.2.5/src/finbrain/aio/endpoints/_utils.py +12 -0
  10. finbrain_python-0.2.5/src/finbrain/aio/endpoints/patent_filings.py +48 -0
  11. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/screener.py +12 -0
  12. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/client.py +18 -3
  13. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/_utils.py +7 -1
  14. finbrain_python-0.2.5/src/finbrain/endpoints/patent_filings.py +74 -0
  15. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/screener.py +12 -0
  16. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/exceptions.py +24 -0
  17. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/plotting.py +381 -164
  18. finbrain_python-0.2.4/README.md → finbrain_python-0.2.5/src/finbrain_python.egg-info/PKG-INFO +53 -0
  19. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain_python.egg-info/SOURCES.txt +6 -1
  20. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain_python.egg-info/requires.txt +2 -0
  21. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_async_client.py +1 -0
  22. finbrain_python-0.2.5/tests/test_client.py +46 -0
  23. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_envelope.py +43 -1
  24. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_integration.py +41 -0
  25. finbrain_python-0.2.5/tests/test_patent_filings.py +207 -0
  26. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_plotting.py +95 -0
  27. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_screener.py +36 -0
  28. finbrain_python-0.2.5/tests/test_utils.py +24 -0
  29. finbrain_python-0.2.4/src/finbrain/aio/endpoints/_utils.py +0 -37
  30. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/.gitattributes +0 -0
  31. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/.gitignore +0 -0
  32. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/LICENSE +0 -0
  33. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/examples/async_example.py +0 -0
  34. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/examples/transactions_plotting_example.py +0 -0
  35. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/setup.cfg +0 -0
  36. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/__init__.py +0 -0
  37. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/__init__.py +0 -0
  38. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/__init__.py +0 -0
  39. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/analyst_ratings.py +0 -0
  40. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/app_ratings.py +0 -0
  41. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/available.py +0 -0
  42. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/corporate_lobbying.py +0 -0
  43. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/government_contracts.py +0 -0
  44. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/house_trades.py +0 -0
  45. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/insider_transactions.py +0 -0
  46. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/linkedin_data.py +0 -0
  47. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/news.py +0 -0
  48. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/options.py +0 -0
  49. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/predictions.py +0 -0
  50. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/recent.py +0 -0
  51. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/reddit_mentions.py +0 -0
  52. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/senate_trades.py +0 -0
  53. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/aio/endpoints/sentiments.py +0 -0
  54. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/__init__.py +0 -0
  55. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/analyst_ratings.py +0 -0
  56. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/app_ratings.py +0 -0
  57. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/available.py +0 -0
  58. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/corporate_lobbying.py +0 -0
  59. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/government_contracts.py +0 -0
  60. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/house_trades.py +0 -0
  61. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/insider_transactions.py +0 -0
  62. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/linkedin_data.py +0 -0
  63. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/news.py +0 -0
  64. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/options.py +0 -0
  65. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/predictions.py +0 -0
  66. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/recent.py +0 -0
  67. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/reddit_mentions.py +0 -0
  68. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/senate_trades.py +0 -0
  69. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/endpoints/sentiments.py +0 -0
  70. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain/py.typed +0 -0
  71. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain_python.egg-info/dependency_links.txt +0 -0
  72. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/src/finbrain_python.egg-info/top_level.txt +0 -0
  73. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/__init__.py +0 -0
  74. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/conftest.py +0 -0
  75. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_analyst_ratings.py +0 -0
  76. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_app_ratings.py +0 -0
  77. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_available.py +0 -0
  78. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_corporate_lobbying.py +0 -0
  79. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_government_contracts.py +0 -0
  80. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_house_trades.py +0 -0
  81. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_insider_transactions.py +0 -0
  82. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_linkedin_data.py +0 -0
  83. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_news.py +0 -0
  84. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_options.py +0 -0
  85. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_predictions.py +0 -0
  86. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_recent.py +0 -0
  87. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_reddit_mentions.py +0 -0
  88. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_senate_trades.py +0 -0
  89. {finbrain_python-0.2.4 → finbrain_python-0.2.5}/tests/test_sentiments.py +0 -0
@@ -17,4 +17,5 @@ jobs:
17
17
  python-version: ${{ matrix.python }}
18
18
  - run: pip install -e .[dev]
19
19
  - run: ruff check .
20
+ - run: mypy
20
21
  - run: pytest -q
@@ -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.4
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&nbsp;<!-- 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&nbsp;<!-- omit in toc -->
28
2
 
29
3
  [![PyPI version](https://img.shields.io/pypi/v/finbrain-python.svg)](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 move items from `[Unreleased]` to a new version section:
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
- # 500 – exponential back-off then retry
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
- # 500 – exponential back-off then retry
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)