hookbase 2.8.0__tar.gz → 3.0.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 (82) hide show
  1. {hookbase-2.8.0 → hookbase-3.0.0}/PKG-INFO +1 -1
  2. hookbase-3.0.0/src/hookbase/_version.py +1 -0
  3. hookbase-3.0.0/src/hookbase/models/sources.py +180 -0
  4. hookbase-3.0.0/tests/resources/test_source_fields.py +105 -0
  5. {hookbase-2.8.0 → hookbase-3.0.0}/tests/resources/test_sources.py +6 -3
  6. hookbase-2.8.0/src/hookbase/_version.py +0 -1
  7. hookbase-2.8.0/src/hookbase/models/sources.py +0 -119
  8. {hookbase-2.8.0 → hookbase-3.0.0}/.github/workflows/python-publish.yml +0 -0
  9. {hookbase-2.8.0 → hookbase-3.0.0}/.gitignore +0 -0
  10. {hookbase-2.8.0 → hookbase-3.0.0}/LICENSE +0 -0
  11. {hookbase-2.8.0 → hookbase-3.0.0}/README.md +0 -0
  12. {hookbase-2.8.0 → hookbase-3.0.0}/examples/async_usage.py +0 -0
  13. {hookbase-2.8.0 → hookbase-3.0.0}/examples/basic_inbound.py +0 -0
  14. {hookbase-2.8.0 → hookbase-3.0.0}/examples/error_handling.py +0 -0
  15. {hookbase-2.8.0 → hookbase-3.0.0}/examples/send_webhooks.py +0 -0
  16. {hookbase-2.8.0 → hookbase-3.0.0}/examples/verify_webhooks.py +0 -0
  17. {hookbase-2.8.0 → hookbase-3.0.0}/pyproject.toml +0 -0
  18. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/__init__.py +0 -0
  19. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/_client.py +0 -0
  20. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/_constants.py +0 -0
  21. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/_pagination.py +0 -0
  22. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/client.py +0 -0
  23. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/errors.py +0 -0
  24. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/__init__.py +0 -0
  25. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/_base.py +0 -0
  26. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/analytics.py +0 -0
  27. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/api_keys.py +0 -0
  28. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/applications.py +0 -0
  29. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/common.py +0 -0
  30. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/cron_jobs.py +0 -0
  31. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/deliveries.py +0 -0
  32. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/destinations.py +0 -0
  33. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/dlq.py +0 -0
  34. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/endpoints.py +0 -0
  35. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/event_types.py +0 -0
  36. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/events.py +0 -0
  37. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/filters.py +0 -0
  38. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/messages.py +0 -0
  39. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/organizations.py +0 -0
  40. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/portal_tokens.py +0 -0
  41. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/routes.py +0 -0
  42. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/schemas.py +0 -0
  43. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/subscriptions.py +0 -0
  44. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/transforms.py +0 -0
  45. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/models/tunnels.py +0 -0
  46. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/py.typed +0 -0
  47. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/__init__.py +0 -0
  48. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/_base.py +0 -0
  49. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/analytics.py +0 -0
  50. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/api_keys.py +0 -0
  51. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/applications.py +0 -0
  52. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/cron_jobs.py +0 -0
  53. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/deliveries.py +0 -0
  54. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/destinations.py +0 -0
  55. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/dlq.py +0 -0
  56. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/endpoints.py +0 -0
  57. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/event_types.py +0 -0
  58. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/events.py +0 -0
  59. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/filters.py +0 -0
  60. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/message_log.py +0 -0
  61. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/messages.py +0 -0
  62. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/organizations.py +0 -0
  63. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/portal_tokens.py +0 -0
  64. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/routes.py +0 -0
  65. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/schemas.py +0 -0
  66. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/sources.py +0 -0
  67. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/subscriptions.py +0 -0
  68. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/transforms.py +0 -0
  69. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/resources/tunnels.py +0 -0
  70. {hookbase-2.8.0 → hookbase-3.0.0}/src/hookbase/webhook.py +0 -0
  71. {hookbase-2.8.0 → hookbase-3.0.0}/tests/__init__.py +0 -0
  72. {hookbase-2.8.0 → hookbase-3.0.0}/tests/conftest.py +0 -0
  73. {hookbase-2.8.0 → hookbase-3.0.0}/tests/resources/__init__.py +0 -0
  74. {hookbase-2.8.0 → hookbase-3.0.0}/tests/resources/test_applications.py +0 -0
  75. {hookbase-2.8.0 → hookbase-3.0.0}/tests/resources/test_cron_jobs.py +0 -0
  76. {hookbase-2.8.0 → hookbase-3.0.0}/tests/resources/test_destinations.py +0 -0
  77. {hookbase-2.8.0 → hookbase-3.0.0}/tests/resources/test_dlq.py +0 -0
  78. {hookbase-2.8.0 → hookbase-3.0.0}/tests/resources/test_messages.py +0 -0
  79. {hookbase-2.8.0 → hookbase-3.0.0}/tests/test_client.py +0 -0
  80. {hookbase-2.8.0 → hookbase-3.0.0}/tests/test_errors.py +0 -0
  81. {hookbase-2.8.0 → hookbase-3.0.0}/tests/test_pagination.py +0 -0
  82. {hookbase-2.8.0 → hookbase-3.0.0}/tests/test_webhook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hookbase
