finbrain-python 0.2.5__tar.gz → 0.2.7__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 (90) hide show
  1. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/.github/workflows/ci.yml +4 -1
  2. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/.github/workflows/release.yml +22 -0
  3. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/CHANGELOG.md +26 -0
  4. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/PKG-INFO +35 -1
  5. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/README.md +34 -0
  6. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/pyproject.toml +19 -0
  7. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/house_trades.py +13 -1
  8. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/screener.py +14 -2
  9. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/senate_trades.py +13 -1
  10. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/house_trades.py +24 -0
  11. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/screener.py +14 -2
  12. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/senate_trades.py +24 -0
  13. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain_python.egg-info/PKG-INFO +35 -1
  14. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain_python.egg-info/SOURCES.txt +2 -0
  15. finbrain_python-0.2.7/src/finbrain_python.egg-info/scm_file_list.json +85 -0
  16. finbrain_python-0.2.7/src/finbrain_python.egg-info/scm_version.json +8 -0
  17. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_house_trades.py +14 -1
  18. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_integration.py +16 -0
  19. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_screener.py +48 -8
  20. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_senate_trades.py +14 -1
  21. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/.gitattributes +0 -0
  22. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/.gitignore +0 -0
  23. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/LICENSE +0 -0
  24. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/RELEASE.md +0 -0
  25. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/examples/async_example.py +0 -0
  26. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/examples/transactions_plotting_example.py +0 -0
  27. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/setup.cfg +0 -0
  28. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/__init__.py +0 -0
  29. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/__init__.py +0 -0
  30. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/client.py +0 -0
  31. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/__init__.py +0 -0
  32. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/_utils.py +0 -0
  33. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/analyst_ratings.py +0 -0
  34. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/app_ratings.py +0 -0
  35. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/available.py +0 -0
  36. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/corporate_lobbying.py +0 -0
  37. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/government_contracts.py +0 -0
  38. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/insider_transactions.py +0 -0
  39. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/linkedin_data.py +0 -0
  40. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/news.py +0 -0
  41. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/options.py +0 -0
  42. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/patent_filings.py +0 -0
  43. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/predictions.py +0 -0
  44. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/recent.py +0 -0
  45. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/reddit_mentions.py +0 -0
  46. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/aio/endpoints/sentiments.py +0 -0
  47. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/client.py +0 -0
  48. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/__init__.py +0 -0
  49. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/_utils.py +0 -0
  50. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/analyst_ratings.py +0 -0
  51. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/app_ratings.py +0 -0
  52. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/available.py +0 -0
  53. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/corporate_lobbying.py +0 -0
  54. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/government_contracts.py +0 -0
  55. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/insider_transactions.py +0 -0
  56. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/linkedin_data.py +0 -0
  57. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/news.py +0 -0
  58. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/options.py +0 -0
  59. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/patent_filings.py +0 -0
  60. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/predictions.py +0 -0
  61. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/recent.py +0 -0
  62. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/reddit_mentions.py +0 -0
  63. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/endpoints/sentiments.py +0 -0
  64. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/exceptions.py +0 -0
  65. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/plotting.py +0 -0
  66. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain/py.typed +0 -0
  67. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain_python.egg-info/dependency_links.txt +0 -0
  68. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain_python.egg-info/requires.txt +0 -0
  69. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/src/finbrain_python.egg-info/top_level.txt +0 -0
  70. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/__init__.py +0 -0
  71. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/conftest.py +0 -0
  72. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_analyst_ratings.py +0 -0
  73. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_app_ratings.py +0 -0
  74. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_async_client.py +0 -0
  75. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_available.py +0 -0
  76. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_client.py +0 -0
  77. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_corporate_lobbying.py +0 -0
  78. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_envelope.py +0 -0
  79. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_government_contracts.py +0 -0
  80. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_insider_transactions.py +0 -0
  81. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_linkedin_data.py +0 -0
  82. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_news.py +0 -0
  83. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_options.py +0 -0
  84. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_patent_filings.py +0 -0
  85. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_plotting.py +0 -0
  86. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_predictions.py +0 -0
  87. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_recent.py +0 -0
  88. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_reddit_mentions.py +0 -0
  89. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_sentiments.py +0 -0
  90. {finbrain_python-0.2.5 → finbrain_python-0.2.7}/tests/test_utils.py +0 -0
