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 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
@@ -0,0 +1,8 @@
1
+ """Endpoint modules for the Lettermint SDK."""
2
+
3
+ from .email import AsyncEmailEndpoint, EmailEndpoint
4
+
5
+ __all__ = [
6
+ "EmailEndpoint",
7
+ "AsyncEmailEndpoint",
8
+ ]