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.
Files changed (80) hide show
  1. hookbase-2.2.0/.github/workflows/python-publish.yml +86 -0
  2. {hookbase-2.0.0 → hookbase-2.2.0}/PKG-INFO +2 -1
  3. {hookbase-2.0.0 → hookbase-2.2.0}/pyproject.toml +1 -0
  4. hookbase-2.2.0/src/hookbase/_version.py +1 -0
  5. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/__init__.py +12 -0
  6. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/deliveries.py +4 -2
  7. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/destinations.py +76 -2
  8. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/events.py +1 -1
  9. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/portal_tokens.py +0 -2
  10. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/sources.py +4 -1
  11. hookbase-2.2.0/tests/resources/test_destinations.py +193 -0
  12. {hookbase-2.0.0 → hookbase-2.2.0}/tests/resources/test_sources.py +79 -0
  13. hookbase-2.0.0/src/hookbase/_version.py +0 -1
  14. hookbase-2.0.0/tests/resources/test_destinations.py +0 -72
  15. {hookbase-2.0.0 → hookbase-2.2.0}/.gitignore +0 -0
  16. {hookbase-2.0.0 → hookbase-2.2.0}/LICENSE +0 -0
  17. {hookbase-2.0.0 → hookbase-2.2.0}/README.md +0 -0
  18. {hookbase-2.0.0 → hookbase-2.2.0}/examples/async_usage.py +0 -0
  19. {hookbase-2.0.0 → hookbase-2.2.0}/examples/basic_inbound.py +0 -0
  20. {hookbase-2.0.0 → hookbase-2.2.0}/examples/error_handling.py +0 -0
  21. {hookbase-2.0.0 → hookbase-2.2.0}/examples/send_webhooks.py +0 -0
  22. {hookbase-2.0.0 → hookbase-2.2.0}/examples/verify_webhooks.py +0 -0
  23. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/__init__.py +0 -0
  24. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/_client.py +0 -0
  25. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/_constants.py +0 -0
  26. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/_pagination.py +0 -0
  27. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/client.py +0 -0
  28. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/errors.py +0 -0
  29. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/_base.py +0 -0
  30. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/analytics.py +0 -0
  31. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/api_keys.py +0 -0
  32. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/applications.py +0 -0
  33. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/common.py +0 -0
  34. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/cron_jobs.py +0 -0
  35. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/dlq.py +0 -0
  36. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/endpoints.py +0 -0
  37. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/event_types.py +0 -0
  38. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/filters.py +0 -0
  39. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/messages.py +0 -0
  40. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/organizations.py +0 -0
  41. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/routes.py +0 -0
  42. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/schemas.py +0 -0
  43. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/subscriptions.py +0 -0
  44. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/transforms.py +0 -0
  45. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/models/tunnels.py +0 -0
  46. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/py.typed +0 -0
  47. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/__init__.py +0 -0
  48. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/_base.py +0 -0
  49. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/analytics.py +0 -0
  50. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/api_keys.py +0 -0
  51. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/applications.py +0 -0
  52. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/cron_jobs.py +0 -0
  53. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/deliveries.py +0 -0
  54. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/destinations.py +0 -0
  55. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/dlq.py +0 -0
  56. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/endpoints.py +0 -0
  57. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/event_types.py +0 -0
  58. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/events.py +0 -0
  59. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/filters.py +0 -0
  60. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/message_log.py +0 -0
  61. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/messages.py +0 -0
  62. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/organizations.py +0 -0
  63. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/portal_tokens.py +0 -0
  64. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/routes.py +0 -0
  65. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/schemas.py +0 -0
  66. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/sources.py +0 -0
  67. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/subscriptions.py +0 -0
  68. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/transforms.py +0 -0
  69. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/resources/tunnels.py +0 -0
  70. {hookbase-2.0.0 → hookbase-2.2.0}/src/hookbase/webhook.py +0 -0
  71. {hookbase-2.0.0 → hookbase-2.2.0}/tests/__init__.py +0 -0
  72. {hookbase-2.0.0 → hookbase-2.2.0}/tests/conftest.py +0 -0
  73. {hookbase-2.0.0 → hookbase-2.2.0}/tests/resources/__init__.py +0 -0
  74. {hookbase-2.0.0 → hookbase-2.2.0}/tests/resources/test_applications.py +0 -0
  75. {hookbase-2.0.0 → hookbase-2.2.0}/tests/resources/test_dlq.py +0 -0
  76. {hookbase-2.0.0 → hookbase-2.2.0}/tests/resources/test_messages.py +0 -0
  77. {hookbase-2.0.0 → hookbase-2.2.0}/tests/test_client.py +0 -0
  78. {hookbase-2.0.0 → hookbase-2.2.0}/tests/test_errors.py +0 -0
  79. {hookbase-2.0.0 → hookbase-2.2.0}/tests/test_pagination.py +0 -0
  80. {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.0.0
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
@@ -29,6 +29,7 @@ classifiers = [
29
29
  dependencies = [
30
30
  "httpx>=0.25.0,<1.0.0",
31
31
  "pydantic>=2.0.0,<3.0.0",
32
+ "eval_type_backport>=0.2.0; python_version < '3.10'",
32
33
  ]
33
34
 
34
35
  [project.optional-dependencies]
@@ -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, Literal
3
+ from typing import Any
4
4
 
5
5
  from ._base import HookbaseModel
6
6
 
7
- DeliveryStatus = str # API may return: pending, success, failed, retrying, delivered, failed_over, schema_failed, exhausted, etc.
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
- url: str
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
- url: str
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):
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
- from typing import Any, Literal
4
+ from typing import Any
5
5
 
6
6
  from pydantic import field_validator
7
7
 
@@ -1,7 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any
4
-
5
3
  from ._base import HookbaseModel
6
4
 
7
5
 
@@ -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