@@ -1,6 +1,9 @@
1
1
  name: CI
2
2
 
3
- on: [push, pull_request]
3
+ on:
4
+ push:
5
+ branches: ['**'] # tags are covered by the gate in release.yml
6
+ pull_request:
4
7
 
5
8
  jobs:
6
9
  test:
@@ -5,7 +5,29 @@ on:
5
5
  tags: ['v[0-9]*'] # e.g. v0.1.0, v1.2.3
6
6
 
7
7
  jobs:
8
+ # Gate the upload on the tagged commit actually passing. The publish job
9
+ # previously ran straight off the tag push with no dependency on CI, so a
10
+ # commit with failing checks could still reach PyPI -- and PyPI versions are
11
+ # immutable, so a bad upload cannot be replaced, only superseded.
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ matrix:
16
+ python: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0
21
+ - uses: actions/setup-python@v5
22
+ with:
23
+ python-version: ${{ matrix.python }}
24
+ - run: pip install -e .[dev]
25
+ - run: ruff check .
26
+ - run: mypy
27
+ - run: pytest -q
28
+
8
29
  build:
30
+ needs: test
9
31
  runs-on: ubuntu-latest
10
32
 
11
33
  steps:
@@ -5,6 +5,32 @@ 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.7] - 2026-07-24
9
+
10
+ ### Changed
11
+
12
+ - **Tooling**: pinned the ruff rule set to `E4`/`E7`/`E9`/`F` in `[tool.ruff.lint]`. `ruff check .` previously ran with no ruff configuration, so it followed whatever ruff's current defaults were; 0.16.0 widened that selection and turned CI red with 455 findings in unchanged code. These four groups are ruff's long-standing defaults, so enforcement is unchanged — it just no longer moves when ruff releases
13
+ - **Tooling**: mypy no longer follows numpy's stubs. numpy ships inline stubs using PEP 695 `type` statements, which mypy only parses when both its target `python_version` and the interpreter it runs under are 3.12+, so a numpy release aborted the entire run on the lower matrix jobs. Raising `python_version` alone does not fix this — it clears the target check but not the interpreter one. `python_version` stays at `3.10`, so the 3.9 target is still exercised, and no public API surfaces numpy types
14
+
15
+ ## [0.2.6] - 2026-07-24
16
+
17
+ ### Added
18
+
19
+ - **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
20
+ - **Congressional trade date documentation**: README section explaining the two dates, and expanded docstrings on the sync/async ticker and screener methods
21
+ - **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
22
+
23
+ ### Changed
24
+
25
+ - **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
26
+
27
+ ### Notes
28
+
29
+ - `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`
30
+ - **`.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
31
+ - `date_from` / `date_to` bound the transaction date, not the disclosure date
32
+ - The raw-dict branch is purely additive — no call-site changes are required there to receive the new field
33
+
8
34
  ## [0.2.5] - 2026-06-18
9
35
 
10
36
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: finbrain-python
3
- Version: 0.2.5
3
+ Version: 0.2.7
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
@@ -84,6 +84,7 @@ fb.analyst_ratings.ticker("AMZN",
84
84
  as_dataframe=True)
85
85
 
86
86
  # ---------- house trades ----------
87
+ # Rows include `disclosureDate` alongside the transaction `date`.
87
88
  fb.house_trades.ticker("AMZN",
88
89
  date_from="2025-01-01",
89
90
  date_to="2025-06-30",
@@ -155,12 +156,45 @@ fb.screener.insider_trading(limit=50)
155
156
  fb.screener.reddit_mentions(limit=100, as_dataframe=True)
156
157
  fb.screener.government_contracts(limit=100, as_dataframe=True)
157
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)
158
161
 
159
162
  # ---------- recent data ----------
160
163
  fb.recent.news(limit=100, as_dataframe=True)
161
164
  fb.recent.analyst_ratings(limit=50)
162
165
  ```
163
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
+
164
198
  ## ⚡ Async Usage
165
199
 
166
200
  For async/await support, install with the `async` extra:
@@ -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",
@@ -127,12 +128,45 @@ fb.screener.insider_trading(limit=50)
127
128
  fb.screener.reddit_mentions(limit=100, as_dataframe=True)
128
129
  fb.screener.government_contracts(limit=100, as_dataframe=True)
129
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)
130
133
 
