replylayer 0.14.0__py3-none-any.whl
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.
- replylayer/__init__.py +143 -0
- replylayer/_client.py +128 -0
- replylayer/_http.py +326 -0
- replylayer/_pagination.py +34 -0
- replylayer/errors.py +152 -0
- replylayer/py.typed +0 -0
- replylayer/resources/__init__.py +0 -0
- replylayer/resources/account.py +36 -0
- replylayer/resources/api_keys.py +59 -0
- replylayer/resources/attachments.py +115 -0
- replylayer/resources/domains.py +79 -0
- replylayer/resources/drafts.py +342 -0
- replylayer/resources/health.py +21 -0
- replylayer/resources/inbound_blocklist.py +93 -0
- replylayer/resources/legal_holds.py +107 -0
- replylayer/resources/mailboxes.py +768 -0
- replylayer/resources/messages.py +425 -0
- replylayer/resources/recipients.py +59 -0
- replylayer/resources/suppressions.py +84 -0
- replylayer/resources/threads.py +117 -0
- replylayer/resources/webhooks.py +175 -0
- replylayer/types.py +1578 -0
- replylayer-0.14.0.dist-info/METADATA +502 -0
- replylayer-0.14.0.dist-info/RECORD +25 -0
- replylayer-0.14.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .._http import AsyncHttpClient, SyncHttpClient
|
|
9
|
+
from ..errors import WebhookSignatureError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def verify_webhook_signature(
|
|
13
|
+
payload: str | bytes,
|
|
14
|
+
signature: str,
|
|
15
|
+
secret: str,
|
|
16
|
+
*,
|
|
17
|
+
tolerance: int = 300,
|
|
18
|
+
) -> bool:
|
|
19
|
+
parts = signature.split(",")
|
|
20
|
+
timestamp: str | None = None
|
|
21
|
+
v1_hex: str | None = None
|
|
22
|
+
|
|
23
|
+
for part in parts:
|
|
24
|
+
trimmed = part.strip()
|
|
25
|
+
if trimmed.startswith("t="):
|
|
26
|
+
timestamp = trimmed[2:]
|
|
27
|
+
if trimmed.startswith("v1="):
|
|
28
|
+
v1_hex = trimmed[3:]
|
|
29
|
+
|
|
30
|
+
if not timestamp or not v1_hex:
|
|
31
|
+
raise WebhookSignatureError("Invalid signature format: missing t= or v1= component")
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
ts = int(timestamp)
|
|
35
|
+
except ValueError:
|
|
36
|
+
raise WebhookSignatureError("Invalid signature format: timestamp is not a number")
|
|
37
|
+
|
|
38
|
+
now = int(time.time())
|
|
39
|
+
if abs(now - ts) > tolerance:
|
|
40
|
+
raise WebhookSignatureError(
|
|
41
|
+
f"Webhook timestamp too old ({abs(now - ts)}s exceeds {tolerance}s tolerance)"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
body = payload if isinstance(payload, str) else payload.decode("utf-8")
|
|
45
|
+
expected = hmac.new(
|
|
46
|
+
secret.encode("utf-8"),
|
|
47
|
+
f"{timestamp}.{body}".encode("utf-8"),
|
|
48
|
+
hashlib.sha256,
|
|
49
|
+
).hexdigest()
|
|
50
|
+
|
|
51
|
+
if not hmac.compare_digest(expected, v1_hex):
|
|
52
|
+
raise WebhookSignatureError("Webhook signature mismatch")
|
|
53
|
+
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SyncWebhooks:
|
|
58
|
+
def __init__(self, http: SyncHttpClient) -> None:
|
|
59
|
+
self._http = http
|
|
60
|
+
|
|
61
|
+
def create(self, *, url: str, enabled_events: list[str], description: str | None = None, enabled: bool = True) -> dict[str, Any]:
|
|
62
|
+
"""Create a new webhook subscription.
|
|
63
|
+
|
|
64
|
+
Raises ``ForbiddenError`` (HTTP 403) when the account is at its
|
|
65
|
+
tier cap. The error's ``details`` carries
|
|
66
|
+
``{"feature": "webhook_count_extended", "current_count": int,
|
|
67
|
+
"max_allowed": int}`` so callers can render actionable upgrade
|
|
68
|
+
prompts. Cap matrix: Sandbox 1, Starter 3, Pro 25, Team 100,
|
|
69
|
+
Scale 250, Enterprise unbounded.
|
|
70
|
+
"""
|
|
71
|
+
payload: dict[str, Any] = {"url": url, "enabled_events": enabled_events, "enabled": enabled}
|
|
72
|
+
if description is not None:
|
|
73
|
+
payload["description"] = description
|
|
74
|
+
return self._http.request("POST", "/v1/webhooks", body=payload)
|
|
75
|
+
|
|
76
|
+
def list(self) -> dict[str, Any]:
|
|
77
|
+
return self._http.request("GET", "/v1/webhooks")
|
|
78
|
+
|
|
79
|
+
def get(self, id: str) -> dict[str, Any]:
|
|
80
|
+
return self._http.request("GET", f"/v1/webhooks/{id}")
|
|
81
|
+
|
|
82
|
+
def update(self, id: str, **kwargs: Any) -> dict[str, Any]:
|
|
83
|
+
return self._http.request("PATCH", f"/v1/webhooks/{id}", body=kwargs)
|
|
84
|
+
|
|
85
|
+
def delete(self, id: str) -> dict[str, Any]:
|
|
86
|
+
return self._http.request("DELETE", f"/v1/webhooks/{id}")
|
|
87
|
+
|
|
88
|
+
def rotate_secret(self, id: str) -> dict[str, Any]:
|
|
89
|
+
return self._http.request("POST", f"/v1/webhooks/{id}/rotate-secret")
|
|
90
|
+
|
|
91
|
+
def test(self, id: str) -> dict[str, Any]:
|
|
92
|
+
return self._http.request("POST", f"/v1/webhooks/{id}/test")
|
|
93
|
+
|
|
94
|
+
def list_deliveries(
|
|
95
|
+
self,
|
|
96
|
+
id: str,
|
|
97
|
+
*,
|
|
98
|
+
limit: int | None = None,
|
|
99
|
+
before_at: str | None = None,
|
|
100
|
+
before_id: str | None = None,
|
|
101
|
+
) -> dict[str, Any]:
|
|
102
|
+
# The API requires before_at + before_id together (400s otherwise).
|
|
103
|
+
# Enforce the pair at the SDK layer so a partial call doesn't build
|
|
104
|
+
# a request the server will reject.
|
|
105
|
+
query: dict[str, str | None] = {}
|
|
106
|
+
if limit is not None:
|
|
107
|
+
query["limit"] = str(limit)
|
|
108
|
+
if before_at and before_id:
|
|
109
|
+
query["before_at"] = before_at
|
|
110
|
+
query["before_id"] = before_id
|
|
111
|
+
return self._http.request("GET", f"/v1/webhooks/{id}/deliveries", query=query)
|
|
112
|
+
|
|
113
|
+
def retry_delivery(self, id: str, delivery_id: str) -> dict[str, Any]:
|
|
114
|
+
return self._http.request(
|
|
115
|
+
"POST", f"/v1/webhooks/{id}/deliveries/{delivery_id}/retry"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def verify_signature(payload: str | bytes, signature: str, secret: str, *, tolerance: int = 300) -> bool:
|
|
120
|
+
return verify_webhook_signature(payload, signature, secret, tolerance=tolerance)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class AsyncWebhooks:
|
|
124
|
+
def __init__(self, http: AsyncHttpClient) -> None:
|
|
125
|
+
self._http = http
|
|
126
|
+
|
|
127
|
+
async def create(self, *, url: str, enabled_events: list[str], description: str | None = None, enabled: bool = True) -> dict[str, Any]:
|
|
128
|
+
"""Create a new webhook subscription. See SyncWebhooks.create for tier-cap details."""
|
|
129
|
+
payload: dict[str, Any] = {"url": url, "enabled_events": enabled_events, "enabled": enabled}
|
|
130
|
+
if description is not None:
|
|
131
|
+
payload["description"] = description
|
|
132
|
+
return await self._http.request("POST", "/v1/webhooks", body=payload)
|
|
133
|
+
|
|
134
|
+
async def list(self) -> dict[str, Any]:
|
|
135
|
+
return await self._http.request("GET", "/v1/webhooks")
|
|
136
|
+
|
|
137
|
+
async def get(self, id: str) -> dict[str, Any]:
|
|
138
|
+
return await self._http.request("GET", f"/v1/webhooks/{id}")
|
|
139
|
+
|
|
140
|
+
async def update(self, id: str, **kwargs: Any) -> dict[str, Any]:
|
|
141
|
+
return await self._http.request("PATCH", f"/v1/webhooks/{id}", body=kwargs)
|
|
142
|
+
|
|
143
|
+
async def delete(self, id: str) -> dict[str, Any]:
|
|
144
|
+
return await self._http.request("DELETE", f"/v1/webhooks/{id}")
|
|
145
|
+
|
|
146
|
+
async def rotate_secret(self, id: str) -> dict[str, Any]:
|
|
147
|
+
return await self._http.request("POST", f"/v1/webhooks/{id}/rotate-secret")
|
|
148
|
+
|
|
149
|
+
async def test(self, id: str) -> dict[str, Any]:
|
|
150
|
+
return await self._http.request("POST", f"/v1/webhooks/{id}/test")
|
|
151
|
+
|
|
152
|
+
async def list_deliveries(
|
|
153
|
+
self,
|
|
154
|
+
id: str,
|
|
155
|
+
*,
|
|
156
|
+
limit: int | None = None,
|
|
157
|
+
before_at: str | None = None,
|
|
158
|
+
before_id: str | None = None,
|
|
159
|
+
) -> dict[str, Any]:
|
|
160
|
+
query: dict[str, str | None] = {}
|
|
161
|
+
if limit is not None:
|
|
162
|
+
query["limit"] = str(limit)
|
|
163
|
+
if before_at and before_id:
|
|
164
|
+
query["before_at"] = before_at
|
|
165
|
+
query["before_id"] = before_id
|
|
166
|
+
return await self._http.request("GET", f"/v1/webhooks/{id}/deliveries", query=query)
|
|
167
|
+
|
|
168
|
+
async def retry_delivery(self, id: str, delivery_id: str) -> dict[str, Any]:
|
|
169
|
+
return await self._http.request(
|
|
170
|
+
"POST", f"/v1/webhooks/{id}/deliveries/{delivery_id}/retry"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def verify_signature(payload: str | bytes, signature: str, secret: str, *, tolerance: int = 300) -> bool:
|
|
175
|
+
return verify_webhook_signature(payload, signature, secret, tolerance=tolerance)
|