form4api 0.7.1__tar.gz → 0.9.0__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 (26) hide show
  1. {form4api-0.7.1 → form4api-0.9.0}/PKG-INFO +47 -2
  2. {form4api-0.7.1 → form4api-0.9.0}/README.md +46 -1
  3. {form4api-0.7.1 → form4api-0.9.0}/form4api/__init__.py +10 -0
  4. {form4api-0.7.1 → form4api-0.9.0}/form4api/_client.py +25 -0
  5. {form4api-0.7.1 → form4api-0.9.0}/form4api/_generated.py +188 -38
  6. form4api-0.9.0/form4api/_types.py +200 -0
  7. {form4api-0.7.1 → form4api-0.9.0}/form4api/resources/_insiders.py +2 -2
  8. {form4api-0.7.1 → form4api-0.9.0}/form4api/resources/_transactions.py +37 -8
  9. {form4api-0.7.1 → form4api-0.9.0}/form4api.egg-info/PKG-INFO +47 -2
  10. {form4api-0.7.1 → form4api-0.9.0}/pyproject.toml +1 -1
  11. {form4api-0.7.1 → form4api-0.9.0}/tests/test_client.py +187 -0
  12. {form4api-0.7.1 → form4api-0.9.0}/tests/test_generated.py +48 -1
  13. form4api-0.7.1/form4api/_types.py +0 -98
  14. {form4api-0.7.1 → form4api-0.9.0}/LICENSE +0 -0
  15. {form4api-0.7.1 → form4api-0.9.0}/form4api/_errors.py +0 -0
  16. {form4api-0.7.1 → form4api-0.9.0}/form4api/_webhook_utils.py +0 -0
  17. {form4api-0.7.1 → form4api-0.9.0}/form4api/resources/__init__.py +0 -0
  18. {form4api-0.7.1 → form4api-0.9.0}/form4api/resources/_companies.py +0 -0
  19. {form4api-0.7.1 → form4api-0.9.0}/form4api/resources/_signals.py +0 -0
  20. {form4api-0.7.1 → form4api-0.9.0}/form4api/resources/_webhooks.py +0 -0
  21. {form4api-0.7.1 → form4api-0.9.0}/form4api.egg-info/SOURCES.txt +0 -0
  22. {form4api-0.7.1 → form4api-0.9.0}/form4api.egg-info/dependency_links.txt +0 -0
  23. {form4api-0.7.1 → form4api-0.9.0}/form4api.egg-info/requires.txt +0 -0
  24. {form4api-0.7.1 → form4api-0.9.0}/form4api.egg-info/top_level.txt +0 -0
  25. {form4api-0.7.1 → form4api-0.9.0}/setup.cfg +0 -0
  26. {form4api-0.7.1 → form4api-0.9.0}/tests/test_method_name.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: form4api
3
- Version: 0.7.1
3
+ Version: 0.9.0
4
4
  Summary: Python client for the Form4API — real-time SEC Form 4 insider trading, Form 144 intent-to-sell, institutional 13F-HR, and congressional STOCK Act trading data, including the insider/Congress convergence signal
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://www.form4api.com
@@ -81,6 +81,7 @@ asyncio.run(main())
81
81
 
82
82
  | Resource | Methods |
83
83
  |---|---|
84
+ | `client.search(q, limit=None)` | Combined company + insider lookup — not namespaced under a resource |
84
85
  | `client.transactions` | `.list(**params)`, `.paginate(**params)` |
85
86
  | `client.insiders` | `.search(name, **params)`, `.get(cik)`, `.list(**params)`, `.directory(**params)`, `.transactions(cik, **params)`, `.summary(cik)` *(Pro)*, `.scorecard(cik)` *(Pro)*, `.leaderboard(**params)` *(Business)* |
86
87
  | `client.companies` | `.get(ticker)`, `.insiders(ticker)`, `.list(**params)` |
@@ -94,6 +95,19 @@ asyncio.run(main())
94
95
  | `client.status` | `.history(**params)` |
95
96
  | `client.webhooks` | `.create(url, event_types)`, `.list()`, `.delete(id)`, `.events(**params)` |
96
97
 
