hookbase 2.5.0__tar.gz → 2.6.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-2.5.0 → hookbase-2.6.0}/PKG-INFO +1 -1
- hookbase-2.6.0/src/hookbase/_version.py +1 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/__init__.py +6 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/destinations.py +38 -7
- {hookbase-2.5.0 → hookbase-2.6.0}/tests/resources/test_destinations.py +89 -1
- hookbase-2.5.0/src/hookbase/_version.py +0 -1
- {hookbase-2.5.0 → hookbase-2.6.0}/.github/workflows/python-publish.yml +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/.gitignore +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/LICENSE +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/README.md +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/examples/async_usage.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/examples/basic_inbound.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/examples/error_handling.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/examples/send_webhooks.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/examples/verify_webhooks.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/pyproject.toml +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/__init__.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/_client.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/_constants.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/_pagination.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/client.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/errors.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/_base.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/analytics.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/api_keys.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/applications.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/common.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/cron_jobs.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/deliveries.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/dlq.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/endpoints.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/event_types.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/events.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/filters.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/messages.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/organizations.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/portal_tokens.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/routes.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/schemas.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/sources.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/subscriptions.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/transforms.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/models/tunnels.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/py.typed +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/__init__.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/_base.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/analytics.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/api_keys.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/applications.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/cron_jobs.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/deliveries.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/destinations.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/dlq.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/endpoints.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/event_types.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/events.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/filters.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/message_log.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/messages.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/organizations.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/portal_tokens.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/routes.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/schemas.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/sources.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/subscriptions.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/transforms.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/resources/tunnels.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/src/hookbase/webhook.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/tests/__init__.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/tests/conftest.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/tests/resources/__init__.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/tests/resources/test_applications.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/tests/resources/test_dlq.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/tests/resources/test_messages.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/tests/resources/test_sources.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/tests/test_client.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/tests/test_errors.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/tests/test_pagination.py +0 -0
- {hookbase-2.5.0 → hookbase-2.6.0}/tests/test_webhook.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.6.0"
|
|
@@ -26,8 +26,11 @@ from .destinations import (
|
|
|
26
26
|
GCSConfig,
|
|
27
27
|
HttpMethod,
|
|
28
28
|
R2Config,
|
|
29
|
+
RateUnit,
|
|
29
30
|
S3Config,
|
|
30
31
|
TestResult,
|
|
32
|
+
Throttle,
|
|
33
|
+
ThrottleMode,
|
|
31
34
|
UpdateDestinationParams,
|
|
32
35
|
)
|
|
33
36
|
from .dlq import (
|
|
@@ -137,8 +140,11 @@ __all__ = [
|
|
|
137
140
|
"GCSConfig",
|
|
138
141
|
"HttpMethod",
|
|
139
142
|
"R2Config",
|
|
143
|
+
"RateUnit",
|
|
140
144
|
"S3Config",
|
|
141
145
|
"TestResult",
|
|
146
|
+
"Throttle",
|
|
147
|
+
"ThrottleMode",
|
|
142
148
|
"UpdateDestinationParams",
|
|
143
149
|
# DLQ
|
|
144
150
|
"DlqBulkDeleteResult",
|
|
@@ -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 field_validator
|
|
6
|
+
from pydantic import field_validator, model_validator
|
|
7
7
|
|
|
8
8
|
from ._base import HookbaseModel
|
|
9
9
|
|
|
@@ -13,6 +13,8 @@ DestinationType = Literal["http", "s3", "r2", "gcs", "azure_blob"]
|
|
|
13
13
|
FileFormat = Literal["json", "jsonl"]
|
|
14
14
|
PartitionBy = Literal["date", "hour", "source"]
|
|
15
15
|
FieldMappingType = Literal["string", "number", "boolean", "timestamp", "json"]
|
|
16
|
+
ThrottleMode = Literal["off", "rate", "concurrency"]
|
|
17
|
+
RateUnit = Literal["second", "minute", "hour"]
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class FieldMapping(HookbaseModel):
|
|
@@ -57,6 +59,14 @@ class AzureBlobConfig(HookbaseModel):
|
|
|
57
59
|
partition_by: PartitionBy | None = None
|
|
58
60
|
|
|
59
61
|
|
|
62
|
+
class Throttle(HookbaseModel):
|
|
63
|
+
mode: ThrottleMode = "off"
|
|
64
|
+
rate_limit: int | None = None
|
|
65
|
+
rate_unit: RateUnit | None = None
|
|
66
|
+
max_concurrency: int | None = None
|
|
67
|
+
queue_limit: int | None = None
|
|
68
|
+
|
|
69
|
+
|
|
60
70
|
class Destination(HookbaseModel):
|
|
61
71
|
id: str
|
|
62
72
|
organization_id: str | None = None
|
|
@@ -72,8 +82,7 @@ class Destination(HookbaseModel):
|
|
|
72
82
|
timeout: int = 30
|
|
73
83
|
retry_count: int = 3
|
|
74
84
|
retry_interval: int = 60
|
|
75
|
-
|
|
76
|
-
rate_limit_window: int | None = None
|
|
85
|
+
throttle: Throttle | None = None
|
|
77
86
|
is_active: bool = True
|
|
78
87
|
use_static_ip: bool = True
|
|
79
88
|
config: dict[str, Any] | None = None
|
|
@@ -85,6 +94,30 @@ class Destination(HookbaseModel):
|
|
|
85
94
|
created_at: str = ""
|
|
86
95
|
updated_at: str = ""
|
|
87
96
|
|
|
97
|
+
@model_validator(mode="before")
|
|
98
|
+
@classmethod
|
|
99
|
+
def _normalize_throttle(cls, data: Any) -> Any:
|
|
100
|
+
"""Normalize the two response shapes the API uses for throttle data.
|
|
101
|
+
|
|
102
|
+
POST /api/destinations (create) and GET /api/destinations/export nest
|
|
103
|
+
throttle fields under a `throttle` object, but GET /api/destinations
|
|
104
|
+
(list) and GET /api/destinations/:id return them as flat top-level
|
|
105
|
+
`throttleMode`/`throttleRateLimit`/`throttleRateUnit`/
|
|
106
|
+
`throttleMaxConcurrency`/`throttleQueueLimit` fields (spread directly
|
|
107
|
+
from the DB row). Fold the flat shape into a nested `throttle` dict
|
|
108
|
+
so both response shapes populate the same field.
|
|
109
|
+
"""
|
|
110
|
+
if isinstance(data, dict) and "throttle" not in data and "throttleMode" in data:
|
|
111
|
+
data = dict(data)
|
|
112
|
+
data["throttle"] = {
|
|
113
|
+
"mode": data.pop("throttleMode", None) or "off",
|
|
114
|
+
"rateLimit": data.pop("throttleRateLimit", None),
|
|
115
|
+
"rateUnit": data.pop("throttleRateUnit", None),
|
|
116
|
+
"maxConcurrency": data.pop("throttleMaxConcurrency", None),
|
|
117
|
+
"queueLimit": data.pop("throttleQueueLimit", None),
|
|
118
|
+
}
|
|
119
|
+
return data
|
|
120
|
+
|
|
88
121
|
@field_validator("headers", mode="before")
|
|
89
122
|
@classmethod
|
|
90
123
|
def parse_headers(cls, v: Any) -> Any:
|
|
@@ -146,8 +179,7 @@ class CreateDestinationParams(HookbaseModel):
|
|
|
146
179
|
timeout: int | None = None
|
|
147
180
|
retry_count: int | None = None
|
|
148
181
|
retry_interval: int | None = None
|
|
149
|
-
|
|
150
|
-
rate_limit_window: int | None = None
|
|
182
|
+
throttle: Throttle | None = None
|
|
151
183
|
config: dict[str, Any] | None = None
|
|
152
184
|
field_mapping: list[FieldMapping] | None = None
|
|
153
185
|
use_static_ip: bool | None = None
|
|
@@ -166,8 +198,7 @@ class UpdateDestinationParams(HookbaseModel):
|
|
|
166
198
|
timeout: int | None = None
|
|
167
199
|
retry_count: int | None = None
|
|
168
200
|
retry_interval: int | None = None
|
|
169
|
-
|
|
170
|
-
rate_limit_window: int | None = None
|
|
201
|
+
throttle: Throttle | None = None
|
|
171
202
|
is_active: bool | None = None
|
|
172
203
|
config: dict[str, Any] | None = None
|
|
173
204
|
field_mapping: list[FieldMapping] | None = None
|
|
@@ -4,7 +4,14 @@ import pytest
|
|
|
4
4
|
import respx
|
|
5
5
|
|
|
6
6
|
from hookbase import Hookbase
|
|
7
|
-
from hookbase.models import
|
|
7
|
+
from hookbase.models import (
|
|
8
|
+
CreateDestinationParams,
|
|
9
|
+
Destination,
|
|
10
|
+
FieldMapping,
|
|
11
|
+
TestResult,
|
|
12
|
+
Throttle,
|
|
13
|
+
UpdateDestinationParams,
|
|
14
|
+
)
|
|
8
15
|
|
|
9
16
|
from ..conftest import make_paginated_response
|
|
10
17
|
|
|
@@ -183,6 +190,87 @@ def test_destination_without_type_defaults():
|
|
|
183
190
|
assert dest.field_mapping is None
|
|
184
191
|
|
|
185
192
|
|
|
193
|
+
def test_throttle_model():
|
|
194
|
+
throttle = Throttle(mode="rate", rate_limit=10, rate_unit="minute")
|
|
195
|
+
assert throttle.mode == "rate"
|
|
196
|
+
assert throttle.rate_limit == 10
|
|
197
|
+
assert throttle.rate_unit == "minute"
|
|
198
|
+
assert throttle.max_concurrency is None
|
|
199
|
+
assert throttle.queue_limit is None
|
|
200
|
+
|
|
201
|
+
default_throttle = Throttle()
|
|
202
|
+
assert default_throttle.mode == "off"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_create_destination_response_nests_throttle(mock_api, client):
|
|
206
|
+
"""POST /api/destinations nests throttle fields under `throttle`."""
|
|
207
|
+
data = {
|
|
208
|
+
**DEST_DATA,
|
|
209
|
+
"throttle": {
|
|
210
|
+
"mode": "rate",
|
|
211
|
+
"rateLimit": 100,
|
|
212
|
+
"rateUnit": "minute",
|
|
213
|
+
"maxConcurrency": None,
|
|
214
|
+
"queueLimit": 50,
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
mock_api.post("/api/destinations").respond(200, json={"destination": data})
|
|
218
|
+
dest = client.destinations.create({"name": "Backend", "url": "https://example.com/webhooks"})
|
|
219
|
+
assert dest.throttle is not None
|
|
220
|
+
assert dest.throttle.mode == "rate"
|
|
221
|
+
assert dest.throttle.rate_limit == 100
|
|
222
|
+
assert dest.throttle.rate_unit == "minute"
|
|
223
|
+
assert dest.throttle.max_concurrency is None
|
|
224
|
+
assert dest.throttle.queue_limit == 50
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_get_destination_response_has_flat_throttle_fields(mock_api, client):
|
|
228
|
+
"""GET /api/destinations/:id returns throttle fields flat (spread from the
|
|
229
|
+
DB row) rather than nested under `throttle`; the model must normalize
|
|
230
|
+
this into the same `throttle` field used by the create/export shape."""
|
|
231
|
+
data = {
|
|
232
|
+
**DEST_DATA,
|
|
233
|
+
"throttleMode": "concurrency",
|
|
234
|
+
"throttleRateLimit": None,
|
|
235
|
+
"throttleRateUnit": None,
|
|
236
|
+
"throttleMaxConcurrency": 5,
|
|
237
|
+
"throttleQueueLimit": None,
|
|
238
|
+
}
|
|
239
|
+
mock_api.get("/api/destinations/dst_1").respond(200, json={"destination": data})
|
|
240
|
+
dest = client.destinations.get("dst_1")
|
|
241
|
+
assert dest.throttle is not None
|
|
242
|
+
assert dest.throttle.mode == "concurrency"
|
|
243
|
+
assert dest.throttle.max_concurrency == 5
|
|
244
|
+
assert dest.throttle.rate_limit is None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_destination_without_throttle_data():
|
|
248
|
+
dest = Destination(**DEST_DATA)
|
|
249
|
+
assert dest.throttle is None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_create_destination_params_throttle_serializes_camelcase():
|
|
253
|
+
params = CreateDestinationParams(
|
|
254
|
+
name="Backend",
|
|
255
|
+
url="https://example.com/webhooks",
|
|
256
|
+
throttle=Throttle(mode="rate", rate_limit=10, rate_unit="minute"),
|
|
257
|
+
)
|
|
258
|
+
body = params.model_dump(by_alias=True, exclude_none=True)
|
|
259
|
+
assert body["throttle"] == {"mode": "rate", "rateLimit": 10, "rateUnit": "minute"}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_update_destination_params_throttle_serializes_camelcase():
|
|
263
|
+
params = UpdateDestinationParams(
|
|
264
|
+
throttle=Throttle(mode="concurrency", max_concurrency=25, queue_limit=100),
|
|
265
|
+
)
|
|
266
|
+
body = params.model_dump(by_alias=True, exclude_none=True)
|
|
267
|
+
assert body["throttle"] == {
|
|
268
|
+
"mode": "concurrency",
|
|
269
|
+
"maxConcurrency": 25,
|
|
270
|
+
"queueLimit": 100,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
|
|
186
274
|
def test_test_destination(mock_api, client):
|
|
187
275
|
mock_api.post("/api/destinations/dst_1/test").respond(200, json={
|
|
188
276
|
"success": True, "statusCode": 200, "duration": 150.5,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.5.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|