finbrain-python 0.2.4__tar.gz → 0.2.6__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 (91) hide show
  1. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/.github/workflows/ci.yml +1 -0
  2. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/.github/workflows/release.yml +2 -2
  3. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/CHANGELOG.md +43 -0
  4. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/PKG-INFO +62 -1
  5. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/README.md +59 -0
  6. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/RELEASE.md +1 -1
  7. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/pyproject.toml +9 -0
  8. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/client.py +5 -3
  9. finbrain_python-0.2.6/src/finbrain/aio/endpoints/_utils.py +12 -0
  10. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/house_trades.py +13 -1
  11. finbrain_python-0.2.6/src/finbrain/aio/endpoints/patent_filings.py +48 -0
  12. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/screener.py +26 -2
  13. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/senate_trades.py +13 -1
  14. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/client.py +18 -3
  15. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/_utils.py +7 -1
  16. finbrain_python-0.2.6/src/finbrain/endpoints/house_trades.py +97 -0
  17. finbrain_python-0.2.4/src/finbrain/endpoints/house_trades.py → finbrain_python-0.2.6/src/finbrain/endpoints/patent_filings.py +15 -14
  18. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/screener.py +26 -2
  19. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/senate_trades.py +24 -0
  20. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/exceptions.py +24 -0
  21. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/plotting.py +381 -164
  22. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain_python.egg-info/PKG-INFO +62 -1
  23. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain_python.egg-info/SOURCES.txt +8 -1
  24. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain_python.egg-info/requires.txt +2 -0
  25. finbrain_python-0.2.6/src/finbrain_python.egg-info/scm_file_list.json +85 -0
  26. finbrain_python-0.2.6/src/finbrain_python.egg-info/scm_version.json +8 -0
  27. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_async_client.py +1 -0
  28. finbrain_python-0.2.6/tests/test_client.py +46 -0
  29. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_envelope.py +43 -1
  30. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_house_trades.py +14 -1
  31. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_integration.py +57 -0
  32. finbrain_python-0.2.6/tests/test_patent_filings.py +207 -0
  33. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_plotting.py +95 -0
  34. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_screener.py +84 -8
  35. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_senate_trades.py +14 -1
  36. finbrain_python-0.2.6/tests/test_utils.py +24 -0
  37. finbrain_python-0.2.4/src/finbrain/aio/endpoints/_utils.py +0 -37
  38. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/.gitattributes +0 -0
  39. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/.gitignore +0 -0
  40. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/LICENSE +0 -0
  41. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/examples/async_example.py +0 -0
  42. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/examples/transactions_plotting_example.py +0 -0
  43. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/setup.cfg +0 -0
  44. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/__init__.py +0 -0
  45. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/__init__.py +0 -0
  46. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/__init__.py +0 -0
  47. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/analyst_ratings.py +0 -0
  48. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/app_ratings.py +0 -0
  49. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/available.py +0 -0
  50. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/corporate_lobbying.py +0 -0
  51. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/government_contracts.py +0 -0
  52. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/insider_transactions.py +0 -0
  53. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/linkedin_data.py +0 -0
  54. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/news.py +0 -0
  55. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/options.py +0 -0
  56. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/predictions.py +0 -0
  57. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/recent.py +0 -0
  58. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/reddit_mentions.py +0 -0
  59. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/sentiments.py +0 -0
  60. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/__init__.py +0 -0
  61. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/analyst_ratings.py +0 -0
  62. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/app_ratings.py +0 -0
  63. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/available.py +0 -0
  64. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/corporate_lobbying.py +0 -0
  65. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/government_contracts.py +0 -0
  66. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/insider_transactions.py +0 -0
  67. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/linkedin_data.py +0 -0
  68. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/news.py +0 -0
  69. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/options.py +0 -0
  70. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/predictions.py +0 -0
  71. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/recent.py +0 -0
  72. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/reddit_mentions.py +0 -0
  73. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/sentiments.py +0 -0
  74. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/py.typed +0 -0
  75. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain_python.egg-info/dependency_links.txt +0 -0
  76. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain_python.egg-info/top_level.txt +0 -0
  77. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/__init__.py +0 -0
  78. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/conftest.py +0 -0
  79. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_analyst_ratings.py +0 -0
  80. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_app_ratings.py +0 -0
  81. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_available.py +0 -0
  82. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_corporate_lobbying.py +0 -0
  83. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_government_contracts.py +0 -0
  84. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_insider_transactions.py +0 -0
  85. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_linkedin_data.py +0 -0
  86. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_news.py +0 -0
  87. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_options.py +0 -0
  88. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_predictions.py +0 -0
  89. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_recent.py +0 -0
  90. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_reddit_mentions.py +0 -0
  91. {finbrain_python-0.2.4 → finbrain_python-0.2.6}/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,49 @@ 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.6] - 2026-07-24