98
+ **Congress date quality.** Each congress trade (and convergence leg) has
99
+ `disclosure_lag_days: int | None` and `date_quality: str | None`. When the
100
+ filing's own dates are impossible or implausible, the lag is `None` and
101
+ `date_quality` is one of `"transaction_after_disclosure"`,
102
+ `"future_transaction_date"` or `"implausible_lag"`; the raw dates are still
103
+ returned. A lag over 45 days is not a legal finding.
104
+
105
+ ```python
106
+ for t in client.congress.trades(ticker="NVDA"):
107
+ lag = f"dates unreliable ({t.date_quality})" if t.disclosure_lag_days is None else f"{t.disclosure_lag_days}d"
108
+ print(t.politician.full_name, t.transaction_date, lag)
109
+ ```
110
+
97
111
  Every plan-gated endpoint the API exposes has a typed method here. Calling one
98
112
  your key isn't entitled to raises `PlanError` (HTTP 402) carrying
99
113
  `required_plan`, `current_plan`, and `upgrade_url` rather than failing opaquely.
@@ -108,16 +122,21 @@ For LLM workflows, `form4api-mcp` exposes the same endpoints as tools.
108
122
 
109
123
  ```python
110
124
  client.transactions.list(
111
- ticker="AAPL", # filter by ticker
125
+ ticker="AAPL", # filter by ticker — accepts up to 25 comma-separated symbols
112
126
  cik="0000320193", # or by company CIK
113
127
  insider_cik="...", # filter by insider CIK
114
128
  code="P", # transaction code: P=purchase, S=sale, A=grant, etc.
115
129
  from_date="2026-01-01",
116
130
  to_date="2026-12-31",
131
+ filed_from="2026-01-01", # filters on Filing.FiledAt (when it hit EDGAR), not the trade date
132
+ filed_to="2026-12-31",
117
133
  exclude_10b5=True, # omit trades filed under a Rule 10b5-1 plan
118
134
  per_page=100,
119
135
  page=1,
120
136
  )
137
+
138
+ # Multi-ticker lookup in one call
139
+ client.transactions.list(ticker="AAPL,MSFT,NVDA")
121
140
  ```
122
141
 
123
142
  ### Granular filtering (v0.4.0+)
@@ -166,6 +185,11 @@ client.transactions.list(min_shares=10_000, max_shares=100_000)
166
185
  | `is_derivative` | `bool` | Derivative security flag |
167
186
  | `transaction_date` | `str` | ISO datetime |
168
187
  | `period_of_report` | `str` | ISO datetime |
188
+ | `return1d`/`return1w`/`return1m`/`return3m`/`return6m` | `float \| None` | Forward return since the transaction date, as a fraction (0.109 = +10.9%); `None` until enough time/price data has elapsed |
189
+ | `value_quality` | `str \| None` | Confidence flag on `total_value`'s pricing source |
190
+ | `accepted_at` | `str \| None` | Precise UTC SEC-acceptance instant; `None` if not captured |
191
+ | `document_url` | `str \| None` | Public SEC document URL |
192
+ | `institutional_ownership` | `InstitutionalOwnership \| None` | Latest 13F institutional ownership snapshot for the ticker, when available |
169
193
 
170
194
  ### Company fields
171
195
 
@@ -181,6 +205,27 @@ client.transactions.list(min_shares=10_000, max_shares=100_000)
181
205
  | `state_of_incorporation` | `str \| None` | Two-letter state code |
182
206
  | `website` | `str \| None` | Company website as filed with SEC |
183
207
 
208
+ ### Search
209
+
210
+ `client.search(q, limit=None)` looks up companies and insiders by name or
211
+ ticker in a single call — useful for a combined search box where you don't
212
+ know in advance whether the user typed a ticker or a person's name. `q` must
213
+ be 2-64 characters; outside that range the API returns HTTP 400 and the SDK
214
+ raises `Form4ApiError` with `error_code` `"QUERY_TOO_SHORT"` or
215
+ `"QUERY_TOO_LONG"`. Insider matching is whitespace-tokenised, so `"tim cook"`
216
+ matches an insider named `"Cook Timothy D"`. `limit` caps the number of
217
+ results per array (1-20, API default 8).
218
+
219
+ ```python
220
+ result = client.search("tim cook")
221
+ for c in result.companies:
222
+ print(c.ticker, c.name, c.cik)
223
+ for i in result.insiders:
224
+ # i.ticker is a ticker associated with the insider and may be None —
225
+ # its exact meaning is still settling on the backend.
226
+ print(i.cik, i.name, i.title, i.ticker)
227
+ ```
228
+
184
229
  ### Pagination
185
230
 
186
231
  `paginate()` pages through the data until it runs out (a short or empty page)
