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.
- {form4api-0.7.1 → form4api-0.9.0}/PKG-INFO +47 -2
- {form4api-0.7.1 → form4api-0.9.0}/README.md +46 -1
- {form4api-0.7.1 → form4api-0.9.0}/form4api/__init__.py +10 -0
- {form4api-0.7.1 → form4api-0.9.0}/form4api/_client.py +25 -0
- {form4api-0.7.1 → form4api-0.9.0}/form4api/_generated.py +188 -38
- form4api-0.9.0/form4api/_types.py +200 -0
- {form4api-0.7.1 → form4api-0.9.0}/form4api/resources/_insiders.py +2 -2
- {form4api-0.7.1 → form4api-0.9.0}/form4api/resources/_transactions.py +37 -8
- {form4api-0.7.1 → form4api-0.9.0}/form4api.egg-info/PKG-INFO +47 -2
- {form4api-0.7.1 → form4api-0.9.0}/pyproject.toml +1 -1
- {form4api-0.7.1 → form4api-0.9.0}/tests/test_client.py +187 -0
- {form4api-0.7.1 → form4api-0.9.0}/tests/test_generated.py +48 -1
- form4api-0.7.1/form4api/_types.py +0 -98
- {form4api-0.7.1 → form4api-0.9.0}/LICENSE +0 -0
- {form4api-0.7.1 → form4api-0.9.0}/form4api/_errors.py +0 -0
- {form4api-0.7.1 → form4api-0.9.0}/form4api/_webhook_utils.py +0 -0
- {form4api-0.7.1 → form4api-0.9.0}/form4api/resources/__init__.py +0 -0
- {form4api-0.7.1 → form4api-0.9.0}/form4api/resources/_companies.py +0 -0
- {form4api-0.7.1 → form4api-0.9.0}/form4api/resources/_signals.py +0 -0
- {form4api-0.7.1 → form4api-0.9.0}/form4api/resources/_webhooks.py +0 -0
- {form4api-0.7.1 → form4api-0.9.0}/form4api.egg-info/SOURCES.txt +0 -0
- {form4api-0.7.1 → form4api-0.9.0}/form4api.egg-info/dependency_links.txt +0 -0
- {form4api-0.7.1 → form4api-0.9.0}/form4api.egg-info/requires.txt +0 -0
- {form4api-0.7.1 → form4api-0.9.0}/form4api.egg-info/top_level.txt +0 -0
- {form4api-0.7.1 → form4api-0.9.0}/setup.cfg +0 -0
- {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.
|
|
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
|