9
+
10
+ ### Added
11
+
12
+ - **Congressional disclosure date**: House and Senate trade rows now carry `disclosureDate` alongside the transaction `date` — the date the trade was publicly disclosed in the member's periodic transaction report. The gap between the two is the reporting lag. Available on `fb.house_trades.ticker()`, `fb.senate_trades.ticker()`, their async equivalents, and the `fb.screener.congress_house()` / `fb.screener.congress_senate()` rows
13
+ - **Congressional trade date documentation**: README section explaining the two dates, and expanded docstrings on the sync/async ticker and screener methods
14
+ - **Tests**: Unit tests assert `disclosureDate` passes through in both the raw-dict and DataFrame branches, including the null case for historical rows; integration tests assert the key is present on live responses
15
+
16
+ ### Changed
17
+
18
+ - **DataFrame shape**: `fb.house_trades.ticker(..., as_dataframe=True)`, `fb.senate_trades.ticker(..., as_dataframe=True)` and the congress screener frames gain a `disclosureDate` column. Code that asserts an exact column set (`set(df.columns) == {...}`) or compares frames for equality needs updating
19
+
20
+ ### Notes
21
+
22
+ - `disclosureDate` is `null` for rows collected before the upstream pipeline captured the field. In a DataFrame the missing value reads as `None` or `NaN` depending on the pandas version, so test it with `pandas.isna()` rather than `is None`
23
+ - **`.dropna()` caveat**: because historical rows have no disclosure date, calling `.dropna()` on a congressional trades DataFrame now drops those rows entirely. Use `.dropna(subset=[...])` if that is not what you want
24
+ - `date_from` / `date_to` bound the transaction date, not the disclosure date
25
+ - The raw-dict branch is purely additive — no call-site changes are required there to receive the new field
26
+
27
+ ## [0.2.5] - 2026-06-18
28
+
29
+ ### Added
30
+
31
+ - **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}`)
32
+ - **Patent Filings Screener**: `fb.screener.patent_filings()` — cross-ticker patent filings with summary stats (total patents, tickers, top CPC sections) (`/screener/patent-filings`)
33
+ - **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
34
+ - **Async Patent Filings**: Full async support via `AsyncPatentFilingsAPI`
35
+ - **Patent Filings Tests**: Unit tests (`tests/test_patent_filings.py`), screener tests, integration tests, and plotting tests
36
+ - **Sync Context Manager**: `FinBrainClient` now supports `with FinBrainClient(...) as fb:` and exposes `close()` to release the underlying HTTP session (parity with `AsyncFinBrainClient`)
37
+ - **Exception Types**: Dedicated `BadGateway` (502), `ServiceUnavailable` (503), and `GatewayTimeout` (504) exceptions for transient gateway errors
38
+ - **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
39
+
40
+ ### Fixed
41
+
42
+ - **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
43
+
44
+ ### Changed
45
+
46
+ - **Retry coverage**: Transient gateway errors (502/503/504) are now retried alongside 500, with exponential back-off
47
+ - **Internal**: Async endpoint date helper re-exports the synchronous `to_datestr` to keep a single source of truth
48
+ - **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
49
+ - **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
50
+
8
51
  ## [0.2.4] - 2026-03-30
9
52
 
10
53
  ### 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.6
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 -->
@@ -82,6 +84,7 @@ fb.analyst_ratings.ticker("AMZN",
82
84
  as_dataframe=True)
83
85
 
84
86
  # ---------- house trades ----------
87
+ # Rows include `disclosureDate` alongside the transaction `date`.
85
88
  fb.house_trades.ticker("AMZN",
86
89
  date_from="2025-01-01",
87
90
  date_to="2025-06-30",
@@ -112,6 +115,13 @@ fb.government_contracts.ticker("LMT",
112
115
  limit=50,
113
116
  as_dataframe=True)
114
117
 
118
+ # ---------- patent filings ----------
119
+ fb.patent_filings.ticker("AAPL",
120
+ date_from="2025-01-01",
121
+ date_to="2025-12-31",
122
+ limit=50,
123
+ as_dataframe=True)
124
+
115
125
  # ---------- insider transactions ----------
116
126
  fb.insider_transactions.ticker("AMZN", as_dataframe=True)
117
127
 
@@ -145,12 +155,46 @@ fb.screener.predictions_daily(limit=100, as_dataframe=True)
145
155
  fb.screener.insider_trading(limit=50)
146
156
  fb.screener.reddit_mentions(limit=100, as_dataframe=True)
147
157
  fb.screener.government_contracts(limit=100, as_dataframe=True)
158
+ fb.screener.patent_filings(limit=100, as_dataframe=True)
159
+ fb.screener.congress_house(limit=50) # rows carry `disclosureDate`
160
+ fb.screener.congress_senate(limit=50)
148
161
 
149
162
  # ---------- recent data ----------
150
163
  fb.recent.news(limit=100, as_dataframe=True)
151
164
  fb.recent.analyst_ratings(limit=50)
152
165
  ```
