vortex-python-sdk 0.9.4__tar.gz → 0.10.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 (19) hide show
  1. {vortex_python_sdk-0.9.4/src/vortex_python_sdk.egg-info → vortex_python_sdk-0.10.0}/PKG-INFO +48 -1
  2. {vortex_python_sdk-0.9.4 → vortex_python_sdk-0.10.0}/README.md +47 -0
  3. {vortex_python_sdk-0.9.4 → vortex_python_sdk-0.10.0}/pyproject.toml +1 -1
  4. {vortex_python_sdk-0.9.4 → vortex_python_sdk-0.10.0/src/vortex_python_sdk.egg-info}/PKG-INFO +48 -1
  5. {vortex_python_sdk-0.9.4 → vortex_python_sdk-0.10.0}/src/vortex_python_sdk.egg-info/SOURCES.txt +4 -1
  6. {vortex_python_sdk-0.9.4 → vortex_python_sdk-0.10.0}/src/vortex_sdk/__init__.py +20 -1
  7. vortex_python_sdk-0.10.0/src/vortex_sdk/webhook_types.py +117 -0
  8. vortex_python_sdk-0.10.0/src/vortex_sdk/webhooks.py +120 -0
  9. vortex_python_sdk-0.10.0/tests/test_webhooks.py +118 -0
  10. {vortex_python_sdk-0.9.4 → vortex_python_sdk-0.10.0}/CHANGELOG.md +0 -0
  11. {vortex_python_sdk-0.9.4 → vortex_python_sdk-0.10.0}/LICENSE +0 -0
  12. {vortex_python_sdk-0.9.4 → vortex_python_sdk-0.10.0}/MANIFEST.in +0 -0
  13. {vortex_python_sdk-0.9.4 → vortex_python_sdk-0.10.0}/setup.cfg +0 -0
  14. {vortex_python_sdk-0.9.4 → vortex_python_sdk-0.10.0}/src/vortex_python_sdk.egg-info/dependency_links.txt +0 -0
  15. {vortex_python_sdk-0.9.4 → vortex_python_sdk-0.10.0}/src/vortex_python_sdk.egg-info/requires.txt +0 -0
  16. {vortex_python_sdk-0.9.4 → vortex_python_sdk-0.10.0}/src/vortex_python_sdk.egg-info/top_level.txt +0 -0
  17. {vortex_python_sdk-0.9.4 → vortex_python_sdk-0.10.0}/src/vortex_sdk/py.typed +0 -0
  18. {vortex_python_sdk-0.9.4 → vortex_python_sdk-0.10.0}/src/vortex_sdk/types.py +0 -0
  19. {vortex_python_sdk-0.9.4 → vortex_python_sdk-0.10.0}/src/vortex_sdk/vortex.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortex-python-sdk
3
- Version: 0.9.4
3
+ Version: 0.10.0
4
4
  Summary: Vortex Python SDK for invitation management and JWT generation
5
5
  Author-email: TeamVortexSoftware <support@vortexsoftware.com>
6
6
  License-Expression: MIT
@@ -295,6 +295,53 @@ ruff check src/ tests/
295
295
  mypy src/
296
296
  ```
297
297
 
298
+ ## Webhooks
299
+
300
+ The SDK provides built-in support for verifying and parsing incoming webhook events from Vortex.
301
+
302
+ ### Setup
303
+
304
+ ```python
305
+ import os
306
+ from vortex_sdk import VortexWebhooks, is_webhook_event, is_analytics_event
307
+
308
+ webhooks = VortexWebhooks(secret=os.environ["VORTEX_WEBHOOK_SECRET"])
309
+ ```
310
+
311
+ ### Verifying and Parsing Events
312
+
313
+ ```python
314
+ # In your HTTP handler (Flask example):
315
+ @app.route("/webhooks/vortex", methods=["POST"])
316
+ def handle_webhook():
317
+ payload = request.get_data(as_text=True)
318
+ signature = request.headers.get("X-Vortex-Signature", "")
319
+
320
+ try:
321
+ event = webhooks.construct_event(payload, signature)
322
+ except VortexWebhookSignatureError:
323
+ return "Invalid signature", 400
324
+
325
+ if is_webhook_event(event.__dict__):
326
+ print(f"Webhook event: {event.type}")
327
+ elif is_analytics_event(event.__dict__):
328
+ print(f"Analytics event: {event.name}")
329
+
330
+ return "OK", 200
331
+ ```
332
+
333
+ ### Event Types
334
+
335
+ Webhook event types are available as the `WebhookEventType` enum:
336
+
337
+ ```python
338
+ from vortex_sdk import WebhookEventType
339
+
340
+ if event.type == WebhookEventType.INVITATION_ACCEPTED:
341
+ # Handle invitation accepted
342
+ pass
343
+ ```
344
+
298
345
  ## License
299
346
 
300
347
  MIT
@@ -257,6 +257,53 @@ ruff check src/ tests/
257
257
  mypy src/
258
258
  ```