131
134
  # ---------- recent data ----------
132
135
  fb.recent.news(limit=100, as_dataframe=True)
133
136
  fb.recent.analyst_ratings(limit=50)
134
137
  ```
135
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
+
136
170
  ## ⚡ Async Usage
137
171
 
138
172
  For async/await support, install with the `async` extra:
@@ -60,3 +60,22 @@ markers = [
60
60
  python_version = "3.10"
61
61
  files = ["src/finbrain"]
62
62
  ignore_missing_imports = true # third-party libs (pandas, plotly, …) ship no stubs here
63
+
64
+ # numpy ships inline stubs that use PEP 695 `type` statements. mypy refuses to
65
+ # parse those unless BOTH its target `python_version` and the interpreter it is
66
+ # running under are 3.12+, so a numpy release can abort the whole run ("Type
67
+ # statement is only supported in Python 3.12 and greater") on any matrix job
68
+ # below 3.12 -- with no change to this codebase. Nothing in the public API
69
+ # surfaces numpy types, so skip its stubs rather than chasing numpy's baseline.
70
+ [[tool.mypy.overrides]]
71
+ module = ["numpy.*"]
72
+ follow_imports = "skip"
73
+ follow_imports_for_stubs = true
74
+
75
+ [tool.ruff.lint]
76
+ # Pin the rule set explicitly. `ruff check .` otherwise follows whatever ruff's
77
+ # current defaults happen to be, so a new ruff release can turn CI red with no
78
+ # code change (0.16.0 widened the defaults and surfaced hundreds of findings in
79
+ # code that had been passing). These four are ruff's long-standing defaults, so
80
+ # this preserves existing behaviour rather than changing it.
81
+ select = ["E4", "E7", "E9", "F"]
@@ -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)
@@ -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
 
@@ -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)
@@ -49,6 +49,30 @@ class HouseTradesAPI:
49
49
  Returns
50
50
  -------
51
51
  dict | pandas.DataFrame
52
+ The raw dict has a ``trades`` list whose rows carry ``date``
53
+ (the transaction date), ``politician``, ``transactionType``,
54
+ ``amount`` and ``disclosureDate`` — the date the trade was
55
+ publicly disclosed in the member's periodic transaction report.
56
+ The gap between the two dates is the reporting lag.
57
+
58
+ ``disclosureDate`` is ``None`` for historical rows collected
59
+ before the field was captured upstream. In the DataFrame branch
60
+ ``date`` becomes the index and ``disclosureDate`` is a column
61
+ whose missing values read as ``None`` or ``NaN`` depending on the
62
+ pandas version — test them with :func:`pandas.isna`.
63
+
64
+ Notes
65
+ -----
66
+ ``date_from`` and ``date_to`` bound the **transaction** date, not the
67
+ disclosure date. A trade executed inside the window is returned even
68
+ if it was disclosed after ``date_to``.
69
+
70
+ Example row::
71
+
72
+ {"date": "2026-06-15", "politician": "Jane Doe",
73
+ "transactionType": "Purchase", "amount": "$1,001 - $15,000",
74
+ "disclosureDate": "2026-07-01"}
75
+
52
76
  """
53
77
  params: Dict[str, str] = {}
54
78
  if date_from:
@@ -112,7 +112,13 @@ class ScreenerAPI:
112
112
  limit: int | None = None,
113
113
  as_dataframe: bool = False,
114
114
  ) -> List[Dict[str, Any]] | pd.DataFrame:
115
- """Screen House trades across all tickers."""
115
+ """
116
+ Screen House trades across all tickers.
117
+
118
+ Rows carry ``symbol``, ``name``, ``date``, ``politician``,
119
+ ``transactionType``, ``amount`` and ``disclosureDate`` (the public
120
+ disclosure date, ``None`` on historical rows).
121
+ """
116
122
  params = self._build_params(limit=limit)
117
123
  return self._get("screener/congress/house", params, as_dataframe)
118
124
 
@@ -124,7 +130,13 @@ class ScreenerAPI:
124
130
  limit: int | None = None,
125
131
  as_dataframe: bool = False,
126
132
  ) -> List[Dict[str, Any]] | pd.DataFrame:
127
- """Screen Senate trades across all tickers."""
133
+ """
134
+ Screen Senate trades across all tickers.
135
+
136
+ Rows carry ``symbol``, ``name``, ``date``, ``politician``,
137
+ ``transactionType``, ``amount`` and ``disclosureDate`` (the public
138
+ disclosure date, ``None`` on historical rows).
139
+ """
128
140
  params = self._build_params(limit=limit)
129
141
  return self._get("screener/congress/senate", params, as_dataframe)
130
142
 
@@ -49,6 +49,30 @@ class SenateTradesAPI:
49
49
  Returns
50
50
  -------
51
51
  dict | pandas.DataFrame
52
+ The raw dict has a ``trades`` list whose rows carry ``date``
53
+ (the transaction date), ``politician``, ``transactionType``,
54
+ ``amount`` and ``disclosureDate`` — the date the trade was
55
+ publicly disclosed in the member's periodic transaction report.
56
+ The gap between the two dates is the reporting lag.
57
+
58
+ ``disclosureDate`` is ``None`` for historical rows collected
59
+ before the field was captured upstream. In the DataFrame branch
60
+ ``date`` becomes the index and ``disclosureDate`` is a column
61
+ whose missing values read as ``None`` or ``NaN`` depending on the
62
+ pandas version — test them with :func:`pandas.isna`.
63
+
64
+ Notes
65
+ -----
66
+ ``date_from`` and ``date_to`` bound the **transaction** date, not the
67
+ disclosure date. A trade executed inside the window is returned even
68
+ if it was disclosed after ``date_to``.
69
+
70
+ Example row::
71
+
72
+ {"date": "2026-06-10", "politician": "Jane Doe",
73
+ "transactionType": "Purchase", "amount": "$1,001 - $15,000",
74
+ "disclosureDate": "2026-06-25"}
75
+
52
76
  """
53
77
  params: Dict[str, str] = {}
54
78
  if date_from:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: finbrain-python
3
- Version: 0.2.5
3
+ Version: 0.2.7
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
@@ -84,6 +84,7 @@ fb.analyst_ratings.ticker("AMZN",
84
84
  as_dataframe=True)
85
85
 
86
86
  # ---------- house trades ----------
87
+ # Rows include `disclosureDate` alongside the transaction `date`.
87
88
  fb.house_trades.ticker("AMZN",
88
89
  date_from="2025-01-01",
89
90
  date_to="2025-06-30",
@@ -155,12 +156,45 @@ fb.screener.insider_trading(limit=50)
155
156
  fb.screener.reddit_mentions(limit=100, as_dataframe=True)
156
157
  fb.screener.government_contracts(limit=100, as_dataframe=True)
157
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)
158
161
 
159
162
  # ---------- recent data ----------
160
163
  fb.recent.news(limit=100, as_dataframe=True)
161
164
  fb.recent.analyst_ratings(limit=50)
162
165
  ```
