form4api 0.4.3__tar.gz → 0.6.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 (30) hide show
  1. {form4api-0.4.3 → form4api-0.6.0}/PKG-INFO +19 -14
  2. {form4api-0.4.3 → form4api-0.6.0}/README.md +18 -13
  3. {form4api-0.4.3 → form4api-0.6.0}/form4api/_client.py +78 -13
  4. {form4api-0.4.3 → form4api-0.6.0}/form4api/_errors.py +47 -30
  5. form4api-0.6.0/form4api/_generated.py +2015 -0
  6. form4api-0.6.0/form4api/resources/_companies.py +51 -0
  7. form4api-0.6.0/form4api/resources/_insiders.py +90 -0
  8. form4api-0.6.0/form4api/resources/_signals.py +117 -0
  9. form4api-0.6.0/form4api/resources/_transactions.py +225 -0
  10. form4api-0.6.0/form4api/resources/_webhooks.py +56 -0
  11. {form4api-0.4.3 → form4api-0.6.0}/form4api.egg-info/PKG-INFO +19 -14
  12. {form4api-0.4.3 → form4api-0.6.0}/form4api.egg-info/SOURCES.txt +4 -1
  13. {form4api-0.4.3 → form4api-0.6.0}/pyproject.toml +1 -1
  14. {form4api-0.4.3 → form4api-0.6.0}/tests/test_client.py +33 -1
  15. form4api-0.6.0/tests/test_generated.py +433 -0
  16. form4api-0.6.0/tests/test_method_name.py +88 -0
  17. form4api-0.4.3/form4api/resources/_companies.py +0 -24
  18. form4api-0.4.3/form4api/resources/_insiders.py +0 -48
  19. form4api-0.4.3/form4api/resources/_signals.py +0 -57
  20. form4api-0.4.3/form4api/resources/_transactions.py +0 -120
  21. form4api-0.4.3/form4api/resources/_webhooks.py +0 -34
  22. {form4api-0.4.3 → form4api-0.6.0}/LICENSE +0 -0
  23. {form4api-0.4.3 → form4api-0.6.0}/form4api/__init__.py +0 -0
  24. {form4api-0.4.3 → form4api-0.6.0}/form4api/_types.py +0 -0
  25. {form4api-0.4.3 → form4api-0.6.0}/form4api/_webhook_utils.py +0 -0
  26. {form4api-0.4.3 → form4api-0.6.0}/form4api/resources/__init__.py +0 -0
  27. {form4api-0.4.3 → form4api-0.6.0}/form4api.egg-info/dependency_links.txt +0 -0
  28. {form4api-0.4.3 → form4api-0.6.0}/form4api.egg-info/requires.txt +0 -0
  29. {form4api-0.4.3 → form4api-0.6.0}/form4api.egg-info/top_level.txt +0 -0
  30. {form4api-0.4.3 → form4api-0.6.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: form4api
3
- Version: 0.4.3
3
+ Version: 0.6.0
4
4
  Summary: Python client for the Form4API — real-time SEC Form 4 insider trading data
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://www.form4api.com
@@ -82,22 +82,27 @@ asyncio.run(main())
82
82
  | Resource | Methods |
83
83
  |---|---|
84
84
  | `client.transactions` | `.list(**params)`, `.paginate(**params)` |
85
- | `client.insiders` | `.search(name, **params)`, `.get(cik)`, `.transactions(cik, **params)` |
86
- | `client.companies` | `.get(ticker)`, `.insiders(ticker)` |
87
- | `client.signals` | `.list(**params)`, `.paginate(**params)` — Business plan |
85
+ | `client.insiders` | `.search(name, **params)`, `.get(cik)`, `.list(**params)`, `.directory(**params)`, `.transactions(cik, **params)`, `.summary(cik)` *(Pro)*, `.scorecard(cik)` *(Pro)*, `.leaderboard(**params)` *(Business)* |
86
+ | `client.companies` | `.get(ticker)`, `.insiders(ticker)`, `.list(**params)` |
87
+ | `client.signals` | `.list(**params)`, `.paginate(**params)`, `.explain(ticker)`, `.sentiment(ticker, **params)` — Business; `.convergence(**params)` — Pro |
88
+ | `client.congress` | `.trades(**params)`, `.politicians(**params)` *(Pro)*, `.politician(id_or_slug)` *(Pro)*, `.ticker(ticker)` *(Pro)* |
89
+ | `client.form144` | `.list(**params)` — Business plan |
90
+ | `client.holdings` | `.list(**params)`, `.managers(**params)` — Business plan |
91
+ | `client.filings` | `.list(**params)`, `.recent(**params)`, `.get(accession_number)` |
92
+ | `client.stats` | `.get()` — public, no key required |
93
+ | `client.data_quality` | `.get()` — public, no key required |
94
+ | `client.status` | `.history(**params)` |
88
95
  | `client.webhooks` | `.create(url, event_types)`, `.list()`, `.delete(id)`, `.events(**params)` |
89
96
 
90
- ### Not yet in this SDK
97
+ Every plan-gated endpoint the API exposes has a typed method here. Calling one
98
+ your key isn't entitled to raises `PlanError` (HTTP 402) carrying
99
+ `required_plan`, `current_plan`, and `upgrade_url` rather than failing opaquely.
91
100
 
92
- The API surface is broader than the typed client. These backend features are **available via the REST API and the `form4api-mcp` server today, but don't have a typed SDK resource yet**:
101
+ An `AsyncForm4ApiClient` mirrors the whole surface with the same resources and
102
+ method names — `await client.insiders.leaderboard()`.
93
103
 
94
- - **Form 144** notice-of-proposed-sale `GET /v1/form144` *(Business)*
95
- - **Institutional holdings (13F-HR)** — `GET /v1/holdings`, **managers** `GET /v1/managers` *(Business)*
96
- - **Sentiment** (MSPR-style, 10b5-1-clean) — `GET /v1/signals/sentiment/{ticker}` *(Business)*
97
- - **Insider career summary** — `GET /v1/insiders/{cik}/summary` *(Pro)*
98
- - **Post-trade returns** (1d/1w/1m/3m/6m) + `min_return_*` screening filters on `/v1/transactions` *(visible free; screening Pro)*
99
-
100
- Until they land in the SDK, call them directly (`client._get("/v1/holdings", {...})`) or see the [full REST reference](https://form4api.com/docs). For LLM workflows, `form4api-mcp` exposes all of the above as tools.
104
+ For the full parameter reference see the [REST docs](https://form4api.com/docs).
105
+ For LLM workflows, `form4api-mcp` exposes the same endpoints as tools.
101
106
 
102
107
  ### Transaction filters
103
108
 
@@ -181,7 +186,7 @@ client.transactions.list(min_shares=10_000, max_shares=100_000)
181
186
  ```python
182
187
  # transactions.paginate() — yields one list per page automatically
183
188
  all_txns = []
184
- for batch in client.transactions.paginate(ticker="NVDA", exclude_10b5=True, per_page=500):
189
+ for batch in client.transactions.paginate(ticker="NVDA", exclude_10b5=True, per_page=100):
185
190
  all_txns.extend(batch)
186
191
 
187
192
  # signals.paginate()
@@ -63,22 +63,27 @@ asyncio.run(main())
63
63
  | Resource | Methods |
64
64
  |---|---|
65
65
  | `client.transactions` | `.list(**params)`, `.paginate(**params)` |
66
- | `client.insiders` | `.search(name, **params)`, `.get(cik)`, `.transactions(cik, **params)` |
67
- | `client.companies` | `.get(ticker)`, `.insiders(ticker)` |
68
- | `client.signals` | `.list(**params)`, `.paginate(**params)` — Business plan |
66
+ | `client.insiders` | `.search(name, **params)`, `.get(cik)`, `.list(**params)`, `.directory(**params)`, `.transactions(cik, **params)`, `.summary(cik)` *(Pro)*, `.scorecard(cik)` *(Pro)*, `.leaderboard(**params)` *(Business)* |
67
+ | `client.companies` | `.get(ticker)`, `.insiders(ticker)`, `.list(**params)` |
68
+ | `client.signals` | `.list(**params)`, `.paginate(**params)`, `.explain(ticker)`, `.sentiment(ticker, **params)` — Business; `.convergence(**params)` — Pro |
69
+ | `client.congress` | `.trades(**params)`, `.politicians(**params)` *(Pro)*, `.politician(id_or_slug)` *(Pro)*, `.ticker(ticker)` *(Pro)* |
70
+ | `client.form144` | `.list(**params)` — Business plan |
71
+ | `client.holdings` | `.list(**params)`, `.managers(**params)` — Business plan |
72
+ | `client.filings` | `.list(**params)`, `.recent(**params)`, `.get(accession_number)` |
73
+ | `client.stats` | `.get()` — public, no key required |
74
+ | `client.data_quality` | `.get()` — public, no key required |
75
+ | `client.status` | `.history(**params)` |
69
76
  | `client.webhooks` | `.create(url, event_types)`, `.list()`, `.delete(id)`, `.events(**params)` |
70
77
 
71
- ### Not yet in this SDK
78
+ Every plan-gated endpoint the API exposes has a typed method here. Calling one
79
+ your key isn't entitled to raises `PlanError` (HTTP 402) carrying
80
+ `required_plan`, `current_plan`, and `upgrade_url` rather than failing opaquely.
72
81
 
73
- The API surface is broader than the typed client. These backend features are **available via the REST API and the `form4api-mcp` server today, but don't have a typed SDK resource yet**:
82
+ An `AsyncForm4ApiClient` mirrors the whole surface with the same resources and
83
+ method names — `await client.insiders.leaderboard()`.
74
84
 
75
- - **Form 144** notice-of-proposed-sale `GET /v1/form144` *(Business)*
76
- - **Institutional holdings (13F-HR)** — `GET /v1/holdings`, **managers** `GET /v1/managers` *(Business)*
77
- - **Sentiment** (MSPR-style, 10b5-1-clean) — `GET /v1/signals/sentiment/{ticker}` *(Business)*
78
- - **Insider career summary** — `GET /v1/insiders/{cik}/summary` *(Pro)*
79
- - **Post-trade returns** (1d/1w/1m/3m/6m) + `min_return_*` screening filters on `/v1/transactions` *(visible free; screening Pro)*
80
-
81
- Until they land in the SDK, call them directly (`client._get("/v1/holdings", {...})`) or see the [full REST reference](https://form4api.com/docs). For LLM workflows, `form4api-mcp` exposes all of the above as tools.
85
+ For the full parameter reference see the [REST docs](https://form4api.com/docs).
86
+ For LLM workflows, `form4api-mcp` exposes the same endpoints as tools.
82
87
 
83
88
  ### Transaction filters
84
89
 
@@ -162,7 +167,7 @@ client.transactions.list(min_shares=10_000, max_shares=100_000)
162
167
  ```python
163
168
  # transactions.paginate() — yields one list per page automatically
164
169
  all_txns = []
165
- for batch in client.transactions.paginate(ticker="NVDA", exclude_10b5=True, per_page=500):
170
+ for batch in client.transactions.paginate(ticker="NVDA", exclude_10b5=True, per_page=100):
166
171
  all_txns.extend(batch)
167
172
 
168
173
  # signals.paginate()
@@ -1,4 +1,4 @@
1
- from __future__ import annotations
1
+ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import re
@@ -18,11 +18,33 @@ try:
18
18
  except PackageNotFoundError: # not installed (e.g. running from a source tree)
19
19
  _SDK_VERSION = "0.0.0"
20
20
  _USER_AGENT = f"form4api-py/{_SDK_VERSION}"
21
- from form4api.resources._companies import CompaniesResource
22
- from form4api.resources._insiders import InsidersResource
23
- from form4api.resources._signals import SignalsResource
24
- from form4api.resources._transactions import TransactionsResource
25
- from form4api.resources._webhooks import WebhooksResource
21
+ from form4api.resources._companies import AsyncCompaniesResource, CompaniesResource
22
+ from form4api.resources._insiders import AsyncInsidersResource, InsidersResource
23
+ from form4api.resources._signals import AsyncSignalsResource, SignalsResource
24
+ from form4api.resources._transactions import AsyncTransactionsResource, TransactionsResource
25
+ from form4api.resources._webhooks import AsyncWebhooksResource, WebhooksResource
26
+
27
+ # Spec-derived families with no hand-written wrapper. Before these, all 6
28
+ # Pro-gated and 9 of the 10 Business-gated endpoints were unreachable from this
29
+ # SDK — a Business customer could not call Form 144 or 13F holdings at all.
30
+ # Sync and async variants are generated separately; see the note on
31
+ # AsyncForm4ApiClient below for why that matters here but not in the JS SDK.
32
+ from form4api._generated import (
33
+ GeneratedAsyncCongressResource,
34
+ GeneratedAsyncDataQualityResource,
35
+ GeneratedAsyncFilingsResource,
36
+ GeneratedAsyncForm144Resource,
37
+ GeneratedAsyncHoldingsResource,
38
+ GeneratedAsyncStatsResource,
39
+ GeneratedAsyncStatusResource,
40
+ GeneratedCongressResource,
41
+ GeneratedDataQualityResource,
42
+ GeneratedFilingsResource,
43
+ GeneratedForm144Resource,
44
+ GeneratedHoldingsResource,
45
+ GeneratedStatsResource,
46
+ GeneratedStatusResource,
47
+ )
26
48
 
27
49
  DEFAULT_BASE_URL = "https://api.form4api.com"
28
50
  _RETRY_DELAYS = [0.5, 1.0, 2.0]
@@ -66,6 +88,13 @@ class Form4ApiClient:
66
88
  self.companies = CompaniesResource(self)
67
89
  self.signals = SignalsResource(self)
68
90
  self.webhooks = WebhooksResource(self)
91
+ self.congress = GeneratedCongressResource(self)
92
+ self.filings = GeneratedFilingsResource(self)
93
+ self.form144 = GeneratedForm144Resource(self)
94
+ self.holdings = GeneratedHoldingsResource(self)
95
+ self.stats = GeneratedStatsResource(self)
96
+ self.status = GeneratedStatusResource(self)
97
+ self.data_quality = GeneratedDataQualityResource(self)
69
98
 
70
99
  def __enter__(self) -> Form4ApiClient:
71
100
  return self
@@ -127,7 +156,17 @@ class Form4ApiClient:
127
156
  if res.status_code == 401:
128
157
  raise AuthError(message, code)
129
158
  if res.status_code == 402:
130
- raise PlanError(message, body.get("requiredPlan") if isinstance(body, dict) else None)
159
+ # Read from the nested `error` object, not the top level. This was
160
+ # reading body["requiredPlan"], which the API has never emitted at
161
+ # any level, so PlanError.required_plan was permanently None. The
162
+ # backend now returns requiredPlan/currentPlan/upgradeUrl inside the
163
+ # standard error envelope.
164
+ raise PlanError(
165
+ message,
166
+ error.get("requiredPlan") if isinstance(error, dict) else None,
167
+ error.get("currentPlan") if isinstance(error, dict) else None,
168
+ error.get("upgradeUrl") if isinstance(error, dict) else None,
169
+ )
131
170
  if res.status_code == 404:
132
171
  raise NotFoundError(message, code)
133
172
  if res.status_code == 429:
@@ -154,11 +193,27 @@ class AsyncForm4ApiClient:
154
193
  timeout=timeout,
155
194
  headers={"X-Api-Key": api_key, "User-Agent": _USER_AGENT},
156
195
  )
157
- self.transactions = TransactionsResource(self) # type: ignore[arg-type]
158
- self.insiders = InsidersResource(self) # type: ignore[arg-type]
159
- self.companies = CompaniesResource(self) # type: ignore[arg-type]
160
- self.signals = SignalsResource(self) # type: ignore[arg-type]
161
- self.webhooks = WebhooksResource(self) # type: ignore[arg-type]
196
+ # Async twins, not the sync classes. Until 2026-08-06 this client reused
197
+ # the sync resources behind `# type: ignore[arg-type]`; because _get here
198
+ # is a coroutine, `Dataclass(**data)` was handed a coroutine and raised
199
+ # "argument after ** must be a mapping, not coroutine" on EVERY call, so
200
+ # every hand-written resource method was broken on the async client and
201
+ # always had been. There were no async tests, and the type: ignore
202
+ # suppressed the exact error that would have caught it. Both are fixed:
203
+ # real async classes, plus async coverage for each of them.
204
+ self.transactions = AsyncTransactionsResource(self)
205
+ self.insiders = AsyncInsidersResource(self)
206
+ self.companies = AsyncCompaniesResource(self)
207
+ self.signals = AsyncSignalsResource(self)
208
+ self.webhooks = AsyncWebhooksResource(self)
209
+ # Spec-derived families, async variants.
210
+ self.congress = GeneratedAsyncCongressResource(self)
211
+ self.filings = GeneratedAsyncFilingsResource(self)
212
+ self.form144 = GeneratedAsyncForm144Resource(self)
213
+ self.holdings = GeneratedAsyncHoldingsResource(self)
214
+ self.stats = GeneratedAsyncStatsResource(self)
215
+ self.status = GeneratedAsyncStatusResource(self)
216
+ self.data_quality = GeneratedAsyncDataQualityResource(self)
162
217
 
163
218
  async def __aenter__(self) -> AsyncForm4ApiClient:
164
219
  return self
@@ -220,7 +275,17 @@ class AsyncForm4ApiClient:
220
275
  if res.status_code == 401:
221
276
  raise AuthError(message, code)
222
277
  if res.status_code == 402:
223
- raise PlanError(message, body.get("requiredPlan") if isinstance(body, dict) else None)
278
+ # Read from the nested `error` object, not the top level. This was
279
+ # reading body["requiredPlan"], which the API has never emitted at
280
+ # any level, so PlanError.required_plan was permanently None. The
281
+ # backend now returns requiredPlan/currentPlan/upgradeUrl inside the
282
+ # standard error envelope.
283
+ raise PlanError(
284
+ message,
285
+ error.get("requiredPlan") if isinstance(error, dict) else None,
286
+ error.get("currentPlan") if isinstance(error, dict) else None,
287
+ error.get("upgradeUrl") if isinstance(error, dict) else None,
288
+ )
224
289
  if res.status_code == 404:
225
290
  raise NotFoundError(message, code)
226
291
  if res.status_code == 429:
@@ -1,30 +1,47 @@
1
- from __future__ import annotations
2
-
3
-
4
- class Form4ApiError(Exception):
5
- def __init__(self, message: str, status_code: int, error_code: str | None = None) -> None:
6
- super().__init__(message)
7
- self.status_code = status_code
8
- self.error_code = error_code
9
-
10
-
11
- class AuthError(Form4ApiError):
12
- def __init__(self, message: str, error_code: str | None = None) -> None:
13
- super().__init__(message, 401, error_code)
14
-
15
-
16
- class PlanError(Form4ApiError):
17
- def __init__(self, message: str, required_plan: str | None = None) -> None:
18
- super().__init__(message, 402, "PLAN_REQUIRED")
19
- self.required_plan = required_plan
20
-
21
-
22
- class NotFoundError(Form4ApiError):
23
- def __init__(self, message: str, error_code: str | None = None) -> None:
24
- super().__init__(message, 404, error_code)
25
-
26
-
27
- class RateLimitError(Form4ApiError):
28
- def __init__(self, message: str, retry_after: int | None = None) -> None:
29
- super().__init__(message, 429, "RATE_LIMIT_EXCEEDED")
30
- self.retry_after = retry_after
1
+ from __future__ import annotations
2
+
3
+
4
+ class Form4ApiError(Exception):
5
+ def __init__(self, message: str, status_code: int, error_code: str | None = None) -> None:
6
+ super().__init__(message)
7
+ self.status_code = status_code
8
+ self.error_code = error_code
9
+
10
+
11
+ class AuthError(Form4ApiError):
12
+ def __init__(self, message: str, error_code: str | None = None) -> None:
13
+ super().__init__(message, 401, error_code)
14
+
15
+
16
+ class PlanError(Form4ApiError):
17
+ """Raised on 402 PLAN_REQUIRED.
18
+
19
+ ``required_plan`` is the minimum plan that unlocks the endpoint (e.g.
20
+ ``"Business"``), ``current_plan`` is the plan the calling key is on, and
21
+ ``upgrade_url`` is where to upgrade. All three may be ``None`` against
22
+ backends older than 2026-08-05, which carried the plan names only as prose
23
+ inside ``message``.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ message: str,
29
+ required_plan: str | None = None,
30
+ current_plan: str | None = None,
31
+ upgrade_url: str | None = None,
32
+ ) -> None:
33
+ super().__init__(message, 402, "PLAN_REQUIRED")
34
+ self.required_plan = required_plan
35
+ self.current_plan = current_plan
36
+ self.upgrade_url = upgrade_url
37
+
38
+
39
+ class NotFoundError(Form4ApiError):
40
+ def __init__(self, message: str, error_code: str | None = None) -> None:
41
+ super().__init__(message, 404, error_code)
42
+
43
+
44
+ class RateLimitError(Form4ApiError):
45
+ def __init__(self, message: str, retry_after: int | None = None) -> None:
46
+ super().__init__(message, 429, "RATE_LIMIT_EXCEEDED")
47
+ self.retry_after = retry_after