259
259
 
260
+ ## Webhooks
261
+
262
+ The SDK provides built-in support for verifying and parsing incoming webhook events from Vortex.
263
+
264
+ ### Setup
265
+
266
+ ```python
267
+ import os
268
+ from vortex_sdk import VortexWebhooks, is_webhook_event, is_analytics_event
269
+
270
+ webhooks = VortexWebhooks(secret=os.environ["VORTEX_WEBHOOK_SECRET"])
271
+ ```
272
+
273
+ ### Verifying and Parsing Events
274
+
275
+ ```python
276
+ # In your HTTP handler (Flask example):
277
+ @app.route("/webhooks/vortex", methods=["POST"])
278
+ def handle_webhook():
279
+ payload = request.get_data(as_text=True)
280
+ signature = request.headers.get("X-Vortex-Signature", "")
281
+
282
+ try:
283
+ event = webhooks.construct_event(payload, signature)
284
+ except VortexWebhookSignatureError:
285
+ return "Invalid signature", 400
286
+
287
+ if is_webhook_event(event.__dict__):
288
+ print(f"Webhook event: {event.type}")
289
+ elif is_analytics_event(event.__dict__):
290
+ print(f"Analytics event: {event.name}")
291
+
292
+ return "OK", 200
293
+ ```
294
+
295
+ ### Event Types
296
+
297
+ Webhook event types are available as the `WebhookEventType` enum:
298
+
299
+ ```python
300
+ from vortex_sdk import WebhookEventType
301
+
302
+ if event.type == WebhookEventType.INVITATION_ACCEPTED:
303
+ # Handle invitation accepted
304
+ pass
305
+ ```
306
+
260
307
  ## License
261
308
 
262
309
  MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vortex-python-sdk"
7
- version = "0.9.4"
7
+ version = "0.10.0"
8
8
  description = "Vortex Python SDK for invitation management and JWT generation"
9
9
  authors = [{name = "TeamVortexSoftware", email = "support@vortexsoftware.com"}]
10
10
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortex-python-sdk
3
- Version: 0.9.4
3
+ Version: 0.10.0
4
4
  Summary: Vortex Python SDK for invitation management and JWT generation
5
5
  Author-email: TeamVortexSoftware <support@vortexsoftware.com>
6
6
  License-Expression: MIT
@@ -295,6 +295,53 @@ ruff check src/ tests/
295
295
  mypy src/