163
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
+
164
198
  ## ⚡ Async Usage
165
199
 
166
200
  For async/await support, install with the `async` extra:
@@ -58,6 +58,8 @@ src/finbrain_python.egg-info/PKG-INFO
58
58
  src/finbrain_python.egg-info/SOURCES.txt
59
59
  src/finbrain_python.egg-info/dependency_links.txt
60
60
  src/finbrain_python.egg-info/requires.txt
61
+ src/finbrain_python.egg-info/scm_file_list.json
62
+ src/finbrain_python.egg-info/scm_version.json
61
63
  src/finbrain_python.egg-info/top_level.txt
62
64
  tests/__init__.py
63
65
  tests/conftest.py
@@ -0,0 +1,85 @@
1
+ {
2
+ "files": [
3
+ "CHANGELOG.md",
4
+ ".gitattributes",
5
+ "pyproject.toml",
6
+ "LICENSE",
7
+ "README.md",
8
+ "RELEASE.md",
9
+ ".gitignore",
10
+ ".github/workflows/ci.yml",
11
+ ".github/workflows/release.yml",
12
+ "examples/transactions_plotting_example.py",
13
+ "examples/async_example.py",
14
+ "tests/test_available.py",
15
+ "tests/test_async_client.py",
16
+ "tests/test_insider_transactions.py",
17
+ "tests/test_reddit_mentions.py",
18
+ "tests/test_plotting.py",
19
+ "tests/test_house_trades.py",
20
+ "tests/test_sentiments.py",
21
+ "tests/test_corporate_lobbying.py",
22
+ "tests/test_options.py",
23
+ "tests/test_envelope.py",
24
+ "tests/__init__.py",
25
+ "tests/test_patent_filings.py",
26
+ "tests/test_integration.py",
27
+ "tests/test_linkedin_data.py",
28
+ "tests/test_client.py",
29
+ "tests/test_app_ratings.py",
30
+ "tests/test_recent.py",
31
+ "tests/test_screener.py",
32
+ "tests/test_news.py",
33
+ "tests/conftest.py",
34
+ "tests/test_utils.py",
35
+ "tests/test_analyst_ratings.py",
36
+ "tests/test_predictions.py",
37
+ "tests/test_senate_trades.py",
38
+ "tests/test_government_contracts.py",
39
+ "src/finbrain/py.typed",
40
+ "src/finbrain/__init__.py",
41
+ "src/finbrain/client.py",
42
+ "src/finbrain/exceptions.py",
43
+ "src/finbrain/plotting.py",
44
+ "src/finbrain/aio/__init__.py",
45
+ "src/finbrain/aio/client.py",
46
+ "src/finbrain/aio/endpoints/app_ratings.py",
47
+ "src/finbrain/aio/endpoints/available.py",
48
+ "src/finbrain/aio/endpoints/predictions.py",
49
+ "src/finbrain/aio/endpoints/corporate_lobbying.py",
50
+ "src/finbrain/aio/endpoints/reddit_mentions.py",
51
+ "src/finbrain/aio/endpoints/__init__.py",
52
+ "src/finbrain/aio/endpoints/patent_filings.py",
53
+ "src/finbrain/aio/endpoints/options.py",
54
+ "src/finbrain/aio/endpoints/linkedin_data.py",
55
+ "src/finbrain/aio/endpoints/_utils.py",
56
+ "src/finbrain/aio/endpoints/senate_trades.py",
57
+ "src/finbrain/aio/endpoints/insider_transactions.py",
58
+ "src/finbrain/aio/endpoints/analyst_ratings.py",
59
+ "src/finbrain/aio/endpoints/house_trades.py",
60
+ "src/finbrain/aio/endpoints/news.py",
61
+ "src/finbrain/aio/endpoints/sentiments.py",
62
+ "src/finbrain/aio/endpoints/recent.py",
63
+ "src/finbrain/aio/endpoints/screener.py",
64
+ "src/finbrain/aio/endpoints/government_contracts.py",
65
+ "src/finbrain/endpoints/app_ratings.py",
66
+ "src/finbrain/endpoints/available.py",
67
+ "src/finbrain/endpoints/predictions.py",
68
+ "src/finbrain/endpoints/corporate_lobbying.py",
69
+ "src/finbrain/endpoints/reddit_mentions.py",
70
+ "src/finbrain/endpoints/__init__.py",
71
+ "src/finbrain/endpoints/patent_filings.py",
72
+ "src/finbrain/endpoints/options.py",
73
+ "src/finbrain/endpoints/linkedin_data.py",
74
+ "src/finbrain/endpoints/_utils.py",
75
+ "src/finbrain/endpoints/senate_trades.py",
76
+ "src/finbrain/endpoints/insider_transactions.py",
77
+ "src/finbrain/endpoints/analyst_ratings.py",
78
+ "src/finbrain/endpoints/house_trades.py",
79
+ "src/finbrain/endpoints/news.py",
80
+ "src/finbrain/endpoints/sentiments.py",
81
+ "src/finbrain/endpoints/recent.py",
82
+ "src/finbrain/endpoints/screener.py",
83
+ "src/finbrain/endpoints/government_contracts.py"
84
+ ]
85
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.2.7",
3
+ "distance": 0,
4
+ "node": "g417d20b49dbd91b3e9804d09e94527a560dce33e",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-24"
8
+ }
@@ -21,6 +21,7 @@ def test_house_trades_raw_ok(client, _activate_responses):
21
21
  "politician": "Nancy Pelosi",