@@ -62,6 +62,7 @@ asyncio.run(main())
62
62
 
63
63
  | Resource | Methods |
64
64
  |---|---|
65
+ | `client.search(q, limit=None)` | Combined company + insider lookup — not namespaced under a resource |
65
66
  | `client.transactions` | `.list(**params)`, `.paginate(**params)` |
66
67
  | `client.insiders` | `.search(name, **params)`, `.get(cik)`, `.list(**params)`, `.directory(**params)`, `.transactions(cik, **params)`, `.summary(cik)` *(Pro)*, `.scorecard(cik)` *(Pro)*, `.leaderboard(**params)` *(Business)* |
67
68
  | `client.companies` | `.get(ticker)`, `.insiders(ticker)`, `.list(**params)` |
@@ -75,6 +76,19 @@ asyncio.run(main())
75
76
  | `client.status` | `.history(**params)` |
76
77
  | `client.webhooks` | `.create(url, event_types)`, `.list()`, `.delete(id)`, `.events(**params)` |
77
78
 
79
+ **Congress date quality.** Each congress trade (and convergence leg) has
80
+ `disclosure_lag_days: int | None` and `date_quality: str | None`. When the
81
+ filing's own dates are impossible or implausible, the lag is `None` and
82
+ `date_quality` is one of `"transaction_after_disclosure"`,
83
+ `"future_transaction_date"` or `"implausible_lag"`; the raw dates are still
84
+ returned. A lag over 45 days is not a legal finding.
85
+
86
+ ```python
87
+ for t in client.congress.trades(ticker="NVDA"):
88
+ lag = f"dates unreliable ({t.date_quality})" if t.disclosure_lag_days is None else f"{t.disclosure_lag_days}d"
89
+ print(t.politician.full_name, t.transaction_date, lag)
90
+ ```
91
+
78
92
  Every plan-gated endpoint the API exposes has a typed method here. Calling one
79
93
  your key isn't entitled to raises `PlanError` (HTTP 402) carrying
80
94
  `required_plan`, `current_plan`, and `upgrade_url` rather than failing opaquely.
@@ -89,16 +103,21 @@ For LLM workflows, `form4api-mcp` exposes the same endpoints as tools.
89
103
 
90
104
  ```python
91
105
  client.transactions.list(
92
- ticker="AAPL", # filter by ticker
106
+ ticker="AAPL", # filter by ticker — accepts up to 25 comma-separated symbols
93
107
  cik="0000320193", # or by company CIK
94
108
  insider_cik="...", # filter by insider CIK
95
109
  code="P", # transaction code: P=purchase, S=sale, A=grant, etc.
96
110
  from_date="2026-01-01",
97
111
  to_date="2026-12-31",
112
+ filed_from="2026-01-01", # filters on Filing.FiledAt (when it hit EDGAR), not the trade date
113
+ filed_to="2026-12-31",
98
114
  exclude_10b5=True, # omit trades filed under a Rule 10b5-1 plan
99
115
  per_page=100,
100
116
  page=1,
101
117
  )
118
+
119
+ # Multi-ticker lookup in one call
120
+ client.transactions.list(ticker="AAPL,MSFT,NVDA")
102
121
  ```
103
122
 
104
123
  ### Granular filtering (v0.4.0+)
@@ -147,6 +166,11 @@ client.transactions.list(min_shares=10_000, max_shares=100_000)
147
166
  | `is_derivative` | `bool` | Derivative security flag |
148
167
  | `transaction_date` | `str` | ISO datetime |
149
168
  | `period_of_report` | `str` | ISO datetime |
169
+ | `return1d`/`return1w`/`return1m`/`return3m`/`return6m` | `float \| None` | Forward return since the transaction date, as a fraction (0.109 = +10.9%); `None` until enough time/price data has elapsed |
170
+ | `value_quality` | `str \| None` | Confidence flag on `total_value`'s pricing source |
171
+ | `accepted_at` | `str \| None` | Precise UTC SEC-acceptance instant; `None` if not captured |
172
+ | `document_url` | `str \| None` | Public SEC document URL |
173
+ | `institutional_ownership` | `InstitutionalOwnership \| None` | Latest 13F institutional ownership snapshot for the ticker, when available |
150
174
 
151
175
  ### Company fields
152
176
 
@@ -162,6 +186,27 @@ client.transactions.list(min_shares=10_000, max_shares=100_000)
162
186
  | `state_of_incorporation` | `str \| None` | Two-letter state code |