296
296
  ```
297
297
 
298
+ ## Webhooks
299
+
300
+ The SDK provides built-in support for verifying and parsing incoming webhook events from Vortex.
301
+
302
+ ### Setup
303
+
304
+ ```python
305
+ import os
306
+ from vortex_sdk import VortexWebhooks, is_webhook_event, is_analytics_event
307
+
308
+ webhooks = VortexWebhooks(secret=os.environ["VORTEX_WEBHOOK_SECRET"])
309
+ ```
310
+
311
+ ### Verifying and Parsing Events
312
+
313
+ ```python
314
+ # In your HTTP handler (Flask example):
315
+ @app.route("/webhooks/vortex", methods=["POST"])
316
+ def handle_webhook():
317
+ payload = request.get_data(as_text=True)
318
+ signature = request.headers.get("X-Vortex-Signature", "")
319
+
320
+ try:
321
+ event = webhooks.construct_event(payload, signature)
322
+ except VortexWebhookSignatureError:
323
+ return "Invalid signature", 400
324
+
325
+ if is_webhook_event(event.__dict__):
326
+ print(f"Webhook event: {event.type}")
327
+ elif is_analytics_event(event.__dict__):
328
+ print(f"Analytics event: {event.name}")
329
+
330
+ return "OK", 200
331
+ ```
332
+
333
+ ### Event Types
334
+
335
+ Webhook event types are available as the `WebhookEventType` enum:
336
+
337
+ ```python
338
+ from vortex_sdk import WebhookEventType
339
+
340
+ if event.type == WebhookEventType.INVITATION_ACCEPTED:
341
+ # Handle invitation accepted
342
+ pass
343
+ ```
344
+
298
345
  ## License
299
346
 
300
347
  MIT
@@ -11,4 +11,7 @@ src/vortex_python_sdk.egg-info/top_level.txt
11
11
  src/vortex_sdk/__init__.py
12
12
  src/vortex_sdk/py.typed
13
13
  src/vortex_sdk/types.py
14
- src/vortex_sdk/vortex.py
14
+ src/vortex_sdk/vortex.py
15
+ src/vortex_sdk/webhook_types.py
16
+ src/vortex_sdk/webhooks.py
17
+ tests/test_webhooks.py
@@ -28,8 +28,18 @@ from .types import (
28
28
  VortexApiError,
29
29
  )
30
30
  from .vortex import Vortex
31
+ from .webhook_types import (
32
+ AnalyticsEventType,
33
+ VortexAnalyticsEvent,
34
+ VortexEvent,
35
+ VortexWebhookEvent,
36
+ WebhookEventType,
37
+ is_analytics_event,
38
+ is_webhook_event,
39
+ )
40
+ from .webhooks import VortexWebhookSignatureError, VortexWebhooks
31
41
 
32
- __version__ = "0.9.2"
42
+ __version__ = "0.10.0"
33
43
  __author__ = "TeamVortexSoftware"
34
44
  __email__ = "support@vortexsoftware.com"
35
45
 
@@ -56,4 +66,13 @@ __all__ = [
56
66
  "ApiResponseJson",
57
67
  "ApiRequestBody",
58
68
  "VortexApiError",
69
+ "VortexWebhooks",
70
+ "VortexWebhookSignatureError",
71
+ "VortexWebhookEvent",
72
+ "VortexAnalyticsEvent",
73
+ "VortexEvent",
74
+ "WebhookEventType",
75
+ "AnalyticsEventType",
76
+ "is_webhook_event",
77
+ "is_analytics_event",
59
78
  ]
@@ -0,0 +1,117 @@
1
+ """
2
+ Vortex Webhook Types
3
+
4
+ Type definitions for webhook event handling.
5
+ These mirror the server-side types but are kept independent
6
+ so the SDK has no internal dependencies.
7
+
8
+ @see DEV-1769
9
+ """
10
+
11
+ from enum import Enum
12
+ from typing import Any, Dict, List, Optional, Union
13
+
14
+ from pydantic import BaseModel, Field
15
+
16
+
17
+ # ─── Webhook Event Type Constants ──────────────────────────────────────
18
+
19
+
20
+ class WebhookEventType(str, Enum):
21
+ """Webhook event types for Vortex state changes."""
22
+
23
+ # Invitation Lifecycle
24
+ INVITATION_CREATED = "invitation.created"
25
+ INVITATION_ACCEPTED = "invitation.accepted"
26
+ INVITATION_DEACTIVATED = "invitation.deactivated"
27
+ INVITATION_EMAIL_DELIVERED = "invitation.email.delivered"
28
+ INVITATION_EMAIL_BOUNCED = "invitation.email.bounced"
29
+ INVITATION_EMAIL_OPENED = "invitation.email.opened"
30
+ INVITATION_LINK_CLICKED = "invitation.link.clicked"
31
+ INVITATION_REMINDER_SENT = "invitation.reminder.sent"
32
+
33
+ # Deployment Lifecycle
34
+ DEPLOYMENT_CREATED = "deployment.created"
35
+ DEPLOYMENT_DEACTIVATED = "deployment.deactivated"
36
+
37
+ # A/B Testing
38
+ ABTEST_STARTED = "abtest.started"
39
+ ABTEST_WINNER_DECLARED = "abtest.winner_declared"
40
+
41
+ # Member/Group
42
+ MEMBER_CREATED = "member.created"
43
+ GROUP_MEMBER_ADDED = "group.member.added"
44
+
45
+ # Email
46
+ EMAIL_COMPLAINED = "email.complained"
47
+
48
+
49
+ class AnalyticsEventType(str, Enum):
50
+ """Analytics event types for behavioral telemetry."""
51
+
52
+ WIDGET_LOADED = "widget_loaded"
53
+ INVITATION_SENT = "invitation_sent"
54
+ INVITATION_CLICKED = "invitation_clicked"
55
+ INVITATION_ACCEPTED = "invitation_accepted"
56
+ SHARE_TRIGGERED = "share_triggered"
57
+
58
+
59
+ # ─── Webhook Event Payload ─────────────────────────────────────────────
60
+
61
+
62
+ class VortexWebhookEvent(BaseModel):
63
+ """A Vortex webhook event representing a server-side state change."""
64
+
65
+ id: str
66
+ type: str
67
+ timestamp: str
68
+ account_id: str = Field(alias="accountId")
69
+ environment_id: Optional[str] = Field(None, alias="environmentId")
70
+ source_table: str = Field(alias="sourceTable")
71
+ operation: str # "insert" | "update" | "delete"
72
+ data: Dict[str, Any]
73
+
74
+ class Config:
75
+ populate_by_name = True
76
+
77
+
78
+ # ─── Analytics Event Payload ───────────────────────────────────────────
79
+
80
+
81
+ class VortexAnalyticsEvent(BaseModel):
82
+ """An analytics event representing client-side behavioral telemetry."""
83
+
84
+ id: str
85
+ name: str
86
+ account_id: str = Field(alias="accountId")
87
+ organization_id: str = Field(alias="organizationId")
88
+ project_id: str = Field(alias="projectId")
89
+ environment_id: str = Field(alias="environmentId")
90
+ deployment_id: Optional[str] = Field(None, alias="deploymentId")
91
+ widget_configuration_id: Optional[str] = Field(
92
+ None, alias="widgetConfigurationId"
93
+ )
94
+ foreign_user_id: Optional[str] = Field(None, alias="foreignUserId")
95
+ session_id: Optional[str] = Field(None, alias="sessionId")
96
+ payload: Optional[Dict[str, Any]] = None
97
+ platform: Optional[str] = None
98
+ segmentation: Optional[str] = None
99
+ timestamp: str
100
+
101
+ class Config:
102
+ populate_by_name = True
103
+
104
+
105
+ # ─── Union & Discriminator ─────────────────────────────────────────────
106
+
107
+ VortexEvent = Union[VortexWebhookEvent, VortexAnalyticsEvent]
108
+
109
+
110
+ def is_webhook_event(event: Dict[str, Any]) -> bool:
111
+ """Returns True if the event dict is a webhook event (has 'type', no 'name')."""
112
+ return "type" in event and "name" not in event
113
+
114
+
115
+ def is_analytics_event(event: Dict[str, Any]) -> bool:
116
+ """Returns True if the event dict is an analytics event (has 'name')."""
117
+ return "name" in event
@@ -0,0 +1,120 @@
1
+ """
2
+ Vortex Webhooks
3
+
4
+ Core webhook verification and parsing for the Vortex Python SDK.
5
+
6
+ Example::
7
+
8
+ from vortex_sdk import VortexWebhooks
9
+
10
+ webhooks = VortexWebhooks(secret=os.environ["VORTEX_WEBHOOK_SECRET"])
11
+
12
+ # In any HTTP handler:
13
+ event = webhooks.construct_event(raw_body, signature_header)
14
+
15
+ @see DEV-1769
16
+ """
17
+
18
+ import hashlib
19
+ import hmac
20
+ import json
21
+ from typing import Any, Dict, Union
22
+
23
+ from .webhook_types import (
24
+ VortexAnalyticsEvent,
25
+ VortexEvent,
26
+ VortexWebhookEvent,
27
+ is_analytics_event,
28
+ is_webhook_event,
29
+ )
30
+
31
+
32
+ class VortexWebhookSignatureError(Exception):
33
+ """Raised when webhook signature verification fails."""
34
+
35
+ pass
36
+
37
+
38
+ class VortexWebhooks:
39
+ """
40
+ Core webhook verification and parsing.
41
+
42
+ This class is framework-agnostic — use it directly or with
43
+ framework-specific integrations (Flask, Django, FastAPI).
44
+
45
+ Args:
46
+ secret: The webhook signing secret from your Vortex dashboard.
47
+
48
+ Example::
49
+
50
+ from vortex_sdk import VortexWebhooks
51
+
52
+ webhooks = VortexWebhooks(secret=os.environ["VORTEX_WEBHOOK_SECRET"])
53
+ event = webhooks.construct_event(request.body, request.headers["X-Vortex-Signature"])
54
+ """
55
+
56
+ def __init__(self, secret: str) -> None:
57
+ if not secret:
58
+ raise ValueError("VortexWebhooks requires a secret")
59
+ self._secret = secret
60
+
61
+ def verify_signature(self, payload: Union[str, bytes], signature: str) -> bool:
62
+ """
63
+ Verify the HMAC-SHA256 signature of an incoming webhook payload.
64
+
65
+ Args:
66
+ payload: The raw request body (str or bytes).
67
+ signature: The value of the ``X-Vortex-Signature`` header.
68
+
69
+ Returns:
70
+ ``True`` if the signature is valid.
71
+ """
72
+ if not signature:
73
+ return False
74
+
75
+ if isinstance(payload, str):
76
+ payload = payload.encode("utf-8")
77
+
78
+ expected = hmac.new(
79
+ self._secret.encode("utf-8"),
80
+ payload,
81
+ hashlib.sha256,
82
+ ).hexdigest()
83
+
84
+ # Timing-safe comparison to prevent timing attacks
85
+ return hmac.compare_digest(signature, expected)
86
+
87
+ def construct_event(
88
+ self, payload: Union[str, bytes], signature: str
89
+ ) -> VortexEvent:
90
+ """
91
+ Verify and parse an incoming webhook payload.
92
+
93
+ Args:
94
+ payload: The raw request body (str or bytes). Must be the raw body,
95
+ not a parsed dict — signature verification requires the exact
96
+ bytes that were signed.
97
+ signature: The value of the ``X-Vortex-Signature`` header.
98
+
99
+ Returns:
100
+ A :class:`VortexWebhookEvent` or :class:`VortexAnalyticsEvent`.
101
+
102
+ Raises:
103
+ VortexWebhookSignatureError: If the signature is invalid.
104
+ """
105
+ if not self.verify_signature(payload, signature):
106
+ raise VortexWebhookSignatureError(
107
+ "Webhook signature verification failed. Ensure you are using "
108
+ "the raw request body and the correct signing secret."
109
+ )
110
+
111
+ body = payload if isinstance(payload, str) else payload.decode("utf-8")
112
+ parsed: Dict[str, Any] = json.loads(body)
113
+
114
+ if is_webhook_event(parsed):
115
+ return VortexWebhookEvent(**parsed)
116
+ elif is_analytics_event(parsed):
117
+ return VortexAnalyticsEvent(**parsed)
118
+ else:
119
+ # Return as webhook event by default
120
+ return VortexWebhookEvent(**parsed)
@@ -0,0 +1,118 @@
1
+ """Tests for Vortex webhook signature verification and event construction."""
2
+
3
+ import hashlib
4
+ import hmac
5
+ import json
6
+
7
+ import pytest
8
+
9
+ from vortex_sdk import (
10
+ VortexAnalyticsEvent,
11
+ VortexWebhookEvent,
12
+ VortexWebhookSignatureError,
13
+ VortexWebhooks,
14
+ is_analytics_event,
15
+ is_webhook_event,
16
+ )
17
+
18
+
19
+ SECRET = "whsec_test_secret_123"
20
+
21
+ WEBHOOK_EVENT_PAYLOAD = json.dumps(
22
+ {
23
+ "id": "evt_123",
24
+ "type": "invitation.accepted",
25
+ "timestamp": "2025-01-15T12:00:00.000Z",
26
+ "accountId": "acc_123",
27
+ "environmentId": "env_456",
28
+ "sourceTable": "invitations",
29
+ "operation": "update",
30
+ "data": {"invitationId": "inv_789", "targetEmail": "user@example.com"},
31
+ }
32
+ )
33
+
34
+ ANALYTICS_EVENT_PAYLOAD = json.dumps(
35
+ {
36
+ "id": "evt_456",
37
+ "name": "widget_loaded",
38
+ "accountId": "acc_123",
39
+ "organizationId": "org_123",
40
+ "projectId": "proj_123",
41
+ "environmentId": "env_456",
42
+ "deploymentId": None,
43
+ "widgetConfigurationId": "wc_123",
44
+ "foreignUserId": "user_123",
45
+ "sessionId": "sess_123",
46
+ "payload": {"page": "/dashboard"},
47
+ "platform": "web",
48
+ "segmentation": None,
49
+ "timestamp": "2025-01-15T12:00:00.000Z",
50
+ }
51
+ )
52
+
53
+
54
+ def _sign(payload: str, secret: str = SECRET) -> str:
55
+ return hmac.new(secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256).hexdigest()
56
+
57
+
58
+ class TestVortexWebhooks:
59
+ def test_constructor_requires_secret(self) -> None:
60
+ with pytest.raises(ValueError, match="requires a secret"):
61
+ VortexWebhooks(secret="")
62
+
63
+ def test_verify_signature_valid(self) -> None:
64
+ wh = VortexWebhooks(secret=SECRET)
65
+ sig = _sign(WEBHOOK_EVENT_PAYLOAD)
66
+ assert wh.verify_signature(WEBHOOK_EVENT_PAYLOAD, sig) is True
67
+
68
+ def test_verify_signature_invalid(self) -> None:
69
+ wh = VortexWebhooks(secret=SECRET)
70
+ assert wh.verify_signature(WEBHOOK_EVENT_PAYLOAD, "bad_signature") is False
71
+
72
+ def test_verify_signature_empty(self) -> None:
73
+ wh = VortexWebhooks(secret=SECRET)
74
+ assert wh.verify_signature(WEBHOOK_EVENT_PAYLOAD, "") is False
75
+
76
+ def test_verify_signature_wrong_secret(self) -> None:
77
+ wh = VortexWebhooks(secret=SECRET)
78
+ sig = _sign(WEBHOOK_EVENT_PAYLOAD, "wrong_secret")
79
+ assert wh.verify_signature(WEBHOOK_EVENT_PAYLOAD, sig) is False
80
+
81
+ def test_verify_signature_bytes_payload(self) -> None:
82
+ wh = VortexWebhooks(secret=SECRET)
83
+ sig = _sign(WEBHOOK_EVENT_PAYLOAD)
84
+ assert wh.verify_signature(WEBHOOK_EVENT_PAYLOAD.encode("utf-8"), sig) is True
85
+
86
+ def test_construct_webhook_event(self) -> None:
87
+ wh = VortexWebhooks(secret=SECRET)
88
+ sig = _sign(WEBHOOK_EVENT_PAYLOAD)
89
+ event = wh.construct_event(WEBHOOK_EVENT_PAYLOAD, sig)
90
+ assert isinstance(event, VortexWebhookEvent)
91
+ assert event.id == "evt_123"
92
+ assert event.type == "invitation.accepted"
93
+ assert event.account_id == "acc_123"
94
+ assert event.data["targetEmail"] == "user@example.com"
95
+
96
+ def test_construct_analytics_event(self) -> None:
97
+ wh = VortexWebhooks(secret=SECRET)
98
+ sig = _sign(ANALYTICS_EVENT_PAYLOAD)
99
+ event = wh.construct_event(ANALYTICS_EVENT_PAYLOAD, sig)
100
+ assert isinstance(event, VortexAnalyticsEvent)
101
+ assert event.id == "evt_456"
102
+ assert event.name == "widget_loaded"
103
+ assert event.account_id == "acc_123"
104
+
105
+ def test_construct_event_bad_signature(self) -> None:
106
+ wh = VortexWebhooks(secret=SECRET)
107
+ with pytest.raises(VortexWebhookSignatureError):
108
+ wh.construct_event(WEBHOOK_EVENT_PAYLOAD, "bad_sig")
109
+
110
+
111
+ class TestTypeGuards:
112
+ def test_is_webhook_event(self) -> None:
113
+ assert is_webhook_event({"type": "invitation.accepted", "id": "1"}) is True
114
+ assert is_webhook_event({"name": "widget_loaded", "id": "1"}) is False
115
+
116
+ def test_is_analytics_event(self) -> None:
117
+ assert is_analytics_event({"name": "widget_loaded", "id": "1"}) is True
118
+ assert is_analytics_event({"type": "invitation.accepted", "id": "1"}) is False