153
166
 
167
+ ### Congressional trade dates
168
+
169
+ House and Senate trade rows carry **two** dates, and the gap between them is
170
+ the reporting lag — often weeks:
171
+
172
+ | Field | Meaning |
173
+ | ---------------- | ---------------------------------------------------------------- |
174
+ | `date` | Transaction date — when the member actually bought or sold |
175
+ | `disclosureDate` | Public disclosure date — when the periodic transaction report ran |
176
+
177
+ `disclosureDate` is `null` on historical rows collected before the field was
178
+ captured upstream. With `as_dataframe=True`, `date` is the **index** and
179
+ `disclosureDate` is a column whose missing values read as `None` or `NaN`
180
+ depending on your pandas version — test them with `pandas.isna()` rather than
181
+ `is None`. Note that `.dropna()` on such a frame will now discard every
182
+ historical row. The same field is present on `fb.screener.congress_house()`
183
+ and `fb.screener.congress_senate()` rows.
184
+
185
+ `date_from` / `date_to` bound the **transaction** date, not the disclosure
186
+ date — a trade executed inside the window is returned even if it was disclosed
187
+ after `date_to`.
188
+
189
+ ```python
190
+ trades = fb.house_trades.ticker("AMZN")["trades"]
191
+ lag_days = [
192
+ (pd.Timestamp(t["disclosureDate"]) - pd.Timestamp(t["date"])).days
193
+ for t in trades
194
+ if t["disclosureDate"]
195
+ ]
196
+ ```
197
+
154
198
  ## ⚡ Async Usage
155
199
 
156
200
  For async/await support, install with the `async` extra:
@@ -265,6 +309,18 @@ fb.plot.reddit_mentions("TSLA",
265
309
  price_data=price_df,
266
310
  date_from="2026-03-01",
267
311
  date_to="2026-03-17")
312
+
313
+ # Plot patent grants (bars sized by claim count) on your price chart
314
+ fb.plot.patent_filings("AAPL",
315
+ price_data=price_df,
316
+ date_from="2024-01-01",
317
+ date_to="2025-06-30")
318
+
319
+ # Plot analyst ratings & price targets (markers coloured by action) on your price chart
320
+ fb.plot.analyst_ratings("AAPL",
321
+ price_data=price_df,
322
+ date_from="2024-01-01",
323
+ date_to="2025-06-30")
268
324
  ```
269
325
 
270
326
  ```python
@@ -321,6 +377,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
321
377
  | Corporate lobbying | `client.corporate_lobbying.ticker()` | `/lobbying/{SYMBOL}` |