163
187
  | `website` | `str \| None` | Company website as filed with SEC |
164
188
 
189
+ ### Search
190
+
191
+ `client.search(q, limit=None)` looks up companies and insiders by name or
192
+ ticker in a single call — useful for a combined search box where you don't
193
+ know in advance whether the user typed a ticker or a person's name. `q` must
194
+ be 2-64 characters; outside that range the API returns HTTP 400 and the SDK
195
+ raises `Form4ApiError` with `error_code` `"QUERY_TOO_SHORT"` or
196
+ `"QUERY_TOO_LONG"`. Insider matching is whitespace-tokenised, so `"tim cook"`
197
+ matches an insider named `"Cook Timothy D"`. `limit` caps the number of
198
+ results per array (1-20, API default 8).
199
+
200
+ ```python
201
+ result = client.search("tim cook")
202
+ for c in result.companies:
203
+ print(c.ticker, c.name, c.cik)
204
+ for i in result.insiders:
205
+ # i.ticker is a ticker associated with the insider and may be None —
206
+ # its exact meaning is still settling on the backend.
207
+ print(i.cik, i.name, i.title, i.ticker)
208
+ ```
209
+
165
210
  ### Pagination
166
211
 
167
212
  `paginate()` pages through the data until it runs out (a short or empty page)
@@ -11,6 +11,11 @@ from form4api._types import (
11
11
  Company,
12
12
  Insider,
13
13
  InsiderSignal,
14
+ InstitutionalOwnership,
15
+ SearchCompany,
16
+ SearchInsider,
17
+ SearchResults,
18
+ TopHolder,
14
19
  Transaction,
15
20
  WebhookCreated,
16
21
  WebhookEvent,
@@ -31,6 +36,11 @@ __all__ = [
31
36
  "Insider",
32
37
  "Company",
33
38
  "InsiderSignal",
39
+ "InstitutionalOwnership",
40
+ "SearchCompany",
41
+ "SearchInsider",
42
+ "SearchResults",
43
+ "TopHolder",
34
44
  "WebhookCreated",
35
45
  "WebhookEvent",
36
46
  "WebhookSubscription",
@@ -9,6 +9,7 @@ from typing import Any, TypeVar
9
9
  import httpx
10
10
 
11
11
  from form4api._errors import AuthError, Form4ApiError, NotFoundError, PlanError, RateLimitError
12
+ from form4api._types import SearchResults
12
13
 
13
14
  # Sent as the User-Agent so the backend can attribute traffic to the Python SDK
14
15
  # channel (the admin dashboard buckets by client). Read from installed package
@@ -105,6 +106,22 @@ class Form4ApiClient:
105
106
  def close(self) -> None:
106
107
  self._http.close()
107
108
 
109
+ def search(self, q: str, limit: int | None = None) -> SearchResults:
110
+ """Search companies and insiders by name or ticker in one call.
111
+
112
+ ``q`` must be 2-64 characters, or the API raises `Form4ApiError`
113
+ (HTTP 400) with `error_code` `QUERY_TOO_SHORT` or `QUERY_TOO_LONG`.
114
+ Insider matching is whitespace-tokenised, so `"tim cook"` matches an
115
+ insider named `"Cook Timothy D"`. `limit` caps the number of results
116
+ per array (1-20, API default 8) and defaults to the API's own default
117
+ when omitted. Free tier, no plan gate.
118
+ """
119
+ params: dict[str, str] = {"q": q}
120
+ if limit is not None:
121
+ params["limit"] = str(limit)
122
+ data = self._get("/v1/search", params)
123
+ return SearchResults._from_dict(data)
124
+
108
125
  def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
109
126
  url = self._base_url + path
110
127
  last_exc: Exception | None = None
@@ -224,6 +241,14 @@ class AsyncForm4ApiClient:
224
241
  async def close(self) -> None:
225
242
  await self._http.aclose()
226
243
 
244
+ async def search(self, q: str, limit: int | None = None) -> SearchResults:
245
+ """Async twin of `Form4ApiClient.search`. See there for parameter details."""
246
+ params: dict[str, str] = {"q": q}
247
+ if limit is not None:
248
+ params["limit"] = str(limit)
249
+ data = await self._get("/v1/search", params)
250
+ return SearchResults._from_dict(data)
251
+
227
252
  async def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
228
253
  url = self._base_url + path
229
254
  last_exc: Exception | None = None