form4api 0.10.0__tar.gz → 0.11.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.10.0 → form4api-0.11.0}/PKG-INFO +21 -2
- {form4api-0.10.0 → form4api-0.11.0}/README.md +20 -1
- {form4api-0.10.0 → form4api-0.11.0}/form4api/_client.py +8 -0
- {form4api-0.10.0 → form4api-0.11.0}/form4api/_generated.py +15 -0
- {form4api-0.10.0 → form4api-0.11.0}/form4api/_types.py +6 -0
- {form4api-0.10.0 → form4api-0.11.0}/form4api/resources/_webhooks.py +18 -0
- {form4api-0.10.0 → form4api-0.11.0}/form4api.egg-info/PKG-INFO +21 -2
- {form4api-0.10.0 → form4api-0.11.0}/form4api.egg-info/SOURCES.txt +1 -0
- {form4api-0.10.0 → form4api-0.11.0}/pyproject.toml +1 -1
- form4api-0.11.0/tests/test_release_0_11_0.py +149 -0
- {form4api-0.10.0 → form4api-0.11.0}/LICENSE +0 -0
- {form4api-0.10.0 → form4api-0.11.0}/form4api/__init__.py +0 -0
- {form4api-0.10.0 → form4api-0.11.0}/form4api/_errors.py +0 -0
- {form4api-0.10.0 → form4api-0.11.0}/form4api/_webhook_utils.py +0 -0
- {form4api-0.10.0 → form4api-0.11.0}/form4api/resources/__init__.py +0 -0
- {form4api-0.10.0 → form4api-0.11.0}/form4api/resources/_companies.py +0 -0
- {form4api-0.10.0 → form4api-0.11.0}/form4api/resources/_insiders.py +0 -0
- {form4api-0.10.0 → form4api-0.11.0}/form4api/resources/_signals.py +0 -0
- {form4api-0.10.0 → form4api-0.11.0}/form4api/resources/_transactions.py +0 -0
- {form4api-0.10.0 → form4api-0.11.0}/form4api.egg-info/dependency_links.txt +0 -0
- {form4api-0.10.0 → form4api-0.11.0}/form4api.egg-info/requires.txt +0 -0
- {form4api-0.10.0 → form4api-0.11.0}/form4api.egg-info/top_level.txt +0 -0
- {form4api-0.10.0 → form4api-0.11.0}/setup.cfg +0 -0
- {form4api-0.10.0 → form4api-0.11.0}/tests/test_client.py +0 -0
- {form4api-0.10.0 → form4api-0.11.0}/tests/test_generated.py +0 -0
- {form4api-0.10.0 → form4api-0.11.0}/tests/test_method_name.py +0 -0
- {form4api-0.10.0 → form4api-0.11.0}/tests/test_release_0_10_0.py +0 -0
- {form4api-0.10.0 → form4api-0.11.0}/tests/test_unknown_fields.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: form4api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.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
|
|
@@ -94,7 +94,7 @@ asyncio.run(main())
|
|
|
94
94
|
| `client.stats` | `.get()` — public, no key required |
|
|
95
95
|
| `client.data_quality` | `.get()` — public, no key required |
|
|
96
96
|
| `client.status` | `.history()` — takes no parameters |
|
|
97
|
-
| `client.webhooks` | `.create(url, event_types, auth_header=None)`, `.list()`, `.delete(id)`, `.events(**params)`, `.enable(id)`, `.test(id)`, `.set_auth_header(id, name, value)`, `.delete_auth_header(id)` |
|
|
97
|
+
| `client.webhooks` | `.create(url, event_types, auth_header=None)`, `.list()`, `.update(id, url)`, `.delete(id)`, `.events(**params)`, `.enable(id)`, `.test(id)`, `.set_auth_header(id, name, value)`, `.delete_auth_header(id)` |
|
|
98
98
|
| `verify_webhook(body, signature, secret)` | Top-level function (not on the client) that checks the `X-Insider-Signature` header on a delivery |
|
|
99
99
|
|
|
100
100
|
**Congress date quality.** Each congress trade (and convergence leg) has
|
|
@@ -234,6 +234,7 @@ sub = client.webhooks.create(
|
|
|
234
234
|
)
|
|
235
235
|
print(sub.secret) # shown once; store it
|
|
236
236
|
|
|
237
|
+
client.webhooks.update(sub.subscription_id, "https://example.com/new-hook") # change the URL; returns WebhookSubscription incl. url_warning
|
|
237
238
|
client.webhooks.test(sub.subscription_id) # WebhookTestResult(delivered, status_code, duration_ms, error)
|
|
238
239
|
client.webhooks.enable(sub.subscription_id) # undo an auto-disable (disabled_reason == "delivery_failures")
|
|
239
240
|
client.webhooks.set_auth_header(sub.subscription_id, "Authorization", "Bearer new")
|
|
@@ -262,6 +263,24 @@ body. `CongressTradeFiled` and `ConvergenceSignal` carry the full event body.
|
|
|
262
263
|
write-only and never returned. `webhooks.test()` reports a failed send as
|
|
263
264
|
`delivered=False` with an `error`, it does not raise.
|
|
264
265
|
|
|
266
|
+
**Moving a webhook to a new URL.** `update(id, url)` changes the delivery URL
|
|
267
|
+
and keeps the id, secret, event types and auth header. It does not re-enable a
|
|
268
|
+
subscription the system disabled, so call `enable(id)` afterwards if needed.
|
|
269
|
+
The `last_*` health fields come back `None` because they described the old URL.
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
# Started on a tunnel, now moving to a stable endpoint
|
|
273
|
+
moved = client.webhooks.update(sub.subscription_id, "https://example.com/hooks/form4api")
|
|
274
|
+
if not moved.is_active:
|
|
275
|
+
client.webhooks.enable(moved.subscription_id)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**`url_warning`.** `create()`, `update()` and each `list()` item carry
|
|
279
|
+
`url_warning`: a string when the URL's host is a temporary tunnel or test host
|
|
280
|
+
(trycloudflare, ngrok, webhook.site, loca.lt, serveo), otherwise `None`. It is a
|
|
281
|
+
warning only; the subscription is saved either way, and deliveries stop when the
|
|
282
|
+
tunnel closes. URLs over 500 characters are rejected with 400 `INVALID_URL`.
|
|
283
|
+
|
|
265
284
|
**Verifying deliveries.** Each delivery carries
|
|
266
285
|
`X-Insider-Signature: sha256=<hex HMAC-SHA256(secret, raw body)>`. Verify it
|
|
267
286
|
against the raw request body, before parsing JSON:
|
|
@@ -75,7 +75,7 @@ asyncio.run(main())
|
|
|
75
75
|
| `client.stats` | `.get()` — public, no key required |
|
|
76
76
|
| `client.data_quality` | `.get()` — public, no key required |
|
|
77
77
|
| `client.status` | `.history()` — takes no parameters |
|
|
78
|
-
| `client.webhooks` | `.create(url, event_types, auth_header=None)`, `.list()`, `.delete(id)`, `.events(**params)`, `.enable(id)`, `.test(id)`, `.set_auth_header(id, name, value)`, `.delete_auth_header(id)` |
|
|
78
|
+
| `client.webhooks` | `.create(url, event_types, auth_header=None)`, `.list()`, `.update(id, url)`, `.delete(id)`, `.events(**params)`, `.enable(id)`, `.test(id)`, `.set_auth_header(id, name, value)`, `.delete_auth_header(id)` |
|
|
79
79
|
| `verify_webhook(body, signature, secret)` | Top-level function (not on the client) that checks the `X-Insider-Signature` header on a delivery |
|
|
80
80
|
|
|
81
81
|
**Congress date quality.** Each congress trade (and convergence leg) has
|
|
@@ -215,6 +215,7 @@ sub = client.webhooks.create(
|
|
|
215
215
|
)
|
|
216
216
|
print(sub.secret) # shown once; store it
|
|
217
217
|
|
|
218
|
+
client.webhooks.update(sub.subscription_id, "https://example.com/new-hook") # change the URL; returns WebhookSubscription incl. url_warning
|
|
218
219
|
client.webhooks.test(sub.subscription_id) # WebhookTestResult(delivered, status_code, duration_ms, error)
|
|
219
220
|
client.webhooks.enable(sub.subscription_id) # undo an auto-disable (disabled_reason == "delivery_failures")
|
|
220
221
|
client.webhooks.set_auth_header(sub.subscription_id, "Authorization", "Bearer new")
|
|
@@ -243,6 +244,24 @@ body. `CongressTradeFiled` and `ConvergenceSignal` carry the full event body.
|
|
|
243
244
|
write-only and never returned. `webhooks.test()` reports a failed send as
|
|
244
245
|
`delivered=False` with an `error`, it does not raise.
|
|
245
246
|
|
|
247
|
+
**Moving a webhook to a new URL.** `update(id, url)` changes the delivery URL
|
|
248
|
+
and keeps the id, secret, event types and auth header. It does not re-enable a
|
|
249
|
+
subscription the system disabled, so call `enable(id)` afterwards if needed.
|
|
250
|
+
The `last_*` health fields come back `None` because they described the old URL.
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
# Started on a tunnel, now moving to a stable endpoint
|
|
254
|
+
moved = client.webhooks.update(sub.subscription_id, "https://example.com/hooks/form4api")
|
|
255
|
+
if not moved.is_active:
|
|
256
|
+
client.webhooks.enable(moved.subscription_id)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**`url_warning`.** `create()`, `update()` and each `list()` item carry
|
|
260
|
+
`url_warning`: a string when the URL's host is a temporary tunnel or test host
|
|
261
|
+
(trycloudflare, ngrok, webhook.site, loca.lt, serveo), otherwise `None`. It is a
|
|
262
|
+
warning only; the subscription is saved either way, and deliveries stop when the
|
|
263
|
+
tunnel closes. URLs over 500 characters are rejected with 400 `INVALID_URL`.
|
|
264
|
+
|
|
246
265
|
**Verifying deliveries.** Each delivery carries
|
|
247
266
|
`X-Insider-Signature: sha256=<hex HMAC-SHA256(secret, raw body)>`. Verify it
|
|
248
267
|
against the raw request body, before parsing JSON:
|
|
@@ -158,6 +158,10 @@ class Form4ApiClient:
|
|
|
158
158
|
res = self._request("PUT", path, json=body)
|
|
159
159
|
return _normalise(self._parse(res))
|
|
160
160
|
|
|
161
|
+
def _patch(self, path: str, body: Any = None) -> Any:
|
|
162
|
+
res = self._request("PATCH", path, json=body)
|
|
163
|
+
return _normalise(self._parse(res))
|
|
164
|
+
|
|
161
165
|
def _delete(self, path: str) -> None:
|
|
162
166
|
res = self._request("DELETE", path)
|
|
163
167
|
if not res.is_success and res.status_code != 204:
|
|
@@ -290,6 +294,10 @@ class AsyncForm4ApiClient:
|
|
|
290
294
|
res = await self._request("PUT", path, json=body)
|
|
291
295
|
return _normalise(self._parse(res))
|
|
292
296
|
|
|
297
|
+
async def _patch(self, path: str, body: Any = None) -> Any:
|
|
298
|
+
res = await self._request("PATCH", path, json=body)
|
|
299
|
+
return _normalise(self._parse(res))
|
|
300
|
+
|
|
293
301
|
async def _delete(self, path: str) -> None:
|
|
294
302
|
res = await self._request("DELETE", path)
|
|
295
303
|
if not res.is_success and res.status_code != 204:
|
|
@@ -1377,6 +1377,21 @@ class TransactionResponse:
|
|
|
1377
1377
|
return cls(**kwargs)
|
|
1378
1378
|
|
|
1379
1379
|
|
|
1380
|
+
@dataclass
|
|
1381
|
+
class UpdateWebhookRequest:
|
|
1382
|
+
url: str | None = None
|
|
1383
|
+
|
|
1384
|
+
@classmethod
|
|
1385
|
+
def _from_dict(cls, data: dict) -> "UpdateWebhookRequest":
|
|
1386
|
+
"""Build from an API payload, ignoring unknown keys.
|
|
1387
|
+
|
|
1388
|
+
Constructing with **data directly (as the hand-written resources do)
|
|
1389
|
+
means the SDK raises TypeError the moment the backend adds a field.
|
|
1390
|
+
Filtering keeps older SDK versions working against a newer API."""
|
|
1391
|
+
known = {f.name for f in fields(cls)}
|
|
1392
|
+
return cls(**{k: v for k, v in data.items() if k in known})
|
|
1393
|
+
|
|
1394
|
+
|
|
1380
1395
|
@dataclass
|
|
1381
1396
|
class UptimeDayBucket:
|
|
1382
1397
|
date: str | None = None
|
|
@@ -194,6 +194,10 @@ class WebhookCreated(_FromDictMixin):
|
|
|
194
194
|
# events are still delivered). The auth header VALUE is never returned.
|
|
195
195
|
is_read_only: bool | None = None
|
|
196
196
|
auth_header_name: str | None = None
|
|
197
|
+
# Added 0.11.0. A string when the URL's host is a temporary tunnel or test
|
|
198
|
+
# host (ngrok, trycloudflare, webhook.site, ...), otherwise None. A warning
|
|
199
|
+
# only; the subscription is created either way.
|
|
200
|
+
url_warning: str | None = None
|
|
197
201
|
|
|
198
202
|
|
|
199
203
|
@dataclass
|
|
@@ -215,6 +219,8 @@ class WebhookSubscription(_FromDictMixin):
|
|
|
215
219
|
last_status_code: int | None = None
|
|
216
220
|
last_error_class: str | None = None
|
|
217
221
|
auth_header_name: str | None = None
|
|
222
|
+
# Added 0.11.0. Same meaning as on WebhookCreated; also returned by update().
|
|
223
|
+
url_warning: str | None = None
|
|
218
224
|
|
|
219
225
|
|
|
220
226
|
@dataclass
|
|
@@ -47,6 +47,19 @@ class WebhooksResource:
|
|
|
47
47
|
data = self._client._get("/v1/webhooks")
|
|
48
48
|
return [WebhookSubscription._from_dict(item) for item in data]
|
|
49
49
|
|
|
50
|
+
def update(self, subscription_id: int, url: str) -> WebhookSubscription:
|
|
51
|
+
"""Change the delivery URL of a subscription, keeping its id, secret,
|
|
52
|
+
event types and auth header. The URL is checked like `create` (HTTPS,
|
|
53
|
+
public address, at most 500 characters); a bad URL raises `Form4ApiError`
|
|
54
|
+
(HTTP 400) with `error_code` INVALID_REQUEST, INVALID_URL, INSECURE_URL,
|
|
55
|
+
PRIVATE_ADDRESS or UNRESOLVABLE_HOST. Works on active and auto-disabled
|
|
56
|
+
subscriptions but does NOT re-enable a disabled one: call `enable()`
|
|
57
|
+
afterwards. The `last_*` health fields come back `None` because they
|
|
58
|
+
described the old URL. Shares the 3-per-minute webhook budget with
|
|
59
|
+
`create`. All plans."""
|
|
60
|
+
data = self._client._patch(f"/v1/webhooks/{subscription_id}", {"url": url})
|
|
61
|
+
return WebhookSubscription._from_dict(data)
|
|
62
|
+
|
|
50
63
|
def delete(self, subscription_id: int) -> None:
|
|
51
64
|
self._client._delete(f"/v1/webhooks/{subscription_id}")
|
|
52
65
|
|
|
@@ -101,6 +114,11 @@ class AsyncWebhooksResource:
|
|
|
101
114
|
data = await self._client._get("/v1/webhooks")
|
|
102
115
|
return [WebhookSubscription._from_dict(item) for item in data]
|
|
103
116
|
|
|
117
|
+
async def update(self, subscription_id: int, url: str) -> WebhookSubscription:
|
|
118
|
+
"""See `WebhooksResource.update`."""
|
|
119
|
+
data = await self._client._patch(f"/v1/webhooks/{subscription_id}", {"url": url})
|
|
120
|
+
return WebhookSubscription._from_dict(data)
|
|
121
|
+
|
|
104
122
|
async def delete(self, subscription_id: int) -> None:
|
|
105
123
|
await self._client._delete(f"/v1/webhooks/{subscription_id}")
|
|
106
124
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: form4api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.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
|
|
@@ -94,7 +94,7 @@ asyncio.run(main())
|
|
|
94
94
|
| `client.stats` | `.get()` — public, no key required |
|
|
95
95
|
| `client.data_quality` | `.get()` — public, no key required |
|
|
96
96
|
| `client.status` | `.history()` — takes no parameters |
|
|
97
|
-
| `client.webhooks` | `.create(url, event_types, auth_header=None)`, `.list()`, `.delete(id)`, `.events(**params)`, `.enable(id)`, `.test(id)`, `.set_auth_header(id, name, value)`, `.delete_auth_header(id)` |
|
|
97
|
+
| `client.webhooks` | `.create(url, event_types, auth_header=None)`, `.list()`, `.update(id, url)`, `.delete(id)`, `.events(**params)`, `.enable(id)`, `.test(id)`, `.set_auth_header(id, name, value)`, `.delete_auth_header(id)` |
|
|
98
98
|
| `verify_webhook(body, signature, secret)` | Top-level function (not on the client) that checks the `X-Insider-Signature` header on a delivery |
|
|
99
99
|
|
|
100
100
|
**Congress date quality.** Each congress trade (and convergence leg) has
|
|
@@ -234,6 +234,7 @@ sub = client.webhooks.create(
|
|
|
234
234
|
)
|
|
235
235
|
print(sub.secret) # shown once; store it
|
|
236
236
|
|
|
237
|
+
client.webhooks.update(sub.subscription_id, "https://example.com/new-hook") # change the URL; returns WebhookSubscription incl. url_warning
|
|
237
238
|
client.webhooks.test(sub.subscription_id) # WebhookTestResult(delivered, status_code, duration_ms, error)
|
|
238
239
|
client.webhooks.enable(sub.subscription_id) # undo an auto-disable (disabled_reason == "delivery_failures")
|
|
239
240
|
client.webhooks.set_auth_header(sub.subscription_id, "Authorization", "Bearer new")
|
|
@@ -262,6 +263,24 @@ body. `CongressTradeFiled` and `ConvergenceSignal` carry the full event body.
|
|
|
262
263
|
write-only and never returned. `webhooks.test()` reports a failed send as
|
|
263
264
|
`delivered=False` with an `error`, it does not raise.
|
|
264
265
|
|
|
266
|
+
**Moving a webhook to a new URL.** `update(id, url)` changes the delivery URL
|
|
267
|
+
and keeps the id, secret, event types and auth header. It does not re-enable a
|
|
268
|
+
subscription the system disabled, so call `enable(id)` afterwards if needed.
|
|
269
|
+
The `last_*` health fields come back `None` because they described the old URL.
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
# Started on a tunnel, now moving to a stable endpoint
|
|
273
|
+
moved = client.webhooks.update(sub.subscription_id, "https://example.com/hooks/form4api")
|
|
274
|
+
if not moved.is_active:
|
|
275
|
+
client.webhooks.enable(moved.subscription_id)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**`url_warning`.** `create()`, `update()` and each `list()` item carry
|
|
279
|
+
`url_warning`: a string when the URL's host is a temporary tunnel or test host
|
|
280
|
+
(trycloudflare, ngrok, webhook.site, loca.lt, serveo), otherwise `None`. It is a
|
|
281
|
+
warning only; the subscription is saved either way, and deliveries stop when the
|
|
282
|
+
tunnel closes. URLs over 500 characters are rejected with 400 `INVALID_URL`.
|
|
283
|
+
|
|
265
284
|
**Verifying deliveries.** Each delivery carries
|
|
266
285
|
`X-Insider-Signature: sha256=<hex HMAC-SHA256(secret, raw body)>`. Verify it
|
|
267
286
|
against the raw request body, before parsing JSON:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "form4api"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.11.0"
|
|
8
8
|
description = "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"
|
|
9
9
|
keywords = ["insider-trading", "sec", "sec-edgar", "edgar", "form-4", "form4", "form-144", "13f", "13f-hr", "congress", "stock-act", "financial-data", "stock-market", "stocks", "fintech", "api", "sdk", "webhooks", "form4api"]
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""0.11.0 surface: webhooks.update(id, url) and url_warning, sync and async."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import pytest
|
|
7
|
+
import respx
|
|
8
|
+
|
|
9
|
+
from form4api import Form4ApiClient
|
|
10
|
+
from form4api._client import AsyncForm4ApiClient
|
|
11
|
+
from form4api._errors import Form4ApiError, NotFoundError
|
|
12
|
+
|
|
13
|
+
BASE = "https://api.form4api.com"
|
|
14
|
+
UNKNOWN = {"somethingNewFromTheBackend": 1}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def client() -> Form4ApiClient:
|
|
18
|
+
return Form4ApiClient("test-key", max_retries=0)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def aclient() -> AsyncForm4ApiClient:
|
|
22
|
+
return AsyncForm4ApiClient("test-key", max_retries=0)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
SUB = {
|
|
26
|
+
"subscriptionId": 7,
|
|
27
|
+
"url": "https://abc.ngrok.io/h",
|
|
28
|
+
"eventTypes": ["ClusterBuy"],
|
|
29
|
+
"createdAt": "2026-10-01T00:00:00",
|
|
30
|
+
"isActive": False,
|
|
31
|
+
"isReadOnly": False,
|
|
32
|
+
"disabledAt": "2026-10-05T00:00:00",
|
|
33
|
+
"disabledReason": "delivery_failures",
|
|
34
|
+
"lastAttemptAt": None,
|
|
35
|
+
"lastSuccessAt": None,
|
|
36
|
+
"lastStatusCode": None,
|
|
37
|
+
"lastErrorClass": None,
|
|
38
|
+
"authHeaderName": None,
|
|
39
|
+
"urlWarning": "Temporary tunnel host",
|
|
40
|
+
**UNKNOWN,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@respx.mock
|
|
45
|
+
def test_update_sends_patch_with_url_and_returns_parsed_row():
|
|
46
|
+
route = respx.patch(f"{BASE}/v1/webhooks/7").mock(return_value=httpx.Response(200, json=SUB))
|
|
47
|
+
sub = client().webhooks.update(7, "https://abc.ngrok.io/h")
|
|
48
|
+
req = route.calls.last.request
|
|
49
|
+
assert req.method == "PATCH"
|
|
50
|
+
assert req.url.path == "/v1/webhooks/7"
|
|
51
|
+
assert json.loads(req.content) == {"url": "https://abc.ngrok.io/h"}
|
|
52
|
+
assert sub.subscription_id == 7
|
|
53
|
+
assert sub.url == "https://abc.ngrok.io/h"
|
|
54
|
+
assert sub.url_warning == "Temporary tunnel host"
|
|
55
|
+
assert sub.disabled_reason == "delivery_failures"
|
|
56
|
+
assert sub.last_attempt_at is None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@respx.mock
|
|
60
|
+
def test_update_400_raises_with_error_code():
|
|
61
|
+
respx.patch(f"{BASE}/v1/webhooks/7").mock(
|
|
62
|
+
return_value=httpx.Response(
|
|
63
|
+
400, json={"error": {"code": "INSECURE_URL", "message": "URL must use HTTPS"}}
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
with pytest.raises(Form4ApiError) as exc:
|
|
67
|
+
client().webhooks.update(7, "http://example.com/h")
|
|
68
|
+
assert exc.value.status_code == 400
|
|
69
|
+
assert exc.value.error_code == "INSECURE_URL"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@respx.mock
|
|
73
|
+
def test_update_404_raises_not_found():
|
|
74
|
+
respx.patch(f"{BASE}/v1/webhooks/99").mock(
|
|
75
|
+
return_value=httpx.Response(404, json={"error": {"code": "NOT_FOUND", "message": "nope"}})
|
|
76
|
+
)
|
|
77
|
+
with pytest.raises(NotFoundError):
|
|
78
|
+
client().webhooks.update(99, "https://example.com/h")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@respx.mock
|
|
82
|
+
def test_update_async_sends_patch_and_parses():
|
|
83
|
+
route = respx.patch(f"{BASE}/v1/webhooks/7").mock(return_value=httpx.Response(200, json=SUB))
|
|
84
|
+
|
|
85
|
+
async def run():
|
|
86
|
+
c = aclient()
|
|
87
|
+
try:
|
|
88
|
+
return await c.webhooks.update(7, "https://abc.ngrok.io/h")
|
|
89
|
+
finally:
|
|
90
|
+
await c.close()
|
|
91
|
+
|
|
92
|
+
sub = asyncio.run(run())
|
|
93
|
+
req = route.calls.last.request
|
|
94
|
+
assert req.method == "PATCH"
|
|
95
|
+
assert json.loads(req.content) == {"url": "https://abc.ngrok.io/h"}
|
|
96
|
+
assert sub.url_warning == "Temporary tunnel host"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@respx.mock
|
|
100
|
+
def test_update_async_400_raises_with_error_code():
|
|
101
|
+
respx.patch(f"{BASE}/v1/webhooks/7").mock(
|
|
102
|
+
return_value=httpx.Response(
|
|
103
|
+
400, json={"error": {"code": "PRIVATE_ADDRESS", "message": "private"}}
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
async def run():
|
|
108
|
+
c = aclient()
|
|
109
|
+
try:
|
|
110
|
+
await c.webhooks.update(7, "https://10.0.0.1/h")
|
|
111
|
+
finally:
|
|
112
|
+
await c.close()
|
|
113
|
+
|
|
114
|
+
with pytest.raises(Form4ApiError) as exc:
|
|
115
|
+
asyncio.run(run())
|
|
116
|
+
assert exc.value.error_code == "PRIVATE_ADDRESS"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@respx.mock
|
|
120
|
+
def test_url_warning_parses_on_create():
|
|
121
|
+
respx.post(f"{BASE}/v1/webhooks").mock(
|
|
122
|
+
return_value=httpx.Response(
|
|
123
|
+
200,
|
|
124
|
+
json={
|
|
125
|
+
"subscriptionId": 8,
|
|
126
|
+
"url": "https://x.trycloudflare.com/h",
|
|
127
|
+
"eventTypes": ["ClusterBuy"],
|
|
128
|
+
"secret": "s",
|
|
129
|
+
"createdAt": "2026-10-13T00:00:00",
|
|
130
|
+
"warning": "Store the secret securely",
|
|
131
|
+
"urlWarning": "Temporary tunnel host",
|
|
132
|
+
**UNKNOWN,
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
created = client().webhooks.create("https://x.trycloudflare.com/h", ["ClusterBuy"])
|
|
137
|
+
assert created.url_warning == "Temporary tunnel host"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@respx.mock
|
|
141
|
+
def test_url_warning_parses_on_list_and_defaults_to_none_for_old_payloads():
|
|
142
|
+
old = {k: v for k, v in SUB.items() if k != "urlWarning"}
|
|
143
|
+
respx.get(f"{BASE}/v1/webhooks").mock(
|
|
144
|
+
return_value=httpx.Response(200, json=[SUB, {**old, "subscriptionId": 9}, {**SUB, "subscriptionId": 10, "urlWarning": None}])
|
|
145
|
+
)
|
|
146
|
+
subs = client().webhooks.list()
|
|
147
|
+
assert subs[0].url_warning == "Temporary tunnel host"
|
|
148
|
+
assert subs[1].url_warning is None
|
|
149
|
+
assert subs[2].url_warning is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|