hookbase 4.1.0__tar.gz → 4.2.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-4.1.0 → hookbase-4.2.0}/PKG-INFO +107 -5
- {hookbase-4.1.0 → hookbase-4.2.0}/README.md +106 -4
- {hookbase-4.1.0 → hookbase-4.2.0}/examples/basic_inbound.py +3 -1
- {hookbase-4.1.0 → hookbase-4.2.0}/examples/send_webhooks.py +3 -1
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/_pagination.py +6 -2
- hookbase-4.2.0/src/hookbase/_version.py +1 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/__init__.py +22 -0
- hookbase-4.2.0/src/hookbase/models/_base.py +20 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/api_keys.py +5 -3
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/cron_jobs.py +13 -5
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/deliveries.py +22 -2
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/destinations.py +128 -15
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/endpoints.py +37 -9
- hookbase-4.2.0/src/hookbase/models/events.py +239 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/messages.py +26 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/portal_tokens.py +7 -2
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/routes.py +18 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/sources.py +27 -9
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/transforms.py +49 -3
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/tunnels.py +10 -6
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/_base.py +3 -4
- hookbase-4.2.0/src/hookbase/resources/deliveries.py +186 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/destinations.py +82 -8
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/transforms.py +15 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/resources/test_destinations.py +229 -1
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/resources/test_endpoints.py +73 -1
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/resources/test_inbound_contract.py +244 -0
- hookbase-4.2.0/tests/resources/test_open_types.py +94 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/resources/test_outbound_contract.py +89 -1
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/resources/test_resource_contracts.py +141 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/resources/test_source_fields.py +12 -3
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/resources/test_sources.py +50 -1
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/resources/test_sync_async_parity.py +145 -1
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/test_models_exports.py +12 -0
- hookbase-4.2.0/tests/test_secret_redaction.py +218 -0
- hookbase-4.1.0/src/hookbase/_version.py +0 -1
- hookbase-4.1.0/src/hookbase/models/_base.py +0 -14
- hookbase-4.1.0/src/hookbase/models/events.py +0 -116
- hookbase-4.1.0/src/hookbase/resources/deliveries.py +0 -99
- {hookbase-4.1.0 → hookbase-4.2.0}/.github/workflows/python-publish.yml +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/.gitignore +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/LICENSE +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/examples/async_usage.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/examples/error_handling.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/examples/verify_webhooks.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/pyproject.toml +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/__init__.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/_client.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/_constants.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/client.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/errors.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/_wire.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/analytics.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/applications.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/catalog.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/common.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/dlq.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/event_types.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/filters.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/organizations.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/schemas.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/models/subscriptions.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/py.typed +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/__init__.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/analytics.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/api_keys.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/applications.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/cron_jobs.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/dlq.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/endpoints.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/event_types.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/events.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/filters.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/message_log.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/messages.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/organizations.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/portal_tokens.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/providers.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/routes.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/schemas.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/sources.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/subscriptions.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/resources/tunnels.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/src/hookbase/webhook.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/__init__.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/conftest.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/resources/__init__.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/resources/test_applications.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/resources/test_cron_jobs.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/resources/test_dlq.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/resources/test_messages.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/resources/test_source_contract.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/test_client.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/test_errors.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/test_http_client.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/test_pagination.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/test_retry.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/test_slug_contract.py +0 -0
- {hookbase-4.1.0 → hookbase-4.2.0}/tests/test_webhook.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hookbase
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.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
|
|
@@ -145,23 +145,67 @@ for p in client.providers.list(category="payments").providers:
|
|
|
145
145
|
print(p.hookbase_provider, p.signature_verification and p.signature_verification.secret_source)
|
|
146
146
|
client.providers.get("stripe").signature_verification
|
|
147
147
|
|
|
148
|
+
# Record the catalog entry a source is made from (create only). Entries with no verifier of
|
|
149
|
+
# their own, such as Salesforce, save as Custom HMAC:
|
|
150
|
+
client.sources.create({"name": "Salesforce", "slug": "salesforce", "provider": "custom",
|
|
151
|
+
"catalogProvider": "salesforce"})
|
|
152
|
+
|
|
148
153
|
# Destinations
|
|
149
154
|
client.destinations.list()
|
|
150
|
-
client.destinations.create({"name": "Backend", "url": "https://..."})
|
|
155
|
+
dest = client.destinations.create({"name": "Backend", "url": "https://..."})
|
|
151
156
|
client.destinations.test("dst_id")
|
|
152
157
|
|
|
158
|
+
# HTTP destinations sign every delivery (Standard Webhooks headers), so the receiver can check
|
|
159
|
+
# it with Webhook(secret).verify(). Create returns the secret once: put it in your secret store.
|
|
160
|
+
# repr() and str() leave it out, so logging the destination doesn't leak it.
|
|
161
|
+
signing_secret = dest.signing_secret # whsec_...; dest.has_signing_secret is True
|
|
162
|
+
client.destinations.reveal_secret("dst_id").signing_secret
|
|
163
|
+
rotated = client.destinations.rotate_secret("dst_id", grace_period_seconds=3600)
|
|
164
|
+
rotated.previous_secret_expires_at # until then the old secret signs too
|
|
165
|
+
|
|
166
|
+
# Bridge destinations publish each delivery they receive as an outbound event
|
|
167
|
+
from hookbase.models import BridgeConfig, CreateDestinationParams
|
|
168
|
+
|
|
169
|
+
bridge = client.destinations.create(CreateDestinationParams(
|
|
170
|
+
name="Orders to customers",
|
|
171
|
+
type="bridge",
|
|
172
|
+
config=BridgeConfig(event_type_source="field", event_type_field="type",
|
|
173
|
+
event_type_prefix="order."),
|
|
174
|
+
))
|
|
175
|
+
# A bridge's test is a dry run: nothing is published
|
|
176
|
+
result = client.destinations.test(bridge.id, payload={"type": "created"})
|
|
177
|
+
result.event_type, result.event_type_exists, result.matching_endpoints
|
|
178
|
+
|
|
153
179
|
# Routes
|
|
154
180
|
client.routes.create({"name": "Route", "sourceId": "src_id", "destinationId": "dst_id"})
|
|
155
181
|
client.routes.get_circuit_status("route_id")
|
|
156
182
|
client.routes.reset_circuit("route_id")
|
|
157
183
|
|
|
184
|
+
# While a route's circuit is open its deliveries are held (status "held", hold_expires_at set)
|
|
185
|
+
# and go out when it closes. hold_on_circuit_open=False dead-letters them as soon as the circuit
|
|
186
|
+
# opens instead.
|
|
187
|
+
client.routes.update("route_id", {"holdOnCircuitOpen": False})
|
|
188
|
+
client.routes.get("route_id").held_count # get only; held_count_capped past 10,000
|
|
189
|
+
|
|
158
190
|
# Events & Deliveries
|
|
159
191
|
client.events.list(source_id="src_id", from_date="2024-01-01") # id or slug; unknown -> 404
|
|
160
192
|
client.events.get("evt_id") # Includes payload and deliveries
|
|
193
|
+
client.events.debug("evt_id").curl_command # plus typed deliveries, pattern hints, circuit state
|
|
161
194
|
client.deliveries.replay("del_id")
|
|
162
195
|
client.deliveries.bulk_replay(["del_1", "del_2"])
|
|
163
196
|
|
|
197
|
+
# Replay with edits. payload_patch is an RFC 7386 merge patch on the stored payload (None
|
|
198
|
+
# removes a key), so it works on sources that mask fields; modified_payload replaces it. An
|
|
199
|
+
# argument left as None is not sent.
|
|
200
|
+
client.deliveries.replay(
|
|
201
|
+
"del_id",
|
|
202
|
+
payload_patch={"amount": 10, "coupon": None},
|
|
203
|
+
destination_override="backup-destination", # id or slug
|
|
204
|
+
headers_override={"X-Replay": "1"},
|
|
205
|
+
)
|
|
206
|
+
|
|
164
207
|
# Transforms & Filters
|
|
208
|
+
client.transforms.engines() # languages this deployment runs, and which are available
|
|
165
209
|
client.transforms.create({"name": "Extract", "transformType": "jsonata", "code": "$.data"})
|
|
166
210
|
client.transforms.test(code="$.data", payload={"data": {"key": "value"}}) # .result
|
|
167
211
|
client.filters.test(
|
|
@@ -189,18 +233,26 @@ client.outbound.endpoints.rotate_secret("ep_id", grace_period=3600)
|
|
|
189
233
|
# this". Name the setting in `clear` to send an explicit null for it:
|
|
190
234
|
client.outbound.endpoints.update("ep_id", {"clear": ["retryDelays"]})
|
|
191
235
|
|
|
236
|
+
# While an endpoint's circuit is open its messages are held (status "held") and go out when it
|
|
237
|
+
# recovers; hold_on_circuit_open=False dead-letters them instead. FIFO endpoints never hold.
|
|
238
|
+
client.outbound.endpoints.update("ep_id", {"holdOnCircuitOpen": False})
|
|
239
|
+
client.outbound.endpoints.get("ep_id").held_count
|
|
240
|
+
|
|
192
241
|
# Event Types & Subscriptions
|
|
193
242
|
client.outbound.event_types.create({"name": "order.created", "category": "orders"})
|
|
194
243
|
client.outbound.subscriptions.create({"endpointId": "ep_id", "eventTypeId": "et_id"})
|
|
195
244
|
client.outbound.subscriptions.bulk_create("ep_id", ["et_1", "et_2"])
|
|
196
245
|
|
|
197
246
|
# Send Events
|
|
198
|
-
client.outbound.messages.send("app_id", event_type="order.created",
|
|
247
|
+
sent = client.outbound.messages.send("app_id", event_type="order.created",
|
|
248
|
+
payload={"orderId": "ord_1"})
|
|
249
|
+
sent.messages_held # of messages_queued, those held behind an open circuit (None if none)
|
|
199
250
|
|
|
200
251
|
# Message Log
|
|
201
|
-
client.outbound.message_log.list(application_id="app_id")
|
|
252
|
+
client.outbound.message_log.list(application_id="app_id", status="held") # .hold_expires_at
|
|
253
|
+
client.outbound.message_log.get("msg_id").bridge # set when a bridge destination published it
|
|
202
254
|
client.outbound.message_log.list_attempts("msg_id")
|
|
203
|
-
client.outbound.message_log.stats()
|
|
255
|
+
client.outbound.message_log.stats() # .held among the counts
|
|
204
256
|
|
|
205
257
|
# Dead Letter Queue
|
|
206
258
|
client.outbound.dlq.list()
|
|
@@ -385,6 +437,56 @@ source = client.sources.create({"name": "GitHub", "slug": "github", "provider":
|
|
|
385
437
|
# source with a provider newer than this package still parses.
|
|
386
438
|
```
|
|
387
439
|
|
|
440
|
+
## Upgrading to 4.2
|
|
441
|
+
|
|
442
|
+
- **Fix:** `destinations.list()` and `get()` raised a `ValidationError` when the organization had
|
|
443
|
+
a destination of a type other than `http`, `s3`, `r2`, `gcs` or `azure_blob` (`sqs`,
|
|
444
|
+
`eventbridge`, `servicebus`, `pubsub`, `oci_queue`, `bridge`). `DestinationType`,
|
|
445
|
+
`TransformType` and `ContentFormat` now accept any string, as `MessageStatus` does, so types
|
|
446
|
+
added later parse too. `"form"`, which the API never accepted, is gone from `ContentFormat`.
|
|
447
|
+
- **Fix:** `events.debug()` returned an empty `curl_command`, `event.headers` and
|
|
448
|
+
`event.payload`; they are filled in now. Its `deliveries` are typed (`EventDebugDelivery`).
|
|
449
|
+
- `destinations.create()` returns a `DestinationWithSecret`, whose `signing_secret` is the only
|
|
450
|
+
time the secret is returned; afterwards use `reveal_secret()`. `rotate_secret()` takes
|
|
451
|
+
`grace_period_seconds`. `Destination.has_signing_secret` says whether deliveries are signed.
|
|
452
|
+
- Bridge destinations: `BridgeConfig` for `config`, `Destination.bridge_config` to read it,
|
|
453
|
+
`destinations.test(id, payload=...)` for a dry run, and the dry-run fields on `TestResult`.
|
|
454
|
+
- `deliveries.replay()` takes `modified_payload`, `payload_patch`, `destination_override`,
|
|
455
|
+
`transform_override`, `headers_override` and `persist_transform`; `ReplayResult` reports what
|
|
456
|
+
changed. Passing both payload edits raises `ValueError`. An argument left as None is not sent,
|
|
457
|
+
so `modified_payload` can be any JSON value but null. The same goes for `payload` on
|
|
458
|
+
`destinations.test()`.
|
|
459
|
+
- Holds: the `held` status on deliveries and outbound messages, with `hold_expires_at`;
|
|
460
|
+
`hold_on_circuit_open`, `held_count` and `held_count_capped` on routes and webhook endpoints;
|
|
461
|
+
`messages_held` on `SendEventResponse` and `held` on `StatsSummary`. `MessageStatus` also
|
|
462
|
+
gains `awaiting_pull` and `fifo_waiting`.
|
|
463
|
+
- `transforms.engines()` lists the transform languages the deployment runs.
|
|
464
|
+
- `catalog_provider` on sources (set on create only).
|
|
465
|
+
- `sources.export()` returns a raw dict, and the API now sends its `ipAllowlist`, `ipDenylist`,
|
|
466
|
+
`encryptFields` and `maskFields` as JSON arrays; they used to be JSON-encoded strings. Code
|
|
467
|
+
that calls `json.loads` on them must stop. The `Source` model already read either form.
|
|
468
|
+
- Secrets and credentials no longer appear in `repr()` or `str()`:
|
|
469
|
+
- The secrets the API returns, on `DestinationWithSecret`, `DestinationSecret`,
|
|
470
|
+
`SourceWithSecret`, `PendingVerification`, `WebhookEndpoint`, `EndpointWithSecret`,
|
|
471
|
+
`RotateSecretResult`, `ApiKeyWithSecret`, `Tunnel`, `TunnelToken` and `PortalToken`.
|
|
472
|
+
- The headers and auth configs the API returns in full to a key with the `write` scope:
|
|
473
|
+
`Destination.headers` and `auth_config`, `WebhookEndpoint.headers` (and each
|
|
474
|
+
`CustomHeader.value`), and `CronJob.headers`.
|
|
475
|
+
- The credentials you pass in: `signing_secret` and `verify_token` on `CreateSourceParams`
|
|
476
|
+
and `UpdateSourceParams`; `headers`, `auth_config` and `config` on `CreateDestinationParams`
|
|
477
|
+
and `UpdateDestinationParams`; `headers` on the endpoint and cron job params; and the keys
|
|
478
|
+
on `S3Config`, `GCSConfig` and `AzureBlobConfig`.
|
|
479
|
+
|
|
480
|
+
A `pydantic.ValidationError` from one of these models, or from a list of them, leaves out the
|
|
481
|
+
input it was given. The values are still sent and still readable from their attributes
|
|
482
|
+
(`dest.signing_secret`).
|
|
483
|
+
- **Typing:** `CreateDestinationParams.config` and `UpdateDestinationParams.config` are now
|
|
484
|
+
`dict[str, Any] | BridgeConfig | None`, so `params.config["bucket"]` no longer type-checks
|
|
485
|
+
until you narrow it with `isinstance(params.config, dict)`. Because `DestinationType`,
|
|
486
|
+
`TransformType` and `ContentFormat` now include `str`, mypy reads `Destination.type`,
|
|
487
|
+
`Transform.transform_type`, `input_format` and `output_format` as plain `str`. An exhaustive
|
|
488
|
+
`match` or `assert_never` over their literals no longer type-checks.
|
|
489
|
+
|
|
388
490
|
## Upgrading to 4.1
|
|
389
491
|
|
|
390
492
|
- `APIError.code` now carries the API's `code` from the response body (for example
|
|
@@ -107,23 +107,67 @@ for p in client.providers.list(category="payments").providers:
|
|
|
107
107
|
print(p.hookbase_provider, p.signature_verification and p.signature_verification.secret_source)
|
|
108
108
|
client.providers.get("stripe").signature_verification
|
|
109
109
|
|
|
110
|
+
# Record the catalog entry a source is made from (create only). Entries with no verifier of
|
|
111
|
+
# their own, such as Salesforce, save as Custom HMAC:
|
|
112
|
+
client.sources.create({"name": "Salesforce", "slug": "salesforce", "provider": "custom",
|
|
113
|
+
"catalogProvider": "salesforce"})
|
|
114
|
+
|
|
110
115
|
# Destinations
|
|
111
116
|
client.destinations.list()
|
|
112
|
-
client.destinations.create({"name": "Backend", "url": "https://..."})
|
|
117
|
+
dest = client.destinations.create({"name": "Backend", "url": "https://..."})
|
|
113
118
|
client.destinations.test("dst_id")
|
|
114
119
|
|
|
120
|
+
# HTTP destinations sign every delivery (Standard Webhooks headers), so the receiver can check
|
|
121
|
+
# it with Webhook(secret).verify(). Create returns the secret once: put it in your secret store.
|
|
122
|
+
# repr() and str() leave it out, so logging the destination doesn't leak it.
|
|
123
|
+
signing_secret = dest.signing_secret # whsec_...; dest.has_signing_secret is True
|
|
124
|
+
client.destinations.reveal_secret("dst_id").signing_secret
|
|
125
|
+
rotated = client.destinations.rotate_secret("dst_id", grace_period_seconds=3600)
|
|
126
|
+
rotated.previous_secret_expires_at # until then the old secret signs too
|
|
127
|
+
|
|
128
|
+
# Bridge destinations publish each delivery they receive as an outbound event
|
|
129
|
+
from hookbase.models import BridgeConfig, CreateDestinationParams
|
|
130
|
+
|
|
131
|
+
bridge = client.destinations.create(CreateDestinationParams(
|
|
132
|
+
name="Orders to customers",
|
|
133
|
+
type="bridge",
|
|
134
|
+
config=BridgeConfig(event_type_source="field", event_type_field="type",
|
|
135
|
+
event_type_prefix="order."),
|
|
136
|
+
))
|
|
137
|
+
# A bridge's test is a dry run: nothing is published
|
|
138
|
+
result = client.destinations.test(bridge.id, payload={"type": "created"})
|
|
139
|
+
result.event_type, result.event_type_exists, result.matching_endpoints
|
|
140
|
+
|
|
115
141
|
# Routes
|
|
116
142
|
client.routes.create({"name": "Route", "sourceId": "src_id", "destinationId": "dst_id"})
|
|
117
143
|
client.routes.get_circuit_status("route_id")
|
|
118
144
|
client.routes.reset_circuit("route_id")
|
|
119
145
|
|
|
146
|
+
# While a route's circuit is open its deliveries are held (status "held", hold_expires_at set)
|
|
147
|
+
# and go out when it closes. hold_on_circuit_open=False dead-letters them as soon as the circuit
|
|
148
|
+
# opens instead.
|
|
149
|
+
client.routes.update("route_id", {"holdOnCircuitOpen": False})
|
|
150
|
+
client.routes.get("route_id").held_count # get only; held_count_capped past 10,000
|
|
151
|
+
|
|
120
152
|
# Events & Deliveries
|
|
121
153
|
client.events.list(source_id="src_id", from_date="2024-01-01") # id or slug; unknown -> 404
|
|
122
154
|
client.events.get("evt_id") # Includes payload and deliveries
|
|
155
|
+
client.events.debug("evt_id").curl_command # plus typed deliveries, pattern hints, circuit state
|
|
123
156
|
client.deliveries.replay("del_id")
|
|
124
157
|
client.deliveries.bulk_replay(["del_1", "del_2"])
|
|
125
158
|
|
|
159
|
+
# Replay with edits. payload_patch is an RFC 7386 merge patch on the stored payload (None
|
|
160
|
+
# removes a key), so it works on sources that mask fields; modified_payload replaces it. An
|
|
161
|
+
# argument left as None is not sent.
|
|
162
|
+
client.deliveries.replay(
|
|
163
|
+
"del_id",
|
|
164
|
+
payload_patch={"amount": 10, "coupon": None},
|
|
165
|
+
destination_override="backup-destination", # id or slug
|
|
166
|
+
headers_override={"X-Replay": "1"},
|
|
167
|
+
)
|
|
168
|
+
|
|
126
169
|
# Transforms & Filters
|
|
170
|
+
client.transforms.engines() # languages this deployment runs, and which are available
|
|
127
171
|
client.transforms.create({"name": "Extract", "transformType": "jsonata", "code": "$.data"})
|
|
128
172
|
client.transforms.test(code="$.data", payload={"data": {"key": "value"}}) # .result
|
|
129
173
|
client.filters.test(
|
|
@@ -151,18 +195,26 @@ client.outbound.endpoints.rotate_secret("ep_id", grace_period=3600)
|
|
|
151
195
|
# this". Name the setting in `clear` to send an explicit null for it:
|
|
152
196
|
client.outbound.endpoints.update("ep_id", {"clear": ["retryDelays"]})
|
|
153
197
|
|
|
198
|
+
# While an endpoint's circuit is open its messages are held (status "held") and go out when it
|
|
199
|
+
# recovers; hold_on_circuit_open=False dead-letters them instead. FIFO endpoints never hold.
|
|
200
|
+
client.outbound.endpoints.update("ep_id", {"holdOnCircuitOpen": False})
|
|
201
|
+
client.outbound.endpoints.get("ep_id").held_count
|
|
202
|
+
|
|
154
203
|
# Event Types & Subscriptions
|
|
155
204
|
client.outbound.event_types.create({"name": "order.created", "category": "orders"})
|
|
156
205
|
client.outbound.subscriptions.create({"endpointId": "ep_id", "eventTypeId": "et_id"})
|
|
157
206
|
client.outbound.subscriptions.bulk_create("ep_id", ["et_1", "et_2"])
|
|
158
207
|
|
|
159
208
|
# Send Events
|
|
160
|
-
client.outbound.messages.send("app_id", event_type="order.created",
|
|
209
|
+
sent = client.outbound.messages.send("app_id", event_type="order.created",
|
|
210
|
+
payload={"orderId": "ord_1"})
|
|
211
|
+
sent.messages_held # of messages_queued, those held behind an open circuit (None if none)
|
|
161
212
|
|
|
162
213
|
# Message Log
|
|
163
|
-
client.outbound.message_log.list(application_id="app_id")
|
|
214
|
+
client.outbound.message_log.list(application_id="app_id", status="held") # .hold_expires_at
|
|
215
|
+
client.outbound.message_log.get("msg_id").bridge # set when a bridge destination published it
|
|
164
216
|
client.outbound.message_log.list_attempts("msg_id")
|
|
165
|
-
client.outbound.message_log.stats()
|
|
217
|
+
client.outbound.message_log.stats() # .held among the counts
|
|
166
218
|
|
|
167
219
|
# Dead Letter Queue
|
|
168
220
|
client.outbound.dlq.list()
|
|
@@ -347,6 +399,56 @@ source = client.sources.create({"name": "GitHub", "slug": "github", "provider":
|
|
|
347
399
|
# source with a provider newer than this package still parses.
|
|
348
400
|
```
|
|
349
401
|
|
|
402
|
+
## Upgrading to 4.2
|
|
403
|
+
|
|
404
|
+
- **Fix:** `destinations.list()` and `get()` raised a `ValidationError` when the organization had
|
|
405
|
+
a destination of a type other than `http`, `s3`, `r2`, `gcs` or `azure_blob` (`sqs`,
|
|
406
|
+
`eventbridge`, `servicebus`, `pubsub`, `oci_queue`, `bridge`). `DestinationType`,
|
|
407
|
+
`TransformType` and `ContentFormat` now accept any string, as `MessageStatus` does, so types
|
|
408
|
+
added later parse too. `"form"`, which the API never accepted, is gone from `ContentFormat`.
|
|
409
|
+
- **Fix:** `events.debug()` returned an empty `curl_command`, `event.headers` and
|
|
410
|
+
`event.payload`; they are filled in now. Its `deliveries` are typed (`EventDebugDelivery`).
|
|
411
|
+
- `destinations.create()` returns a `DestinationWithSecret`, whose `signing_secret` is the only
|
|
412
|
+
time the secret is returned; afterwards use `reveal_secret()`. `rotate_secret()` takes
|
|
413
|
+
`grace_period_seconds`. `Destination.has_signing_secret` says whether deliveries are signed.
|
|
414
|
+
- Bridge destinations: `BridgeConfig` for `config`, `Destination.bridge_config` to read it,
|
|
415
|
+
`destinations.test(id, payload=...)` for a dry run, and the dry-run fields on `TestResult`.
|
|
416
|
+
- `deliveries.replay()` takes `modified_payload`, `payload_patch`, `destination_override`,
|
|
417
|
+
`transform_override`, `headers_override` and `persist_transform`; `ReplayResult` reports what
|
|
418
|
+
changed. Passing both payload edits raises `ValueError`. An argument left as None is not sent,
|
|
419
|
+
so `modified_payload` can be any JSON value but null. The same goes for `payload` on
|
|
420
|
+
`destinations.test()`.
|
|
421
|
+
- Holds: the `held` status on deliveries and outbound messages, with `hold_expires_at`;
|
|
422
|
+
`hold_on_circuit_open`, `held_count` and `held_count_capped` on routes and webhook endpoints;
|
|
423
|
+
`messages_held` on `SendEventResponse` and `held` on `StatsSummary`. `MessageStatus` also
|
|
424
|
+
gains `awaiting_pull` and `fifo_waiting`.
|
|
425
|
+
- `transforms.engines()` lists the transform languages the deployment runs.
|
|
426
|
+
- `catalog_provider` on sources (set on create only).
|
|
427
|
+
- `sources.export()` returns a raw dict, and the API now sends its `ipAllowlist`, `ipDenylist`,
|
|
428
|
+
`encryptFields` and `maskFields` as JSON arrays; they used to be JSON-encoded strings. Code
|
|
429
|
+
that calls `json.loads` on them must stop. The `Source` model already read either form.
|
|
430
|
+
- Secrets and credentials no longer appear in `repr()` or `str()`:
|
|
431
|
+
- The secrets the API returns, on `DestinationWithSecret`, `DestinationSecret`,
|
|
432
|
+
`SourceWithSecret`, `PendingVerification`, `WebhookEndpoint`, `EndpointWithSecret`,
|
|
433
|
+
`RotateSecretResult`, `ApiKeyWithSecret`, `Tunnel`, `TunnelToken` and `PortalToken`.
|
|
434
|
+
- The headers and auth configs the API returns in full to a key with the `write` scope:
|
|
435
|
+
`Destination.headers` and `auth_config`, `WebhookEndpoint.headers` (and each
|
|
436
|
+
`CustomHeader.value`), and `CronJob.headers`.
|
|
437
|
+
- The credentials you pass in: `signing_secret` and `verify_token` on `CreateSourceParams`
|
|
438
|
+
and `UpdateSourceParams`; `headers`, `auth_config` and `config` on `CreateDestinationParams`
|
|
439
|
+
and `UpdateDestinationParams`; `headers` on the endpoint and cron job params; and the keys
|
|
440
|
+
on `S3Config`, `GCSConfig` and `AzureBlobConfig`.
|
|
441
|
+
|
|
442
|
+
A `pydantic.ValidationError` from one of these models, or from a list of them, leaves out the
|
|
443
|
+
input it was given. The values are still sent and still readable from their attributes
|
|
444
|
+
(`dest.signing_secret`).
|
|
445
|
+
- **Typing:** `CreateDestinationParams.config` and `UpdateDestinationParams.config` are now
|
|
446
|
+
`dict[str, Any] | BridgeConfig | None`, so `params.config["bucket"]` no longer type-checks
|
|
447
|
+
until you narrow it with `isinstance(params.config, dict)`. Because `DestinationType`,
|
|
448
|
+
`TransformType` and `ContentFormat` now include `str`, mypy reads `Destination.type`,
|
|
449
|
+
`Transform.transform_type`, `input_format` and `output_format` as plain `str`. An exhaustive
|
|
450
|
+
`match` or `assert_never` over their literals no longer type-checks.
|
|
451
|
+
|
|
350
452
|
## Upgrading to 4.1
|
|
351
453
|
|
|
352
454
|
- `APIError.code` now carries the API's `code` from the response body (for example
|
|
@@ -13,7 +13,9 @@ source = client.sources.create({
|
|
|
13
13
|
})
|
|
14
14
|
print(f"Source created: {source.id}")
|
|
15
15
|
print(f"Ingest URL: {source.ingest_url}")
|
|
16
|
-
|
|
16
|
+
# The signing secret is returned this once. Put it in your secret store (and GitHub's webhook
|
|
17
|
+
# settings) instead of printing or logging it; client.sources.reveal_secret() shows it again.
|
|
18
|
+
github_signing_secret = source.signing_secret
|
|
17
19
|
|
|
18
20
|
# Create a destination to forward webhooks to
|
|
19
21
|
dest = client.destinations.create({
|
|
@@ -18,7 +18,9 @@ endpoint = client.outbound.endpoints.create(app.id, {
|
|
|
18
18
|
"description": "Production webhook endpoint",
|
|
19
19
|
})
|
|
20
20
|
print(f"Endpoint: {endpoint.id}")
|
|
21
|
-
|
|
21
|
+
# The endpoint's signing secret is returned this once. Put it in your secret store, and hand it to
|
|
22
|
+
# the customer to verify deliveries with, instead of printing or logging it.
|
|
23
|
+
endpoint_secret = endpoint.secret
|
|
22
24
|
|
|
23
25
|
# Create event types
|
|
24
26
|
order_type = client.outbound.event_types.create({
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from collections.abc import AsyncIterator, Iterator
|
|
4
4
|
from typing import Any, Generic, TypeVar
|
|
5
5
|
|
|
6
|
-
from pydantic import TypeAdapter
|
|
6
|
+
from pydantic import ConfigDict, TypeAdapter
|
|
7
7
|
|
|
8
8
|
from ._client import AsyncTransport, SyncTransport
|
|
9
9
|
|
|
@@ -360,7 +360,11 @@ class AsyncLimitOffsetPage(Generic[T]):
|
|
|
360
360
|
|
|
361
361
|
|
|
362
362
|
def _parse_list(raw: Any, model: type[T]) -> list[T]:
|
|
363
|
-
|
|
363
|
+
# pydantic takes hide_input_in_errors from the outermost type, so a list of a model that
|
|
364
|
+
# carries a secret has to ask for it too (models/_base.py SECRET_MODEL_CONFIG).
|
|
365
|
+
hides_input = getattr(model, "model_config", {}).get("hide_input_in_errors", False)
|
|
366
|
+
config = ConfigDict(hide_input_in_errors=True) if hides_input else None
|
|
367
|
+
adapter = TypeAdapter(list[model], config=config) # type: ignore[valid-type]
|
|
364
368
|
return adapter.validate_python(raw)
|
|
365
369
|
|
|
366
370
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "4.2.0"
|
|
@@ -63,9 +63,13 @@ from .deliveries import (
|
|
|
63
63
|
from .destinations import (
|
|
64
64
|
AuthType,
|
|
65
65
|
AzureBlobConfig,
|
|
66
|
+
BridgeConfig,
|
|
67
|
+
BridgeEventTypeSource,
|
|
66
68
|
CreateDestinationParams,
|
|
67
69
|
Destination,
|
|
70
|
+
DestinationSecret,
|
|
68
71
|
DestinationType,
|
|
72
|
+
DestinationWithSecret,
|
|
69
73
|
FieldMapping,
|
|
70
74
|
FieldMappingType,
|
|
71
75
|
FileFormat,
|
|
@@ -109,8 +113,12 @@ from .event_types import CreateEventTypeParams, EventType, UpdateEventTypeParams
|
|
|
109
113
|
from .events import (
|
|
110
114
|
DeliveryStats,
|
|
111
115
|
Event,
|
|
116
|
+
EventDebugDelivery,
|
|
117
|
+
EventDebugDestination,
|
|
112
118
|
EventDebugEvent,
|
|
113
119
|
EventDebugInfo,
|
|
120
|
+
EventDebugPatternHint,
|
|
121
|
+
EventDebugRouteCircuit,
|
|
114
122
|
EventDelivery,
|
|
115
123
|
EventDetail,
|
|
116
124
|
InboundEventStatus,
|
|
@@ -127,6 +135,7 @@ from .messages import (
|
|
|
127
135
|
MessageStatus,
|
|
128
136
|
OutboundAttempt,
|
|
129
137
|
OutboundMessage,
|
|
138
|
+
OutboundMessageBridge,
|
|
130
139
|
SendEventEndpoint,
|
|
131
140
|
SendEventParams,
|
|
132
141
|
SendEventResponse,
|
|
@@ -176,6 +185,8 @@ from .transforms import (
|
|
|
176
185
|
ContentFormat,
|
|
177
186
|
CreateTransformParams,
|
|
178
187
|
Transform,
|
|
188
|
+
TransformEngine,
|
|
189
|
+
TransformEnginesResponse,
|
|
179
190
|
TransformTestResult,
|
|
180
191
|
TransformType,
|
|
181
192
|
UpdateTransformParams,
|
|
@@ -249,9 +260,13 @@ __all__ = [
|
|
|
249
260
|
# Destinations
|
|
250
261
|
"AuthType",
|
|
251
262
|
"AzureBlobConfig",
|
|
263
|
+
"BridgeConfig",
|
|
264
|
+
"BridgeEventTypeSource",
|
|
252
265
|
"CreateDestinationParams",
|
|
253
266
|
"Destination",
|
|
267
|
+
"DestinationSecret",
|
|
254
268
|
"DestinationType",
|
|
269
|
+
"DestinationWithSecret",
|
|
255
270
|
"FieldMapping",
|
|
256
271
|
"GCSConfig",
|
|
257
272
|
"HttpMethod",
|
|
@@ -299,6 +314,10 @@ __all__ = [
|
|
|
299
314
|
"EventDetail",
|
|
300
315
|
"InboundEventStatus",
|
|
301
316
|
"EventDebugEvent",
|
|
317
|
+
"EventDebugDelivery",
|
|
318
|
+
"EventDebugDestination",
|
|
319
|
+
"EventDebugPatternHint",
|
|
320
|
+
"EventDebugRouteCircuit",
|
|
302
321
|
"EventDelivery",
|
|
303
322
|
# Filters
|
|
304
323
|
"CreateFilterParams",
|
|
@@ -311,6 +330,7 @@ __all__ = [
|
|
|
311
330
|
"MessageStatus",
|
|
312
331
|
"OutboundAttempt",
|
|
313
332
|
"OutboundMessage",
|
|
333
|
+
"OutboundMessageBridge",
|
|
314
334
|
"OutboundReplayResult",
|
|
315
335
|
"SendEventParams",
|
|
316
336
|
"SendEventResponse",
|
|
@@ -360,6 +380,8 @@ __all__ = [
|
|
|
360
380
|
"ContentFormat",
|
|
361
381
|
"CreateTransformParams",
|
|
362
382
|
"Transform",
|
|
383
|
+
"TransformEngine",
|
|
384
|
+
"TransformEnginesResponse",
|
|
363
385
|
"TransformTestResult",
|
|
364
386
|
"TransformType",
|
|
365
387
|
"UpdateTransformParams",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
from pydantic.alias_generators import to_camel
|
|
5
|
+
|
|
6
|
+
#: `model_config` for a model that carries a secret or credential; pydantic merges it over
|
|
7
|
+
#: HookbaseModel's. A validation error then leaves out its input, which pydantic otherwise prints
|
|
8
|
+
#: truncated and whose tail can be the secret; `_pagination._parse_list` does the same for a list
|
|
9
|
+
#: of one. Give the secret field `Field(repr=False)` too, for repr() and str().
|
|
10
|
+
SECRET_MODEL_CONFIG = ConfigDict(hide_input_in_errors=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HookbaseModel(BaseModel):
|
|
14
|
+
"""Base model with camelCase alias support for API compatibility."""
|
|
15
|
+
|
|
16
|
+
model_config = ConfigDict(
|
|
17
|
+
alias_generator=to_camel,
|
|
18
|
+
populate_by_name=True,
|
|
19
|
+
extra="ignore",
|
|
20
|
+
)
|
|
@@ -3,9 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from pydantic import field_validator
|
|
6
|
+
from pydantic import Field, field_validator
|
|
7
7
|
|
|
8
|
-
from ._base import HookbaseModel
|
|
8
|
+
from ._base import SECRET_MODEL_CONFIG, HookbaseModel
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class ApiKey(HookbaseModel):
|
|
@@ -27,7 +27,9 @@ class ApiKey(HookbaseModel):
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class ApiKeyWithSecret(ApiKey):
|
|
30
|
-
|
|
30
|
+
model_config = SECRET_MODEL_CONFIG
|
|
31
|
+
|
|
32
|
+
key: str = Field(default="", repr=False)
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
class CreateApiKeyParams(HookbaseModel):
|
|
@@ -3,21 +3,25 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
from typing import Literal
|
|
5
5
|
|
|
6
|
-
from pydantic import field_validator
|
|
6
|
+
from pydantic import Field, field_validator
|
|
7
7
|
|
|
8
|
-
from ._base import HookbaseModel
|
|
8
|
+
from ._base import SECRET_MODEL_CONFIG, HookbaseModel
|
|
9
9
|
|
|
10
10
|
CronJobExecutionStatus = Literal["success", "failed", "running"]
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class CronJob(HookbaseModel):
|
|
14
|
+
# The API returns headers, which hold the job's credentials, in full to a key with the write
|
|
15
|
+
# scope.
|
|
16
|
+
model_config = SECRET_MODEL_CONFIG
|
|
17
|
+
|
|
14
18
|
id: str
|
|
15
19
|
organization_id: str | None = None
|
|
16
20
|
name: str
|
|
17
21
|
description: str | None = None
|
|
18
22
|
url: str = ""
|
|
19
23
|
method: str = "GET"
|
|
20
|
-
headers: dict[str, str] | None = None
|
|
24
|
+
headers: dict[str, str] | None = Field(default=None, repr=False)
|
|
21
25
|
payload: str | None = None
|
|
22
26
|
cron_expression: str = ""
|
|
23
27
|
timezone: str = "UTC"
|
|
@@ -66,11 +70,13 @@ class CronJobExecution(HookbaseModel):
|
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
class CreateCronJobParams(HookbaseModel):
|
|
73
|
+
model_config = SECRET_MODEL_CONFIG
|
|
74
|
+
|
|
69
75
|
name: str
|
|
70
76
|
description: str | None = None
|
|
71
77
|
url: str
|
|
72
78
|
method: str | None = None
|
|
73
|
-
headers: dict[str, str] | None = None
|
|
79
|
+
headers: dict[str, str] | None = Field(default=None, repr=False)
|
|
74
80
|
payload: str | None = None
|
|
75
81
|
cron_expression: str
|
|
76
82
|
timezone: str | None = None
|
|
@@ -84,11 +90,13 @@ class CreateCronJobParams(HookbaseModel):
|
|
|
84
90
|
|
|
85
91
|
|
|
86
92
|
class UpdateCronJobParams(HookbaseModel):
|
|
93
|
+
model_config = SECRET_MODEL_CONFIG
|
|
94
|
+
|
|
87
95
|
name: str | None = None
|
|
88
96
|
description: str | None = None
|
|
89
97
|
url: str | None = None
|
|
90
98
|
method: str | None = None
|
|
91
|
-
headers: dict[str, str] | None = None
|
|
99
|
+
headers: dict[str, str] | None = Field(default=None, repr=False)
|
|
92
100
|
payload: str | None = None
|
|
93
101
|
cron_expression: str | None = None
|
|
94
102
|
timezone: str | None = None
|
|
@@ -8,8 +8,9 @@ from pydantic.alias_generators import to_camel
|
|
|
8
8
|
|
|
9
9
|
from ._base import HookbaseModel
|
|
10
10
|
|
|
11
|
-
# API
|
|
12
|
-
#
|
|
11
|
+
# The API stores: pending, delivered, failed, failed_over, schema_failed, throttled,
|
|
12
|
+
# awaiting_retry (a retry scheduled for later; see `next_retry_at`) and held (parked while the
|
|
13
|
+
# route's circuit is open; see `hold_expires_at`). Any string, so a status added later still parses.
|
|
13
14
|
DeliveryStatus = str
|
|
14
15
|
|
|
15
16
|
|
|
@@ -23,6 +24,11 @@ class Delivery(HookbaseModel):
|
|
|
23
24
|
attempt_count: int = 0
|
|
24
25
|
max_attempts: int = 5
|
|
25
26
|
next_retry_at: str | None = None
|
|
27
|
+
"""When the next retry is due. None for a held delivery: see `hold_expires_at`."""
|
|
28
|
+
hold_expires_at: str | None = None
|
|
29
|
+
"""For a `held` delivery (parked because the route's circuit is open and the route has
|
|
30
|
+
`hold_on_circuit_open`): when the hold ends and the delivery fails if the circuit has not
|
|
31
|
+
closed. None for every other status."""
|
|
26
32
|
response_status: int | None = None
|
|
27
33
|
response_key: str | None = None
|
|
28
34
|
response_headers: dict[str, Any] | None = None
|
|
@@ -121,8 +127,22 @@ class DeliveryDetail(Delivery):
|
|
|
121
127
|
|
|
122
128
|
|
|
123
129
|
class ReplayResult(HookbaseModel):
|
|
130
|
+
"""The result of `deliveries.replay`. The replay is a new delivery; the original keeps its
|
|
131
|
+
status."""
|
|
132
|
+
|
|
124
133
|
delivery_id: str
|
|
134
|
+
"""The new delivery's id."""
|
|
125
135
|
message: str = ""
|
|
136
|
+
modified: bool = False
|
|
137
|
+
"""Whether the replay sent an edited payload (`modified_payload` or `payload_patch`)."""
|
|
138
|
+
destination_overridden: bool = False
|
|
139
|
+
transform_overridden: bool = False
|
|
140
|
+
transform_persisted: bool = False
|
|
141
|
+
"""Whether the transform override was also saved as the route's transform."""
|
|
142
|
+
headers_overridden: bool = False
|
|
143
|
+
target_destination_id: str | None = None
|
|
144
|
+
"""The destination the replay goes to: the override when one was given, otherwise the
|
|
145
|
+
original's."""
|
|
126
146
|
|
|
127
147
|
|
|
128
148
|
class BulkReplayResult(HookbaseModel):
|