hookbase 1.0.0__tar.gz → 2.1.1__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 (79) hide show
  1. hookbase-2.1.1/.github/workflows/python-publish.yml +86 -0
  2. {hookbase-1.0.0 → hookbase-2.1.1}/PKG-INFO +2 -1
  3. {hookbase-1.0.0 → hookbase-2.1.1}/pyproject.toml +1 -0
  4. hookbase-2.1.1/src/hookbase/_version.py +1 -0
  5. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/client.py +6 -2
  6. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/deliveries.py +4 -2
  7. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/destinations.py +30 -0
  8. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/events.py +25 -2
  9. hookbase-2.1.1/src/hookbase/models/portal_tokens.py +23 -0
  10. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/sources.py +25 -1
  11. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/transforms.py +1 -1
  12. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/__init__.py +2 -0
  13. hookbase-2.1.1/src/hookbase/resources/portal_tokens.py +60 -0
  14. {hookbase-1.0.0 → hookbase-2.1.1}/tests/resources/test_sources.py +79 -0
  15. hookbase-1.0.0/src/hookbase/_version.py +0 -1
  16. {hookbase-1.0.0 → hookbase-2.1.1}/.gitignore +0 -0
  17. {hookbase-1.0.0 → hookbase-2.1.1}/LICENSE +0 -0
  18. {hookbase-1.0.0 → hookbase-2.1.1}/README.md +0 -0
  19. {hookbase-1.0.0 → hookbase-2.1.1}/examples/async_usage.py +0 -0
  20. {hookbase-1.0.0 → hookbase-2.1.1}/examples/basic_inbound.py +0 -0
  21. {hookbase-1.0.0 → hookbase-2.1.1}/examples/error_handling.py +0 -0
  22. {hookbase-1.0.0 → hookbase-2.1.1}/examples/send_webhooks.py +0 -0
  23. {hookbase-1.0.0 → hookbase-2.1.1}/examples/verify_webhooks.py +0 -0
  24. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/__init__.py +0 -0
  25. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/_client.py +0 -0
  26. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/_constants.py +0 -0
  27. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/_pagination.py +0 -0
  28. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/errors.py +0 -0
  29. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/__init__.py +0 -0
  30. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/_base.py +0 -0
  31. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/analytics.py +0 -0
  32. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/api_keys.py +0 -0
  33. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/applications.py +0 -0
  34. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/common.py +0 -0
  35. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/cron_jobs.py +0 -0
  36. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/dlq.py +0 -0
  37. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/endpoints.py +0 -0
  38. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/event_types.py +0 -0
  39. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/filters.py +0 -0
  40. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/messages.py +0 -0
  41. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/organizations.py +0 -0
  42. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/routes.py +0 -0
  43. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/schemas.py +0 -0
  44. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/subscriptions.py +0 -0
  45. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/models/tunnels.py +0 -0
  46. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/py.typed +0 -0
  47. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/_base.py +0 -0
  48. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/analytics.py +0 -0
  49. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/api_keys.py +0 -0
  50. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/applications.py +0 -0
  51. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/cron_jobs.py +0 -0
  52. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/deliveries.py +0 -0
  53. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/destinations.py +0 -0
  54. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/dlq.py +0 -0
  55. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/endpoints.py +0 -0
  56. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/event_types.py +0 -0
  57. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/events.py +0 -0
  58. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/filters.py +0 -0
  59. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/message_log.py +0 -0
  60. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/messages.py +0 -0
  61. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/organizations.py +0 -0
  62. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/routes.py +0 -0
  63. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/schemas.py +0 -0
  64. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/sources.py +0 -0
  65. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/subscriptions.py +0 -0
  66. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/transforms.py +0 -0
  67. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/resources/tunnels.py +0 -0
  68. {hookbase-1.0.0 → hookbase-2.1.1}/src/hookbase/webhook.py +0 -0
  69. {hookbase-1.0.0 → hookbase-2.1.1}/tests/__init__.py +0 -0
  70. {hookbase-1.0.0 → hookbase-2.1.1}/tests/conftest.py +0 -0
  71. {hookbase-1.0.0 → hookbase-2.1.1}/tests/resources/__init__.py +0 -0
  72. {hookbase-1.0.0 → hookbase-2.1.1}/tests/resources/test_applications.py +0 -0
  73. {hookbase-1.0.0 → hookbase-2.1.1}/tests/resources/test_destinations.py +0 -0
  74. {hookbase-1.0.0 → hookbase-2.1.1}/tests/resources/test_dlq.py +0 -0
  75. {hookbase-1.0.0 → hookbase-2.1.1}/tests/resources/test_messages.py +0 -0
  76. {hookbase-1.0.0 → hookbase-2.1.1}/tests/test_client.py +0 -0
  77. {hookbase-1.0.0 → hookbase-2.1.1}/tests/test_errors.py +0 -0
  78. {hookbase-1.0.0 → hookbase-2.1.1}/tests/test_pagination.py +0 -0
  79. {hookbase-1.0.0 → hookbase-2.1.1}/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: 1.0.0