22
22
  "transactionType": "Purchase",
23
23
  "amount": "$15,001 - $50,000",
24
+ "disclosureDate": "2024-02-05",
24
25
  }
25
26
  ],
26
27
  })
@@ -31,6 +32,7 @@ def test_house_trades_raw_ok(client, _activate_responses):
31
32
  assert data["symbol"] == TICKER
32
33
  assert isinstance(data["trades"], list)
33
34
  assert data["trades"][0]["politician"] == "Nancy Pelosi"
35
+ assert data["trades"][0]["disclosureDate"] == "2024-02-05"
34
36
 
35
37
 
36
38
  # ─────────── DataFrame branch ───────────────────────────────────────────
@@ -46,12 +48,14 @@ def test_house_trades_dataframe_ok(client, _activate_responses):
46
48
  "politician": "Nancy Pelosi",
47
49
  "transactionType": "Purchase",
48
50
  "amount": "$15,001 - $50,000",
51
+ "disclosureDate": "2024-02-05",
49
52
  },
50
53
  {
51
54
  "date": "2024-01-10",
52
55
  "politician": "Pete Sessions",
53
56
  "transactionType": "Sale",
54
57
  "amount": "$1,001 - $15,000",
58
+ "disclosureDate": None,
55
59
  },
56
60
  ],
