hookbase 3.0.0__tar.gz → 3.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-3.0.0 → hookbase-3.2.0}/PKG-INFO +32 -6
- {hookbase-3.0.0 → hookbase-3.2.0}/README.md +31 -5
- {hookbase-3.0.0 → hookbase-3.2.0}/examples/async_usage.py +1 -1
- {hookbase-3.0.0 → hookbase-3.2.0}/examples/basic_inbound.py +1 -1
- {hookbase-3.0.0 → hookbase-3.2.0}/examples/send_webhooks.py +1 -1
- hookbase-3.2.0/src/hookbase/_version.py +1 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/client.py +8 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/__init__.py +35 -0
- hookbase-3.2.0/src/hookbase/models/_wire.py +254 -0
- hookbase-3.2.0/src/hookbase/models/applications.py +70 -0
- hookbase-3.2.0/src/hookbase/models/catalog.py +120 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/destinations.py +120 -12
- hookbase-3.2.0/src/hookbase/models/endpoints.py +275 -0
- hookbase-3.2.0/src/hookbase/models/sources.py +283 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/__init__.py +2 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/_base.py +8 -2
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/applications.py +38 -6
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/destinations.py +7 -5
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/endpoints.py +18 -7
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/events.py +20 -0
- hookbase-3.2.0/src/hookbase/resources/providers.py +54 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/sources.py +82 -2
- {hookbase-3.0.0 → hookbase-3.2.0}/tests/conftest.py +6 -0
- hookbase-3.2.0/tests/resources/test_applications.py +138 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/tests/resources/test_destinations.py +196 -7
- hookbase-3.2.0/tests/resources/test_endpoints.py +432 -0
- hookbase-3.2.0/tests/resources/test_source_contract.py +302 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/tests/resources/test_source_fields.py +55 -2
- hookbase-3.2.0/tests/test_slug_contract.py +143 -0
- hookbase-3.0.0/src/hookbase/_version.py +0 -1
- hookbase-3.0.0/src/hookbase/models/applications.py +0 -26
- hookbase-3.0.0/src/hookbase/models/endpoints.py +0 -79
- hookbase-3.0.0/src/hookbase/models/sources.py +0 -180
- hookbase-3.0.0/tests/resources/test_applications.py +0 -77
- {hookbase-3.0.0 → hookbase-3.2.0}/.github/workflows/python-publish.yml +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/.gitignore +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/LICENSE +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/examples/error_handling.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/examples/verify_webhooks.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/pyproject.toml +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/__init__.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/_client.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/_constants.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/_pagination.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/errors.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/_base.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/analytics.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/api_keys.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/common.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/cron_jobs.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/deliveries.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/dlq.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/event_types.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/events.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/filters.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/messages.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/organizations.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/portal_tokens.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/routes.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/schemas.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/subscriptions.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/transforms.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/models/tunnels.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/py.typed +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/analytics.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/api_keys.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/cron_jobs.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/deliveries.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/dlq.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/event_types.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/filters.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/message_log.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/messages.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/organizations.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/portal_tokens.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/routes.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/schemas.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/subscriptions.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/transforms.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/resources/tunnels.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/src/hookbase/webhook.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/tests/__init__.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/tests/resources/__init__.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/tests/resources/test_cron_jobs.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/tests/resources/test_dlq.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/tests/resources/test_messages.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/tests/resources/test_sources.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/tests/test_client.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/tests/test_errors.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/tests/test_pagination.py +0 -0
- {hookbase-3.0.0 → hookbase-3.2.0}/tests/test_webhook.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hookbase
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.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
|
|
@@ -98,7 +98,26 @@ client.sources.create({"name": "GitHub", "slug": "github", "provider": "github"}
|
|
|
98
98
|
client.sources.get("src_id")
|
|
99
99
|
client.sources.update("src_id", {"name": "Updated"})
|
|
100
100
|
client.sources.delete("src_id")
|
|
101
|
-
client.sources.rotate_secret("src_id")
|
|
101
|
+
client.sources.rotate_secret("src_id") # 400 for vendor-issued/public-key/built-in secrets
|
|
102
|
+
client.sources.reveal_secret("src_id")
|
|
103
|
+
|
|
104
|
+
# A provider that verifies with no secret: signing_secret comes back None
|
|
105
|
+
wise = client.sources.create({"name": "Wise", "slug": "wise", "provider": "wise"})
|
|
106
|
+
assert wise.verifies_without_secret and wise.signing_secret is None
|
|
107
|
+
|
|
108
|
+
# WhatsApp (Meta) sources also carry a verify token
|
|
109
|
+
wa = client.sources.create({"name": "WhatsApp", "slug": "whatsapp", "provider": "whatsapp"})
|
|
110
|
+
print(wa.verify_token)
|
|
111
|
+
client.sources.rotate_verify_token(wa.id)
|
|
112
|
+
|
|
113
|
+
# Notion parks its verification token until you adopt it
|
|
114
|
+
pending = client.sources.get_pending_verification("notion")
|
|
115
|
+
client.sources.adopt_pending_verification("notion")
|
|
116
|
+
|
|
117
|
+
# Provider catalog: which secret each provider needs (public, no auth needed)
|
|
118
|
+
for p in client.providers.list(category="payments").providers:
|
|
119
|
+
print(p.hookbase_provider, p.signature_verification and p.signature_verification.secret_source)
|
|
120
|
+
client.providers.get("stripe").signature_verification
|
|
102
121
|
|
|
103
122
|
# Destinations
|
|
104
123
|
client.destinations.list()
|
|
@@ -111,7 +130,7 @@ client.routes.get_circuit_status("route_id")
|
|
|
111
130
|
client.routes.reset_circuit("route_id")
|
|
112
131
|
|
|
113
132
|
# Events & Deliveries
|
|
114
|
-
client.events.list(source_id="src_id", from_date="2024-01-01")
|
|
133
|
+
client.events.list(source_id="src_id", from_date="2024-01-01") # id or slug; unknown -> 404
|
|
115
134
|
client.events.get("evt_id") # Includes payload and deliveries
|
|
116
135
|
client.deliveries.replay("del_id")
|
|
117
136
|
client.deliveries.bulk_replay(["del_1", "del_2"])
|
|
@@ -129,13 +148,17 @@ client.schemas.create({"name": "OrderSchema", "jsonSchema": {"type": "object", .
|
|
|
129
148
|
|
|
130
149
|
```python
|
|
131
150
|
# Applications
|
|
132
|
-
client.outbound.applications.create({"name": "Acme", "
|
|
151
|
+
client.outbound.applications.create({"name": "Acme", "externalId": "cust_123"})
|
|
133
152
|
client.outbound.applications.get_by_external_id("cust_123")
|
|
134
153
|
|
|
135
154
|
# Endpoints
|
|
136
155
|
client.outbound.endpoints.create("app_id", {"url": "https://..."})
|
|
137
156
|
client.outbound.endpoints.rotate_secret("ep_id", grace_period=3600)
|
|
138
157
|
|
|
158
|
+
# An unset field is left alone by an update, so `None` cannot also mean "reset
|
|
159
|
+
# this". Name the setting in `clear` to send an explicit null for it:
|
|
160
|
+
client.outbound.endpoints.update("ep_id", {"clear": ["retryDelays"]})
|
|
161
|
+
|
|
139
162
|
# Event Types & Subscriptions
|
|
140
163
|
client.outbound.event_types.create({"name": "order.created", "category": "orders"})
|
|
141
164
|
client.outbound.subscriptions.create({"endpointId": "ep_id", "eventTypeId": "et_id"})
|
|
@@ -248,11 +271,14 @@ All responses are typed Pydantic models with camelCase alias support:
|
|
|
248
271
|
from hookbase.models import Source, CreateSourceParams
|
|
249
272
|
|
|
250
273
|
# Use Pydantic models for type safety
|
|
251
|
-
params = CreateSourceParams(name="GitHub", provider="github")
|
|
274
|
+
params = CreateSourceParams(name="GitHub", slug="github", provider="github")
|
|
252
275
|
source = client.sources.create(params)
|
|
253
276
|
|
|
254
277
|
# Or use plain dicts
|
|
255
|
-
source = client.sources.create({"name": "GitHub", "provider": "github"})
|
|
278
|
+
source = client.sources.create({"name": "GitHub", "slug": "github", "provider": "github"})
|
|
279
|
+
|
|
280
|
+
# `provider` is any string (hookbase.models.KNOWN_SOURCE_PROVIDERS lists today's values), so a
|
|
281
|
+
# source with a provider newer than this package still parses.
|
|
256
282
|
```
|
|
257
283
|
|
|
258
284
|
## Development
|
|
@@ -63,7 +63,26 @@ client.sources.create({"name": "GitHub", "slug": "github", "provider": "github"}
|
|
|
63
63
|
client.sources.get("src_id")
|
|
64
64
|
client.sources.update("src_id", {"name": "Updated"})
|
|
65
65
|
client.sources.delete("src_id")
|
|
66
|
-
client.sources.rotate_secret("src_id")
|
|
66
|
+
client.sources.rotate_secret("src_id") # 400 for vendor-issued/public-key/built-in secrets
|
|
67
|
+
client.sources.reveal_secret("src_id")
|
|
68
|
+
|
|
69
|
+
# A provider that verifies with no secret: signing_secret comes back None
|
|
70
|
+
wise = client.sources.create({"name": "Wise", "slug": "wise", "provider": "wise"})
|
|
71
|
+
assert wise.verifies_without_secret and wise.signing_secret is None
|
|
72
|
+
|
|
73
|
+
# WhatsApp (Meta) sources also carry a verify token
|
|
74
|
+
wa = client.sources.create({"name": "WhatsApp", "slug": "whatsapp", "provider": "whatsapp"})
|
|
75
|
+
print(wa.verify_token)
|
|
76
|
+
client.sources.rotate_verify_token(wa.id)
|
|
77
|
+
|
|
78
|
+
# Notion parks its verification token until you adopt it
|
|
79
|
+
pending = client.sources.get_pending_verification("notion")
|
|
80
|
+
client.sources.adopt_pending_verification("notion")
|
|
81
|
+
|
|
82
|
+
# Provider catalog: which secret each provider needs (public, no auth needed)
|
|
83
|
+
for p in client.providers.list(category="payments").providers:
|
|
84
|
+
print(p.hookbase_provider, p.signature_verification and p.signature_verification.secret_source)
|
|
85
|
+
client.providers.get("stripe").signature_verification
|
|
67
86
|
|
|
68
87
|
# Destinations
|
|
69
88
|
client.destinations.list()
|
|
@@ -76,7 +95,7 @@ client.routes.get_circuit_status("route_id")
|
|
|
76
95
|
client.routes.reset_circuit("route_id")
|
|
77
96
|
|
|
78
97
|
# Events & Deliveries
|
|
79
|
-
client.events.list(source_id="src_id", from_date="2024-01-01")
|
|
98
|
+
client.events.list(source_id="src_id", from_date="2024-01-01") # id or slug; unknown -> 404
|
|
80
99
|
client.events.get("evt_id") # Includes payload and deliveries
|
|
81
100
|
client.deliveries.replay("del_id")
|
|
82
101
|
client.deliveries.bulk_replay(["del_1", "del_2"])
|
|
@@ -94,13 +113,17 @@ client.schemas.create({"name": "OrderSchema", "jsonSchema": {"type": "object", .
|
|
|
94
113
|
|
|
95
114
|
```python
|
|
96
115
|
# Applications
|
|
97
|
-
client.outbound.applications.create({"name": "Acme", "
|
|
116
|
+
client.outbound.applications.create({"name": "Acme", "externalId": "cust_123"})
|
|
98
117
|
client.outbound.applications.get_by_external_id("cust_123")
|
|
99
118
|
|
|
100
119
|
# Endpoints
|
|
101
120
|
client.outbound.endpoints.create("app_id", {"url": "https://..."})
|
|
102
121
|
client.outbound.endpoints.rotate_secret("ep_id", grace_period=3600)
|
|
103
122
|
|
|
123
|
+
# An unset field is left alone by an update, so `None` cannot also mean "reset
|
|
124
|
+
# this". Name the setting in `clear` to send an explicit null for it:
|
|
125
|
+
client.outbound.endpoints.update("ep_id", {"clear": ["retryDelays"]})
|
|
126
|
+
|
|
104
127
|
# Event Types & Subscriptions
|
|
105
128
|
client.outbound.event_types.create({"name": "order.created", "category": "orders"})
|
|
106
129
|
client.outbound.subscriptions.create({"endpointId": "ep_id", "eventTypeId": "et_id"})
|
|
@@ -213,11 +236,14 @@ All responses are typed Pydantic models with camelCase alias support:
|
|
|
213
236
|
from hookbase.models import Source, CreateSourceParams
|
|
214
237
|
|
|
215
238
|
# Use Pydantic models for type safety
|
|
216
|
-
params = CreateSourceParams(name="GitHub", provider="github")
|
|
239
|
+
params = CreateSourceParams(name="GitHub", slug="github", provider="github")
|
|
217
240
|
source = client.sources.create(params)
|
|
218
241
|
|
|
219
242
|
# Or use plain dicts
|
|
220
|
-
source = client.sources.create({"name": "GitHub", "provider": "github"})
|
|
243
|
+
source = client.sources.create({"name": "GitHub", "slug": "github", "provider": "github"})
|
|
244
|
+
|
|
245
|
+
# `provider` is any string (hookbase.models.KNOWN_SOURCE_PROVIDERS lists today's values), so a
|
|
246
|
+
# source with a provider newer than this package still parses.
|
|
221
247
|
```
|
|
222
248
|
|
|
223
249
|
## Development
|
|
@@ -29,7 +29,7 @@ async def main():
|
|
|
29
29
|
# Paginate through all applications
|
|
30
30
|
page = await client.outbound.applications.list(limit=10)
|
|
31
31
|
async for app in page.auto_paging_iter():
|
|
32
|
-
print(f" App: {app.name} (
|
|
32
|
+
print(f" App: {app.name} (externalId={app.external_id})")
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
if __name__ == "__main__":
|
|
@@ -7,7 +7,7 @@ client = Hookbase(api_key="whr_your_api_key")
|
|
|
7
7
|
# Create an application (represents a customer/tenant)
|
|
8
8
|
app = client.outbound.applications.create({
|
|
9
9
|
"name": "Acme Corp",
|
|
10
|
-
"
|
|
10
|
+
"externalId": "cust_123",
|
|
11
11
|
"metadata": {"plan": "pro"},
|
|
12
12
|
})
|
|
13
13
|
print(f"Application: {app.id}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.2.0"
|
|
@@ -26,6 +26,7 @@ from .resources import (
|
|
|
26
26
|
AsyncMessages,
|
|
27
27
|
AsyncOrganizations,
|
|
28
28
|
AsyncPortalTokens,
|
|
29
|
+
AsyncProviders,
|
|
29
30
|
AsyncRoutes,
|
|
30
31
|
AsyncSchemas,
|
|
31
32
|
AsyncSources,
|
|
@@ -43,6 +44,7 @@ from .resources import (
|
|
|
43
44
|
Messages,
|
|
44
45
|
Organizations,
|
|
45
46
|
PortalTokens,
|
|
47
|
+
Providers,
|
|
46
48
|
Routes,
|
|
47
49
|
Schemas,
|
|
48
50
|
Sources,
|
|
@@ -118,6 +120,8 @@ class Hookbase:
|
|
|
118
120
|
transforms: Transforms
|
|
119
121
|
filters: Filters
|
|
120
122
|
schemas: Schemas
|
|
123
|
+
#: The public provider catalog (what each provider signs with, which secret it needs).
|
|
124
|
+
providers: Providers
|
|
121
125
|
|
|
122
126
|
# Outbound
|
|
123
127
|
outbound: _OutboundNamespace
|
|
@@ -160,6 +164,7 @@ class Hookbase:
|
|
|
160
164
|
self.transforms = Transforms(self._transport)
|
|
161
165
|
self.filters = Filters(self._transport)
|
|
162
166
|
self.schemas = Schemas(self._transport)
|
|
167
|
+
self.providers = Providers(self._transport)
|
|
163
168
|
|
|
164
169
|
# Outbound namespace
|
|
165
170
|
self.outbound = _OutboundNamespace(self._transport)
|
|
@@ -210,6 +215,8 @@ class AsyncHookbase:
|
|
|
210
215
|
transforms: AsyncTransforms
|
|
211
216
|
filters: AsyncFilters
|
|
212
217
|
schemas: AsyncSchemas
|
|
218
|
+
#: The public provider catalog (what each provider signs with, which secret it needs).
|
|
219
|
+
providers: AsyncProviders
|
|
213
220
|
|
|
214
221
|
# Outbound
|
|
215
222
|
outbound: _AsyncOutboundNamespace
|
|
@@ -252,6 +259,7 @@ class AsyncHookbase:
|
|
|
252
259
|
self.transforms = AsyncTransforms(self._transport)
|
|
253
260
|
self.filters = AsyncFilters(self._transport)
|
|
254
261
|
self.schemas = AsyncSchemas(self._transport)
|
|
262
|
+
self.providers = AsyncProviders(self._transport)
|
|
255
263
|
|
|
256
264
|
# Outbound namespace
|
|
257
265
|
self.outbound = _AsyncOutboundNamespace(self._transport)
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
from .analytics import AnalyticsTimeline, DashboardData
|
|
2
2
|
from .api_keys import ApiKey, ApiKeyWithSecret, CreateApiKeyParams
|
|
3
3
|
from .applications import Application, CreateApplicationParams, UpdateApplicationParams
|
|
4
|
+
from .catalog import (
|
|
5
|
+
CatalogProvider,
|
|
6
|
+
CatalogProviderDetail,
|
|
7
|
+
ProviderCatalogList,
|
|
8
|
+
ProviderEvents,
|
|
9
|
+
ProviderEventType,
|
|
10
|
+
ProviderSignatureVerification,
|
|
11
|
+
ProviderSignatureVerificationSummary,
|
|
12
|
+
ProviderVerifyTokenInfo,
|
|
13
|
+
)
|
|
4
14
|
from .common import BulkDeleteResult, ImportResult, ImportResultItem
|
|
5
15
|
from .cron_jobs import (
|
|
6
16
|
CreateCronGroupParams,
|
|
@@ -42,7 +52,11 @@ from .dlq import (
|
|
|
42
52
|
DlqStats,
|
|
43
53
|
)
|
|
44
54
|
from .endpoints import (
|
|
55
|
+
CLEARABLE_ENDPOINT_FIELDS,
|
|
56
|
+
BackoffType,
|
|
57
|
+
ClearableEndpointField,
|
|
45
58
|
CreateEndpointParams,
|
|
59
|
+
CustomHeader,
|
|
46
60
|
EndpointStats,
|
|
47
61
|
EndpointWithSecret,
|
|
48
62
|
RotateSecretResult,
|
|
@@ -85,9 +99,13 @@ from .routes import (
|
|
|
85
99
|
)
|
|
86
100
|
from .schemas import CreateSchemaParams, Schema, SchemaValidationResult, UpdateSchemaParams
|
|
87
101
|
from .sources import (
|
|
102
|
+
CLEARABLE_SOURCE_FIELDS,
|
|
103
|
+
KNOWN_SOURCE_PROVIDERS,
|
|
104
|
+
ClearableSourceField,
|
|
88
105
|
CreateSourceParams,
|
|
89
106
|
DedupStrategy,
|
|
90
107
|
IpFilterMode,
|
|
108
|
+
PendingVerification,
|
|
91
109
|
Source,
|
|
92
110
|
SourceProvider,
|
|
93
111
|
SourceWithSecret,
|
|
@@ -115,6 +133,15 @@ __all__ = [
|
|
|
115
133
|
"Application",
|
|
116
134
|
"CreateApplicationParams",
|
|
117
135
|
"UpdateApplicationParams",
|
|
136
|
+
# Catalog
|
|
137
|
+
"CatalogProvider",
|
|
138
|
+
"CatalogProviderDetail",
|
|
139
|
+
"ProviderCatalogList",
|
|
140
|
+
"ProviderEventType",
|
|
141
|
+
"ProviderEvents",
|
|
142
|
+
"ProviderSignatureVerification",
|
|
143
|
+
"ProviderSignatureVerificationSummary",
|
|
144
|
+
"ProviderVerifyTokenInfo",
|
|
118
145
|
# Common
|
|
119
146
|
"BulkDeleteResult",
|
|
120
147
|
"ImportResult",
|
|
@@ -155,7 +182,11 @@ __all__ = [
|
|
|
155
182
|
"DlqRetryResult",
|
|
156
183
|
"DlqStats",
|
|
157
184
|
# Endpoints
|
|
185
|
+
"CLEARABLE_ENDPOINT_FIELDS",
|
|
186
|
+
"BackoffType",
|
|
187
|
+
"ClearableEndpointField",
|
|
158
188
|
"CreateEndpointParams",
|
|
189
|
+
"CustomHeader",
|
|
159
190
|
"EndpointStats",
|
|
160
191
|
"EndpointWithSecret",
|
|
161
192
|
"RotateSecretResult",
|
|
@@ -203,9 +234,13 @@ __all__ = [
|
|
|
203
234
|
"SchemaValidationResult",
|
|
204
235
|
"UpdateSchemaParams",
|
|
205
236
|
# Sources
|
|
237
|
+
"CLEARABLE_SOURCE_FIELDS",
|
|
238
|
+
"KNOWN_SOURCE_PROVIDERS",
|
|
239
|
+
"ClearableSourceField",
|
|
206
240
|
"CreateSourceParams",
|
|
207
241
|
"DedupStrategy",
|
|
208
242
|
"IpFilterMode",
|
|
243
|
+
"PendingVerification",
|
|
209
244
|
"Source",
|
|
210
245
|
"SourceProvider",
|
|
211
246
|
"SourceWithSecret",
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Normalization of request bodies onto the shapes the API actually accepts.
|
|
2
|
+
|
|
3
|
+
The SDK has historically put field names on the wire that the API never
|
|
4
|
+
accepted (endpoint `filterTypes`/`rateLimitPeriod`/`metadata`, destination
|
|
5
|
+
`description`/`retryCount`/`retryInterval`) alongside a few it renamed
|
|
6
|
+
(`rateLimit` -> `rateLimitPerSecond`, `timeout` -> `timeoutMs`, `uid` ->
|
|
7
|
+
`externalId`). The API validates with `z.object()`, which silently strips
|
|
8
|
+
unknown keys, so those requests came back 2xx with the field quietly ignored —
|
|
9
|
+
and the resource configured differently from what the caller asked for. Those
|
|
10
|
+
schemas are moving to `.strict()`, at which point the same requests become
|
|
11
|
+
400s.
|
|
12
|
+
|
|
13
|
+
Deleting the public fields would break callers, so the models keep them,
|
|
14
|
+
document them as deprecated, and these helpers translate them at the wire
|
|
15
|
+
boundary. They deliberately operate on the already-serialized body rather than
|
|
16
|
+
on the model, so a caller who passes a plain dict — which every resource method
|
|
17
|
+
accepts, and which skips model validation entirely — gets the same translation.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import re
|
|
24
|
+
import unicodedata
|
|
25
|
+
from collections.abc import Iterable
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from ..errors import HookbaseError
|
|
29
|
+
|
|
30
|
+
# Params models serialize through `to_camel`, but `populate_by_name` means a
|
|
31
|
+
# caller may also use the snake_case field name, and a caller passing a plain
|
|
32
|
+
# dict can put that spelling straight on the wire.
|
|
33
|
+
_CAMEL_BOUNDARY = re.compile(r"(?<!^)(?=[A-Z])")
|
|
34
|
+
|
|
35
|
+
# Slug derivation mirrors `deriveDestinationSlug` in
|
|
36
|
+
# sdk/node-sdk/src/resources/wire.ts step for step, so all four SDKs derive
|
|
37
|
+
# byte-identical slugs from the same name. Keep the two in sync.
|
|
38
|
+
|
|
39
|
+
_SLUG_SEPARATORS = re.compile(r"[^a-z0-9]+")
|
|
40
|
+
_SLUG_EDGE_HYPHENS = re.compile(r"^-+|-+$")
|
|
41
|
+
_SLUG_TRAILING_HYPHENS = re.compile(r"-+$")
|
|
42
|
+
SLUG_MAX_LENGTH = 50
|
|
43
|
+
|
|
44
|
+
# Which characters count as combining marks is a function of the Unicode version the *runtime*
|
|
45
|
+
# ships, and the four SDKs' runtimes do not agree: CPython 3.12's `unicodedata` is Unicode 15.0,
|
|
46
|
+
# while the reference implementation's `\p{Mn}` (V8) is a version ahead. Reading the category alone
|
|
47
|
+
# therefore makes the same name slug differently depending on which SDK created the destination,
|
|
48
|
+
# which is the thing this derivation exists to prevent.
|
|
49
|
+
#
|
|
50
|
+
# These two tables close that gap, generated from the reference runtime's own `\p{Mn}` compared
|
|
51
|
+
# against Unicode 15.0's. Both self-heal: once this interpreter's tables cover the additions, the
|
|
52
|
+
# set is redundant but harmless, and `test_slug_contract.py` says so when that happens.
|
|
53
|
+
#
|
|
54
|
+
# 75 code points in 21 ranges, the same table `slugMarkAdditions` carries in
|
|
55
|
+
# sdk/go-sdk/slug_fold.go. Change one and you change both.
|
|
56
|
+
_SLUG_MARK_ADDITIONS: frozenset[str] = frozenset(
|
|
57
|
+
chr(cp)
|
|
58
|
+
for lo, hi in (
|
|
59
|
+
("\u0897", "\u0897"),
|
|
60
|
+
("\u1acf", "\u1add"),
|
|
61
|
+
("\u1ae0", "\u1aeb"),
|
|
62
|
+
("\U00010d69", "\U00010d6d"),
|
|
63
|
+
("\U00010efa", "\U00010efc"),
|
|
64
|
+
("\U000113bb", "\U000113c0"),
|
|
65
|
+
("\U000113ce", "\U000113ce"),
|
|
66
|
+
("\U000113d0", "\U000113d0"),
|
|
67
|
+
("\U000113d2", "\U000113d2"),
|
|
68
|
+
("\U000113e1", "\U000113e2"),
|
|
69
|
+
("\U00011b60", "\U00011b60"),
|
|
70
|
+
("\U00011b62", "\U00011b64"),
|
|
71
|
+
("\U00011b66", "\U00011b66"),
|
|
72
|
+
("\U00011f5a", "\U00011f5a"),
|
|
73
|
+
("\U0001611e", "\U00016129"),
|
|
74
|
+
("\U0001612d", "\U0001612f"),
|
|
75
|
+
("\U0001e5ee", "\U0001e5ef"),
|
|
76
|
+
("\U0001e6e3", "\U0001e6e3"),
|
|
77
|
+
("\U0001e6e6", "\U0001e6e6"),
|
|
78
|
+
("\U0001e6ee", "\U0001e6ef"),
|
|
79
|
+
("\U0001e6f5", "\U0001e6f5"),
|
|
80
|
+
)
|
|
81
|
+
for cp in range(ord(lo), ord(hi) + 1)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# The correction in the other direction: AHOM CONSONANT SIGN MEDIAL RA was Mn in Unicode 15.0 and
|
|
85
|
+
# is Mc (a *spacing* mark) from 15.1, so the reference does not strip it and neither may this
|
|
86
|
+
# SDK -- it has to separate, the way any other non-alphanumeric does.
|
|
87
|
+
_SLUG_MARK_RECLASSIFIED = "\U0001171e"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# The same version skew, one layer down: NFKD itself. These characters are unassigned in Unicode
|
|
91
|
+
# 15.0, so this interpreter leaves them alone and they become hyphen separators, while the
|
|
92
|
+
# reference runtime decomposes them to ASCII letters and digits. Folding them first makes NFKD's
|
|
93
|
+
# answer the same on both. Every mapping is the reference's own NFKD output, not a guess at the
|
|
94
|
+
# character's meaning; a later interpreter that knows them decomposes them identically, so this
|
|
95
|
+
# stays a no-op rather than a divergence of its own.
|
|
96
|
+
#
|
|
97
|
+
# U+A7F1 Latin Extended-D, decomposes to "S"
|
|
98
|
+
# U+1CCD6 - U+1CCF9 36 contiguous additions in Symbols for Legacy Computing Supplement,
|
|
99
|
+
# decomposing to A-Z and then 0-9 in order
|
|
100
|
+
_SLUG_FOLD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
101
|
+
_SLUG_FOLD_ADDITIONS = {
|
|
102
|
+
0xA7F1: "S",
|
|
103
|
+
**{0x1CCD6 + offset: char for offset, char in enumerate(_SLUG_FOLD_ALPHABET)},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _is_slug_mark(char: str) -> bool:
|
|
108
|
+
"""Whether `char` is a combining mark the slug fold drops. See the tables above."""
|
|
109
|
+
if char == _SLUG_MARK_RECLASSIFIED:
|
|
110
|
+
return False
|
|
111
|
+
return unicodedata.category(char) == "Mn" or char in _SLUG_MARK_ADDITIONS
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def dump(params: Any) -> dict[str, Any]:
|
|
115
|
+
"""Serialize a params model into a wire body, or copy a caller's dict.
|
|
116
|
+
|
|
117
|
+
The copy matters: these helpers mutate the body in place and callers hand
|
|
118
|
+
us dicts they may reuse.
|
|
119
|
+
"""
|
|
120
|
+
if hasattr(params, "model_dump"):
|
|
121
|
+
body: dict[str, Any] = params.model_dump(by_alias=True, exclude_none=True)
|
|
122
|
+
return body
|
|
123
|
+
return dict(params or {})
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _spellings(key: str) -> tuple[str, ...]:
|
|
127
|
+
"""Every spelling of one wire key a caller may have used."""
|
|
128
|
+
snake = _CAMEL_BOUNDARY.sub("_", key).lower()
|
|
129
|
+
return (key,) if snake == key else (key, snake)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def drop(body: dict[str, Any], *keys: str) -> None:
|
|
133
|
+
"""Remove keys the API never accepted."""
|
|
134
|
+
for key in keys:
|
|
135
|
+
for spelling in _spellings(key):
|
|
136
|
+
body.pop(spelling, None)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def canonicalize(body: dict[str, Any], *keys: str) -> None:
|
|
140
|
+
"""Move any snake_case spelling of `keys` onto its camelCase wire key."""
|
|
141
|
+
for key in keys:
|
|
142
|
+
for spelling in _spellings(key)[1:]:
|
|
143
|
+
if spelling in body:
|
|
144
|
+
body.setdefault(key, body.pop(spelling))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def rename(body: dict[str, Any], old: str, new: str) -> None:
|
|
148
|
+
"""Send `old`'s value under `new`; an explicit `new` wins over `old`."""
|
|
149
|
+
canonicalize(body, new)
|
|
150
|
+
value: Any = None
|
|
151
|
+
found = False
|
|
152
|
+
for spelling in _spellings(old):
|
|
153
|
+
if spelling in body:
|
|
154
|
+
if not found:
|
|
155
|
+
value = body[spelling]
|
|
156
|
+
found = True
|
|
157
|
+
del body[spelling]
|
|
158
|
+
if found and new not in body:
|
|
159
|
+
body[new] = value
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def apply_clear(body: dict[str, Any], allowed: tuple[str, ...], what: str) -> None:
|
|
163
|
+
"""Turn a params model's `clear` list into explicit JSON nulls on the body.
|
|
164
|
+
|
|
165
|
+
Every optional field on every params model is `X | None = None`, and `dump`
|
|
166
|
+
excludes `None` -- so `None` means "leave this setting alone" and there is
|
|
167
|
+
nothing left to mean "reset it", even where the API accepts an explicit
|
|
168
|
+
null. A caller who had set a custom retry schedule could not remove it.
|
|
169
|
+
|
|
170
|
+
`clear` closes that: naming a field puts `null` on the wire for it. Only
|
|
171
|
+
the fields in `allowed` are accepted, and a field both set and cleared is
|
|
172
|
+
an error rather than a silent choice between the two. `clear` itself never
|
|
173
|
+
reaches the API.
|
|
174
|
+
"""
|
|
175
|
+
requested: Any = None
|
|
176
|
+
for spelling in _spellings("clear"):
|
|
177
|
+
if spelling in body:
|
|
178
|
+
requested = body.pop(spelling)
|
|
179
|
+
|
|
180
|
+
if requested is None:
|
|
181
|
+
return
|
|
182
|
+
if isinstance(requested, str) or not isinstance(requested, Iterable):
|
|
183
|
+
raise HookbaseError(
|
|
184
|
+
f"`clear` takes a list of {what} field names, not {type(requested).__name__}."
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
for field in requested:
|
|
188
|
+
key = _CAMEL_BOUNDARY.sub("_", str(field)).lower()
|
|
189
|
+
wire = {_CAMEL_BOUNDARY.sub("_", name).lower(): name for name in allowed}.get(key)
|
|
190
|
+
if wire is None:
|
|
191
|
+
raise HookbaseError(
|
|
192
|
+
f"`clear` names {str(field)!r}, which is not one of the {what} fields the API "
|
|
193
|
+
f"accepts a null for. It accepts: {', '.join(sorted(allowed))}."
|
|
194
|
+
)
|
|
195
|
+
if wire in body:
|
|
196
|
+
raise HookbaseError(
|
|
197
|
+
f"`clear` names {str(field)!r} but that field is also set; clear it or set it, "
|
|
198
|
+
"not both."
|
|
199
|
+
)
|
|
200
|
+
body[wire] = None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def headers_to_pairs(body: dict[str, Any]) -> None:
|
|
204
|
+
"""Convert endpoint `headers` from a mapping to the array the API takes.
|
|
205
|
+
|
|
206
|
+
`POST`/`PATCH /api/webhook-endpoints` validate headers as
|
|
207
|
+
`z.array({name, value}).max(10)`, so the mapping the SDK has always typed
|
|
208
|
+
here is rejected outright — a 400 today rather than a silently stripped
|
|
209
|
+
field. A mapping is converted in insertion order; an array a caller built
|
|
210
|
+
themselves passes through untouched.
|
|
211
|
+
"""
|
|
212
|
+
value = body.get("headers")
|
|
213
|
+
if isinstance(value, dict):
|
|
214
|
+
body["headers"] = [{"name": str(name), "value": str(v)} for name, v in value.items()]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def derive_destination_slug(name: str) -> str:
|
|
218
|
+
"""Derive a slug the API accepts (`^[a-z0-9-]+$`, max 50) from a name.
|
|
219
|
+
|
|
220
|
+
The API requires `slug` on create; this SDK has always had it optional, so
|
|
221
|
+
every call that left it out was a 400. Deriving one keeps those calls
|
|
222
|
+
working. Accents are folded first, so `Café EU` gives `cafe-eu` rather than
|
|
223
|
+
`caf-eu`.
|
|
224
|
+
|
|
225
|
+
Every step mirrors `deriveDestinationSlug` in the Node SDK
|
|
226
|
+
(sdk/node-sdk/src/resources/wire.ts) so all four SDKs agree byte for byte:
|
|
227
|
+
NFKD normalize, drop the combining marks the decomposition leaves behind,
|
|
228
|
+
lowercase, collapse each run of non-alphanumerics to one hyphen, trim the
|
|
229
|
+
ends, then cut to 50 characters. The cut is plain and may land mid-word —
|
|
230
|
+
that is what keeps the implementations identical, so do not trim back to a
|
|
231
|
+
word boundary — and can leave a trailing hyphen to remove.
|
|
232
|
+
|
|
233
|
+
Raises:
|
|
234
|
+
HookbaseError: if the name has no alphanumeric characters to derive
|
|
235
|
+
from, rather than sending a slug the API would reject.
|
|
236
|
+
"""
|
|
237
|
+
# Drop the whole Mn category, not just the U+0300-U+036F block: a mark outside that block
|
|
238
|
+
# (Arabic, Hebrew, Devanagari) would otherwise survive to become a hyphen separator, and the
|
|
239
|
+
# same name would slug differently depending on which SDK created the destination.
|
|
240
|
+
# `_is_slug_mark` rather than the category alone, because the category answer depends on this
|
|
241
|
+
# interpreter's Unicode version -- see the tables above it.
|
|
242
|
+
decomposed = unicodedata.normalize("NFKD", name.translate(_SLUG_FOLD_ADDITIONS))
|
|
243
|
+
folded = "".join(ch for ch in decomposed if not _is_slug_mark(ch)).lower()
|
|
244
|
+
slug = _SLUG_EDGE_HYPHENS.sub("", _SLUG_SEPARATORS.sub("-", folded))
|
|
245
|
+
slug = _SLUG_TRAILING_HYPHENS.sub("", slug[:SLUG_MAX_LENGTH])
|
|
246
|
+
|
|
247
|
+
if not slug:
|
|
248
|
+
raise HookbaseError(
|
|
249
|
+
"Cannot derive a destination slug from name "
|
|
250
|
+
f"{json.dumps(name, ensure_ascii=False)}: pass "
|
|
251
|
+
"`slug` explicitly, as a string matching ^[a-z0-9-]+$ (max 50 characters)."
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return slug
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import model_validator
|
|
6
|
+
|
|
7
|
+
from . import _wire
|
|
8
|
+
from ._base import HookbaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Application(HookbaseModel):
|
|
12
|
+
id: str
|
|
13
|
+
name: str
|
|
14
|
+
organization_id: str
|
|
15
|
+
external_id: str = ""
|
|
16
|
+
metadata: dict[str, Any] | None = None
|
|
17
|
+
created_at: str = ""
|
|
18
|
+
updated_at: str = ""
|
|
19
|
+
|
|
20
|
+
uid: str = ""
|
|
21
|
+
"""Deprecated: the API calls this `externalId`; read `external_id` instead.
|
|
22
|
+
Kept in step with it so existing attribute access keeps working."""
|
|
23
|
+
|
|
24
|
+
@model_validator(mode="before")
|
|
25
|
+
@classmethod
|
|
26
|
+
def _mirror_external_id(cls, data: Any) -> Any:
|
|
27
|
+
"""Keep `uid` and `external_id` showing the same value.
|
|
28
|
+
|
|
29
|
+
Application responses carry the field as `externalId`; `uid` is only
|
|
30
|
+
the SDK's older name for it, so populate both from whichever spelling
|
|
31
|
+
the response used. `externalId` is null for applications created
|
|
32
|
+
without one, so fall back to the empty string both fields default to.
|
|
33
|
+
"""
|
|
34
|
+
if not isinstance(data, dict):
|
|
35
|
+
return data
|
|
36
|
+
value = data.get("externalId")
|
|
37
|
+
if value is None:
|
|
38
|
+
value = data.get("external_id")
|
|
39
|
+
if value is None:
|
|
40
|
+
value = data.get("uid")
|
|
41
|
+
return {**data, "externalId": value or "", "uid": value or ""}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CreateApplicationParams(HookbaseModel):
|
|
45
|
+
name: str
|
|
46
|
+
external_id: str | None = None
|
|
47
|
+
metadata: dict[str, Any] | None = None
|
|
48
|
+
|
|
49
|
+
uid: str | None = None
|
|
50
|
+
"""Deprecated: renamed to `external_id`, which is the only name the API
|
|
51
|
+
accepts. The value is sent under that name, and `external_id` wins if both
|
|
52
|
+
are set."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class UpdateApplicationParams(HookbaseModel):
|
|
56
|
+
name: str | None = None
|
|
57
|
+
metadata: dict[str, Any] | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def application_body(params: CreateApplicationParams | dict[str, Any]) -> dict[str, Any]:
|
|
61
|
+
"""Build the request body for an application create or upsert.
|
|
62
|
+
|
|
63
|
+
The API has always called the caller's own identifier `externalId`; the SDK
|
|
64
|
+
sent `uid`, which `z.object()` stripped, so every application the Python
|
|
65
|
+
SDK created came back with no external ID at all. Map the old name onto the
|
|
66
|
+
new one the way the Node SDK already does.
|
|
67
|
+
"""
|
|
68
|
+
body = _wire.dump(params)
|
|
69
|
+
_wire.rename(body, "uid", "externalId")
|
|
70
|
+
return body
|