322
378
  | Reddit mentions | `client.reddit_mentions.ticker()` | `/reddit-mentions/{SYMBOL}` |
323
379
  | Gov. contracts | `client.government_contracts.ticker()` | `/government-contracts/{SYMBOL}` |
380
+ | Patent filings | `client.patent_filings.ticker()` | `/patent-filings/{SYMBOL}` |
324
381
  | Insider transactions | `client.insider_transactions.ticker()` | `/insider-trading/{SYMBOL}` |
325
382
  | LinkedIn | `client.linkedin_data.ticker()` | `/linkedin/{SYMBOL}` |
326
383
  | Options – Put/Call | `client.options.put_call()` | `/put-call-ratio/{SYMBOL}` |
@@ -329,6 +386,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
329
386
  | | `client.screener.insider_trading()` | `/screener/insider-trading` |
330
387
  | | `client.screener.reddit_mentions()` | `/screener/reddit-mentions` |
331
388
  | | `client.screener.government_contracts()` | `/screener/government-contracts` |
389
+ | | `client.screener.patent_filings()` | `/screener/patent-filings` |
332
390
  | | ... and 8 more screener methods | |
333
391
  | Recent | `client.recent.news()` | `/recent/news` |
334
392
  | | `client.recent.analyst_ratings()` | `/recent/analyst-ratings` |
@@ -356,6 +414,9 @@ except BadRequest as exc:
356
414
  | 405 | `MethodNotAllowed` | HTTP method not supported on endpoint |
357
415
  | 429 | `RateLimitError` | Too many requests |
358
416
  | 500 | `ServerError` | FinBrain internal error |
417
+ | 502 | `BadGateway` | Invalid response from upstream server |
418
+ | 503 | `ServiceUnavailable` | Service temporarily unavailable |
419
+ | 504 | `GatewayTimeout` | Upstream server timed out |
359
420
 
360
421
  ---
361
422
 
@@ -56,6 +56,7 @@ fb.analyst_ratings.ticker("AMZN",
56
56
  as_dataframe=True)
57
57
 
58
58
  # ---------- house trades ----------