3
+ Version: 2.1.1
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.1.1"
@@ -25,6 +25,7 @@ from .resources import (
25
25
  AsyncMessageLog,
26
26
  AsyncMessages,
27
27
  AsyncOrganizations,
28
+ AsyncPortalTokens,
28
29
  AsyncRoutes,
29
30
  AsyncSchemas,
30
31
  AsyncSources,
@@ -41,6 +42,7 @@ from .resources import (
41
42
  MessageLog,
42
43
  Messages,
43
44
  Organizations,
45
+ PortalTokens,
44
46
  Routes,
45
47
  Schemas,
46
48
  Sources,
@@ -55,7 +57,7 @@ class _OutboundNamespace:
55
57
 
56
58
  __slots__ = (
57
59
  "applications", "endpoints", "event_types", "subscriptions",
58
- "messages", "message_log", "dlq",
60
+ "messages", "message_log", "portal_tokens", "dlq",
59
61
  )
60
62
 
61
63
  def __init__(self, transport: SyncTransport) -> None:
@@ -65,6 +67,7 @@ class _OutboundNamespace:
65
67
  self.subscriptions = Subscriptions(transport)
66
68
  self.messages = Messages(transport)
67
69
  self.message_log = MessageLog(transport)
70
+ self.portal_tokens = PortalTokens(transport)
68
71
  self.dlq = DLQ(transport)
69
72
 
70
73
 
@@ -73,7 +76,7 @@ class _AsyncOutboundNamespace:
73
76
 
74
77
  __slots__ = (
75
78
  "applications", "endpoints", "event_types", "subscriptions",
76
- "messages", "message_log", "dlq",
79
+ "messages", "message_log", "portal_tokens", "dlq",
77
80
  )
78
81
 
79
82
  def __init__(self, transport: AsyncTransport) -> None:
@@ -83,6 +86,7 @@ class _AsyncOutboundNamespace:
83
86
  self.subscriptions = AsyncSubscriptions(transport)
84
87
  self.messages = AsyncMessages(transport)
85
88
  self.message_log = AsyncMessageLog(transport)
89
+ self.portal_tokens = AsyncPortalTokens(transport)
86
90
  self.dlq = AsyncDLQ(transport)
87
91
 
88
92
 
@@ -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 = Literal["pending", "success", "failed", "retrying"]
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):
@@ -1,7 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  from typing import Any, Literal
4
5
 
6
+ from pydantic import field_validator
7
+
5
8
  from ._base import HookbaseModel
6
9
 
7
10
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE"]
@@ -30,6 +33,33 @@ class Destination(HookbaseModel):
30
33
  created_at: str = ""
31
34
  updated_at: str = ""
32
35
 
36
+ @field_validator("headers", mode="before")
37
+ @classmethod
38
+ def parse_headers(cls, v: Any) -> Any:
39
+ if isinstance(v, str):
40
+ try:
41
+ return json.loads(v)
42
+ except (json.JSONDecodeError, ValueError):
43
+ return None
44
+ return v
45
+
46
+ @field_validator("auth_config", mode="before")
47
+ @classmethod
48
+ def parse_auth_config(cls, v: Any) -> Any:
49
+ if isinstance(v, str):
50
+ try:
51
+ return json.loads(v)
52
+ except (json.JSONDecodeError, ValueError):
53
+ return None
54
+ return v
55
+
56
+ @field_validator("is_active", mode="before")
57
+ @classmethod
58
+ def parse_is_active(cls, v: Any) -> Any:
59
+ if isinstance(v, int):
60
+ return bool(v)
61
+ return v
62
+
33
63
 
34
64
  class CreateDestinationParams(HookbaseModel):
35
65
  name: str
@@ -1,10 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Literal
3
+ import json
4
+ from typing import Any
5
+
6
+ from pydantic import field_validator
4
7
 
5
8
  from ._base import HookbaseModel
6
9
 
7
- InboundEventStatus = Literal["delivered", "failed", "pending", "partial"]
10
+ InboundEventStatus = str # API may return: delivered, failed, pending, partial, schema_failed, etc.
8
11
 
9
12
 
10
13
  class DeliveryStats(HookbaseModel):
@@ -53,6 +56,16 @@ class EventDetail(HookbaseModel):
53
56
  source_name: str = ""
54
57
  deliveries: list[EventDelivery] = []
55
58
 
59
+ @field_validator("headers", mode="before")
60
+ @classmethod
61
+ def parse_headers(cls, v: Any) -> Any:
62
+ if isinstance(v, str):
63
+ try:
64
+ return json.loads(v)
65
+ except (json.JSONDecodeError, ValueError):
66
+ return {}
67
+ return v if v is not None else {}
68
+
56
69
 
57
70
  class EventDebugEvent(HookbaseModel):
58
71
  id: str
@@ -64,6 +77,16 @@ class EventDebugEvent(HookbaseModel):
64
77
  received_at: str = ""
65
78
  ip_address: str | None = None
66
79
 
80
+ @field_validator("headers", mode="before")
81
+ @classmethod
82
+ def parse_headers(cls, v: Any) -> Any:
83
+ if isinstance(v, str):
84
+ try:
85
+ return json.loads(v)
86
+ except (json.JSONDecodeError, ValueError):
87
+ return {}
88
+ return v if v is not None else {}
89
+
67
90
 
68
91
  class EventDebugInfo(HookbaseModel):
69
92
  event: EventDebugEvent
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from ._base import HookbaseModel
4
+
5
+
6
+ class PortalToken(HookbaseModel):
7
+ id: str
8
+ application_id: str = ""
9
+ token: str | None = None
10
+ token_prefix: str | None = None
11
+ name: str | None = None
12
+ scopes: list[str] = []
13
+ expires_at: str = ""
14
+ created_at: str = ""
15
+ is_expired: bool | None = None
16
+ is_revoked: bool | None = None
17
+
18
+
19
+ class CreatePortalTokenParams(HookbaseModel):
20
+ name: str | None = None
21
+ scopes: list[str] | None = None
22
+ expires_in_days: int | None = None
23
+ allowed_ips: list[str] | None = None
@@ -1,6 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Literal
3
+ import json
4
+ from typing import Any, Literal
5
+
6
+ from pydantic import field_validator
4
7
 
5
8
  from ._base import HookbaseModel
6
9
 
@@ -31,11 +34,30 @@ class Source(HookbaseModel):
31
34
  ip_denylist: list[str] | None = None
32
35
  rate_limit: int | None = None
33
36
  rate_limit_window: int | None = None
37
+ transient_mode: bool = False
34
38
  event_count: int = 0
35
39
  last_event_at: str | None = None
36
40
  created_at: str = ""
37
41
  updated_at: str = ""
38
42
 
43
+ @field_validator("is_active", "verify_signature", "transient_mode", mode="before")
44
+ @classmethod
45
+ def parse_bool_from_int(cls, v: Any) -> Any:
46
+ if isinstance(v, int):
47
+ return bool(v)
48
+ return v
49
+
50
+ @field_validator("ip_allowlist", "ip_denylist", mode="before")
51
+ @classmethod
52
+ def parse_json_list(cls, v: Any) -> Any:
53
+ if isinstance(v, str):
54
+ try:
55
+ parsed = json.loads(v)
56
+ return parsed if isinstance(parsed, list) else None
57
+ except (json.JSONDecodeError, ValueError):
58
+ return None
59
+ return v
60
+
39
61
 
40
62
  class SourceWithSecret(Source):
41
63
  signing_secret: str = ""
@@ -55,6 +77,7 @@ class CreateSourceParams(HookbaseModel):
55
77
  ip_denylist: list[str] | None = None
56
78
  rate_limit: int | None = None
57
79
  rate_limit_window: int | None = None
80
+ transient_mode: bool | None = None
58
81
 
59
82
 
60
83
  class UpdateSourceParams(HookbaseModel):
@@ -70,3 +93,4 @@ class UpdateSourceParams(HookbaseModel):
70
93
  ip_denylist: list[str] | None = None
71
94
  rate_limit: int | None = None
72
95
  rate_limit_window: int | None = None
96
+ transient_mode: bool | None = None
@@ -4,7 +4,7 @@ from typing import Any, Literal
4
4
 
5
5
  from ._base import HookbaseModel
6
6
 
7
- TransformType = Literal["jsonata", "javascript", "mapping"]
7
+ TransformType = Literal["jsonata", "javascript", "mapping", "liquid", "xslt"]
8
8
  ContentFormat = Literal["json", "xml", "form", "text"]
9
9
 
10
10
 
@@ -12,6 +12,7 @@ from .filters import AsyncFilters, Filters
12
12
  from .message_log import AsyncMessageLog, MessageLog
13
13
  from .messages import AsyncMessages, Messages
14
14
  from .organizations import AsyncOrganizations, Organizations
15
+ from .portal_tokens import AsyncPortalTokens, PortalTokens
15
16
  from .routes import AsyncRoutes, Routes
16
17
  from .schemas import AsyncSchemas, Schemas
17
18
  from .sources import AsyncSources, Sources
@@ -34,6 +35,7 @@ __all__ = [
34
35
  "MessageLog", "AsyncMessageLog",
35
36
  "Messages", "AsyncMessages",
36
37
  "Organizations", "AsyncOrganizations",
38
+ "PortalTokens", "AsyncPortalTokens",
37
39
  "Routes", "AsyncRoutes",
38
40
  "Schemas", "AsyncSchemas",
39
41
  "Sources", "AsyncSources",
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..models.portal_tokens import CreatePortalTokenParams, PortalToken
6
+ from ._base import AsyncResource, SyncResource, _to_body
7
+
8
+
9
+ class PortalTokens(SyncResource):
10
+ def create(
11
+ self,
12
+ application_id: str,
13
+ params: CreatePortalTokenParams | dict[str, Any] | None = None,
14
+ ) -> PortalToken:
15
+ body = _to_body(params) if params else {}
16
+ resp = self._request(
17
+ "POST",
18
+ f"/api/portal/webhook-applications/{application_id}/tokens",
19
+ json=body,
20
+ )
21
+ data = resp.get("data", resp)
22
+ return self._parse(PortalToken, data)
23
+
24
+ def list(self, application_id: str) -> list[PortalToken]:
25
+ resp = self._request(
26
+ "GET",
27
+ f"/api/portal/webhook-applications/{application_id}/tokens",
28
+ )
29
+ items = resp.get("data", [])
30
+ return self._parse_list(PortalToken, items if isinstance(items, list) else [])
31
+
32
+ def revoke(self, token_id: str) -> None:
33
+ self._request("DELETE", f"/api/portal/tokens/{token_id}")
34
+
35
+
36
+ class AsyncPortalTokens(AsyncResource):
37
+ async def create(
38
+ self,
39
+ application_id: str,
40
+ params: CreatePortalTokenParams | dict[str, Any] | None = None,
41
+ ) -> PortalToken:
42
+ body = _to_body(params) if params else {}
43
+ resp = await self._request(
44
+ "POST",
45
+ f"/api/portal/webhook-applications/{application_id}/tokens",
46
+ json=body,
47
+ )
48
+ data = resp.get("data", resp)
49
+ return self._parse(PortalToken, data)
50
+
51
+ async def list(self, application_id: str) -> list[PortalToken]:
52
+ resp = await self._request(
53
+ "GET",
54
+ f"/api/portal/webhook-applications/{application_id}/tokens",
55
+ )
56
+ items = resp.get("data", [])
57
+ return self._parse_list(PortalToken, items if isinstance(items, list) else [])
58
+
59
+ async def revoke(self, token_id: str) -> None:
60
+ await self._request("DELETE", f"/api/portal/tokens/{token_id}")
@@ -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__ = "1.0.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