lettermint 0.1.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.
- lettermint/__init__.py +80 -0
- lettermint/client.py +371 -0
- lettermint/endpoints/__init__.py +8 -0
- lettermint/endpoints/email.py +542 -0
- lettermint/endpoints/endpoint.py +27 -0
- lettermint/exceptions.py +88 -0
- lettermint/lettermint.py +148 -0
- lettermint/py.typed +0 -0
- lettermint/types.py +77 -0
- lettermint/webhook.py +234 -0
- lettermint-0.1.0.dist-info/METADATA +320 -0
- lettermint-0.1.0.dist-info/RECORD +14 -0
- lettermint-0.1.0.dist-info/WHEEL +4 -0
- lettermint-0.1.0.dist-info/licenses/LICENSE +21 -0
lettermint/__init__.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lettermint Python SDK
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
Official Python SDK for the Lettermint email API.
|
|
6
|
+
|
|
7
|
+
Basic Usage:
|
|
8
|
+
>>> from lettermint import Lettermint
|
|
9
|
+
>>>
|
|
10
|
+
>>> client = Lettermint(api_token="your-api-token")
|
|
11
|
+
>>>
|
|
12
|
+
>>> response = (
|
|
13
|
+
... client.email
|
|
14
|
+
... .from_("sender@example.com")
|
|
15
|
+
... .to("recipient@example.com")
|
|
16
|
+
... .subject("Hello from Python!")
|
|
17
|
+
... .html("<h1>Welcome!</h1>")
|
|
18
|
+
... .send()
|
|
19
|
+
... )
|
|
20
|
+
>>> print(response["message_id"])
|
|
21
|
+
|
|
22
|
+
Async Usage:
|
|
23
|
+
>>> from lettermint import AsyncLettermint
|
|
24
|
+
>>>
|
|
25
|
+
>>> async with AsyncLettermint(api_token="your-api-token") as client:
|
|
26
|
+
... response = await (
|
|
27
|
+
... client.email
|
|
28
|
+
... .from_("sender@example.com")
|
|
29
|
+
... .to("recipient@example.com")
|
|
30
|
+
... .subject("Hello from Python!")
|
|
31
|
+
... .html("<h1>Welcome!</h1>")
|
|
32
|
+
... .send()
|
|
33
|
+
... )
|
|
34
|
+
|
|
35
|
+
Webhook Verification:
|
|
36
|
+
>>> from lettermint import Webhook
|
|
37
|
+
>>>
|
|
38
|
+
>>> webhook = Webhook(secret="your-webhook-secret")
|
|
39
|
+
>>> payload = webhook.verify_headers(request.headers, request.body)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from .exceptions import (
|
|
43
|
+
ClientError,
|
|
44
|
+
HttpRequestError,
|
|
45
|
+
InvalidSignatureError,
|
|
46
|
+
JsonDecodeError,
|
|
47
|
+
LettermintError,
|
|
48
|
+
TimeoutError,
|
|
49
|
+
TimestampToleranceError,
|
|
50
|
+
ValidationError,
|
|
51
|
+
WebhookVerificationError,
|
|
52
|
+
)
|
|
53
|
+
from .lettermint import AsyncLettermint, Lettermint
|
|
54
|
+
from .types import EmailAttachment, EmailPayload, EmailStatus, SendEmailResponse
|
|
55
|
+
from .webhook import Webhook
|
|
56
|
+
|
|
57
|
+
__version__ = "0.1.0"
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
# Main clients
|
|
61
|
+
"Lettermint",
|
|
62
|
+
"AsyncLettermint",
|
|
63
|
+
# Webhook
|
|
64
|
+
"Webhook",
|
|
65
|
+
# Exceptions
|
|
66
|
+
"LettermintError",
|
|
67
|
+
"HttpRequestError",
|
|
68
|
+
"ValidationError",
|
|
69
|
+
"ClientError",
|
|
70
|
+
"TimeoutError",
|
|
71
|
+
"WebhookVerificationError",
|
|
72
|
+
"InvalidSignatureError",
|
|
73
|
+
"TimestampToleranceError",
|
|
74
|
+
"JsonDecodeError",
|
|
75
|
+
# Types
|
|
76
|
+
"EmailAttachment",
|
|
77
|
+
"EmailPayload",
|
|
78
|
+
"EmailStatus",
|
|
79
|
+
"SendEmailResponse",
|
|
80
|
+
]
|
lettermint/client.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""HTTP client implementations for the Lettermint SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
ClientError,
|
|
11
|
+
HttpRequestError,
|
|
12
|
+
TimeoutError,
|
|
13
|
+
ValidationError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
DEFAULT_BASE_URL = "https://api.lettermint.co/v1"
|
|
17
|
+
DEFAULT_TIMEOUT = 30.0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LettermintClient:
|
|
21
|
+
"""Synchronous HTTP client for the Lettermint API.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
api_token: API token for authentication.
|
|
25
|
+
base_url: Base URL for the API. Defaults to https://api.lettermint.co/v1.
|
|
26
|
+
timeout: Request timeout in seconds. Defaults to 30.0.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
api_token: str,
|
|
32
|
+
base_url: str | None = None,
|
|
33
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
34
|
+
) -> None:
|
|
35
|
+
self._api_token = api_token
|
|
36
|
+
self._base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
|
|
37
|
+
self._timeout = timeout
|
|
38
|
+
self._client = httpx.Client(
|
|
39
|
+
base_url=self._base_url,
|
|
40
|
+
timeout=self._timeout,
|
|
41
|
+
headers={
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
"Accept": "application/json",
|
|
44
|
+
"x-lettermint-token": self._api_token,
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def close(self) -> None:
|
|
49
|
+
"""Close the HTTP client."""
|
|
50
|
+
self._client.close()
|
|
51
|
+
|
|
52
|
+
def __enter__(self) -> LettermintClient:
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def __exit__(self, *args: Any) -> None:
|
|
56
|
+
self.close()
|
|
57
|
+
|
|
58
|
+
def _handle_response(self, response: httpx.Response) -> Any:
|
|
59
|
+
"""Handle the HTTP response and raise appropriate exceptions."""
|
|
60
|
+
if response.is_success:
|
|
61
|
+
return response.json()
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
response_body = response.json()
|
|
65
|
+
except Exception:
|
|
66
|
+
response_body = None
|
|
67
|
+
|
|
68
|
+
if response.status_code == 422:
|
|
69
|
+
error_type = (
|
|
70
|
+
response_body.get("error", "ValidationError")
|
|
71
|
+
if isinstance(response_body, dict)
|
|
72
|
+
else "ValidationError"
|
|
73
|
+
)
|
|
74
|
+
raise ValidationError(
|
|
75
|
+
f"Validation error: {error_type}",
|
|
76
|
+
error_type,
|
|
77
|
+
response_body,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if response.status_code == 400:
|
|
81
|
+
error_message = (
|
|
82
|
+
response_body.get("error", "Unknown client error")
|
|
83
|
+
if isinstance(response_body, dict)
|
|
84
|
+
else "Unknown client error"
|
|
85
|
+
)
|
|
86
|
+
raise ClientError(f"Client error: {error_message}", response_body)
|
|
87
|
+
|
|
88
|
+
raise HttpRequestError(
|
|
89
|
+
f"HTTP error {response.status_code} {response.reason_phrase}",
|
|
90
|
+
response.status_code,
|
|
91
|
+
response_body,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def get(
|
|
95
|
+
self,
|
|
96
|
+
path: str,
|
|
97
|
+
params: dict[str, str] | None = None,
|
|
98
|
+
headers: dict[str, str] | None = None,
|
|
99
|
+
) -> Any:
|
|
100
|
+
"""Make a GET request to the API.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
path: API endpoint path.
|
|
104
|
+
params: Query parameters.
|
|
105
|
+
headers: Additional request headers.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The parsed JSON response.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
HttpRequestError: On HTTP errors.
|
|
112
|
+
TimeoutError: On request timeout.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
response = self._client.get(path, params=params, headers=headers)
|
|
116
|
+
return self._handle_response(response)
|
|
117
|
+
except httpx.TimeoutException as e:
|
|
118
|
+
raise TimeoutError(f"Request timeout after {self._timeout}s") from e
|
|
119
|
+
|
|
120
|
+
def post(
|
|
121
|
+
self,
|
|
122
|
+
path: str,
|
|
123
|
+
data: Any | None = None,
|
|
124
|
+
headers: dict[str, str] | None = None,
|
|
125
|
+
) -> Any:
|
|
126
|
+
"""Make a POST request to the API.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
path: API endpoint path.
|
|
130
|
+
data: Request payload to be JSON-encoded.
|
|
131
|
+
headers: Additional request headers.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The parsed JSON response.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
HttpRequestError: On HTTP errors.
|
|
138
|
+
TimeoutError: On request timeout.
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
response = self._client.post(path, json=data, headers=headers)
|
|
142
|
+
return self._handle_response(response)
|
|
143
|
+
except httpx.TimeoutException as e:
|
|
144
|
+
raise TimeoutError(f"Request timeout after {self._timeout}s") from e
|
|
145
|
+
|
|
146
|
+
def put(
|
|
147
|
+
self,
|
|
148
|
+
path: str,
|
|
149
|
+
data: Any | None = None,
|
|
150
|
+
headers: dict[str, str] | None = None,
|
|
151
|
+
) -> Any:
|
|
152
|
+
"""Make a PUT request to the API.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
path: API endpoint path.
|
|
156
|
+
data: Request payload to be JSON-encoded.
|
|
157
|
+
headers: Additional request headers.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
The parsed JSON response.
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
HttpRequestError: On HTTP errors.
|
|
164
|
+
TimeoutError: On request timeout.
|
|
165
|
+
"""
|
|
166
|
+
try:
|
|
167
|
+
response = self._client.put(path, json=data, headers=headers)
|
|
168
|
+
return self._handle_response(response)
|
|
169
|
+
except httpx.TimeoutException as e:
|
|
170
|
+
raise TimeoutError(f"Request timeout after {self._timeout}s") from e
|
|
171
|
+
|
|
172
|
+
def delete(
|
|
173
|
+
self,
|
|
174
|
+
path: str,
|
|
175
|
+
headers: dict[str, str] | None = None,
|
|
176
|
+
) -> Any:
|
|
177
|
+
"""Make a DELETE request to the API.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
path: API endpoint path.
|
|
181
|
+
headers: Additional request headers.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
The parsed JSON response.
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
HttpRequestError: On HTTP errors.
|
|
188
|
+
TimeoutError: On request timeout.
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
response = self._client.delete(path, headers=headers)
|
|
192
|
+
return self._handle_response(response)
|
|
193
|
+
except httpx.TimeoutException as e:
|
|
194
|
+
raise TimeoutError(f"Request timeout after {self._timeout}s") from e
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class AsyncLettermintClient:
|
|
198
|
+
"""Asynchronous HTTP client for the Lettermint API.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
api_token: API token for authentication.
|
|
202
|
+
base_url: Base URL for the API. Defaults to https://api.lettermint.co/v1.
|
|
203
|
+
timeout: Request timeout in seconds. Defaults to 30.0.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def __init__(
|
|
207
|
+
self,
|
|
208
|
+
api_token: str,
|
|
209
|
+
base_url: str | None = None,
|
|
210
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
211
|
+
) -> None:
|
|
212
|
+
self._api_token = api_token
|
|
213
|
+
self._base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
|
|
214
|
+
self._timeout = timeout
|
|
215
|
+
self._client = httpx.AsyncClient(
|
|
216
|
+
base_url=self._base_url,
|
|
217
|
+
timeout=self._timeout,
|
|
218
|
+
headers={
|
|
219
|
+
"Content-Type": "application/json",
|
|
220
|
+
"Accept": "application/json",
|
|
221
|
+
"x-lettermint-token": self._api_token,
|
|
222
|
+
},
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
async def close(self) -> None:
|
|
226
|
+
"""Close the HTTP client."""
|
|
227
|
+
await self._client.aclose()
|
|
228
|
+
|
|
229
|
+
async def __aenter__(self) -> AsyncLettermintClient:
|
|
230
|
+
return self
|
|
231
|
+
|
|
232
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
233
|
+
await self.close()
|
|
234
|
+
|
|
235
|
+
def _handle_response(self, response: httpx.Response) -> Any:
|
|
236
|
+
"""Handle the HTTP response and raise appropriate exceptions."""
|
|
237
|
+
if response.is_success:
|
|
238
|
+
return response.json()
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
response_body = response.json()
|
|
242
|
+
except Exception:
|
|
243
|
+
response_body = None
|
|
244
|
+
|
|
245
|
+
if response.status_code == 422:
|
|
246
|
+
error_type = (
|
|
247
|
+
response_body.get("error", "ValidationError")
|
|
248
|
+
if isinstance(response_body, dict)
|
|
249
|
+
else "ValidationError"
|
|
250
|
+
)
|
|
251
|
+
raise ValidationError(
|
|
252
|
+
f"Validation error: {error_type}",
|
|
253
|
+
error_type,
|
|
254
|
+
response_body,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if response.status_code == 400:
|
|
258
|
+
error_message = (
|
|
259
|
+
response_body.get("error", "Unknown client error")
|
|
260
|
+
if isinstance(response_body, dict)
|
|
261
|
+
else "Unknown client error"
|
|
262
|
+
)
|
|
263
|
+
raise ClientError(f"Client error: {error_message}", response_body)
|
|
264
|
+
|
|
265
|
+
raise HttpRequestError(
|
|
266
|
+
f"HTTP error {response.status_code} {response.reason_phrase}",
|
|
267
|
+
response.status_code,
|
|
268
|
+
response_body,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
async def get(
|
|
272
|
+
self,
|
|
273
|
+
path: str,
|
|
274
|
+
params: dict[str, str] | None = None,
|
|
275
|
+
headers: dict[str, str] | None = None,
|
|
276
|
+
) -> Any:
|
|
277
|
+
"""Make a GET request to the API.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
path: API endpoint path.
|
|
281
|
+
params: Query parameters.
|
|
282
|
+
headers: Additional request headers.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
The parsed JSON response.
|
|
286
|
+
|
|
287
|
+
Raises:
|
|
288
|
+
HttpRequestError: On HTTP errors.
|
|
289
|
+
TimeoutError: On request timeout.
|
|
290
|
+
"""
|
|
291
|
+
try:
|
|
292
|
+
response = await self._client.get(path, params=params, headers=headers)
|
|
293
|
+
return self._handle_response(response)
|
|
294
|
+
except httpx.TimeoutException as e:
|
|
295
|
+
raise TimeoutError(f"Request timeout after {self._timeout}s") from e
|
|
296
|
+
|
|
297
|
+
async def post(
|
|
298
|
+
self,
|
|
299
|
+
path: str,
|
|
300
|
+
data: Any | None = None,
|
|
301
|
+
headers: dict[str, str] | None = None,
|
|
302
|
+
) -> Any:
|
|
303
|
+
"""Make a POST request to the API.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
path: API endpoint path.
|
|
307
|
+
data: Request payload to be JSON-encoded.
|
|
308
|
+
headers: Additional request headers.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
The parsed JSON response.
|
|
312
|
+
|
|
313
|
+
Raises:
|
|
314
|
+
HttpRequestError: On HTTP errors.
|
|
315
|
+
TimeoutError: On request timeout.
|
|
316
|
+
"""
|
|
317
|
+
try:
|
|
318
|
+
response = await self._client.post(path, json=data, headers=headers)
|
|
319
|
+
return self._handle_response(response)
|
|
320
|
+
except httpx.TimeoutException as e:
|
|
321
|
+
raise TimeoutError(f"Request timeout after {self._timeout}s") from e
|
|
322
|
+
|
|
323
|
+
async def put(
|
|
324
|
+
self,
|
|
325
|
+
path: str,
|
|
326
|
+
data: Any | None = None,
|
|
327
|
+
headers: dict[str, str] | None = None,
|
|
328
|
+
) -> Any:
|
|
329
|
+
"""Make a PUT request to the API.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
path: API endpoint path.
|
|
333
|
+
data: Request payload to be JSON-encoded.
|
|
334
|
+
headers: Additional request headers.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
The parsed JSON response.
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
HttpRequestError: On HTTP errors.
|
|
341
|
+
TimeoutError: On request timeout.
|
|
342
|
+
"""
|
|
343
|
+
try:
|
|
344
|
+
response = await self._client.put(path, json=data, headers=headers)
|
|
345
|
+
return self._handle_response(response)
|
|
346
|
+
except httpx.TimeoutException as e:
|
|
347
|
+
raise TimeoutError(f"Request timeout after {self._timeout}s") from e
|
|
348
|
+
|
|
349
|
+
async def delete(
|
|
350
|
+
self,
|
|
351
|
+
path: str,
|
|
352
|
+
headers: dict[str, str] | None = None,
|
|
353
|
+
) -> Any:
|
|
354
|
+
"""Make a DELETE request to the API.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
path: API endpoint path.
|
|
358
|
+
headers: Additional request headers.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
The parsed JSON response.
|
|
362
|
+
|
|
363
|
+
Raises:
|
|
364
|
+
HttpRequestError: On HTTP errors.
|
|
365
|
+
TimeoutError: On request timeout.
|
|
366
|
+
"""
|
|
367
|
+
try:
|
|
368
|
+
response = await self._client.delete(path, headers=headers)
|
|
369
|
+
return self._handle_response(response)
|
|
370
|
+
except httpx.TimeoutException as e:
|
|
371
|
+
raise TimeoutError(f"Request timeout after {self._timeout}s") from e
|