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.
Files changed (28) hide show
  1. {form4api-0.10.0 → form4api-0.11.0}/PKG-INFO +21 -2
  2. {form4api-0.10.0 → form4api-0.11.0}/README.md +20 -1
  3. {form4api-0.10.0 → form4api-0.11.0}/form4api/_client.py +8 -0
  4. {form4api-0.10.0 → form4api-0.11.0}/form4api/_generated.py +15 -0
  5. {form4api-0.10.0 → form4api-0.11.0}/form4api/_types.py +6 -0
  6. {form4api-0.10.0 → form4api-0.11.0}/form4api/resources/_webhooks.py +18 -0
  7. {form4api-0.10.0 → form4api-0.11.0}/form4api.egg-info/PKG-INFO +21 -2
  8. {form4api-0.10.0 → form4api-0.11.0}/form4api.egg-info/SOURCES.txt +1 -0
  9. {form4api-0.10.0 → form4api-0.11.0}/pyproject.toml +1 -1
  10. form4api-0.11.0/tests/test_release_0_11_0.py +149 -0
  11. {form4api-0.10.0 → form4api-0.11.0}/LICENSE +0 -0
  12. {form4api-0.10.0 → form4api-0.11.0}/form4api/__init__.py +0 -0
  13. {form4api-0.10.0 → form4api-0.11.0}/form4api/_errors.py +0 -0
  14. {form4api-0.10.0 → form4api-0.11.0}/form4api/_webhook_utils.py +0 -0
  15. {form4api-0.10.0 → form4api-0.11.0}/form4api/resources/__init__.py +0 -0
  16. {form4api-0.10.0 → form4api-0.11.0}/form4api/resources/_companies.py +0 -0
  17. {form4api-0.10.0 → form4api-0.11.0}/form4api/resources/_insiders.py +0 -0
  18. {form4api-0.10.0 → form4api-0.11.0}/form4api/resources/_signals.py +0 -0
  19. {form4api-0.10.0 → form4api-0.11.0}/form4api/resources/_transactions.py +0 -0
  20. {form4api-0.10.0 → form4api-0.11.0}/form4api.egg-info/dependency_links.txt +0 -0
  21. {form4api-0.10.0 → form4api-0.11.0}/form4api.egg-info/requires.txt +0 -0
  22. {form4api-0.10.0 → form4api-0.11.0}/form4api.egg-info/top_level.txt +0 -0
  23. {form4api-0.10.0 → form4api-0.11.0}/setup.cfg +0 -0
  24. {form4api-0.10.0 → form4api-0.11.0}/tests/test_client.py +0 -0
  25. {form4api-0.10.0 → form4api-0.11.0}/tests/test_generated.py +0 -0
  26. {form4api-0.10.0 → form4api-0.11.0}/tests/test_method_name.py +0 -0
  27. {form4api-0.10.0 → form4api-0.11.0}/tests/test_release_0_10_0.py +0 -0
  28. {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.10.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.10.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:
@@ -22,4 +22,5 @@ tests/test_client.py
22
22
  tests/test_generated.py
23
23
  tests/test_method_name.py
24
24
  tests/test_release_0_10_0.py
25
+ tests/test_release_0_11_0.py
25
26
  tests/test_unknown_fields.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "form4api"
7
- version = "0.10.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