59
+ # Rows include `disclosureDate` alongside the transaction `date`.
59
60
  fb.house_trades.ticker("AMZN",
60
61
  date_from="2025-01-01",
61
62
  date_to="2025-06-30",
@@ -86,6 +87,13 @@ fb.government_contracts.ticker("LMT",
86
87
  limit=50,
87
88
  as_dataframe=True)
88
89
 
90
+ # ---------- patent filings ----------
91
+ fb.patent_filings.ticker("AAPL",
92
+ date_from="2025-01-01",
93
+ date_to="2025-12-31",
94
+ limit=50,
95
+ as_dataframe=True)
96
+
89
97
  # ---------- insider transactions ----------
90
98
  fb.insider_transactions.ticker("AMZN", as_dataframe=True)
91
99
 
@@ -119,12 +127,46 @@ fb.screener.predictions_daily(limit=100, as_dataframe=True)
119
127
  fb.screener.insider_trading(limit=50)
120
128
  fb.screener.reddit_mentions(limit=100, as_dataframe=True)
121
129
  fb.screener.government_contracts(limit=100, as_dataframe=True)
130
+ fb.screener.patent_filings(limit=100, as_dataframe=True)
131
+ fb.screener.congress_house(limit=50) # rows carry `disclosureDate`
132
+ fb.screener.congress_senate(limit=50)
122
133
 
123
134
  # ---------- recent data ----------
124
135
  fb.recent.news(limit=100, as_dataframe=True)
125
136
  fb.recent.analyst_ratings(limit=50)
126
137
  ```
127
138
 
139
+ ### Congressional trade dates
140
+
141
+ House and Senate trade rows carry **two** dates, and the gap between them is
142
+ the reporting lag — often weeks:
143
+
144
+ | Field | Meaning |
145
+ | ---------------- | ---------------------------------------------------------------- |
146
+ | `date` | Transaction date — when the member actually bought or sold |
147
+ | `disclosureDate` | Public disclosure date — when the periodic transaction report ran |
148
+
149
+ `disclosureDate` is `null` on historical rows collected before the field was
150
+ captured upstream. With `as_dataframe=True`, `date` is the **index** and
151
+ `disclosureDate` is a column whose missing values read as `None` or `NaN`
152
+ depending on your pandas version — test them with `pandas.isna()` rather than
153
+ `is None`. Note that `.dropna()` on such a frame will now discard every
154
+ historical row. The same field is present on `fb.screener.congress_house()`
155
+ and `fb.screener.congress_senate()` rows.
156
+
157
+ `date_from` / `date_to` bound the **transaction** date, not the disclosure
158
+ date — a trade executed inside the window is returned even if it was disclosed
159
+ after `date_to`.
160
+
161
+ ```python
162
+ trades = fb.house_trades.ticker("AMZN")["trades"]
163
+ lag_days = [
164
+ (pd.Timestamp(t["disclosureDate"]) - pd.Timestamp(t["date"])).days
165
+ for t in trades
166
+ if t["disclosureDate"]
167
+ ]
168
+ ```
169
+
128
170
  ## ⚡ Async Usage
129
171
 
130
172
  For async/await support, install with the `async` extra:
@@ -239,6 +281,18 @@ fb.plot.reddit_mentions("TSLA",
239
281
  price_data=price_df,
240
282
  date_from="2026-03-01",
241
283
  date_to="2026-03-17")
284
+
285
+ # Plot patent grants (bars sized by claim count) on your price chart
286
+ fb.plot.patent_filings("AAPL",
287
+ price_data=price_df,
288
+ date_from="2024-01-01",
289
+ date_to="2025-06-30")
290
+
291
+ # Plot analyst ratings & price targets (markers coloured by action) on your price chart
292
+ fb.plot.analyst_ratings("AAPL",
293
+ price_data=price_df,
294
+ date_from="2024-01-01",
295
+ date_to="2025-06-30")
242
296
  ```
243
297
 
244
298
  ```python
@@ -295,6 +349,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
295
349
  | Corporate lobbying | `client.corporate_lobbying.ticker()` | `/lobbying/{SYMBOL}` |
296
350
  | Reddit mentions | `client.reddit_mentions.ticker()` | `/reddit-mentions/{SYMBOL}` |
297
351
  | Gov. contracts | `client.government_contracts.ticker()` | `/government-contracts/{SYMBOL}` |
352
+ | Patent filings | `client.patent_filings.ticker()` | `/patent-filings/{SYMBOL}` |
298
353
  | Insider transactions | `client.insider_transactions.ticker()` | `/insider-trading/{SYMBOL}` |
299
354
  | LinkedIn | `client.linkedin_data.ticker()` | `/linkedin/{SYMBOL}` |
300
355
  | Options – Put/Call | `client.options.put_call()` | `/put-call-ratio/{SYMBOL}` |
@@ -303,6 +358,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
303
358
  | | `client.screener.insider_trading()` | `/screener/insider-trading` |
304
359
  | | `client.screener.reddit_mentions()` | `/screener/reddit-mentions` |
305
360
  | | `client.screener.government_contracts()` | `/screener/government-contracts` |
361
+ | | `client.screener.patent_filings()` | `/screener/patent-filings` |
306
362
  | | ... and 8 more screener methods | |
307
363
  | Recent | `client.recent.news()` | `/recent/news` |
308
364
  | | `client.recent.analyst_ratings()` | `/recent/analyst-ratings` |
@@ -330,6 +386,9 @@ except BadRequest as exc:
330
386
  | 405 | `MethodNotAllowed` | HTTP method not supported on endpoint |
331
387
  | 429 | `RateLimitError` | Too many requests |
332
388
  | 500 | `ServerError` | FinBrain internal error |
389
+ | 502 | `BadGateway` | Invalid response from upstream server |
390
+ | 503 | `ServiceUnavailable` | Service temporarily unavailable |
391
+ | 504 | `GatewayTimeout` | Upstream server timed out |
333
392
 
334
393
  ---
335
394
 
@@ -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"]
@@ -24,7 +24,19 @@ class AsyncHouseTradesAPI:
24
24
  limit: int | None = None,
25
25
  as_dataframe: bool = False,
26
26
  ) -> Dict[str, Any] | pd.DataFrame:
27
- """Fetch House-member trades for a symbol (async)."""
27
+ """
28
+ Fetch House-member trades for a symbol (async).
29
+
30
+ Each row in ``trades`` carries ``date`` (the transaction date),
31
+ ``politician``, ``transactionType``, ``amount`` and
32
+ ``disclosureDate`` — the date the trade was publicly disclosed in the
33
+ member's periodic transaction report. ``disclosureDate`` is ``None``
34
+ for historical rows collected before the field was captured (``NaN``
35
+ in the DataFrame branch on newer pandas — use ``pandas.isna``).
36
+
37
+ ``date_from``/``date_to`` bound the transaction date, not the
38
+ disclosure date.
39
+ """
28
40
  params: Dict[str, str] = {}
29
41
  if date_from:
30
42
  params["startDate"] = to_datestr(date_from)
@@ -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
@@ -107,7 +107,13 @@ class AsyncScreenerAPI:
107
107
  limit: int | None = None,
108
108
  as_dataframe: bool = False,
109
109
  ) -> List[Dict[str, Any]] | pd.DataFrame:
110
- """Screen House trades across all tickers."""
110
+ """
111
+ Screen House trades across all tickers.
112
+
113
+ Rows carry ``symbol``, ``name``, ``date``, ``politician``,
114
+ ``transactionType``, ``amount`` and ``disclosureDate`` (the public
115
+ disclosure date, ``None`` on historical rows).
116
+ """
111
117
  params = self._build_params(limit=limit)
112
118
  return await self._get("screener/congress/house", params, as_dataframe)
113
119
 
@@ -119,7 +125,13 @@ class AsyncScreenerAPI:
119
125
  limit: int | None = None,
120
126
  as_dataframe: bool = False,
121
127
  ) -> List[Dict[str, Any]] | pd.DataFrame:
122
- """Screen Senate trades across all tickers."""
128
+ """
129
+ Screen Senate trades across all tickers.
130
+
131
+ Rows carry ``symbol``, ``name``, ``date``, ``politician``,
132
+ ``transactionType``, ``amount`` and ``disclosureDate`` (the public
133
+ disclosure date, ``None`` on historical rows).
134
+ """
123
135
  params = self._build_params(limit=limit)
124
136
  return await self._get("screener/congress/senate", params, as_dataframe)
125
137
 
@@ -234,3 +246,15 @@ class AsyncScreenerAPI:
234
246
  """Screen government contracts across all tickers (async)."""
235
247
  params = self._build_params(limit=limit)
236
248
  return await self._get("screener/government-contracts", params, as_dataframe)
249
+
250
+ # ── patent filings ────────────────────────────────────────
251
+
252
+ async def patent_filings(
253
+ self,
254
+ *,
255
+ limit: int | None = None,
256
+ as_dataframe: bool = False,
257
+ ) -> List[Dict[str, Any]] | pd.DataFrame:
258
+ """Screen USPTO patent filings across all tickers (async)."""
259
+ params = self._build_params(limit=limit)
260
+ return await self._get("screener/patent-filings", params, as_dataframe)
@@ -24,7 +24,19 @@ class AsyncSenateTradesAPI:
24
24
  limit: int | None = None,
25
25
  as_dataframe: bool = False,
26
26
  ) -> Dict[str, Any] | pd.DataFrame:
27
- """Fetch Senate-member trades for a symbol (async)."""
27
+ """
28
+ Fetch Senate-member trades for a symbol (async).
29
+
30
+ Each row in ``trades`` carries ``date`` (the transaction date),
31
+ ``politician``, ``transactionType``, ``amount`` and
32
+ ``disclosureDate`` — the date the trade was publicly disclosed in the
33
+ member's periodic transaction report. ``disclosureDate`` is ``None``
34
+ for historical rows collected before the field was captured (``NaN``
35
+ in the DataFrame branch on newer pandas — use ``pandas.isna``).
36
+
37
+ ``date_from``/``date_to`` bound the transaction date, not the
38
+ disclosure date.
39
+ """
28
40
  params: Dict[str, str] = {}
29
41
  if date_from:
30
42
  params["startDate"] = to_datestr(date_from)
@@ -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