57
61
  })
@@ -62,7 +66,12 @@ def test_house_trades_dataframe_ok(client, _activate_responses):
62
66
 
63
67
  assert isinstance(df, pd.DataFrame)
64
68
  assert len(df) == 2
65
- assert set(df.columns) == {"politician", "transactionType", "amount"}
69
+ assert set(df.columns) == {
70
+ "politician",
71
+ "transactionType",
72
+ "amount",
73
+ "disclosureDate",
74
+ }
66
75
  assert df.index.name == "date"
67
76
  assert pd.api.types.is_datetime64_any_dtype(df.index)
68
77
  assert pd.Timestamp("2024-01-15") in df.index
@@ -70,6 +79,10 @@ def test_house_trades_dataframe_ok(client, _activate_responses):
70
79
  assert df.loc["2024-01-10", "politician"] == "Pete Sessions"
71
80
  assert df.loc["2024-01-15", "transactionType"] == "Purchase"
72
81
  assert df.loc["2024-01-15", "amount"] == "$15,001 - $50,000"
82
+ assert df.loc["2024-01-15", "disclosureDate"] == "2024-02-05"
83
+ # Historical rows predate the disclosure-date field; JSON null becomes
84
+ # NaN once pandas builds the column.
85
+ assert pd.isna(df.loc["2024-01-10", "disclosureDate"])
73
86
 
74
87
 
75
88
  # ─────────── error mapping ──────────────────────────────────────────────
@@ -230,6 +230,9 @@ class TestHouseTrades:
230
230
  assert "date" in trades[0]
231
231
  assert "politician" in trades[0]
232
232
  assert "transactionType" in trades[0]
233
+ # Present on every row; null on rows collected before the
234
+ # disclosure-date field was added to the pipeline.
235
+ assert "disclosureDate" in trades[0]
233
236
 
234
237
  def test_dataframe(self, fb):
235
238
  df = fb.house_trades.ticker(TICKER, as_dataframe=True)
@@ -237,6 +240,9 @@ class TestHouseTrades:
237
240
  if len(df) > 0:
238
241
  assert df.index.name == "date"
239
242
  assert "politician" in df.columns
243
+ # Survives the DataFrame conversion as a column (values may be
244
+ # null on rows predating the upstream field).
245
+ assert "disclosureDate" in df.columns
240
246
 
241
247
 
242
248
  # =====================================================================
@@ -252,6 +258,9 @@ class TestSenateTrades:
252
258
  if len(trades) > 0:
253
259
  assert "date" in trades[0]
254
260
  assert "politician" in trades[0]
261
+ # Present on every row; null on rows collected before the
262
+ # disclosure-date field was added to the pipeline.
263
+ assert "disclosureDate" in trades[0]
255
264
 
256
265
  def test_dataframe(self, fb):
257
266
  df = fb.senate_trades.ticker(TICKER, as_dataframe=True)
@@ -259,6 +268,9 @@ class TestSenateTrades:
259
268
  if len(df) > 0:
260
269
  assert df.index.name == "date"
261
270
  assert "politician" in df.columns
271
+ # Survives the DataFrame conversion as a column (values may be
272
+ # null on rows predating the upstream field).
273
+ assert "disclosureDate" in df.columns
262
274
 
263
275
 
264
276
  # =====================================================================
@@ -503,10 +515,14 @@ class TestScreener:
503
515
  def test_congress_house(self, fb):
504
516
  data = fb.screener.congress_house(limit=5)