3
- Version: 2.8.0
3
+ Version: 3.0.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://www.hookbase.app/docs/send/sdk
@@ -0,0 +1 @@
1
+ __version__ = "3.0.0"
@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, Literal
5
+
6
+ from pydantic import field_validator
7
+
8
+ from ._base import HookbaseModel
9
+
10
+ # Providers the API will accept for a source.
11
+ #
12
+ # Mirrors SUPPORTED_SIGNATURE_PROVIDERS in the API (api/src/utils/signature-schemes.ts), which
13
+ # is derived from the signature scheme table rather than restated. Regenerate with
14
+ # `npx tsx scripts/print-source-enums.ts` in the api package; do not hand-extend this Literal,
15
+ # because a value the API does not accept is a 400 the type promised would not happen.
16
+ #
17
+ # Removed in the 2026-09 correction: "sendgrid", "mailgun" and "linear". All three were
18
+ # advertised here from the beginning and none was ever accepted by the API. SendGrid signs with
19
+ # ECDSA and Mailgun puts the signature in the POST body, so neither fits this scheme model;
20
+ # Linear's scheme is real but unverified against its docs. Use "custom" for all three.
21
+ #
22
+ # "svix" is an alias of "standard-webhooks"; both resolve to the same scheme server-side.
23
+ SourceProvider = Literal[
24
+ "airtable", "asana", "bitbucket", "calendly", "custom", "generic",
25
+ "github", "gitlab", "heroku", "intercom", "lemonsqueezy", "notion",
26
+ "paddle", "razorpay", "sentry", "shopify", "slack", "standard-webhooks",
27
+ "stripe", "svix", "twilio", "typeform", "workos", "zoom",
28
+ ]
29
+ # Corrected alongside SourceProvider: "header" and "event_id" were never accepted, and the three
30
+ # values the API actually defaults to and documents were missing. "auto" is the default.
31
+ DedupStrategy = Literal["auto", "provider_id", "payload_hash", "idempotency_key", "none"]
32
+ IpFilterMode = Literal["none", "allowlist", "denylist", "both"]
33
+ # HTTP verbs an ingest endpoint can be restricted to. OPTIONS is excluded: CORS preflight is
34
+ # answered before ingest runs, so it is never gateable.
35
+ IngestMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]
36
+
37
+
38
+ class Source(HookbaseModel):
39
+ """A source as the API returns it.
40
+
41
+ The signing secret is not included. Responses carry ``has_signing_secret`` and the last four
42
+ characters; the full value comes back exactly twice, from :meth:`Sources.create` (which
43
+ returns a :class:`SourceWithSecret`) and from :meth:`Sources.reveal_secret`.
44
+ """
45
+
46
+ id: str
47
+ organization_id: str | None = None
48
+ name: str
49
+ slug: str
50
+ provider: SourceProvider | None = None
51
+ description: str | None = None
52
+ #: Whether a signing secret is set. The secret itself is not returned here.
53
+ has_signing_secret: bool = False
54
+ #: Last four characters of the signing secret, prefixed with "...", or None if unset.
55
+ signing_secret_last4: str | None = None
56
+ #: Reject events whose signature fails verification. Defaults to False, in which case a
57
+ #: failing event is flagged with signature_valid=False and delivered anyway.
58
+ reject_invalid_signatures: bool = False
59
+ rate_limit_per_minute: int | None = None
60
+ is_active: bool = True
61
+ custom_domain_id: str | None = None
62
+ ip_filter_mode: IpFilterMode = "none"
63
+ ip_allowlist: list[str] = []
64
+ ip_denylist: list[str] = []
65
+ #: JSONPath expressions whose values are encrypted at rest.
66
+ encrypt_fields: list[str] = []
67
+ #: JSONPath expressions whose values are masked in stored payloads.
68
+ mask_fields: list[str] = []
69
+ dedup_enabled: bool = False
70
+ dedup_strategy: DedupStrategy = "auto"
71
+ dedup_window_hours: int = 24
72
+ dedup_custom_header: str | None = None
73
+ transient_mode: bool = False
74
+ #: HTTP verbs the ingest endpoint accepts. Empty list means any method.
75
+ allowed_methods: list[str] = []
76
+ event_count: int = 0
77
+ route_count: int = 0
78
+ #: Returned by get() and create(). Not included in list() responses.
79
+ ingest_url: str | None = None
80
+ created_at: str = ""
81
+ updated_at: str = ""
82
+
83
+ @field_validator(
84
+ "is_active",
85
+ "reject_invalid_signatures",
86
+ "has_signing_secret",
87
+ "dedup_enabled",
88
+ "transient_mode",
89
+ mode="before",
90
+ )
91
+ @classmethod
92
+ def parse_bool_from_int(cls, v: Any) -> Any:
93
+ if isinstance(v, int):
94
+ return bool(v)
95
+ return v
96
+
97
+ @field_validator(
98
+ "ip_allowlist", "ip_denylist", "encrypt_fields", "mask_fields", mode="before"
99
+ )
100
+ @classmethod
101
+ def parse_json_list(cls, v: Any) -> Any:
102
+ if v is None:
103
+ return []
104
+ if isinstance(v, str):
105
+ try:
106
+ parsed = json.loads(v)
107
+ return parsed if isinstance(parsed, list) else []
108
+ except (json.JSONDecodeError, ValueError):
109
+ return []
110
+ return v
111
+
112
+ @field_validator("allowed_methods", mode="before")
113
+ @classmethod
114
+ def parse_allowed_methods(cls, v: Any) -> Any:
115
+ """Coerce null/JSON-string forms to a list; both mean "any method" when empty."""
116
+ if v is None:
117
+ return []
118
+ if isinstance(v, str):
119
+ try:
120
+ parsed = json.loads(v)
121
+ return parsed if isinstance(parsed, list) else []
122
+ except (json.JSONDecodeError, ValueError):
123
+ return []
124
+ return v
125
+
126
+
127
+ class SourceWithSecret(Source):
128
+ """What create() returns: a source plus the one look at its signing secret you get."""
129
+
130
+ signing_secret: str = ""
131
+
132
+
133
+ class CreateSourceParams(HookbaseModel):
134
+ name: str
135
+ #: Required: it forms the ingest URL, /ingest/<org>/<slug>, and cannot be changed later.
136
+ slug: str
137
+ provider: SourceProvider | None = None
138
+ description: str | None = None
139
+ #: Supply your own secret to match what the provider is already configured with. Omit it and
140
+ #: the API generates one, returned once on the created source.
141
+ signing_secret: str | None = None
142
+ #: Reject events whose signature fails verification. Defaults to False.
143
+ reject_invalid_signatures: bool | None = None
144
+ rate_limit_per_minute: int | None = None
145
+ ip_filter_mode: IpFilterMode | None = None
146
+ ip_allowlist: list[str] | None = None
147
+ ip_denylist: list[str] | None = None
148
+ encrypt_fields: list[str] | None = None
149
+ mask_fields: list[str] | None = None
150
+ dedup_enabled: bool | None = None
151
+ dedup_strategy: DedupStrategy | None = None
152
+ #: Deduplication window in hours, 1 to 168.
153
+ dedup_window_hours: int | None = None
154
+ #: Header to deduplicate on when dedup_strategy is "idempotency_key".
155
+ dedup_custom_header: str | None = None
156
+ transient_mode: bool | None = None
157
+ #: Restrict the ingest endpoint to these verbs. None/[] accepts any method.
158
+ allowed_methods: list[IngestMethod] | None = None
159
+
160
+
161
+ class UpdateSourceParams(HookbaseModel):
162
+ name: str | None = None
163
+ description: str | None = None
164
+ provider: SourceProvider | None = None
165
+ is_active: bool | None = None
166
+ signing_secret: str | None = None
167
+ reject_invalid_signatures: bool | None = None
168
+ rate_limit_per_minute: int | None = None
169
+ ip_filter_mode: IpFilterMode | None = None
170
+ ip_allowlist: list[str] | None = None
171
+ ip_denylist: list[str] | None = None
172
+ encrypt_fields: list[str] | None = None
173
+ mask_fields: list[str] | None = None
174
+ dedup_enabled: bool | None = None
175
+ dedup_strategy: DedupStrategy | None = None
176
+ dedup_window_hours: int | None = None
177
+ dedup_custom_header: str | None = None
178
+ transient_mode: bool | None = None
179
+ #: Restrict the ingest endpoint to these verbs. Pass [] to accept any method again.
180
+ allowed_methods: list[IngestMethod] | None = None
@@ -0,0 +1,105 @@
1
+ """The field names this SDK puts on the wire, as POST/PATCH /api/sources accept them.
2
+
3
+ These lists exist because they were wrong for months and nothing said so. The models named
4
+ ``verify_signature``, ``dedup_window``, ``dedup_header_name``, ``rate_limit`` and
5
+ ``rate_limit_window``, which the camelCase alias generator sent as ``verifySignature`` and
6
+ friends. The API's create schema was a plain zod object, which strips unknown keys rather than
7
+ refusing them, so every one of those calls returned 201 having quietly dropped the setting: a
8
+ source created with ``verify_signature=True`` came back with verification off and raised no error.
9
+
10
+ The create schema is strict now, so a stale name is a 400 rather than a silent drop — which is why
11
+ a rename here has to be deliberate.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import pytest
17
+ from pydantic import ValidationError
18
+
19
+ from hookbase.models.sources import (
20
+ CreateSourceParams,
21
+ Source,
22
+ SourceWithSecret,
23
+ UpdateSourceParams,
24
+ )
25
+
26
+ CREATE_FIELDS = {
27
+ "name", "slug", "provider", "description", "signingSecret",
28
+ "rejectInvalidSignatures", "rateLimitPerMinute", "ipFilterMode", "ipAllowlist",
29
+ "ipDenylist", "encryptFields", "maskFields", "dedupEnabled", "dedupStrategy",
30
+ "dedupWindowHours", "dedupCustomHeader", "transientMode", "allowedMethods",
31
+ }
32
+
33
+ UPDATE_FIELDS = (CREATE_FIELDS - {"slug"}) | {"isActive"}
34
+
35
+
36
+ def _aliases(model: type) -> set[str]:
37
+ return {f.alias or name for name, f in model.model_fields.items()}
38
+
39
+
40
+ def test_create_params_send_exactly_the_accepted_fields() -> None:
41
+ assert _aliases(CreateSourceParams) == CREATE_FIELDS
42
+
43
+
44
+ def test_update_params_send_exactly_the_accepted_fields() -> None:
45
+ assert _aliases(UpdateSourceParams) == UPDATE_FIELDS
46
+
47
+
48
+ def test_create_and_update_differ_only_by_slug_and_is_active() -> None:
49
+ create, update = _aliases(CreateSourceParams), _aliases(UpdateSourceParams)
50
+ assert create - update == {"slug"}
51
+ assert update - create == {"isActive"}
52
+
53
+
54
+ def test_slug_is_required_on_create() -> None:
55
+ """slug forms the ingest URL and cannot be changed afterwards, so create demands it."""
56
+ with pytest.raises(ValidationError, match="slug"):
57
+ CreateSourceParams(name="No slug")
58
+
59
+
60
+ def test_a_full_create_serializes_every_field_under_its_wire_name() -> None:
61
+ params = CreateSourceParams(
62
+ name="Stripe Production",
63
+ slug="stripe",
64
+ provider="stripe",
65
+ description="Live charge events",
66
+ signing_secret="whsec_abc",
67
+ reject_invalid_signatures=True,
68
+ rate_limit_per_minute=600,
69
+ ip_filter_mode="allowlist",
70
+ ip_allowlist=["203.0.113.0/24"],
71
+ ip_denylist=[],
72
+ encrypt_fields=["$.data.object.customer_email"],
73
+ mask_fields=["$.data.object.last4"],
74
+ dedup_enabled=True,
75
+ dedup_strategy="provider_id",
76
+ dedup_window_hours=24,
77
+ dedup_custom_header="X-Idempotency-Key",
78
+ transient_mode=False,
79
+ allowed_methods=["POST"],
80
+ )
81
+ assert set(params.model_dump(by_alias=True, exclude_none=True)) == CREATE_FIELDS
82
+
83
+
84
+ def test_a_listed_source_carries_the_masked_pair_not_the_secret() -> None:
85
+ src = Source(
86
+ id="src_1", name="Stripe", slug="stripe",
87
+ hasSigningSecret=True, signingSecretLast4="...c123",
88
+ rejectInvalidSignatures=True, dedupWindowHours=24, routeCount=3,
89
+ )
90
+ assert src.has_signing_secret is True
91
+ assert src.signing_secret_last4 == "...c123"
92
+ assert src.reject_invalid_signatures is True
93
+ assert src.dedup_window_hours == 24
94
+ assert src.route_count == 3
95
+ assert not hasattr(src, "signing_secret")
96
+
97
+
98
+ def test_a_created_source_carries_the_secret_and_the_ingest_url() -> None:
99
+ created = SourceWithSecret(
100
+ id="src_1", name="Stripe", slug="stripe",
101
+ signingSecret="whsec_abc",
102
+ ingestUrl="https://api.hookbase.app/ingest/acme/stripe",
103
+ )
104
+ assert created.signing_secret == "whsec_abc"
105
+ assert created.ingest_url is not None and "/ingest/" in created.ingest_url
@@ -30,10 +30,13 @@ SOURCE_DATA = {
30
30
  "slug": "github",
31
31
  "provider": "github",
32
32
  "isActive": True,
33
- "verifySignature": True,
33
+ "hasSigningSecret": True,
34
+ "signingSecretLast4": "...c123",
35
+ "rejectInvalidSignatures": True,
34
36
  "dedupStrategy": "none",
35
37
  "ipFilterMode": "none",
36
38
  "eventCount": 42,
39
+ "routeCount": 3,
37
40
  "createdAt": "2024-01-01T00:00:00Z",
38
41
  "updatedAt": "2024-01-01T00:00:00Z",
39
42
  }
@@ -158,7 +161,7 @@ def test_create_source_with_transient_mode(mock_api, client):
158
161
 
159
162
 
160
163
  def test_create_source_params_serialization():
161
- params = CreateSourceParams(name="test", transient_mode=True)
164
+ params = CreateSourceParams(name="test", slug="test", transient_mode=True)
162
165
  dumped = params.model_dump(by_alias=True, exclude_none=True)
163
166
  assert dumped["transientMode"] is True
164
167
 
@@ -227,7 +230,7 @@ def test_source_allowed_methods_malformed_fails_open():
227
230
 
228
231
 
229
232
  def test_create_source_params_serializes_allowed_methods():
230
- params = CreateSourceParams(name="s", allowed_methods=["GET", "POST"])
233
+ params = CreateSourceParams(name="s", slug="s", allowed_methods=["GET", "POST"])
231
234
  assert params.model_dump(by_alias=True, exclude_none=True)["allowedMethods"] == ["GET", "POST"]
232
235
 
233
236
 
@@ -1 +0,0 @@
1
- __version__ = "2.8.0"
@@ -1,119 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- from typing import Any, Literal
5
-
6
- from pydantic import field_validator
7
-
8
- from ._base import HookbaseModel
9
-
10
- SourceProvider = Literal[
11
- "generic", "github", "stripe", "shopify", "slack", "twilio",
12
- "sendgrid", "mailgun", "paddle", "linear", "svix", "custom",
13
- ]
14
- DedupStrategy = Literal["none", "header", "payload_hash", "event_id"]
15
- IpFilterMode = Literal["none", "allowlist", "denylist"]
16
- # HTTP verbs an ingest endpoint can be restricted to. OPTIONS is excluded: CORS preflight is
17
- # answered before ingest runs, so it is never gateable.
18
- IngestMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]
19
-
20
-
21
- class Source(HookbaseModel):
22
- id: str
23
- organization_id: str | None = None
24
- name: str
25
- slug: str
26
- description: str | None = None
27
- provider: SourceProvider = "generic"
28
- is_active: bool = True
29
- signing_secret: str | None = None
30
- ingest_url: str | None = None
31
- verify_signature: bool = False
32
- dedup_strategy: str | None = "none"
33
- dedup_window: int | None = None
34
- dedup_header_name: str | None = None
35
- ip_filter_mode: IpFilterMode = "none"
36
- ip_allowlist: list[str] | None = None
37
- ip_denylist: list[str] | None = None
38
- rate_limit: int | None = None
39
- rate_limit_window: int | None = None
40
- transient_mode: bool = False
41
- #: HTTP verbs the ingest endpoint accepts. Empty list means any method.
42
- allowed_methods: list[str] = []
43
- event_count: int = 0
44
- last_event_at: str | None = None
45
- created_at: str = ""
46
- updated_at: str = ""
47
-
48
- @field_validator("is_active", "verify_signature", "transient_mode", mode="before")
49
- @classmethod
50
- def parse_bool_from_int(cls, v: Any) -> Any:
51
- if isinstance(v, int):
52
- return bool(v)
53
- return v
54
-
55
- @field_validator("ip_allowlist", "ip_denylist", mode="before")
56
- @classmethod
57
- def parse_json_list(cls, v: Any) -> Any:
58
- if isinstance(v, str):
59
- try:
60
- parsed = json.loads(v)
61
- return parsed if isinstance(parsed, list) else None
62
- except (json.JSONDecodeError, ValueError):
63
- return None
64
- return v
65
-
66
- @field_validator("allowed_methods", mode="before")
67
- @classmethod
68
- def parse_allowed_methods(cls, v: Any) -> Any:
69
- """Coerce null/JSON-string forms to a list; both mean "any method" when empty."""
70
- if v is None:
71
- return []
72
- if isinstance(v, str):
73
- try:
74
- parsed = json.loads(v)
75
- return parsed if isinstance(parsed, list) else []
76
- except (json.JSONDecodeError, ValueError):
77
- return []
78
- return v
79
-
80
-
81
- class SourceWithSecret(Source):
82
- signing_secret: str = ""
83
-
84
-
85
- class CreateSourceParams(HookbaseModel):
86
- name: str
87
- slug: str | None = None
88
- description: str | None = None
89
- provider: SourceProvider | None = None
90
- verify_signature: bool | None = None
91
- dedup_strategy: DedupStrategy | None = None
92
- dedup_window: int | None = None
93
- dedup_header_name: str | None = None
94
- ip_filter_mode: IpFilterMode | None = None
95
- ip_allowlist: list[str] | None = None
96
- ip_denylist: list[str] | None = None
97
- rate_limit: int | None = None
98
- rate_limit_window: int | None = None
99
- transient_mode: bool | None = None
100
- #: Restrict the ingest endpoint to these verbs. None/[] accepts any method.
101
- allowed_methods: list[IngestMethod] | None = None
102
-
103
-
104
- class UpdateSourceParams(HookbaseModel):
105
- name: str | None = None
106
- description: str | None = None
107
- is_active: bool | None = None
108
- verify_signature: bool | None = None
109
- dedup_strategy: DedupStrategy | None = None
110
- dedup_window: int | None = None
111
- dedup_header_name: str | None = None
112
- ip_filter_mode: IpFilterMode | None = None
113
- ip_allowlist: list[str] | None = None
114
- ip_denylist: list[str] | None = None
115
- rate_limit: int | None = None
116
- rate_limit_window: int | None = None
117
- transient_mode: bool | None = None
118
- #: Restrict the ingest endpoint to these verbs. Pass [] to accept any method again.
119
- allowed_methods: list[IngestMethod] | None = None
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