hookbase 3.3.0__tar.gz → 4.0.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.
- {hookbase-3.3.0 → hookbase-4.0.0}/.github/workflows/python-publish.yml +3 -1
- {hookbase-3.3.0 → hookbase-4.0.0}/PKG-INFO +9 -3
- {hookbase-3.3.0 → hookbase-4.0.0}/README.md +8 -2
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/_pagination.py +209 -7
- hookbase-4.0.0/src/hookbase/_version.py +1 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/__init__.py +13 -1
- hookbase-4.0.0/src/hookbase/models/common.py +107 -0
- hookbase-4.0.0/src/hookbase/models/deliveries.py +136 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/destinations.py +24 -6
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/events.py +33 -10
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/transforms.py +20 -1
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/deliveries.py +28 -12
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/events.py +25 -17
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/resources/test_destinations.py +9 -5
- hookbase-4.0.0/tests/resources/test_inbound_contract.py +464 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/resources/test_sources.py +42 -4
- hookbase-3.3.0/src/hookbase/_version.py +0 -1
- hookbase-3.3.0/src/hookbase/models/common.py +0 -42
- hookbase-3.3.0/src/hookbase/models/deliveries.py +0 -55
- {hookbase-3.3.0 → hookbase-4.0.0}/.gitignore +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/LICENSE +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/examples/async_usage.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/examples/basic_inbound.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/examples/error_handling.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/examples/send_webhooks.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/examples/verify_webhooks.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/pyproject.toml +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/__init__.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/_client.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/_constants.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/client.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/errors.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/_base.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/_wire.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/analytics.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/api_keys.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/applications.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/catalog.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/cron_jobs.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/dlq.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/endpoints.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/event_types.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/filters.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/messages.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/organizations.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/portal_tokens.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/routes.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/schemas.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/sources.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/subscriptions.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/models/tunnels.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/py.typed +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/__init__.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/_base.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/analytics.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/api_keys.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/applications.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/cron_jobs.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/destinations.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/dlq.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/endpoints.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/event_types.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/filters.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/message_log.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/messages.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/organizations.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/portal_tokens.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/providers.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/routes.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/schemas.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/sources.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/subscriptions.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/transforms.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/resources/tunnels.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/src/hookbase/webhook.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/__init__.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/conftest.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/resources/__init__.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/resources/test_applications.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/resources/test_cron_jobs.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/resources/test_dlq.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/resources/test_endpoints.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/resources/test_messages.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/resources/test_outbound_contract.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/resources/test_source_contract.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/resources/test_source_fields.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/test_client.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/test_errors.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/test_pagination.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/test_slug_contract.py +0 -0
- {hookbase-3.3.0 → hookbase-4.0.0}/tests/test_webhook.py +0 -0
|
@@ -13,7 +13,9 @@ permissions:
|
|
|
13
13
|
|
|
14
14
|
jobs:
|
|
15
15
|
test:
|
|
16
|
-
|
|
16
|
+
# ubuntu-latest (26.04) has no Python 3.9 build for setup-python; 24.04 does, and 3.9 is
|
|
17
|
+
# still in requires-python.
|
|
18
|
+
runs-on: ubuntu-24.04
|
|
17
19
|
strategy:
|
|
18
20
|
matrix:
|
|
19
21
|
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hookbase
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0
|
|
4
4
|
Summary: Official Python SDK for the Hookbase webhook management platform
|
|
5
5
|
Project-URL: Homepage, https://hookbase.app
|
|
6
6
|
Project-URL: Documentation, https://www.hookbase.app/docs/send/sdk
|
|
@@ -137,7 +137,7 @@ client.deliveries.bulk_replay(["del_1", "del_2"])
|
|
|
137
137
|
|
|
138
138
|
# Transforms & Filters
|
|
139
139
|
client.transforms.create({"name": "Extract", "transformType": "jsonata", "code": "$.data"})
|
|
140
|
-
client.transforms.test("
|
|
140
|
+
client.transforms.test(code="$.data", payload={"data": {"key": "value"}}) # .result
|
|
141
141
|
client.filters.test([{"field": "$.type", "operator": "eq", "value": "order"}], payload={...})
|
|
142
142
|
|
|
143
143
|
# Schemas
|
|
@@ -213,10 +213,16 @@ for source in page.auto_paging_iter():
|
|
|
213
213
|
while page.has_more:
|
|
214
214
|
page = page.next_page()
|
|
215
215
|
|
|
216
|
-
# Cursor-based (applications, endpoints, subscriptions, etc.)
|
|
216
|
+
# Cursor-based (inbound events, applications, endpoints, subscriptions, etc.)
|
|
217
217
|
page = client.outbound.applications.list(limit=50)
|
|
218
218
|
for app in page.auto_paging_iter():
|
|
219
219
|
print(app.name)
|
|
220
|
+
for event in client.events.list(limit=100).auto_paging_iter():
|
|
221
|
+
print(event.id)
|
|
222
|
+
|
|
223
|
+
# Limit/offset with no total (deliveries): pages until one comes back short
|
|
224
|
+
for delivery in client.deliveries.list(status="failed", limit=100).auto_paging_iter():
|
|
225
|
+
print(delivery.id, delivery.response_status)
|
|
220
226
|
```
|
|
221
227
|
|
|
222
228
|
### Webhook Verification
|
|
@@ -102,7 +102,7 @@ client.deliveries.bulk_replay(["del_1", "del_2"])
|
|
|
102
102
|
|
|
103
103
|
# Transforms & Filters
|
|
104
104
|
client.transforms.create({"name": "Extract", "transformType": "jsonata", "code": "$.data"})
|
|
105
|
-
client.transforms.test("
|
|
105
|
+
client.transforms.test(code="$.data", payload={"data": {"key": "value"}}) # .result
|
|
106
106
|
client.filters.test([{"field": "$.type", "operator": "eq", "value": "order"}], payload={...})
|
|
107
107
|
|
|
108
108
|
# Schemas
|
|
@@ -178,10 +178,16 @@ for source in page.auto_paging_iter():
|
|
|
178
178
|
while page.has_more:
|
|
179
179
|
page = page.next_page()
|
|
180
180
|
|
|
181
|
-
# Cursor-based (applications, endpoints, subscriptions, etc.)
|
|
181
|
+
# Cursor-based (inbound events, applications, endpoints, subscriptions, etc.)
|
|
182
182
|
page = client.outbound.applications.list(limit=50)
|
|
183
183
|
for app in page.auto_paging_iter():
|
|
184
184
|
print(app.name)
|
|
185
|
+
for event in client.events.list(limit=100).auto_paging_iter():
|
|
186
|
+
print(event.id)
|
|
187
|
+
|
|
188
|
+
# Limit/offset with no total (deliveries): pages until one comes back short
|
|
189
|
+
for delivery in client.deliveries.list(status="failed", limit=100).auto_paging_iter():
|
|
190
|
+
print(delivery.id, delivery.response_status)
|
|
185
191
|
```
|
|
186
192
|
|
|
187
193
|
### Webhook Verification
|
|
@@ -140,6 +140,7 @@ class SyncCursorPage(Generic[T]):
|
|
|
140
140
|
path: str,
|
|
141
141
|
params: dict[str, Any],
|
|
142
142
|
model: type[T],
|
|
143
|
+
data_key: str | None = None,
|
|
143
144
|
) -> None:
|
|
144
145
|
self.data = data
|
|
145
146
|
self.has_more = has_more
|
|
@@ -148,6 +149,7 @@ class SyncCursorPage(Generic[T]):
|
|
|
148
149
|
self._path = path
|
|
149
150
|
self._params = params
|
|
150
151
|
self._model = model
|
|
152
|
+
self._data_key = data_key
|
|
151
153
|
|
|
152
154
|
def next_page(self) -> SyncCursorPage[T]:
|
|
153
155
|
return _fetch_cursor_page(
|
|
@@ -155,6 +157,7 @@ class SyncCursorPage(Generic[T]):
|
|
|
155
157
|
self._path,
|
|
156
158
|
{**self._params, "cursor": self.next_cursor},
|
|
157
159
|
self._model,
|
|
160
|
+
data_key=self._data_key,
|
|
158
161
|
)
|
|
159
162
|
|
|
160
163
|
def auto_paging_iter(self) -> Iterator[T]:
|
|
@@ -189,6 +192,7 @@ class AsyncCursorPage(Generic[T]):
|
|
|
189
192
|
path: str,
|
|
190
193
|
params: dict[str, Any],
|
|
191
194
|
model: type[T],
|
|
195
|
+
data_key: str | None = None,
|
|
192
196
|
) -> None:
|
|
193
197
|
self.data = data
|
|
194
198
|
self.has_more = has_more
|
|
@@ -197,6 +201,7 @@ class AsyncCursorPage(Generic[T]):
|
|
|
197
201
|
self._path = path
|
|
198
202
|
self._params = params
|
|
199
203
|
self._model = model
|
|
204
|
+
self._data_key = data_key
|
|
200
205
|
|
|
201
206
|
async def next_page(self) -> AsyncCursorPage[T]:
|
|
202
207
|
return await _async_fetch_cursor_page(
|
|
@@ -204,6 +209,7 @@ class AsyncCursorPage(Generic[T]):
|
|
|
204
209
|
self._path,
|
|
205
210
|
{**self._params, "cursor": self.next_cursor},
|
|
206
211
|
self._model,
|
|
212
|
+
data_key=self._data_key,
|
|
207
213
|
)
|
|
208
214
|
|
|
209
215
|
async def auto_paging_iter(self) -> AsyncIterator[T]:
|
|
@@ -222,6 +228,128 @@ class AsyncCursorPage(Generic[T]):
|
|
|
222
228
|
return len(self.data)
|
|
223
229
|
|
|
224
230
|
|
|
231
|
+
class SyncLimitOffsetPage(Generic[T]):
|
|
232
|
+
"""`limit`/`offset` paginated response with no total (sync).
|
|
233
|
+
|
|
234
|
+
The API reports no total for these lists, so another page is assumed while
|
|
235
|
+
pages come back full. The next page starts after the rows actually
|
|
236
|
+
returned, not after the requested `limit`: the API clamps `limit`, and
|
|
237
|
+
advancing by the requested size would skip the rows in between.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
data: list[T]
|
|
241
|
+
limit: int
|
|
242
|
+
offset: int
|
|
243
|
+
|
|
244
|
+
def __init__(
|
|
245
|
+
self,
|
|
246
|
+
data: list[T],
|
|
247
|
+
limit: int,
|
|
248
|
+
offset: int,
|
|
249
|
+
*,
|
|
250
|
+
transport: SyncTransport,
|
|
251
|
+
path: str,
|
|
252
|
+
params: dict[str, Any],
|
|
253
|
+
model: type[T],
|
|
254
|
+
data_key: str,
|
|
255
|
+
) -> None:
|
|
256
|
+
self.data = data
|
|
257
|
+
self.limit = limit
|
|
258
|
+
self.offset = offset
|
|
259
|
+
self._transport = transport
|
|
260
|
+
self._path = path
|
|
261
|
+
self._params = params
|
|
262
|
+
self._model = model
|
|
263
|
+
self._data_key = data_key
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def has_more(self) -> bool:
|
|
267
|
+
return len(self.data) > 0 and len(self.data) >= self.limit
|
|
268
|
+
|
|
269
|
+
def next_page(self) -> SyncLimitOffsetPage[T]:
|
|
270
|
+
return _fetch_limit_offset_page(
|
|
271
|
+
self._transport,
|
|
272
|
+
self._path,
|
|
273
|
+
{**self._params, "offset": self.offset + len(self.data)},
|
|
274
|
+
self._model,
|
|
275
|
+
data_key=self._data_key,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def auto_paging_iter(self) -> Iterator[T]:
|
|
279
|
+
page = self
|
|
280
|
+
while True:
|
|
281
|
+
yield from page.data
|
|
282
|
+
if not page.has_more:
|
|
283
|
+
break
|
|
284
|
+
page = page.next_page()
|
|
285
|
+
|
|
286
|
+
def __iter__(self) -> Iterator[T]:
|
|
287
|
+
return iter(self.data)
|
|
288
|
+
|
|
289
|
+
def __len__(self) -> int:
|
|
290
|
+
return len(self.data)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class AsyncLimitOffsetPage(Generic[T]):
|
|
294
|
+
"""`limit`/`offset` paginated response with no total (async).
|
|
295
|
+
|
|
296
|
+
See :class:`SyncLimitOffsetPage`.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
data: list[T]
|
|
300
|
+
limit: int
|
|
301
|
+
offset: int
|
|
302
|
+
|
|
303
|
+
def __init__(
|
|
304
|
+
self,
|
|
305
|
+
data: list[T],
|
|
306
|
+
limit: int,
|
|
307
|
+
offset: int,
|
|
308
|
+
*,
|
|
309
|
+
transport: AsyncTransport,
|
|
310
|
+
path: str,
|
|
311
|
+
params: dict[str, Any],
|
|
312
|
+
model: type[T],
|
|
313
|
+
data_key: str,
|
|
314
|
+
) -> None:
|
|
315
|
+
self.data = data
|
|
316
|
+
self.limit = limit
|
|
317
|
+
self.offset = offset
|
|
318
|
+
self._transport = transport
|
|
319
|
+
self._path = path
|
|
320
|
+
self._params = params
|
|
321
|
+
self._model = model
|
|
322
|
+
self._data_key = data_key
|
|
323
|
+
|
|
324
|
+
@property
|
|
325
|
+
def has_more(self) -> bool:
|
|
326
|
+
return len(self.data) > 0 and len(self.data) >= self.limit
|
|
327
|
+
|
|
328
|
+
async def next_page(self) -> AsyncLimitOffsetPage[T]:
|
|
329
|
+
return await _async_fetch_limit_offset_page(
|
|
330
|
+
self._transport,
|
|
331
|
+
self._path,
|
|
332
|
+
{**self._params, "offset": self.offset + len(self.data)},
|
|
333
|
+
self._model,
|
|
334
|
+
data_key=self._data_key,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
async def auto_paging_iter(self) -> AsyncIterator[T]:
|
|
338
|
+
page = self
|
|
339
|
+
while True:
|
|
340
|
+
for item in page.data:
|
|
341
|
+
yield item
|
|
342
|
+
if not page.has_more:
|
|
343
|
+
break
|
|
344
|
+
page = await page.next_page()
|
|
345
|
+
|
|
346
|
+
def __iter__(self) -> Iterator[T]:
|
|
347
|
+
return iter(self.data)
|
|
348
|
+
|
|
349
|
+
def __len__(self) -> int:
|
|
350
|
+
return len(self.data)
|
|
351
|
+
|
|
352
|
+
|
|
225
353
|
# --- Helper functions for fetching pages ---
|
|
226
354
|
|
|
227
355
|
|
|
@@ -279,9 +407,11 @@ def _fetch_cursor_page(
|
|
|
279
407
|
path: str,
|
|
280
408
|
params: dict[str, Any],
|
|
281
409
|
model: type[T],
|
|
410
|
+
*,
|
|
411
|
+
data_key: str | None = None,
|
|
282
412
|
) -> SyncCursorPage[T]:
|
|
283
413
|
resp = transport.request("GET", path, params=params)
|
|
284
|
-
items, has_more, next_cursor = _extract_cursor_data(resp)
|
|
414
|
+
items, has_more, next_cursor = _extract_cursor_data(resp, data_key)
|
|
285
415
|
return SyncCursorPage(
|
|
286
416
|
data=_parse_list(items, model),
|
|
287
417
|
has_more=has_more,
|
|
@@ -290,6 +420,7 @@ def _fetch_cursor_page(
|
|
|
290
420
|
path=path,
|
|
291
421
|
params=params,
|
|
292
422
|
model=model,
|
|
423
|
+
data_key=data_key,
|
|
293
424
|
)
|
|
294
425
|
|
|
295
426
|
|
|
@@ -298,9 +429,11 @@ async def _async_fetch_cursor_page(
|
|
|
298
429
|
path: str,
|
|
299
430
|
params: dict[str, Any],
|
|
300
431
|
model: type[T],
|
|
432
|
+
*,
|
|
433
|
+
data_key: str | None = None,
|
|
301
434
|
) -> AsyncCursorPage[T]:
|
|
302
435
|
resp = await transport.request("GET", path, params=params)
|
|
303
|
-
items, has_more, next_cursor = _extract_cursor_data(resp)
|
|
436
|
+
items, has_more, next_cursor = _extract_cursor_data(resp, data_key)
|
|
304
437
|
return AsyncCursorPage(
|
|
305
438
|
data=_parse_list(items, model),
|
|
306
439
|
has_more=has_more,
|
|
@@ -309,6 +442,51 @@ async def _async_fetch_cursor_page(
|
|
|
309
442
|
path=path,
|
|
310
443
|
params=params,
|
|
311
444
|
model=model,
|
|
445
|
+
data_key=data_key,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _fetch_limit_offset_page(
|
|
450
|
+
transport: SyncTransport,
|
|
451
|
+
path: str,
|
|
452
|
+
params: dict[str, Any],
|
|
453
|
+
model: type[T],
|
|
454
|
+
*,
|
|
455
|
+
data_key: str,
|
|
456
|
+
) -> SyncLimitOffsetPage[T]:
|
|
457
|
+
resp = transport.request("GET", path, params=params)
|
|
458
|
+
items, limit, offset = _extract_limit_offset_data(resp, data_key, params)
|
|
459
|
+
return SyncLimitOffsetPage(
|
|
460
|
+
data=_parse_list(items, model),
|
|
461
|
+
limit=limit,
|
|
462
|
+
offset=offset,
|
|
463
|
+
transport=transport,
|
|
464
|
+
path=path,
|
|
465
|
+
params=params,
|
|
466
|
+
model=model,
|
|
467
|
+
data_key=data_key,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
async def _async_fetch_limit_offset_page(
|
|
472
|
+
transport: AsyncTransport,
|
|
473
|
+
path: str,
|
|
474
|
+
params: dict[str, Any],
|
|
475
|
+
model: type[T],
|
|
476
|
+
*,
|
|
477
|
+
data_key: str,
|
|
478
|
+
) -> AsyncLimitOffsetPage[T]:
|
|
479
|
+
resp = await transport.request("GET", path, params=params)
|
|
480
|
+
items, limit, offset = _extract_limit_offset_data(resp, data_key, params)
|
|
481
|
+
return AsyncLimitOffsetPage(
|
|
482
|
+
data=_parse_list(items, model),
|
|
483
|
+
limit=limit,
|
|
484
|
+
offset=offset,
|
|
485
|
+
transport=transport,
|
|
486
|
+
path=path,
|
|
487
|
+
params=params,
|
|
488
|
+
model=model,
|
|
489
|
+
data_key=data_key,
|
|
312
490
|
)
|
|
313
491
|
|
|
314
492
|
|
|
@@ -342,14 +520,38 @@ def _extract_offset_data(
|
|
|
342
520
|
return items if isinstance(items, list) else [], total, page, page_size
|
|
343
521
|
|
|
344
522
|
|
|
345
|
-
def _extract_cursor_data(
|
|
346
|
-
|
|
523
|
+
def _extract_cursor_data(
|
|
524
|
+
resp: Any, data_key: str | None = None
|
|
525
|
+
) -> tuple[list[Any], bool, str | None]:
|
|
526
|
+
"""Extract items + pagination from a cursor-based API response.
|
|
527
|
+
|
|
528
|
+
Most cursor lists nest `hasMore`/`nextCursor` under `pagination`; inbound
|
|
529
|
+
events (`GET /api/events`) put them at the top level next to `events`.
|
|
530
|
+
"""
|
|
347
531
|
if not isinstance(resp, dict):
|
|
348
532
|
return [], False, None
|
|
349
533
|
|
|
350
|
-
items = resp.get("data", [])
|
|
351
|
-
pagination = resp.get("pagination"
|
|
534
|
+
items = resp[data_key] if data_key and data_key in resp else resp.get("data", [])
|
|
535
|
+
pagination = resp.get("pagination")
|
|
536
|
+
if not isinstance(pagination, dict):
|
|
537
|
+
pagination = resp
|
|
352
538
|
has_more = pagination.get("hasMore", pagination.get("has_more", False))
|
|
353
539
|
next_cursor = pagination.get("nextCursor", pagination.get("next_cursor"))
|
|
354
540
|
|
|
355
|
-
return items if isinstance(items, list) else [], has_more, next_cursor
|
|
541
|
+
return items if isinstance(items, list) else [], bool(has_more), next_cursor
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _extract_limit_offset_data(
|
|
545
|
+
resp: Any, data_key: str, params: dict[str, Any]
|
|
546
|
+
) -> tuple[list[Any], int, int]:
|
|
547
|
+
"""Extract items + the limit/offset the API actually applied.
|
|
548
|
+
|
|
549
|
+
The API clamps `limit` (deliveries cap it at 100) and echoes the values it
|
|
550
|
+
used, so read those back rather than trusting what was requested.
|
|
551
|
+
"""
|
|
552
|
+
if not isinstance(resp, dict):
|
|
553
|
+
return [], int(params.get("limit") or 50), int(params.get("offset") or 0)
|
|
554
|
+
items = resp.get(data_key, resp.get("data", []))
|
|
555
|
+
limit = resp.get("limit", params.get("limit") or 50)
|
|
556
|
+
offset = resp.get("offset", params.get("offset") or 0)
|
|
557
|
+
return items if isinstance(items, list) else [], int(limit), int(offset)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "4.0.0"
|
|
@@ -11,7 +11,15 @@ from .catalog import (
|
|
|
11
11
|
ProviderSignatureVerificationSummary,
|
|
12
12
|
ProviderVerifyTokenInfo,
|
|
13
13
|
)
|
|
14
|
-
from .common import
|
|
14
|
+
from .common import (
|
|
15
|
+
BulkDeleteResult,
|
|
16
|
+
ImportDetails,
|
|
17
|
+
ImportFailure,
|
|
18
|
+
ImportResult,
|
|
19
|
+
ImportResultItem,
|
|
20
|
+
ImportSummary,
|
|
21
|
+
ImportValidationResult,
|
|
22
|
+
)
|
|
15
23
|
from .cron_jobs import (
|
|
16
24
|
CreateCronGroupParams,
|
|
17
25
|
CreateCronJobParams,
|
|
@@ -146,6 +154,10 @@ __all__ = [
|
|
|
146
154
|
"BulkDeleteResult",
|
|
147
155
|
"ImportResult",
|
|
148
156
|
"ImportResultItem",
|
|
157
|
+
"ImportSummary",
|
|
158
|
+
"ImportDetails",
|
|
159
|
+
"ImportFailure",
|
|
160
|
+
"ImportValidationResult",
|
|
149
161
|
# Cron Jobs
|
|
150
162
|
"CreateCronGroupParams",
|
|
151
163
|
"CreateCronJobParams",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ._base import HookbaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ImportResultItem(HookbaseModel):
|
|
9
|
+
"""Deprecated: no import response carries per-item results in this shape.
|
|
10
|
+
See :class:`ImportResult`."""
|
|
11
|
+
|
|
12
|
+
name: str
|
|
13
|
+
status: str
|
|
14
|
+
error: str | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ImportFailure(HookbaseModel):
|
|
18
|
+
"""An item that failed to import, and why."""
|
|
19
|
+
|
|
20
|
+
name: str = ""
|
|
21
|
+
error: str = ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ImportSummary(HookbaseModel):
|
|
25
|
+
"""Counts for an import. Which keys are set depends on the resource and on
|
|
26
|
+
whether it was a `validate_only` run."""
|
|
27
|
+
|
|
28
|
+
total: int | None = None
|
|
29
|
+
imported: int | None = None
|
|
30
|
+
skipped: int | None = None
|
|
31
|
+
overwritten: int | None = None
|
|
32
|
+
failed: int | None = None
|
|
33
|
+
errors: int | None = None
|
|
34
|
+
to_create: int | None = None
|
|
35
|
+
to_overwrite: int | None = None
|
|
36
|
+
to_skip: int | None = None
|
|
37
|
+
valid: int | None = None
|
|
38
|
+
overwrite: int | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ImportDetails(HookbaseModel):
|
|
42
|
+
"""Names of the items in each outcome (sources and destinations imports)."""
|
|
43
|
+
|
|
44
|
+
imported: list[str] = []
|
|
45
|
+
skipped: list[str] = []
|
|
46
|
+
overwritten: list[str] = []
|
|
47
|
+
failed: list[ImportFailure] = []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ImportValidationResult(HookbaseModel):
|
|
51
|
+
"""How one item fared in validation."""
|
|
52
|
+
|
|
53
|
+
index: int = 0
|
|
54
|
+
name: str = ""
|
|
55
|
+
slug: str | None = None
|
|
56
|
+
status: str = ""
|
|
57
|
+
"""`valid`, `error`, `skip`, `rename` or `overwrite`."""
|
|
58
|
+
errors: list[str] = []
|
|
59
|
+
warnings: list[str] = []
|
|
60
|
+
resolved_slug: str | None = None
|
|
61
|
+
resolved_name: str | None = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ImportResult(HookbaseModel):
|
|
65
|
+
"""The result of `import_sources`, `import_destinations` or `import_routes`.
|
|
66
|
+
|
|
67
|
+
A `validate_only` run sets `valid` (sources, destinations), `validation_results`
|
|
68
|
+
and a `summary` of what would happen. A real import sets `success` and a
|
|
69
|
+
`summary`; sources and destinations list the item names in `details`, while
|
|
70
|
+
routes put them in `imported`, `skipped` and `errors`.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
success: bool = False
|
|
74
|
+
valid: bool | None = None
|
|
75
|
+
validate_only: bool = False
|
|
76
|
+
summary: ImportSummary | None = None
|
|
77
|
+
details: ImportDetails | None = None
|
|
78
|
+
validation_results: list[ImportValidationResult] = []
|
|
79
|
+
imported: list[str] = []
|
|
80
|
+
"""Route imports only; sources and destinations use `details.imported`."""
|
|
81
|
+
skipped: list[str] = []
|
|
82
|
+
"""Route imports only; sources and destinations use `details.skipped`."""
|
|
83
|
+
errors: list[ImportFailure] = []
|
|
84
|
+
"""Route imports only; sources and destinations use `details.failed`."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class BulkDeleteResult(HookbaseModel):
|
|
88
|
+
success: bool = True
|
|
89
|
+
deleted: int = 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class PaginationInfo(HookbaseModel):
|
|
93
|
+
total: int = 0
|
|
94
|
+
page: int = 1
|
|
95
|
+
page_size: int = 20
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class CursorPaginationInfo(HookbaseModel):
|
|
99
|
+
has_more: bool = False
|
|
100
|
+
next_cursor: str | None = None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ApiListResponse(HookbaseModel):
|
|
104
|
+
"""Generic wrapper for paginated list API responses."""
|
|
105
|
+
|
|
106
|
+
data: list[Any] = []
|
|
107
|
+
pagination: PaginationInfo | None = None
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import field_validator, model_validator
|
|
7
|
+
from pydantic.alias_generators import to_camel
|
|
8
|
+
|
|
9
|
+
from ._base import HookbaseModel
|
|
10
|
+
|
|
11
|
+
# API may return: pending, success, failed, retrying, delivered,
|
|
12
|
+
# failed_over, schema_failed, exhausted, etc.
|
|
13
|
+
DeliveryStatus = str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Delivery(HookbaseModel):
|
|
17
|
+
id: str
|
|
18
|
+
event_id: str
|
|
19
|
+
route_id: str
|
|
20
|
+
destination_id: str
|
|
21
|
+
organization_id: str
|
|
22
|
+
status: DeliveryStatus = "pending"
|
|
23
|
+
attempt_count: int = 0
|
|
24
|
+
max_attempts: int = 5
|
|
25
|
+
next_retry_at: str | None = None
|
|
26
|
+
response_status: int | None = None
|
|
27
|
+
response_key: str | None = None
|
|
28
|
+
response_headers: dict[str, Any] | None = None
|
|
29
|
+
response_body: str | None = None
|
|
30
|
+
latency_ms: int | None = None
|
|
31
|
+
error_message: str | None = None
|
|
32
|
+
delivered_at: str | None = None
|
|
33
|
+
created_at: str = ""
|
|
34
|
+
updated_at: str | None = None
|
|
35
|
+
destination_name: str | None = None
|
|
36
|
+
destination_url: str | None = None
|
|
37
|
+
"""Set by `deliveries.get` and on `events.get` deliveries; not by `deliveries.list`."""
|
|
38
|
+
event_type: str | None = None
|
|
39
|
+
"""Set by `deliveries.list` only."""
|
|
40
|
+
|
|
41
|
+
# Deprecated: the SDK's older names for fields the API returns as
|
|
42
|
+
# `responseStatus`, `attemptCount`, `errorMessage`, `latencyMs` and
|
|
43
|
+
# `deliveredAt`. Never sent by the API, so they used to be always empty;
|
|
44
|
+
# now kept in step with the real fields so existing attribute access works.
|
|
45
|
+
status_code: int | None = None
|
|
46
|
+
"""Deprecated: read `response_status`."""
|
|
47
|
+
attempts: int = 0
|
|
48
|
+
"""Deprecated: read `attempt_count`."""
|
|
49
|
+
error: str | None = None
|
|
50
|
+
"""Deprecated: read `error_message`."""
|
|
51
|
+
duration: float | None = None
|
|
52
|
+
"""Deprecated: read `latency_ms`."""
|
|
53
|
+
completed_at: str | None = None
|
|
54
|
+
"""Deprecated: read `delivered_at`."""
|
|
55
|
+
|
|
56
|
+
@field_validator("attempt_count", "max_attempts", mode="before")
|
|
57
|
+
@classmethod
|
|
58
|
+
def _null_counts_to_default(cls, v: Any, info: Any) -> Any:
|
|
59
|
+
# The API sends null for a delivery the worker has not counted yet; a field default only
|
|
60
|
+
# covers a missing key, so without this every list/get containing one failed to validate.
|
|
61
|
+
if v is None:
|
|
62
|
+
return cls.model_fields[info.field_name].default
|
|
63
|
+
return v
|
|
64
|
+
|
|
65
|
+
@field_validator("response_headers", mode="before")
|
|
66
|
+
@classmethod
|
|
67
|
+
def _parse_response_headers(cls, v: Any) -> Any:
|
|
68
|
+
# Stored as JSON text, so the API usually sends a string.
|
|
69
|
+
if isinstance(v, str):
|
|
70
|
+
try:
|
|
71
|
+
parsed = json.loads(v)
|
|
72
|
+
except (json.JSONDecodeError, ValueError):
|
|
73
|
+
return None
|
|
74
|
+
return parsed if isinstance(parsed, dict) else None
|
|
75
|
+
return v
|
|
76
|
+
|
|
77
|
+
@model_validator(mode="before")
|
|
78
|
+
@classmethod
|
|
79
|
+
def _mirror_deprecated(cls, data: Any) -> Any:
|
|
80
|
+
"""Copy each real field onto its deprecated name, whichever spelling it came in."""
|
|
81
|
+
if not isinstance(data, dict):
|
|
82
|
+
return data
|
|
83
|
+
out = dict(data)
|
|
84
|
+
for new, old in _DEPRECATED_DELIVERY_FIELDS:
|
|
85
|
+
value = out.get(to_camel(new))
|
|
86
|
+
if value is None:
|
|
87
|
+
value = out.get(new)
|
|
88
|
+
if value is not None:
|
|
89
|
+
out.pop(old, None)
|
|
90
|
+
out[to_camel(old)] = value
|
|
91
|
+
return out
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# (current field, deprecated field kept in step with it)
|
|
95
|
+
_DEPRECATED_DELIVERY_FIELDS = (
|
|
96
|
+
("response_status", "status_code"),
|
|
97
|
+
("attempt_count", "attempts"),
|
|
98
|
+
("error_message", "error"),
|
|
99
|
+
("latency_ms", "duration"),
|
|
100
|
+
("delivered_at", "completed_at"),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class DeliveryEvent(HookbaseModel):
|
|
105
|
+
id: str
|
|
106
|
+
event_type: str | None = None
|
|
107
|
+
received_at: str = ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class DeliveryDestination(HookbaseModel):
|
|
111
|
+
name: str
|
|
112
|
+
url: str
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class DeliveryDetail(Delivery):
|
|
116
|
+
# Deprecated: `GET /api/deliveries/:id` has never returned nested `event` or
|
|
117
|
+
# `destination` objects, so these are always None. Read `event_id`,
|
|
118
|
+
# `destination_name` and `destination_url` instead.
|
|
119
|
+
event: DeliveryEvent | None = None
|
|
120
|
+
destination: DeliveryDestination | None = None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ReplayResult(HookbaseModel):
|
|
124
|
+
delivery_id: str
|
|
125
|
+
message: str = ""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class BulkReplayResult(HookbaseModel):
|
|
129
|
+
message: str = ""
|
|
130
|
+
queued: int = 0
|
|
131
|
+
skipped: int = 0
|
|
132
|
+
events_processed: int | None = None
|
|
133
|
+
"""Set by `bulk_replay_events` only."""
|
|
134
|
+
overrides: dict[str, Any] | None = None
|
|
135
|
+
"""Set by `bulk_replay_events` only."""
|
|
136
|
+
results: list[dict[str, Any]] = []
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
from typing import Any, Literal
|
|
5
5
|
|
|
6
|
-
from pydantic import
|
|
6
|
+
from pydantic import field_validator, model_validator
|
|
7
7
|
|
|
8
8
|
from . import _wire
|
|
9
9
|
from ._base import HookbaseModel
|
|
@@ -317,10 +317,28 @@ def update_destination_body(
|
|
|
317
317
|
|
|
318
318
|
class TestResult(HookbaseModel):
|
|
319
319
|
success: bool
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
status_code: int | None = Field(default=None, alias="status")
|
|
324
|
-
duration: float | None = Field(default=None, alias="latencyMs")
|
|
320
|
+
status: int | None = None
|
|
321
|
+
"""The HTTP status the destination answered with."""
|
|
322
|
+
latency_ms: int | None = None
|
|
325
323
|
response_body: str | None = None
|
|
326
324
|
error: str | None = None
|
|
325
|
+
|
|
326
|
+
status_code: int | None = None
|
|
327
|
+
"""Deprecated: the API calls this `status`; read `status` instead. Kept in
|
|
328
|
+
step with it so existing attribute access keeps working."""
|
|
329
|
+
duration: float | None = None
|
|
330
|
+
"""Deprecated: the API calls this `latencyMs`; read `latency_ms` instead.
|
|
331
|
+
Kept in step with it so existing attribute access keeps working."""
|
|
332
|
+
|
|
333
|
+
@model_validator(mode="before")
|
|
334
|
+
@classmethod
|
|
335
|
+
def _mirror_deprecated(cls, data: Any) -> Any:
|
|
336
|
+
if not isinstance(data, dict):
|
|
337
|
+
return data
|
|
338
|
+
out = dict(data)
|
|
339
|
+
if out.get("status") is not None:
|
|
340
|
+
out["statusCode"] = out["status"]
|
|
341
|
+
latency = out.get("latencyMs", out.get("latency_ms"))
|
|
342
|
+
if latency is not None:
|
|
343
|
+
out["duration"] = latency
|
|
344
|
+
return out
|