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.
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/.github/workflows/ci.yml +1 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/.github/workflows/release.yml +2 -2
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/CHANGELOG.md +43 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/PKG-INFO +62 -1
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/README.md +59 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/RELEASE.md +1 -1
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/pyproject.toml +9 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/client.py +5 -3
- finbrain_python-0.2.6/src/finbrain/aio/endpoints/_utils.py +12 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/house_trades.py +13 -1
- finbrain_python-0.2.6/src/finbrain/aio/endpoints/patent_filings.py +48 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/screener.py +26 -2
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/senate_trades.py +13 -1
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/client.py +18 -3
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/_utils.py +7 -1
- finbrain_python-0.2.6/src/finbrain/endpoints/house_trades.py +97 -0
- finbrain_python-0.2.4/src/finbrain/endpoints/house_trades.py → finbrain_python-0.2.6/src/finbrain/endpoints/patent_filings.py +15 -14
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/screener.py +26 -2
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/senate_trades.py +24 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/exceptions.py +24 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/plotting.py +381 -164
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain_python.egg-info/PKG-INFO +62 -1
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain_python.egg-info/SOURCES.txt +8 -1
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain_python.egg-info/requires.txt +2 -0
- finbrain_python-0.2.6/src/finbrain_python.egg-info/scm_file_list.json +85 -0
- finbrain_python-0.2.6/src/finbrain_python.egg-info/scm_version.json +8 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_async_client.py +1 -0
- finbrain_python-0.2.6/tests/test_client.py +46 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_envelope.py +43 -1
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_house_trades.py +14 -1
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_integration.py +57 -0
- finbrain_python-0.2.6/tests/test_patent_filings.py +207 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_plotting.py +95 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_screener.py +84 -8
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_senate_trades.py +14 -1
- finbrain_python-0.2.6/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.6}/.gitattributes +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/.gitignore +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/LICENSE +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/examples/async_example.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/examples/transactions_plotting_example.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/setup.cfg +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/__init__.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/__init__.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/__init__.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/analyst_ratings.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/app_ratings.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/available.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/corporate_lobbying.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/government_contracts.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/insider_transactions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/linkedin_data.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/news.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/options.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/predictions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/recent.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/reddit_mentions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/aio/endpoints/sentiments.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/__init__.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/analyst_ratings.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/app_ratings.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/available.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/corporate_lobbying.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/government_contracts.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/insider_transactions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/linkedin_data.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/news.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/options.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/predictions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/recent.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/reddit_mentions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/endpoints/sentiments.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain/py.typed +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain_python.egg-info/dependency_links.txt +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/src/finbrain_python.egg-info/top_level.txt +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/__init__.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/conftest.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_analyst_ratings.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_app_ratings.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_available.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_corporate_lobbying.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_government_contracts.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_insider_transactions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_linkedin_data.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_news.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_options.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_predictions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_recent.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/tests/test_reddit_mentions.py +0 -0
- {finbrain_python-0.2.4 → finbrain_python-0.2.6}/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,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.
|
|
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 <!-- 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
|
|
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"]
|
|
@@ -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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
#
|
|
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
|