505
517
  assert isinstance(data, list)
518
+ if data:
519
+ assert "disclosureDate" in data[0]
506
520
 
507
521
  def test_congress_senate(self, fb):
508
522
  data = fb.screener.congress_senate(limit=5)
509
523
  assert isinstance(data, list)
524
+ if data:
525
+ assert "disclosureDate" in data[0]
510
526
 
511
527
  def test_news(self, fb):
512
528
  data = fb.screener.news(limit=5)
@@ -95,53 +95,93 @@ def test_screener_insider_trading_dataframe(client, _activate_responses):
95
95
  # ── congress house ─────────────────────────────────────────────────────
96
96
  def test_screener_congress_house(client, _activate_responses):
97
97
  payload = wrap_v2([
98
- {"symbol": "AAPL", "name": "Apple", "politician": "Nancy Pelosi"},
98
+ {
99
+ "symbol": "AAPL",
100
+ "name": "Apple",
101
+ "politician": "Nancy Pelosi",
102
+ "disclosureDate": "2026-02-05",
103
+ },
99
104
  ])
100
105
  stub_json(_activate_responses, "GET", "screener/congress/house", payload, params={"limit": "5"})
101
106
  data = client.screener.congress_house(limit=5)
102
107
  assert isinstance(data, list)
103
108
  assert len(data) == 1
104
109
  assert data[0]["politician"] == "Nancy Pelosi"
110
+ assert data[0]["disclosureDate"] == "2026-02-05"
105
111
 
106
112
 
107
113
  def test_screener_congress_house_dataframe(client, _activate_responses):
108
114
  payload = wrap_v2([
109
- {"symbol": "AAPL", "name": "Apple", "politician": "Nancy Pelosi"},
110
- {"symbol": "MSFT", "name": "Microsoft", "politician": "Dan Crenshaw"},
115
+ {
116
+ "symbol": "AAPL",
117
+ "name": "Apple",
118
+ "politician": "Nancy Pelosi",
119
+ "disclosureDate": "2026-02-05",
120
+ },
121
+ {
122
+ "symbol": "MSFT",
123
+ "name": "Microsoft",
124
+ "politician": "Dan Crenshaw",
125
+ "disclosureDate": None,
126
+ },
111
127
  ])
112
128
  stub_json(_activate_responses, "GET", "screener/congress/house", payload, params={"limit": "5"})
113
129
  df = client.screener.congress_house(limit=5, as_dataframe=True)
114
130
  assert isinstance(df, pd.DataFrame)
115
131
  assert len(df) == 2
116
132
  assert df.index.name == "symbol"
117
- assert set(df.columns) == {"name", "politician"}
133
+ assert set(df.columns) == {"name", "politician", "disclosureDate"}
118
134
  assert df.loc["AAPL", "politician"] == "Nancy Pelosi"
135
+ assert df.loc["AAPL", "disclosureDate"] == "2026-02-05"
136
+ # Historical rows predate the disclosure-date field; JSON null becomes
137
+ # NaN once pandas builds the column.
138
+ assert pd.isna(df.loc["MSFT", "disclosureDate"])
119
139
 
120
140
 
121
141
  # ── congress senate ────────────────────────────────────────────────────
122
142
  def test_screener_congress_senate(client, _activate_responses):
123
143
  payload = wrap_v2([
124
- {"symbol": "AAPL", "name": "Apple", "politician": "John Boozman"},
144
+ {
145
+ "symbol": "AAPL",
146
+ "name": "Apple",
147
+ "politician": "John Boozman",
148
+ "disclosureDate": "2026-01-22",
149
+ },
125
150
  ])
126
151
  stub_json(_activate_responses, "GET", "screener/congress/senate", payload, params={"limit": "5"})
127
152
  data = client.screener.congress_senate(limit=5)
128
153
  assert isinstance(data, list)
129
154
  assert len(data) == 1
130
155
  assert data[0]["politician"] == "John Boozman"
156
+ assert data[0]["disclosureDate"] == "2026-01-22"
131
157
 
132
158
 
133
159
  def test_screener_congress_senate_dataframe(client, _activate_responses):
