hookbridge 1.2.0__tar.gz → 1.3.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.
- hookbridge-1.3.0/.claude/settings.local.json +8 -0
- {hookbridge-1.2.0 → hookbridge-1.3.0}/PKG-INFO +1 -1
- {hookbridge-1.2.0 → hookbridge-1.3.0}/hookbridge/__init__.py +2 -6
- {hookbridge-1.2.0 → hookbridge-1.3.0}/hookbridge/client.py +48 -142
- {hookbridge-1.2.0 → hookbridge-1.3.0}/hookbridge/types.py +0 -23
- {hookbridge-1.2.0 → hookbridge-1.3.0}/pyproject.toml +1 -1
- {hookbridge-1.2.0 → hookbridge-1.3.0}/tests/test_async_client.py +39 -0
- {hookbridge-1.2.0 → hookbridge-1.3.0}/tests/test_client_spec_parity.py +33 -66
- hookbridge-1.2.0/.claude/settings.local.json +0 -7
- {hookbridge-1.2.0 → hookbridge-1.3.0}/.gitignore +0 -0
- {hookbridge-1.2.0 → hookbridge-1.3.0}/PUBLISHING.md +0 -0
- {hookbridge-1.2.0 → hookbridge-1.3.0}/README.md +0 -0
- {hookbridge-1.2.0 → hookbridge-1.3.0}/hookbridge/errors.py +0 -0
- {hookbridge-1.2.0 → hookbridge-1.3.0}/hookbridge/py.typed +0 -0
- {hookbridge-1.2.0 → hookbridge-1.3.0}/requirements-dev.txt +0 -0
- {hookbridge-1.2.0 → hookbridge-1.3.0}/requirements.txt +0 -0
- {hookbridge-1.2.0 → hookbridge-1.3.0}/tests/__init__.py +0 -0
- {hookbridge-1.2.0 → hookbridge-1.3.0}/tests/conftest.py +0 -0
- {hookbridge-1.2.0 → hookbridge-1.3.0}/tests/test_client.py +0 -0
- {hookbridge-1.2.0 → hookbridge-1.3.0}/tests/test_errors.py +0 -0
|
@@ -44,16 +44,14 @@ from hookbridge.types import (
|
|
|
44
44
|
MessageSummary,
|
|
45
45
|
Metrics,
|
|
46
46
|
MetricsWindow,
|
|
47
|
-
NotificationPreferences,
|
|
48
47
|
PauseState,
|
|
49
|
-
Personalization,
|
|
50
48
|
PortalSession,
|
|
51
49
|
Project,
|
|
50
|
+
ReplayableMessageStatus,
|
|
52
51
|
ReplayAllMessagesResponse,
|
|
53
52
|
ReplayBatchMessagesResponse,
|
|
54
53
|
ReplayBatchResult,
|
|
55
54
|
ReplayMessageResponse,
|
|
56
|
-
ReplayableMessageStatus,
|
|
57
55
|
RotateSecretResponse,
|
|
58
56
|
SendWebhookResponse,
|
|
59
57
|
SigningKey,
|
|
@@ -67,7 +65,7 @@ from hookbridge.types import (
|
|
|
67
65
|
UsageHistoryRow,
|
|
68
66
|
)
|
|
69
67
|
|
|
70
|
-
__version__ = "1.
|
|
68
|
+
__version__ = "1.3.0"
|
|
71
69
|
|
|
72
70
|
__all__ = [
|
|
73
71
|
"HookBridge",
|
|
@@ -101,9 +99,7 @@ __all__ = [
|
|
|
101
99
|
"MessageSummary",
|
|
102
100
|
"Metrics",
|
|
103
101
|
"MetricsWindow",
|
|
104
|
-
"NotificationPreferences",
|
|
105
102
|
"PauseState",
|
|
106
|
-
"Personalization",
|
|
107
103
|
"PortalSession",
|
|
108
104
|
"Project",
|
|
109
105
|
"ReplayAllMessagesResponse",
|
|
@@ -51,16 +51,14 @@ from hookbridge.types import (
|
|
|
51
51
|
MessageSummary,
|
|
52
52
|
Metrics,
|
|
53
53
|
MetricsWindow,
|
|
54
|
-
NotificationPreferences,
|
|
55
54
|
PauseState,
|
|
56
|
-
Personalization,
|
|
57
55
|
PortalSession,
|
|
58
56
|
Project,
|
|
57
|
+
ReplayableMessageStatus,
|
|
59
58
|
ReplayAllMessagesResponse,
|
|
60
59
|
ReplayBatchMessagesResponse,
|
|
61
60
|
ReplayBatchResult,
|
|
62
61
|
ReplayMessageResponse,
|
|
63
|
-
ReplayableMessageStatus,
|
|
64
62
|
RotateSecretResponse,
|
|
65
63
|
SendWebhookResponse,
|
|
66
64
|
SigningKey,
|
|
@@ -95,9 +93,7 @@ def _query_string(params: dict[str, Any]) -> str:
|
|
|
95
93
|
for key, value in params.items():
|
|
96
94
|
if value is None:
|
|
97
95
|
continue
|
|
98
|
-
if isinstance(value, datetime):
|
|
99
|
-
encoded[key] = value.isoformat()
|
|
100
|
-
elif isinstance(value, date):
|
|
96
|
+
if isinstance(value, (datetime, date)):
|
|
101
97
|
encoded[key] = value.isoformat()
|
|
102
98
|
else:
|
|
103
99
|
encoded[key] = str(value)
|
|
@@ -111,6 +107,14 @@ def _with_query(path: str, params: dict[str, Any]) -> str:
|
|
|
111
107
|
return f"{path}?{query}"
|
|
112
108
|
|
|
113
109
|
|
|
110
|
+
def _json_value(value: Any) -> Any:
|
|
111
|
+
if isinstance(value, datetime):
|
|
112
|
+
return value.isoformat()
|
|
113
|
+
if isinstance(value, date):
|
|
114
|
+
return value.isoformat()
|
|
115
|
+
return value
|
|
116
|
+
|
|
117
|
+
|
|
114
118
|
def _parse_message(data: dict[str, Any]) -> Message:
|
|
115
119
|
return Message(
|
|
116
120
|
id=data["id"],
|
|
@@ -294,31 +298,6 @@ def _parse_rotate_secret(data: dict[str, Any]) -> RotateSecretResponse:
|
|
|
294
298
|
)
|
|
295
299
|
|
|
296
300
|
|
|
297
|
-
def _parse_personalization(data: dict[str, Any]) -> Personalization:
|
|
298
|
-
return Personalization(
|
|
299
|
-
id=data["id"],
|
|
300
|
-
user_id=data["user_id"],
|
|
301
|
-
timezone=data["timezone"],
|
|
302
|
-
date_format=data["date_format"],
|
|
303
|
-
locale=data["locale"],
|
|
304
|
-
created_at=_parse_datetime(data["created_at"]),
|
|
305
|
-
updated_at=_parse_datetime(data["updated_at"]),
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def _parse_notification_preferences(data: dict[str, Any]) -> NotificationPreferences:
|
|
310
|
-
return NotificationPreferences(
|
|
311
|
-
id=data["id"],
|
|
312
|
-
user_id=data["user_id"],
|
|
313
|
-
enabled_alert_types=data["enabled_alert_types"],
|
|
314
|
-
delivery_channels=data["delivery_channels"],
|
|
315
|
-
failure_rate_threshold=data["failure_rate_threshold"],
|
|
316
|
-
pending_backlog_threshold=data["pending_backlog_threshold"],
|
|
317
|
-
created_at=_parse_datetime(data["created_at"]),
|
|
318
|
-
updated_at=_parse_datetime(data["updated_at"]),
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
|
|
322
301
|
def _parse_usage_history_row(data: dict[str, Any]) -> UsageHistoryRow:
|
|
323
302
|
return UsageHistoryRow(
|
|
324
303
|
period_start=_parse_date(data["period_start"]),
|
|
@@ -558,7 +537,9 @@ class HookBridge:
|
|
|
558
537
|
return _parse_replay_all_messages(self._request("POST", path)["data"])
|
|
559
538
|
|
|
560
539
|
def replay_batch_messages(self, message_ids: list[str]) -> ReplayBatchMessagesResponse:
|
|
561
|
-
response = self._request(
|
|
540
|
+
response = self._request(
|
|
541
|
+
"POST", "/v1/messages/replay-batch", json={"message_ids": message_ids}
|
|
542
|
+
)
|
|
562
543
|
return _parse_replay_batch_messages(response["data"])
|
|
563
544
|
|
|
564
545
|
def get_logs(
|
|
@@ -605,7 +586,8 @@ class HookBridge:
|
|
|
605
586
|
def get_dlq_messages(
|
|
606
587
|
self, *, limit: Optional[int] = None, cursor: Optional[str] = None
|
|
607
588
|
) -> DLQMessagesResponse:
|
|
608
|
-
|
|
589
|
+
path = _with_query("/v1/dlq/messages", {"limit": limit, "cursor": cursor})
|
|
590
|
+
response = self._request("GET", path)
|
|
609
591
|
data = response["data"]
|
|
610
592
|
return DLQMessagesResponse(
|
|
611
593
|
messages=[_parse_message_summary(item) for item in data.get("messages", [])],
|
|
@@ -650,7 +632,11 @@ class HookBridge:
|
|
|
650
632
|
return _parse_project(self._request("GET", f"/v1/projects/{project_id}")["data"])
|
|
651
633
|
|
|
652
634
|
def update_project(
|
|
653
|
-
self,
|
|
635
|
+
self,
|
|
636
|
+
project_id: str,
|
|
637
|
+
*,
|
|
638
|
+
name: Optional[str] = None,
|
|
639
|
+
rate_limit_default: Optional[int] = None,
|
|
654
640
|
) -> Project:
|
|
655
641
|
body: dict[str, Any] = {}
|
|
656
642
|
if name is not None:
|
|
@@ -697,7 +683,8 @@ class HookBridge:
|
|
|
697
683
|
def list_endpoints(
|
|
698
684
|
self, *, limit: Optional[int] = None, cursor: Optional[str] = None
|
|
699
685
|
) -> ListEndpointsResponse:
|
|
700
|
-
|
|
686
|
+
path = _with_query("/v1/endpoints", {"limit": limit, "cursor": cursor})
|
|
687
|
+
response = self._request("GET", path)
|
|
701
688
|
meta = response.get("meta", {})
|
|
702
689
|
next_cursor = meta.get("next_cursor")
|
|
703
690
|
return ListEndpointsResponse(
|
|
@@ -746,55 +733,6 @@ class HookBridge:
|
|
|
746
733
|
def rotate_endpoint_secret(self, endpoint_id: str) -> RotateSecretResponse:
|
|
747
734
|
return self.create_endpoint_signing_key(endpoint_id)
|
|
748
735
|
|
|
749
|
-
def get_personalization(self) -> Personalization:
|
|
750
|
-
return _parse_personalization(
|
|
751
|
-
self._request("GET", "/v1/settings/personalization")["data"]
|
|
752
|
-
)
|
|
753
|
-
|
|
754
|
-
def update_personalization(
|
|
755
|
-
self,
|
|
756
|
-
*,
|
|
757
|
-
timezone: Optional[str] = None,
|
|
758
|
-
date_format: Optional[str] = None,
|
|
759
|
-
locale: Optional[str] = None,
|
|
760
|
-
) -> Personalization:
|
|
761
|
-
body: dict[str, Any] = {}
|
|
762
|
-
if timezone is not None:
|
|
763
|
-
body["timezone"] = timezone
|
|
764
|
-
if date_format is not None:
|
|
765
|
-
body["date_format"] = date_format
|
|
766
|
-
if locale is not None:
|
|
767
|
-
body["locale"] = locale
|
|
768
|
-
return _parse_personalization(
|
|
769
|
-
self._request("PUT", "/v1/settings/personalization", json=body)["data"]
|
|
770
|
-
)
|
|
771
|
-
|
|
772
|
-
def get_notification_preferences(self) -> NotificationPreferences:
|
|
773
|
-
return _parse_notification_preferences(
|
|
774
|
-
self._request("GET", "/v1/settings/notifications")["data"]
|
|
775
|
-
)
|
|
776
|
-
|
|
777
|
-
def update_notification_preferences(
|
|
778
|
-
self,
|
|
779
|
-
*,
|
|
780
|
-
enabled_alert_types: Optional[list[str]] = None,
|
|
781
|
-
delivery_channels: Optional[list[str]] = None,
|
|
782
|
-
failure_rate_threshold: Optional[int] = None,
|
|
783
|
-
pending_backlog_threshold: Optional[int] = None,
|
|
784
|
-
) -> NotificationPreferences:
|
|
785
|
-
body: dict[str, Any] = {}
|
|
786
|
-
if enabled_alert_types is not None:
|
|
787
|
-
body["enabled_alert_types"] = enabled_alert_types
|
|
788
|
-
if delivery_channels is not None:
|
|
789
|
-
body["delivery_channels"] = delivery_channels
|
|
790
|
-
if failure_rate_threshold is not None:
|
|
791
|
-
body["failure_rate_threshold"] = failure_rate_threshold
|
|
792
|
-
if pending_backlog_threshold is not None:
|
|
793
|
-
body["pending_backlog_threshold"] = pending_backlog_threshold
|
|
794
|
-
return _parse_notification_preferences(
|
|
795
|
-
self._request("PUT", "/v1/settings/notifications", json=body)["data"]
|
|
796
|
-
)
|
|
797
|
-
|
|
798
736
|
def create_checkout(self, *, plan: str, interval: str) -> CheckoutSession:
|
|
799
737
|
data = self._request(
|
|
800
738
|
"POST", "/v1/billing/checkout", json={"plan": plan, "interval": interval}
|
|
@@ -1038,7 +976,10 @@ class HookBridge:
|
|
|
1038
976
|
status: Optional[MessageStatus] = None,
|
|
1039
977
|
endpoint_id: Optional[str] = None,
|
|
1040
978
|
) -> ExportRecord:
|
|
1041
|
-
body: dict[str, Any] = {
|
|
979
|
+
body: dict[str, Any] = {
|
|
980
|
+
"start_time": _json_value(start_time),
|
|
981
|
+
"end_time": _json_value(end_time),
|
|
982
|
+
}
|
|
1042
983
|
if status is not None:
|
|
1043
984
|
body["status"] = status
|
|
1044
985
|
if endpoint_id is not None:
|
|
@@ -1243,7 +1184,8 @@ class AsyncHookBridge:
|
|
|
1243
1184
|
await self._request("POST", f"/v1/dlq/replay/{message_id}")
|
|
1244
1185
|
|
|
1245
1186
|
async def list_api_keys(self) -> list[APIKeyInfo]:
|
|
1246
|
-
|
|
1187
|
+
response = await self._request("GET", "/v1/api-keys")
|
|
1188
|
+
return [_parse_api_key_info(item) for item in response["data"]]
|
|
1247
1189
|
|
|
1248
1190
|
async def create_api_key(
|
|
1249
1191
|
self, mode: APIKeyMode, *, label: Optional[str] = None
|
|
@@ -1264,9 +1206,12 @@ class AsyncHookBridge:
|
|
|
1264
1206
|
await self._request("DELETE", f"/v1/api-keys/{key_id}")
|
|
1265
1207
|
|
|
1266
1208
|
async def list_projects(self) -> list[Project]:
|
|
1267
|
-
|
|
1209
|
+
response = await self._request("GET", "/v1/projects")
|
|
1210
|
+
return [_parse_project(item) for item in response["data"]]
|
|
1268
1211
|
|
|
1269
|
-
async def create_project(
|
|
1212
|
+
async def create_project(
|
|
1213
|
+
self, name: str, *, rate_limit_default: Optional[int] = None
|
|
1214
|
+
) -> Project:
|
|
1270
1215
|
body: dict[str, Any] = {"name": name}
|
|
1271
1216
|
if rate_limit_default is not None:
|
|
1272
1217
|
body["rate_limit_default"] = rate_limit_default
|
|
@@ -1276,14 +1221,19 @@ class AsyncHookBridge:
|
|
|
1276
1221
|
return _parse_project((await self._request("GET", f"/v1/projects/{project_id}"))["data"])
|
|
1277
1222
|
|
|
1278
1223
|
async def update_project(
|
|
1279
|
-
self,
|
|
1224
|
+
self,
|
|
1225
|
+
project_id: str,
|
|
1226
|
+
*,
|
|
1227
|
+
name: Optional[str] = None,
|
|
1228
|
+
rate_limit_default: Optional[int] = None,
|
|
1280
1229
|
) -> Project:
|
|
1281
1230
|
body: dict[str, Any] = {}
|
|
1282
1231
|
if name is not None:
|
|
1283
1232
|
body["name"] = name
|
|
1284
1233
|
if rate_limit_default is not None:
|
|
1285
1234
|
body["rate_limit_default"] = rate_limit_default
|
|
1286
|
-
|
|
1235
|
+
response = await self._request("PUT", f"/v1/projects/{project_id}", json=body)
|
|
1236
|
+
return _parse_project(response["data"])
|
|
1287
1237
|
|
|
1288
1238
|
async def delete_project(self, project_id: str) -> None:
|
|
1289
1239
|
await self._request("DELETE", f"/v1/projects/{project_id}")
|
|
@@ -1374,55 +1324,6 @@ class AsyncHookBridge:
|
|
|
1374
1324
|
async def rotate_endpoint_secret(self, endpoint_id: str) -> RotateSecretResponse:
|
|
1375
1325
|
return await self.create_endpoint_signing_key(endpoint_id)
|
|
1376
1326
|
|
|
1377
|
-
async def get_personalization(self) -> Personalization:
|
|
1378
|
-
return _parse_personalization(
|
|
1379
|
-
(await self._request("GET", "/v1/settings/personalization"))["data"]
|
|
1380
|
-
)
|
|
1381
|
-
|
|
1382
|
-
async def update_personalization(
|
|
1383
|
-
self,
|
|
1384
|
-
*,
|
|
1385
|
-
timezone: Optional[str] = None,
|
|
1386
|
-
date_format: Optional[str] = None,
|
|
1387
|
-
locale: Optional[str] = None,
|
|
1388
|
-
) -> Personalization:
|
|
1389
|
-
body: dict[str, Any] = {}
|
|
1390
|
-
if timezone is not None:
|
|
1391
|
-
body["timezone"] = timezone
|
|
1392
|
-
if date_format is not None:
|
|
1393
|
-
body["date_format"] = date_format
|
|
1394
|
-
if locale is not None:
|
|
1395
|
-
body["locale"] = locale
|
|
1396
|
-
return _parse_personalization(
|
|
1397
|
-
(await self._request("PUT", "/v1/settings/personalization", json=body))["data"]
|
|
1398
|
-
)
|
|
1399
|
-
|
|
1400
|
-
async def get_notification_preferences(self) -> NotificationPreferences:
|
|
1401
|
-
return _parse_notification_preferences(
|
|
1402
|
-
(await self._request("GET", "/v1/settings/notifications"))["data"]
|
|
1403
|
-
)
|
|
1404
|
-
|
|
1405
|
-
async def update_notification_preferences(
|
|
1406
|
-
self,
|
|
1407
|
-
*,
|
|
1408
|
-
enabled_alert_types: Optional[list[str]] = None,
|
|
1409
|
-
delivery_channels: Optional[list[str]] = None,
|
|
1410
|
-
failure_rate_threshold: Optional[int] = None,
|
|
1411
|
-
pending_backlog_threshold: Optional[int] = None,
|
|
1412
|
-
) -> NotificationPreferences:
|
|
1413
|
-
body: dict[str, Any] = {}
|
|
1414
|
-
if enabled_alert_types is not None:
|
|
1415
|
-
body["enabled_alert_types"] = enabled_alert_types
|
|
1416
|
-
if delivery_channels is not None:
|
|
1417
|
-
body["delivery_channels"] = delivery_channels
|
|
1418
|
-
if failure_rate_threshold is not None:
|
|
1419
|
-
body["failure_rate_threshold"] = failure_rate_threshold
|
|
1420
|
-
if pending_backlog_threshold is not None:
|
|
1421
|
-
body["pending_backlog_threshold"] = pending_backlog_threshold
|
|
1422
|
-
return _parse_notification_preferences(
|
|
1423
|
-
(await self._request("PUT", "/v1/settings/notifications", json=body))["data"]
|
|
1424
|
-
)
|
|
1425
|
-
|
|
1426
1327
|
async def create_checkout(self, *, plan: str, interval: str) -> CheckoutSession:
|
|
1427
1328
|
data = (
|
|
1428
1329
|
await self._request(
|
|
@@ -1674,7 +1575,10 @@ class AsyncHookBridge:
|
|
|
1674
1575
|
status: Optional[MessageStatus] = None,
|
|
1675
1576
|
endpoint_id: Optional[str] = None,
|
|
1676
1577
|
) -> ExportRecord:
|
|
1677
|
-
body: dict[str, Any] = {
|
|
1578
|
+
body: dict[str, Any] = {
|
|
1579
|
+
"start_time": _json_value(start_time),
|
|
1580
|
+
"end_time": _json_value(end_time),
|
|
1581
|
+
}
|
|
1678
1582
|
if status is not None:
|
|
1679
1583
|
body["status"] = status
|
|
1680
1584
|
if endpoint_id is not None:
|
|
@@ -1682,10 +1586,12 @@ class AsyncHookBridge:
|
|
|
1682
1586
|
return _parse_export_record((await self._request("POST", "/v1/exports", json=body))["data"])
|
|
1683
1587
|
|
|
1684
1588
|
async def list_exports(self) -> list[ExportRecord]:
|
|
1685
|
-
|
|
1589
|
+
response = await self._request("GET", "/v1/exports")
|
|
1590
|
+
return [_parse_export_record(item) for item in response["data"]]
|
|
1686
1591
|
|
|
1687
1592
|
async def get_export(self, export_id: str) -> ExportRecord:
|
|
1688
|
-
|
|
1593
|
+
response = await self._request("GET", f"/v1/exports/{export_id}")
|
|
1594
|
+
return _parse_export_record(response["data"])
|
|
1689
1595
|
|
|
1690
1596
|
async def download_export(self, export_id: str) -> str:
|
|
1691
1597
|
response = await self._request(
|
|
@@ -243,29 +243,6 @@ class Project:
|
|
|
243
243
|
created_at: datetime
|
|
244
244
|
|
|
245
245
|
|
|
246
|
-
@dataclass(frozen=True)
|
|
247
|
-
class Personalization:
|
|
248
|
-
id: str
|
|
249
|
-
user_id: str
|
|
250
|
-
timezone: str
|
|
251
|
-
date_format: str
|
|
252
|
-
locale: str
|
|
253
|
-
created_at: datetime
|
|
254
|
-
updated_at: datetime
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
@dataclass(frozen=True)
|
|
258
|
-
class NotificationPreferences:
|
|
259
|
-
id: str
|
|
260
|
-
user_id: str
|
|
261
|
-
enabled_alert_types: list[str]
|
|
262
|
-
delivery_channels: list[str]
|
|
263
|
-
failure_rate_threshold: int
|
|
264
|
-
pending_backlog_threshold: int
|
|
265
|
-
created_at: datetime
|
|
266
|
-
updated_at: datetime
|
|
267
|
-
|
|
268
|
-
|
|
269
246
|
@dataclass(frozen=True)
|
|
270
247
|
class CheckoutSession:
|
|
271
248
|
session_id: str
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
Tests for the asynchronous HookBridge client.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import json
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
|
|
5
8
|
import pytest
|
|
6
9
|
from pytest_httpx import HTTPXMock
|
|
7
10
|
|
|
@@ -305,6 +308,42 @@ class TestAsyncEndpoints:
|
|
|
305
308
|
assert result.id == endpoint_id
|
|
306
309
|
assert result.rate_limit_rps == 10
|
|
307
310
|
|
|
311
|
+
|
|
312
|
+
class TestAsyncExports:
|
|
313
|
+
"""Tests for async export operations."""
|
|
314
|
+
|
|
315
|
+
@pytest.mark.skipif(LIVE_MODE, reason="Mock test only")
|
|
316
|
+
async def test_create_export_serializes_datetimes(
|
|
317
|
+
self, async_client: AsyncHookBridge, httpx_mock: HTTPXMock
|
|
318
|
+
) -> None:
|
|
319
|
+
httpx_mock.add_response(
|
|
320
|
+
method="POST",
|
|
321
|
+
url=f"{async_client._base_url}/v1/exports",
|
|
322
|
+
json={
|
|
323
|
+
"data": {
|
|
324
|
+
"id": "01935abc-def0-7123-4567-890abcdef077",
|
|
325
|
+
"project_id": "proj_abc123",
|
|
326
|
+
"status": "pending",
|
|
327
|
+
"filter_start_time": "2025-12-01T00:00:00+00:00",
|
|
328
|
+
"filter_end_time": "2025-12-06T23:59:59+00:00",
|
|
329
|
+
"created_at": "2025-12-06T12:00:00Z",
|
|
330
|
+
},
|
|
331
|
+
"meta": {"request_id": "req-12345"},
|
|
332
|
+
},
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
start = datetime(2025, 12, 1, 0, 0, 0, tzinfo=UTC)
|
|
336
|
+
end = datetime(2025, 12, 6, 23, 59, 59, tzinfo=UTC)
|
|
337
|
+
await async_client.create_export(start_time=start, end_time=end, endpoint_id="ep_123")
|
|
338
|
+
|
|
339
|
+
request = httpx_mock.get_request()
|
|
340
|
+
assert request is not None
|
|
341
|
+
assert json.loads(request.content) == {
|
|
342
|
+
"start_time": "2025-12-01T00:00:00+00:00",
|
|
343
|
+
"end_time": "2025-12-06T23:59:59+00:00",
|
|
344
|
+
"endpoint_id": "ep_123",
|
|
345
|
+
}
|
|
346
|
+
|
|
308
347
|
@pytest.mark.skipif(LIVE_MODE, reason="Mock test only")
|
|
309
348
|
async def test_list_endpoints(
|
|
310
349
|
self, async_client: AsyncHookBridge, httpx_mock: HTTPXMock
|
|
@@ -3,6 +3,7 @@ Spec parity tests for the synchronous HookBridge client.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
from datetime import UTC, datetime
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
8
9
|
from pytest_httpx import HTTPXMock
|
|
@@ -11,7 +12,6 @@ from hookbridge import HookBridge
|
|
|
11
12
|
|
|
12
13
|
from .conftest import LIVE_MODE
|
|
13
14
|
|
|
14
|
-
|
|
15
15
|
PROJECT_ID = "01935abc-def0-7123-4567-890abcdef012"
|
|
16
16
|
INBOUND_ENDPOINT_ID = "01935abc-def0-7123-4567-890abcdef099"
|
|
17
17
|
MESSAGE_ID = "01935abc-def0-7123-4567-890abcdef013"
|
|
@@ -236,71 +236,6 @@ class TestSigningKeyParity:
|
|
|
236
236
|
client.delete_endpoint_signing_key(ENDPOINT_ID, SIGNING_KEY_ID)
|
|
237
237
|
|
|
238
238
|
|
|
239
|
-
class TestSettingsParity:
|
|
240
|
-
@pytest.mark.skipif(LIVE_MODE, reason="Mock test only")
|
|
241
|
-
def test_personalization_settings(self, client: HookBridge, httpx_mock: HTTPXMock) -> None:
|
|
242
|
-
payload = {
|
|
243
|
-
"data": {
|
|
244
|
-
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
245
|
-
"user_id": "user_abc123",
|
|
246
|
-
"timezone": "America/New_York",
|
|
247
|
-
"date_format": "MM/DD/YYYY",
|
|
248
|
-
"locale": "en-US",
|
|
249
|
-
"created_at": "2025-12-01T10:00:00Z",
|
|
250
|
-
"updated_at": "2025-12-06T12:00:00Z",
|
|
251
|
-
},
|
|
252
|
-
"meta": {"request_id": "req-12345"},
|
|
253
|
-
}
|
|
254
|
-
httpx_mock.add_response(
|
|
255
|
-
method="GET",
|
|
256
|
-
url=f"{client._base_url}/v1/settings/personalization",
|
|
257
|
-
json=payload,
|
|
258
|
-
)
|
|
259
|
-
httpx_mock.add_response(
|
|
260
|
-
method="PUT",
|
|
261
|
-
url=f"{client._base_url}/v1/settings/personalization",
|
|
262
|
-
json=payload,
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
current = client.get_personalization()
|
|
266
|
-
updated = client.update_personalization(timezone="America/New_York")
|
|
267
|
-
|
|
268
|
-
assert current.locale == "en-US"
|
|
269
|
-
assert updated.timezone == "America/New_York"
|
|
270
|
-
|
|
271
|
-
@pytest.mark.skipif(LIVE_MODE, reason="Mock test only")
|
|
272
|
-
def test_notification_settings(self, client: HookBridge, httpx_mock: HTTPXMock) -> None:
|
|
273
|
-
payload = {
|
|
274
|
-
"data": {
|
|
275
|
-
"id": "550e8400-e29b-41d4-a716-446655440001",
|
|
276
|
-
"user_id": "user_abc123",
|
|
277
|
-
"enabled_alert_types": ["high_failure_rate"],
|
|
278
|
-
"delivery_channels": ["email"],
|
|
279
|
-
"failure_rate_threshold": 10,
|
|
280
|
-
"pending_backlog_threshold": 100,
|
|
281
|
-
"created_at": "2025-12-01T10:00:00Z",
|
|
282
|
-
"updated_at": "2025-12-06T12:00:00Z",
|
|
283
|
-
},
|
|
284
|
-
"meta": {"request_id": "req-12345"},
|
|
285
|
-
}
|
|
286
|
-
httpx_mock.add_response(
|
|
287
|
-
method="GET",
|
|
288
|
-
url=f"{client._base_url}/v1/settings/notifications",
|
|
289
|
-
json=payload,
|
|
290
|
-
)
|
|
291
|
-
httpx_mock.add_response(
|
|
292
|
-
method="PUT",
|
|
293
|
-
url=f"{client._base_url}/v1/settings/notifications",
|
|
294
|
-
json=payload,
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
current = client.get_notification_preferences()
|
|
298
|
-
updated = client.update_notification_preferences(delivery_channels=["email"])
|
|
299
|
-
|
|
300
|
-
assert current.enabled_alert_types == ["high_failure_rate"]
|
|
301
|
-
assert updated.pending_backlog_threshold == 100
|
|
302
|
-
|
|
303
|
-
|
|
304
239
|
class TestBillingParity:
|
|
305
240
|
@pytest.mark.skipif(LIVE_MODE, reason="Mock test only")
|
|
306
241
|
def test_checkout_and_portal(self, client: HookBridge, httpx_mock: HTTPXMock) -> None:
|
|
@@ -700,3 +635,35 @@ class TestExportParity:
|
|
|
700
635
|
assert listed[0].row_count == 5000
|
|
701
636
|
assert fetched.status == "completed"
|
|
702
637
|
assert download_url == "https://downloads.example.com/export.csv"
|
|
638
|
+
|
|
639
|
+
@pytest.mark.skipif(LIVE_MODE, reason="Mock test only")
|
|
640
|
+
def test_create_export_serializes_datetimes(
|
|
641
|
+
self, client: HookBridge, httpx_mock: HTTPXMock
|
|
642
|
+
) -> None:
|
|
643
|
+
httpx_mock.add_response(
|
|
644
|
+
method="POST",
|
|
645
|
+
url=f"{client._base_url}/v1/exports",
|
|
646
|
+
json={
|
|
647
|
+
"data": {
|
|
648
|
+
"id": EXPORT_ID,
|
|
649
|
+
"project_id": "proj_abc123",
|
|
650
|
+
"status": "pending",
|
|
651
|
+
"filter_start_time": "2025-12-01T00:00:00+00:00",
|
|
652
|
+
"filter_end_time": "2025-12-06T23:59:59+00:00",
|
|
653
|
+
"created_at": "2025-12-06T12:00:00Z",
|
|
654
|
+
},
|
|
655
|
+
"meta": {"request_id": "req-12345"},
|
|
656
|
+
},
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
start = datetime(2025, 12, 1, 0, 0, 0, tzinfo=UTC)
|
|
660
|
+
end = datetime(2025, 12, 6, 23, 59, 59, tzinfo=UTC)
|
|
661
|
+
client.create_export(start_time=start, end_time=end, endpoint_id=ENDPOINT_ID)
|
|
662
|
+
|
|
663
|
+
request = httpx_mock.get_request()
|
|
664
|
+
assert request is not None
|
|
665
|
+
assert json.loads(request.content) == {
|
|
666
|
+
"start_time": "2025-12-01T00:00:00+00:00",
|
|
667
|
+
"end_time": "2025-12-06T23:59:59+00:00",
|
|
668
|
+
"endpoint_id": ENDPOINT_ID,
|
|
669
|
+
}
|
|
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
|