hookbase 2.0.0__tar.gz → 2.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-2.2.0/.github/workflows/python-publish.yml +86 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/PKG-INFO +2 -1
- {hookbase-2.0.0 → hookbase-2.2.0}/pyproject.toml +1 -0
- hookbase-2.2.0/src/hookbase/_version.py +1 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/__init__.py +12 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/deliveries.py +4 -2
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/destinations.py +76 -2
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/events.py +1 -1
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/portal_tokens.py +0 -2
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/sources.py +4 -1
- hookbase-2.2.0/tests/resources/test_destinations.py +193 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/tests/resources/test_sources.py +79 -0
- hookbase-2.0.0/src/hookbase/_version.py +0 -1
- hookbase-2.0.0/tests/resources/test_destinations.py +0 -72
- {hookbase-2.0.0 → hookbase-2.2.0}/.gitignore +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/LICENSE +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/README.md +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/examples/async_usage.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/examples/basic_inbound.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/examples/error_handling.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/examples/send_webhooks.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/examples/verify_webhooks.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/__init__.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/_client.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/_constants.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/_pagination.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/client.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/errors.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/_base.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/analytics.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/api_keys.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/applications.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/common.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/cron_jobs.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/dlq.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/endpoints.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/event_types.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/filters.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/messages.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/organizations.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/routes.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/schemas.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/subscriptions.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/transforms.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/tunnels.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/py.typed +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/__init__.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/_base.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/analytics.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/api_keys.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/applications.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/cron_jobs.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/deliveries.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/destinations.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/dlq.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/endpoints.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/event_types.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/events.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/filters.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/message_log.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/messages.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/organizations.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/portal_tokens.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/routes.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/schemas.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/sources.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/subscriptions.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/transforms.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/tunnels.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/webhook.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/tests/__init__.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/tests/conftest.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/tests/resources/__init__.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/tests/resources/test_applications.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/tests/resources/test_dlq.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/tests/resources/test_messages.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/tests/test_client.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/tests/test_errors.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/tests/test_pagination.py +0 -0
- {hookbase-2.0.0 → hookbase-2.2.0}/tests/test_webhook.py +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
name: CI / Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master]
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
test:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
strategy:
|
|
18
|
+
matrix:
|
|
19
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: ${{ matrix.python-version }}
|
|
27
|
+
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: |
|
|
30
|
+
python -m pip install --upgrade pip
|
|
31
|
+
pip install -e ".[dev]"
|
|
32
|
+
|
|
33
|
+
- name: Lint with ruff
|
|
34
|
+
run: ruff check src/ tests/
|
|
35
|
+
|
|
36
|
+
- name: Type check with mypy
|
|
37
|
+
run: mypy src/hookbase
|
|
38
|
+
|
|
39
|
+
- name: Run tests
|
|
40
|
+
run: pytest -v
|
|
41
|
+
|
|
42
|
+
release-build:
|
|
43
|
+
if: github.event_name == 'release'
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
needs: test
|
|
46
|
+
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
|
|
50
|
+
- uses: actions/setup-python@v5
|
|
51
|
+
with:
|
|
52
|
+
python-version: "3.x"
|
|
53
|
+
|
|
54
|
+
- name: Build release distributions
|
|
55
|
+
run: |
|
|
56
|
+
python -m pip install build
|
|
57
|
+
python -m build
|
|
58
|
+
|
|
59
|
+
- name: Upload distributions
|
|
60
|
+
uses: actions/upload-artifact@v4
|
|
61
|
+
with:
|
|
62
|
+
name: release-dists
|
|
63
|
+
path: dist/
|
|
64
|
+
|
|
65
|
+
pypi-publish:
|
|
66
|
+
if: github.event_name == 'release'
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
needs: release-build
|
|
69
|
+
permissions:
|
|
70
|
+
id-token: write
|
|
71
|
+
|
|
72
|
+
environment:
|
|
73
|
+
name: pypi
|
|
74
|
+
url: https://pypi.org/project/hookbase/${{ github.event.release.tag_name }}
|
|
75
|
+
|
|
76
|
+
steps:
|
|
77
|
+
- name: Retrieve release distributions
|
|
78
|
+
uses: actions/download-artifact@v4
|
|
79
|
+
with:
|
|
80
|
+
name: release-dists
|
|
81
|
+
path: dist/
|
|
82
|
+
|
|
83
|
+
- name: Publish release distributions to PyPI
|
|
84
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
85
|
+
with:
|
|
86
|
+
packages-dir: dist/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hookbase
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.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://docs.hookbase.app/sdk/python
|
|
@@ -22,6 +22,7 @@ Classifier: Topic :: Internet :: WWW/HTTP
|
|
|
22
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
23
|
Classifier: Typing :: Typed
|
|
24
24
|
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: eval-type-backport>=0.2.0; python_version < '3.10'
|
|
25
26
|
Requires-Dist: httpx<1.0.0,>=0.25.0
|
|
26
27
|
Requires-Dist: pydantic<3.0.0,>=2.0.0
|
|
27
28
|
Provides-Extra: dev
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.2.0"
|
|
@@ -18,9 +18,15 @@ from .deliveries import (
|
|
|
18
18
|
)
|
|
19
19
|
from .destinations import (
|
|
20
20
|
AuthType,
|
|
21
|
+
AzureBlobConfig,
|
|
21
22
|
CreateDestinationParams,
|
|
22
23
|
Destination,
|
|
24
|
+
DestinationType,
|
|
25
|
+
FieldMapping,
|
|
26
|
+
GCSConfig,
|
|
23
27
|
HttpMethod,
|
|
28
|
+
R2Config,
|
|
29
|
+
S3Config,
|
|
24
30
|
TestResult,
|
|
25
31
|
UpdateDestinationParams,
|
|
26
32
|
)
|
|
@@ -123,9 +129,15 @@ __all__ = [
|
|
|
123
129
|
"ReplayResult",
|
|
124
130
|
# Destinations
|
|
125
131
|
"AuthType",
|
|
132
|
+
"AzureBlobConfig",
|
|
126
133
|
"CreateDestinationParams",
|
|
127
134
|
"Destination",
|
|
135
|
+
"DestinationType",
|
|
136
|
+
"FieldMapping",
|
|
137
|
+
"GCSConfig",
|
|
128
138
|
"HttpMethod",
|
|
139
|
+
"R2Config",
|
|
140
|
+
"S3Config",
|
|
129
141
|
"TestResult",
|
|
130
142
|
"UpdateDestinationParams",
|
|
131
143
|
# DLQ
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any
|
|
4
4
|
|
|
5
5
|
from ._base import HookbaseModel
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# API may return: pending, success, failed, retrying, delivered,
|
|
8
|
+
# failed_over, schema_failed, exhausted, etc.
|
|
9
|
+
DeliveryStatus = str
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class Delivery(HookbaseModel):
|
|
@@ -9,6 +9,52 @@ from ._base import HookbaseModel
|
|
|
9
9
|
|
|
10
10
|
HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE"]
|
|
11
11
|
AuthType = Literal["none", "basic", "bearer", "api_key", "custom_header"]
|
|
12
|
+
DestinationType = Literal["http", "s3", "r2", "gcs", "azure_blob"]
|
|
13
|
+
FileFormat = Literal["json", "jsonl"]
|
|
14
|
+
PartitionBy = Literal["date", "hour", "source"]
|
|
15
|
+
FieldMappingType = Literal["string", "number", "boolean", "timestamp", "json"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FieldMapping(HookbaseModel):
|
|
19
|
+
source: str
|
|
20
|
+
target: str
|
|
21
|
+
type: FieldMappingType
|
|
22
|
+
default: str | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class S3Config(HookbaseModel):
|
|
26
|
+
bucket: str
|
|
27
|
+
region: str
|
|
28
|
+
access_key_id: str
|
|
29
|
+
secret_access_key: str
|
|
30
|
+
prefix: str | None = None
|
|
31
|
+
file_format: FileFormat | None = None
|
|
32
|
+
partition_by: PartitionBy | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class R2Config(HookbaseModel):
|
|
36
|
+
bucket: str
|
|
37
|
+
prefix: str | None = None
|
|
38
|
+
file_format: FileFormat | None = None
|
|
39
|
+
partition_by: PartitionBy | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GCSConfig(HookbaseModel):
|
|
43
|
+
bucket: str
|
|
44
|
+
project_id: str
|
|
45
|
+
service_account_key: str
|
|
46
|
+
prefix: str | None = None
|
|
47
|
+
file_format: FileFormat | None = None
|
|
48
|
+
partition_by: PartitionBy | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AzureBlobConfig(HookbaseModel):
|
|
52
|
+
account_name: str
|
|
53
|
+
account_key: str
|
|
54
|
+
container_name: str
|
|
55
|
+
prefix: str | None = None
|
|
56
|
+
file_format: FileFormat | None = None
|
|
57
|
+
partition_by: PartitionBy | None = None
|
|
12
58
|
|
|
13
59
|
|
|
14
60
|
class Destination(HookbaseModel):
|
|
@@ -17,7 +63,8 @@ class Destination(HookbaseModel):
|
|
|
17
63
|
name: str
|
|
18
64
|
slug: str
|
|
19
65
|
description: str | None = None
|
|
20
|
-
|
|
66
|
+
type: DestinationType = "http"
|
|
67
|
+
url: str = ""
|
|
21
68
|
method: HttpMethod = "POST"
|
|
22
69
|
headers: dict[str, str] | None = None
|
|
23
70
|
auth_type: str | None = "none"
|
|
@@ -28,6 +75,8 @@ class Destination(HookbaseModel):
|
|
|
28
75
|
rate_limit: int | None = None
|
|
29
76
|
rate_limit_window: int | None = None
|
|
30
77
|
is_active: bool = True
|
|
78
|
+
config: dict[str, Any] | None = None
|
|
79
|
+
field_mapping: list[FieldMapping] | None = None
|
|
31
80
|
delivery_count: int = 0
|
|
32
81
|
last_delivery_at: str | None = None
|
|
33
82
|
created_at: str = ""
|
|
@@ -53,6 +102,26 @@ class Destination(HookbaseModel):
|
|
|
53
102
|
return None
|
|
54
103
|
return v
|
|
55
104
|
|
|
105
|
+
@field_validator("config", mode="before")
|
|
106
|
+
@classmethod
|
|
107
|
+
def parse_config(cls, v: Any) -> Any:
|
|
108
|
+
if isinstance(v, str):
|
|
109
|
+
try:
|
|
110
|
+
return json.loads(v)
|
|
111
|
+
except (json.JSONDecodeError, ValueError):
|
|
112
|
+
return None
|
|
113
|
+
return v
|
|
114
|
+
|
|
115
|
+
@field_validator("field_mapping", mode="before")
|
|
116
|
+
@classmethod
|
|
117
|
+
def parse_field_mapping(cls, v: Any) -> Any:
|
|
118
|
+
if isinstance(v, str):
|
|
119
|
+
try:
|
|
120
|
+
return json.loads(v)
|
|
121
|
+
except (json.JSONDecodeError, ValueError):
|
|
122
|
+
return None
|
|
123
|
+
return v
|
|
124
|
+
|
|
56
125
|
@field_validator("is_active", mode="before")
|
|
57
126
|
@classmethod
|
|
58
127
|
def parse_is_active(cls, v: Any) -> Any:
|
|
@@ -65,7 +134,8 @@ class CreateDestinationParams(HookbaseModel):
|
|
|
65
134
|
name: str
|
|
66
135
|
slug: str | None = None
|
|
67
136
|
description: str | None = None
|
|
68
|
-
|
|
137
|
+
type: DestinationType | None = None
|
|
138
|
+
url: str | None = None
|
|
69
139
|
method: HttpMethod | None = None
|
|
70
140
|
headers: dict[str, str] | None = None
|
|
71
141
|
auth_type: AuthType | None = None
|
|
@@ -75,6 +145,8 @@ class CreateDestinationParams(HookbaseModel):
|
|
|
75
145
|
retry_interval: int | None = None
|
|
76
146
|
rate_limit: int | None = None
|
|
77
147
|
rate_limit_window: int | None = None
|
|
148
|
+
config: dict[str, Any] | None = None
|
|
149
|
+
field_mapping: list[FieldMapping] | None = None
|
|
78
150
|
|
|
79
151
|
|
|
80
152
|
class UpdateDestinationParams(HookbaseModel):
|
|
@@ -91,6 +163,8 @@ class UpdateDestinationParams(HookbaseModel):
|
|
|
91
163
|
rate_limit: int | None = None
|
|
92
164
|
rate_limit_window: int | None = None
|
|
93
165
|
is_active: bool | None = None
|
|
166
|
+
config: dict[str, Any] | None = None
|
|
167
|
+
field_mapping: list[FieldMapping] | None = None
|
|
94
168
|
|
|
95
169
|
|
|
96
170
|
class TestResult(HookbaseModel):
|
|
@@ -34,12 +34,13 @@ class Source(HookbaseModel):
|
|
|
34
34
|
ip_denylist: list[str] | None = None
|
|
35
35
|
rate_limit: int | None = None
|
|
36
36
|
rate_limit_window: int | None = None
|
|
37
|
+
transient_mode: bool = False
|
|
37
38
|
event_count: int = 0
|
|
38
39
|
last_event_at: str | None = None
|
|
39
40
|
created_at: str = ""
|
|
40
41
|
updated_at: str = ""
|
|
41
42
|
|
|
42
|
-
@field_validator("is_active", "verify_signature", mode="before")
|
|
43
|
+
@field_validator("is_active", "verify_signature", "transient_mode", mode="before")
|
|
43
44
|
@classmethod
|
|
44
45
|
def parse_bool_from_int(cls, v: Any) -> Any:
|
|
45
46
|
if isinstance(v, int):
|
|
@@ -76,6 +77,7 @@ class CreateSourceParams(HookbaseModel):
|
|
|
76
77
|
ip_denylist: list[str] | None = None
|
|
77
78
|
rate_limit: int | None = None
|
|
78
79
|
rate_limit_window: int | None = None
|
|
80
|
+
transient_mode: bool | None = None
|
|
79
81
|
|
|
80
82
|
|
|
81
83
|
class UpdateSourceParams(HookbaseModel):
|
|
@@ -91,3 +93,4 @@ class UpdateSourceParams(HookbaseModel):
|
|
|
91
93
|
ip_denylist: list[str] | None = None
|
|
92
94
|
rate_limit: int | None = None
|
|
93
95
|
rate_limit_window: int | None = None
|
|
96
|
+
transient_mode: bool | None = None
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import respx
|
|
5
|
+
|
|
6
|
+
from hookbase import Hookbase
|
|
7
|
+
from hookbase.models import Destination, FieldMapping, TestResult
|
|
8
|
+
|
|
9
|
+
from ..conftest import make_paginated_response
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def mock_api():
|
|
14
|
+
with respx.mock(base_url="https://api.hookbase.app") as mock:
|
|
15
|
+
yield mock
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def client(mock_api):
|
|
20
|
+
c = Hookbase(api_key="whr_test")
|
|
21
|
+
yield c
|
|
22
|
+
c.close()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
DEST_DATA = {
|
|
26
|
+
"id": "dst_1",
|
|
27
|
+
"organizationId": "org_1",
|
|
28
|
+
"name": "Backend",
|
|
29
|
+
"slug": "backend",
|
|
30
|
+
"type": "http",
|
|
31
|
+
"url": "https://example.com/webhooks",
|
|
32
|
+
"method": "POST",
|
|
33
|
+
"authType": "none",
|
|
34
|
+
"timeout": 30,
|
|
35
|
+
"retryCount": 3,
|
|
36
|
+
"retryInterval": 60,
|
|
37
|
+
"isActive": True,
|
|
38
|
+
"config": None,
|
|
39
|
+
"fieldMapping": None,
|
|
40
|
+
"deliveryCount": 100,
|
|
41
|
+
"createdAt": "2024-01-01T00:00:00Z",
|
|
42
|
+
"updatedAt": "2024-01-01T00:00:00Z",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
S3_DEST_DATA = {
|
|
46
|
+
"id": "dst_s3",
|
|
47
|
+
"organizationId": "org_1",
|
|
48
|
+
"name": "S3 Archive",
|
|
49
|
+
"slug": "s3-archive",
|
|
50
|
+
"type": "s3",
|
|
51
|
+
"url": "",
|
|
52
|
+
"method": "POST",
|
|
53
|
+
"authType": "none",
|
|
54
|
+
"timeout": 30,
|
|
55
|
+
"retryCount": 3,
|
|
56
|
+
"retryInterval": 60,
|
|
57
|
+
"isActive": True,
|
|
58
|
+
"config": {
|
|
59
|
+
"bucket": "my-webhooks",
|
|
60
|
+
"region": "us-east-1",
|
|
61
|
+
"accessKeyId": "AKIAIOSFODNN7EXAMPLE",
|
|
62
|
+
"secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
|
63
|
+
"prefix": "events/",
|
|
64
|
+
"fileFormat": "jsonl",
|
|
65
|
+
"partitionBy": "date",
|
|
66
|
+
},
|
|
67
|
+
"fieldMapping": [
|
|
68
|
+
{"source": "$.id", "target": "event_id", "type": "string"},
|
|
69
|
+
{"source": "$.payload", "target": "data", "type": "json"},
|
|
70
|
+
{"source": "$.timestamp", "target": "received_at", "type": "timestamp", "default": "now()"},
|
|
71
|
+
],
|
|
72
|
+
"deliveryCount": 0,
|
|
73
|
+
"createdAt": "2024-01-01T00:00:00Z",
|
|
74
|
+
"updatedAt": "2024-01-01T00:00:00Z",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_list_destinations(mock_api, client):
|
|
79
|
+
mock_api.get("/api/destinations").respond(200, json=make_paginated_response(
|
|
80
|
+
[DEST_DATA], data_key="destinations", total=1,
|
|
81
|
+
))
|
|
82
|
+
page = client.destinations.list()
|
|
83
|
+
assert len(page) == 1
|
|
84
|
+
assert page.data[0].name == "Backend"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_get_destination(mock_api, client):
|
|
88
|
+
mock_api.get("/api/destinations/dst_1").respond(200, json={"destination": DEST_DATA})
|
|
89
|
+
dest = client.destinations.get("dst_1")
|
|
90
|
+
assert isinstance(dest, Destination)
|
|
91
|
+
assert dest.url == "https://example.com/webhooks"
|
|
92
|
+
assert dest.type == "http"
|
|
93
|
+
assert dest.config is None
|
|
94
|
+
assert dest.field_mapping is None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_get_warehouse_destination(mock_api, client):
|
|
98
|
+
mock_api.get("/api/destinations/dst_s3").respond(200, json={"destination": S3_DEST_DATA})
|
|
99
|
+
dest = client.destinations.get("dst_s3")
|
|
100
|
+
assert isinstance(dest, Destination)
|
|
101
|
+
assert dest.type == "s3"
|
|
102
|
+
assert dest.config is not None
|
|
103
|
+
assert dest.config["bucket"] == "my-webhooks"
|
|
104
|
+
assert dest.config["region"] == "us-east-1"
|
|
105
|
+
assert dest.field_mapping is not None
|
|
106
|
+
assert len(dest.field_mapping) == 3
|
|
107
|
+
assert dest.field_mapping[0].source == "$.id"
|
|
108
|
+
assert dest.field_mapping[0].target == "event_id"
|
|
109
|
+
assert dest.field_mapping[0].type == "string"
|
|
110
|
+
assert dest.field_mapping[2].default == "now()"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_create_destination(mock_api, client):
|
|
114
|
+
mock_api.post("/api/destinations").respond(200, json={"destination": DEST_DATA})
|
|
115
|
+
dest = client.destinations.create({"name": "Backend", "url": "https://example.com/webhooks"})
|
|
116
|
+
assert dest.id == "dst_1"
|
|
117
|
+
assert dest.type == "http"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_create_warehouse_destination(mock_api, client):
|
|
121
|
+
mock_api.post("/api/destinations").respond(200, json={"destination": S3_DEST_DATA})
|
|
122
|
+
dest = client.destinations.create({
|
|
123
|
+
"name": "S3 Archive",
|
|
124
|
+
"type": "s3",
|
|
125
|
+
"config": {
|
|
126
|
+
"bucket": "my-webhooks",
|
|
127
|
+
"region": "us-east-1",
|
|
128
|
+
"accessKeyId": "AKIAIOSFODNN7EXAMPLE",
|
|
129
|
+
"secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
|
130
|
+
},
|
|
131
|
+
"fieldMapping": [
|
|
132
|
+
{"source": "$.id", "target": "event_id", "type": "string"},
|
|
133
|
+
],
|
|
134
|
+
})
|
|
135
|
+
assert dest.id == "dst_s3"
|
|
136
|
+
assert dest.type == "s3"
|
|
137
|
+
assert dest.config is not None
|
|
138
|
+
assert dest.field_mapping is not None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_update_warehouse_destination_config(mock_api, client):
|
|
142
|
+
updated = {**S3_DEST_DATA, "config": {**S3_DEST_DATA["config"], "bucket": "new-bucket"}}
|
|
143
|
+
mock_api.patch("/api/destinations/dst_s3").respond(200, json={"destination": updated})
|
|
144
|
+
client.destinations.update("dst_s3", {
|
|
145
|
+
"config": {
|
|
146
|
+
"bucket": "new-bucket",
|
|
147
|
+
"region": "us-east-1",
|
|
148
|
+
"accessKeyId": "AKIA",
|
|
149
|
+
"secretAccessKey": "secret",
|
|
150
|
+
},
|
|
151
|
+
"fieldMapping": [
|
|
152
|
+
{"source": "$.body", "target": "payload", "type": "json"},
|
|
153
|
+
],
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_field_mapping_model():
|
|
158
|
+
fm = FieldMapping(source="$.id", target="event_id", type="string")
|
|
159
|
+
assert fm.source == "$.id"
|
|
160
|
+
assert fm.target == "event_id"
|
|
161
|
+
assert fm.type == "string"
|
|
162
|
+
assert fm.default is None
|
|
163
|
+
|
|
164
|
+
fm_with_default = FieldMapping(
|
|
165
|
+
source="$.ts", target="timestamp", type="timestamp", default="now()",
|
|
166
|
+
)
|
|
167
|
+
assert fm_with_default.default == "now()"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_destination_without_type_defaults():
|
|
171
|
+
"""Destinations without type field should default to http."""
|
|
172
|
+
data = {
|
|
173
|
+
"id": "dst_old",
|
|
174
|
+
"name": "Old Dest",
|
|
175
|
+
"slug": "old-dest",
|
|
176
|
+
"url": "https://example.com",
|
|
177
|
+
"method": "POST",
|
|
178
|
+
"isActive": True,
|
|
179
|
+
}
|
|
180
|
+
dest = Destination(**data)
|
|
181
|
+
assert dest.type == "http"
|
|
182
|
+
assert dest.config is None
|
|
183
|
+
assert dest.field_mapping is None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_test_destination(mock_api, client):
|
|
187
|
+
mock_api.post("/api/destinations/dst_1/test").respond(200, json={
|
|
188
|
+
"success": True, "statusCode": 200, "duration": 150.5,
|
|
189
|
+
})
|
|
190
|
+
result = client.destinations.test("dst_1")
|
|
191
|
+
assert isinstance(result, TestResult)
|
|
192
|
+
assert result.success is True
|
|
193
|
+
assert result.status_code == 200
|
|
@@ -5,6 +5,7 @@ import respx
|
|
|
5
5
|
|
|
6
6
|
from hookbase import Hookbase
|
|
7
7
|
from hookbase.models import Source, SourceWithSecret
|
|
8
|
+
from hookbase.models.sources import CreateSourceParams, UpdateSourceParams
|
|
8
9
|
|
|
9
10
|
from ..conftest import make_paginated_response
|
|
10
11
|
|
|
@@ -110,3 +111,81 @@ def test_import_sources(mock_api, client):
|
|
|
110
111
|
})
|
|
111
112
|
result = client.sources.import_sources([SOURCE_DATA])
|
|
112
113
|
assert result.imported == 1
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# --- transient_mode tests ---
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_source_transient_mode_defaults_false():
|
|
120
|
+
src = Source(**SOURCE_DATA)
|
|
121
|
+
assert src.transient_mode is False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_source_transient_mode_true():
|
|
125
|
+
data = {**SOURCE_DATA, "transientMode": True}
|
|
126
|
+
src = Source(**data)
|
|
127
|
+
assert src.transient_mode is True
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_source_transient_mode_from_int():
|
|
131
|
+
"""D1/SQLite returns booleans as 0/1 integers."""
|
|
132
|
+
data = {**SOURCE_DATA, "transientMode": 1}
|
|
133
|
+
src = Source(**data)
|
|
134
|
+
assert src.transient_mode is True
|
|
135
|
+
|
|
136
|
+
data_off = {**SOURCE_DATA, "transientMode": 0}
|
|
137
|
+
src_off = Source(**data_off)
|
|
138
|
+
assert src_off.transient_mode is False
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_source_transient_mode_serializes_as_camel():
|
|
142
|
+
src = Source(**{**SOURCE_DATA, "transientMode": True})
|
|
143
|
+
dumped = src.model_dump(by_alias=True)
|
|
144
|
+
assert "transientMode" in dumped
|
|
145
|
+
assert dumped["transientMode"] is True
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_create_source_with_transient_mode(mock_api, client):
|
|
149
|
+
resp_data = {**SOURCE_DATA, "transientMode": True, "signingSecret": "whsec_abc"}
|
|
150
|
+
mock_api.post("/api/sources").respond(200, json={"source": resp_data})
|
|
151
|
+
src = client.sources.create({
|
|
152
|
+
"name": "HIPAA Source",
|
|
153
|
+
"slug": "hipaa",
|
|
154
|
+
"transient_mode": True,
|
|
155
|
+
})
|
|
156
|
+
assert isinstance(src, SourceWithSecret)
|
|
157
|
+
assert src.transient_mode is True
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_create_source_params_serialization():
|
|
161
|
+
params = CreateSourceParams(name="test", transient_mode=True)
|
|
162
|
+
dumped = params.model_dump(by_alias=True, exclude_none=True)
|
|
163
|
+
assert dumped["transientMode"] is True
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_update_source_with_transient_mode(mock_api, client):
|
|
167
|
+
mock_api.patch("/api/sources/src_1").respond(204)
|
|
168
|
+
client.sources.update("src_1", {"transient_mode": True})
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_update_source_params_serialization():
|
|
172
|
+
params = UpdateSourceParams(transient_mode=True)
|
|
173
|
+
dumped = params.model_dump(by_alias=True, exclude_none=True)
|
|
174
|
+
assert dumped["transientMode"] is True
|
|
175
|
+
assert "name" not in dumped
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_get_source_with_transient_mode(mock_api, client):
|
|
179
|
+
data = {**SOURCE_DATA, "transientMode": True}
|
|
180
|
+
mock_api.get("/api/sources/src_1").respond(200, json={"source": data})
|
|
181
|
+
src = client.sources.get("src_1")
|
|
182
|
+
assert src.transient_mode is True
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_list_sources_with_transient_mode(mock_api, client):
|
|
186
|
+
data = {**SOURCE_DATA, "transientMode": True}
|
|
187
|
+
mock_api.get("/api/sources").respond(200, json=make_paginated_response(
|
|
188
|
+
[data], data_key="sources", total=1,
|
|
189
|
+
))
|
|
190
|
+
page = client.sources.list()
|
|
191
|
+
assert page.data[0].transient_mode is True
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.0.0"
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
import respx
|
|
5
|
-
|
|
6
|
-
from hookbase import Hookbase
|
|
7
|
-
from hookbase.models import Destination, TestResult
|
|
8
|
-
|
|
9
|
-
from ..conftest import make_paginated_response
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@pytest.fixture
|
|
13
|
-
def mock_api():
|
|
14
|
-
with respx.mock(base_url="https://api.hookbase.app") as mock:
|
|
15
|
-
yield mock
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@pytest.fixture
|
|
19
|
-
def client(mock_api):
|
|
20
|
-
c = Hookbase(api_key="whr_test")
|
|
21
|
-
yield c
|
|
22
|
-
c.close()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
DEST_DATA = {
|
|
26
|
-
"id": "dst_1",
|
|
27
|
-
"organizationId": "org_1",
|
|
28
|
-
"name": "Backend",
|
|
29
|
-
"slug": "backend",
|
|
30
|
-
"url": "https://example.com/webhooks",
|
|
31
|
-
"method": "POST",
|
|
32
|
-
"authType": "none",
|
|
33
|
-
"timeout": 30,
|
|
34
|
-
"retryCount": 3,
|
|
35
|
-
"retryInterval": 60,
|
|
36
|
-
"isActive": True,
|
|
37
|
-
"deliveryCount": 100,
|
|
38
|
-
"createdAt": "2024-01-01T00:00:00Z",
|
|
39
|
-
"updatedAt": "2024-01-01T00:00:00Z",
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_list_destinations(mock_api, client):
|
|
44
|
-
mock_api.get("/api/destinations").respond(200, json=make_paginated_response(
|
|
45
|
-
[DEST_DATA], data_key="destinations", total=1,
|
|
46
|
-
))
|
|
47
|
-
page = client.destinations.list()
|
|
48
|
-
assert len(page) == 1
|
|
49
|
-
assert page.data[0].name == "Backend"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def test_get_destination(mock_api, client):
|
|
53
|
-
mock_api.get("/api/destinations/dst_1").respond(200, json={"destination": DEST_DATA})
|
|
54
|
-
dest = client.destinations.get("dst_1")
|
|
55
|
-
assert isinstance(dest, Destination)
|
|
56
|
-
assert dest.url == "https://example.com/webhooks"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def test_create_destination(mock_api, client):
|
|
60
|
-
mock_api.post("/api/destinations").respond(200, json={"destination": DEST_DATA})
|
|
61
|
-
dest = client.destinations.create({"name": "Backend", "url": "https://example.com/webhooks"})
|
|
62
|
-
assert dest.id == "dst_1"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def test_test_destination(mock_api, client):
|
|
66
|
-
mock_api.post("/api/destinations/dst_1/test").respond(200, json={
|
|
67
|
-
"success": True, "statusCode": 200, "duration": 150.5,
|
|
68
|
-
})
|
|
69
|
-
result = client.destinations.test("dst_1")
|
|
70
|
-
assert isinstance(result, TestResult)
|
|
71
|
-
assert result.success is True
|
|
72
|
-
assert result.status_code == 200
|
|
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
|