134
160
  payload = wrap_v2([
135
- {"symbol": "AAPL", "name": "Apple", "politician": "John Boozman"},
136
- {"symbol": "NVDA", "name": "NVIDIA", "politician": "Tommy Tuberville"},
161
+ {
162
+ "symbol": "AAPL",
163
+ "name": "Apple",
164
+ "politician": "John Boozman",
165
+ "disclosureDate": "2026-01-22",
166
+ },
167
+ {
168
+ "symbol": "NVDA",
169
+ "name": "NVIDIA",
170
+ "politician": "Tommy Tuberville",
171
+ "disclosureDate": None,
172
+ },
137
173
  ])
138
174
  stub_json(_activate_responses, "GET", "screener/congress/senate", payload, params={"limit": "5"})
139
175
  df = client.screener.congress_senate(limit=5, as_dataframe=True)
140
176
  assert isinstance(df, pd.DataFrame)
141
177
  assert len(df) == 2
142
178
  assert df.index.name == "symbol"
143
- assert set(df.columns) == {"name", "politician"}
179
+ assert set(df.columns) == {"name", "politician", "disclosureDate"}
144
180
  assert df.loc["NVDA", "politician"] == "Tommy Tuberville"
181
+ assert df.loc["AAPL", "disclosureDate"] == "2026-01-22"
182
+ # Historical rows predate the disclosure-date field; JSON null becomes
183
+ # NaN once pandas builds the column.
184
+ assert pd.isna(df.loc["NVDA", "disclosureDate"])
145
185
 
146
186
 
147
187
  # ── news ───────────────────────────────────────────────────────────────
@@ -21,6 +21,7 @@ def test_senate_trades_raw_ok(client, _activate_responses):
21
21
  "politician": "Shelley Moore Capito",
22
22
  "transactionType": "Purchase",
23
23
  "amount": "$1,001 - $15,000",
24
+ "disclosureDate": "2024-01-30",
24
25
  }
25
26
  ],
26
27
  })
@@ -31,6 +32,7 @@ def test_senate_trades_raw_ok(client, _activate_responses):
31
32
  assert data["symbol"] == TICKER
32
33
  assert isinstance(data["trades"], list)
33
34
  assert data["trades"][0]["politician"] == "Shelley Moore Capito"
35
+ assert data["trades"][0]["disclosureDate"] == "2024-01-30"
34
36
 
35
37
 
36
38
  # ─────────── DataFrame branch ───────────────────────────────────────────
@@ -46,12 +48,14 @@ def test_senate_trades_dataframe_ok(client, _activate_responses):
46
48
  "politician": "Shelley Moore Capito",
47
49
  "transactionType": "Purchase",
48
50
  "amount": "$1,001 - $15,000",
51
+ "disclosureDate": "2024-01-30",
49
52
  },
50
53
  {
51
54
  "date": "2024-01-08",
52
55
  "politician": "John Boozman",
53
56
  "transactionType": "Purchase",
54
57
  "amount": "$1,001 - $15,000",
58
+ "disclosureDate": None,
55
59
  },
56
60
  ],
57
61
  })
@@ -62,7 +66,12 @@ def test_senate_trades_dataframe_ok(client, _activate_responses):
62
66
 
63
67
  assert isinstance(df, pd.DataFrame)
64
68
  assert len(df) == 2
65
- assert set(df.columns) == {"politician", "transactionType", "amount"}
69
+ assert set(df.columns) == {
70
+ "politician",
71
+ "transactionType",
72
+ "amount",
73
+ "disclosureDate",
74
+ }
66
75
  assert df.index.name == "date"
67
76
  assert pd.api.types.is_datetime64_any_dtype(df.index)
68
77
  assert pd.Timestamp("2024-01-15") in df.index
@@ -70,6 +79,10 @@ def test_senate_trades_dataframe_ok(client, _activate_responses):
70
79
  assert df.loc["2024-01-08", "politician"] == "John Boozman"
71
80
  assert df.loc["2024-01-15", "transactionType"] == "Purchase"
72
81
  assert df.loc["2024-01-08", "amount"] == "$1,001 - $15,000"
82
+ assert df.loc["2024-01-15", "disclosureDate"] == "2024-01-30"
83
+ # Historical rows predate the disclosure-date field; JSON null becomes
84
+ # NaN once pandas builds the column.
85
+ assert pd.isna(df.loc["2024-01-08", "disclosureDate"])
73
86
 
74
87
 
75
88
  # ─────────── error mapping ──────────────────────────────────────────────
File without changes