postwing 0.3.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.
- postwing/__init__.py +7 -0
- postwing/exceptions.py +9 -0
- postwing/sdk.py +314 -0
- postwing-0.3.0.dist-info/METADATA +528 -0
- postwing-0.3.0.dist-info/RECORD +7 -0
- postwing-0.3.0.dist-info/WHEEL +5 -0
- postwing-0.3.0.dist-info/top_level.txt +1 -0
postwing/__init__.py
ADDED
postwing/exceptions.py
ADDED
postwing/sdk.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor, Future
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from requests import exceptions
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from .exceptions import PostwingSdkException
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PostwingSdk:
|
|
16
|
+
SERVER_URL = "https://api.postwing.app"
|
|
17
|
+
auth = {}
|
|
18
|
+
fail_silently = False
|
|
19
|
+
debug = False
|
|
20
|
+
_executor = None
|
|
21
|
+
_max_workers = 5
|
|
22
|
+
_logger = None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def api_url(self):
|
|
26
|
+
return f"{self.SERVER_URL}/external/"
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def executor(self) -> ThreadPoolExecutor:
|
|
30
|
+
"""Lazy initialization of thread pool executor"""
|
|
31
|
+
if self._executor is None:
|
|
32
|
+
self._logger.info(f"Initializing ThreadPoolExecutor with {self._max_workers} workers")
|
|
33
|
+
self._executor = ThreadPoolExecutor(max_workers=self._max_workers)
|
|
34
|
+
return self._executor
|
|
35
|
+
|
|
36
|
+
def __init__(self, username: str, password: str, fail_silently=False, max_workers=5, log_level=logging.INFO):
|
|
37
|
+
"""
|
|
38
|
+
Initialize PostwingSdk.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
username: API username
|
|
42
|
+
password: API password
|
|
43
|
+
fail_silently: If True, suppress exceptions and return False on errors
|
|
44
|
+
max_workers: Number of worker threads for async operations
|
|
45
|
+
log_level: Logging level (e.g., logging.DEBUG, logging.INFO, logging.WARNING)
|
|
46
|
+
Use logging.DEBUG to see detailed request/response logs
|
|
47
|
+
"""
|
|
48
|
+
self.auth = {"username": username, "password": password}
|
|
49
|
+
self.fail_silently = fail_silently
|
|
50
|
+
self._max_workers = max_workers
|
|
51
|
+
|
|
52
|
+
# Configure logger for this instance
|
|
53
|
+
self._logger = logging.getLogger(f"{__name__}.{id(self)}")
|
|
54
|
+
self._logger.setLevel(log_level)
|
|
55
|
+
|
|
56
|
+
# Add handler if none exists (avoid duplicate handlers)
|
|
57
|
+
if not self._logger.handlers:
|
|
58
|
+
handler = logging.StreamHandler()
|
|
59
|
+
handler.setLevel(log_level)
|
|
60
|
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
61
|
+
handler.setFormatter(formatter)
|
|
62
|
+
self._logger.addHandler(handler)
|
|
63
|
+
|
|
64
|
+
self._logger.info(f"PostwingSdk initialized with username={username}, max_workers={max_workers}, log_level={logging.getLevelName(log_level)}")
|
|
65
|
+
|
|
66
|
+
def send_simple(
|
|
67
|
+
self,
|
|
68
|
+
recipient: str,
|
|
69
|
+
sender: str,
|
|
70
|
+
subject: str,
|
|
71
|
+
body: str,
|
|
72
|
+
idempotency_key: str = None,
|
|
73
|
+
) -> bool:
|
|
74
|
+
payload = {
|
|
75
|
+
"auth": self.auth,
|
|
76
|
+
"recipient": recipient,
|
|
77
|
+
"sender": sender,
|
|
78
|
+
"subject": subject,
|
|
79
|
+
"body": body,
|
|
80
|
+
"idempotency_key": idempotency_key,
|
|
81
|
+
}
|
|
82
|
+
try:
|
|
83
|
+
path = f"{self.api_url}send_email_simple/"
|
|
84
|
+
|
|
85
|
+
# Debug log for request
|
|
86
|
+
self._logger.debug(
|
|
87
|
+
f"Sending simple email - URL: {path}, "
|
|
88
|
+
f"recipient: {recipient}, sender: {sender}, subject: {subject}, "
|
|
89
|
+
f"idempotency_key: {idempotency_key}"
|
|
90
|
+
)
|
|
91
|
+
self._logger.debug(f"Request payload: {json.dumps({**payload, 'auth': '***'})}")
|
|
92
|
+
|
|
93
|
+
res = requests.post(path, json=payload)
|
|
94
|
+
|
|
95
|
+
# Debug log for response
|
|
96
|
+
try:
|
|
97
|
+
response_text = res.text[:200] + "..." if len(res.text) > 200 else res.text
|
|
98
|
+
except (TypeError, AttributeError):
|
|
99
|
+
response_text = str(res.text)
|
|
100
|
+
self._logger.debug(f"Response received - status_code: {res.status_code}, response: {response_text}")
|
|
101
|
+
|
|
102
|
+
if not res.ok:
|
|
103
|
+
self._logger.error(f"API error - status: {res.status_code}, response: {res.text}")
|
|
104
|
+
raise PostwingSdkException(res.text)
|
|
105
|
+
|
|
106
|
+
self._logger.info(f"Simple email sent successfully to {recipient}")
|
|
107
|
+
except exceptions.RequestException as exc:
|
|
108
|
+
self._logger.error(f"Request exception while sending email: {exc}")
|
|
109
|
+
if not self.fail_silently:
|
|
110
|
+
raise PostwingSdkException("Postwing API error") from exc
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
def send(
|
|
114
|
+
self,
|
|
115
|
+
tpl: str,
|
|
116
|
+
recipient: str,
|
|
117
|
+
sender: str,
|
|
118
|
+
lang: str | None = None,
|
|
119
|
+
params: dict | None = None,
|
|
120
|
+
idempotency_key: str | None = None,
|
|
121
|
+
):
|
|
122
|
+
payload = {
|
|
123
|
+
"auth": self.auth,
|
|
124
|
+
"tpl": tpl,
|
|
125
|
+
"recipient": recipient,
|
|
126
|
+
"sender": sender,
|
|
127
|
+
"lang": lang,
|
|
128
|
+
"params": params,
|
|
129
|
+
"idempotency_key": idempotency_key,
|
|
130
|
+
}
|
|
131
|
+
try:
|
|
132
|
+
path = f"{self.api_url}send_email_tpl/"
|
|
133
|
+
|
|
134
|
+
# Debug log for request
|
|
135
|
+
self._logger.debug(
|
|
136
|
+
f"Sending templated email - URL: {path}, "
|
|
137
|
+
f"template: {tpl}, recipient: {recipient}, sender: {sender}, "
|
|
138
|
+
f"lang: {lang}, idempotency_key: {idempotency_key}"
|
|
139
|
+
)
|
|
140
|
+
self._logger.debug(f"Request payload: {json.dumps({**payload, 'auth': '***'})}")
|
|
141
|
+
|
|
142
|
+
res = requests.post(path, json=payload)
|
|
143
|
+
|
|
144
|
+
# Debug log for response
|
|
145
|
+
try:
|
|
146
|
+
response_text = res.text[:200] + "..." if len(res.text) > 200 else res.text
|
|
147
|
+
except (TypeError, AttributeError):
|
|
148
|
+
response_text = str(res.text)
|
|
149
|
+
self._logger.debug(f"Response received - status_code: {res.status_code}, response: {response_text}")
|
|
150
|
+
|
|
151
|
+
if not res.ok:
|
|
152
|
+
self._logger.error(f"API error - status: {res.status_code}, response: {res.text}")
|
|
153
|
+
raise PostwingSdkException(str(res.text))
|
|
154
|
+
|
|
155
|
+
self._logger.info(f"Templated email sent successfully to {recipient} using template '{tpl}'")
|
|
156
|
+
except exceptions.RequestException as exc:
|
|
157
|
+
self._logger.error(f"Request exception while sending email: {exc}")
|
|
158
|
+
if not self.fail_silently:
|
|
159
|
+
raise PostwingSdkException("Postwing API error") from exc
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
def send_simple_async(
|
|
163
|
+
self,
|
|
164
|
+
recipient: str,
|
|
165
|
+
sender: str,
|
|
166
|
+
subject: str,
|
|
167
|
+
body: str,
|
|
168
|
+
idempotency_key: str = None,
|
|
169
|
+
callback: Callable[[bool, Exception | None], None] | None = None,
|
|
170
|
+
) -> Future:
|
|
171
|
+
"""
|
|
172
|
+
Send a simple email asynchronously in a background thread.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
recipient: Email recipient
|
|
176
|
+
sender: Email sender
|
|
177
|
+
subject: Email subject
|
|
178
|
+
body: Email body (HTML)
|
|
179
|
+
idempotency_key: Optional unique key to prevent duplicate sends
|
|
180
|
+
callback: Optional callback function called with (result, exception)
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Future object that can be used to wait for the result or check status
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
# Fire and forget
|
|
187
|
+
sdk.send_simple_async(recipient="user@example.com", ...)
|
|
188
|
+
|
|
189
|
+
# With callback
|
|
190
|
+
def on_complete(success, error):
|
|
191
|
+
if error:
|
|
192
|
+
print(f"Error: {error}")
|
|
193
|
+
else:
|
|
194
|
+
print("Email sent successfully")
|
|
195
|
+
|
|
196
|
+
sdk.send_simple_async(recipient="user@example.com", ..., callback=on_complete)
|
|
197
|
+
|
|
198
|
+
# Wait for result
|
|
199
|
+
future = sdk.send_simple_async(recipient="user@example.com", ...)
|
|
200
|
+
result = future.result() # Blocks until complete
|
|
201
|
+
"""
|
|
202
|
+
self._logger.debug(f"Submitting async simple email task for {recipient}")
|
|
203
|
+
|
|
204
|
+
def task():
|
|
205
|
+
try:
|
|
206
|
+
self._logger.debug(f"Async task started for simple email to {recipient}")
|
|
207
|
+
result = self.send_simple(
|
|
208
|
+
recipient=recipient,
|
|
209
|
+
sender=sender,
|
|
210
|
+
subject=subject,
|
|
211
|
+
body=body,
|
|
212
|
+
idempotency_key=idempotency_key,
|
|
213
|
+
)
|
|
214
|
+
if callback:
|
|
215
|
+
self._logger.debug(f"Calling callback for successful async email to {recipient}")
|
|
216
|
+
callback(result, None)
|
|
217
|
+
self._logger.debug(f"Async task completed successfully for {recipient}")
|
|
218
|
+
return result
|
|
219
|
+
except Exception as e:
|
|
220
|
+
self._logger.error(f"Async task failed for {recipient}: {e}")
|
|
221
|
+
if callback:
|
|
222
|
+
self._logger.debug(f"Calling callback with error for {recipient}")
|
|
223
|
+
callback(False, e)
|
|
224
|
+
raise
|
|
225
|
+
|
|
226
|
+
return self.executor.submit(task)
|
|
227
|
+
|
|
228
|
+
def send_async(
|
|
229
|
+
self,
|
|
230
|
+
tpl: str,
|
|
231
|
+
recipient: str,
|
|
232
|
+
sender: str,
|
|
233
|
+
lang: str | None = None,
|
|
234
|
+
params: dict | None = None,
|
|
235
|
+
idempotency_key: str | None = None,
|
|
236
|
+
callback: Callable[[bool, Exception | None], None] | None = None,
|
|
237
|
+
) -> Future:
|
|
238
|
+
"""
|
|
239
|
+
Send a templated email asynchronously in a background thread.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
tpl: Template name
|
|
243
|
+
recipient: Email recipient
|
|
244
|
+
sender: Email sender
|
|
245
|
+
lang: Language code (e.g., 'en', 'ru')
|
|
246
|
+
params: Template parameters
|
|
247
|
+
idempotency_key: Optional unique key to prevent duplicate sends
|
|
248
|
+
callback: Optional callback function called with (result, exception)
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Future object that can be used to wait for the result or check status
|
|
252
|
+
|
|
253
|
+
Example:
|
|
254
|
+
# Fire and forget
|
|
255
|
+
sdk.send_async(tpl="welcome", recipient="user@example.com", ...)
|
|
256
|
+
|
|
257
|
+
# With callback
|
|
258
|
+
def on_complete(success, error):
|
|
259
|
+
if error:
|
|
260
|
+
print(f"Error: {error}")
|
|
261
|
+
else:
|
|
262
|
+
print("Email sent successfully")
|
|
263
|
+
|
|
264
|
+
sdk.send_async(tpl="welcome", ..., callback=on_complete)
|
|
265
|
+
|
|
266
|
+
# Wait for result
|
|
267
|
+
future = sdk.send_async(tpl="welcome", ...)
|
|
268
|
+
result = future.result() # Blocks until complete
|
|
269
|
+
"""
|
|
270
|
+
self._logger.debug(f"Submitting async templated email task for {recipient} with template '{tpl}'")
|
|
271
|
+
|
|
272
|
+
def task():
|
|
273
|
+
try:
|
|
274
|
+
self._logger.debug(f"Async task started for templated email to {recipient} (template: {tpl})")
|
|
275
|
+
result = self.send(
|
|
276
|
+
tpl=tpl,
|
|
277
|
+
recipient=recipient,
|
|
278
|
+
sender=sender,
|
|
279
|
+
lang=lang,
|
|
280
|
+
params=params,
|
|
281
|
+
idempotency_key=idempotency_key,
|
|
282
|
+
)
|
|
283
|
+
if callback:
|
|
284
|
+
self._logger.debug(f"Calling callback for successful async email to {recipient}")
|
|
285
|
+
callback(result, None)
|
|
286
|
+
self._logger.debug(f"Async task completed successfully for {recipient}")
|
|
287
|
+
return result
|
|
288
|
+
except Exception as e:
|
|
289
|
+
self._logger.error(f"Async task failed for {recipient}: {e}")
|
|
290
|
+
if callback:
|
|
291
|
+
self._logger.debug(f"Calling callback with error for {recipient}")
|
|
292
|
+
callback(False, e)
|
|
293
|
+
raise
|
|
294
|
+
|
|
295
|
+
return self.executor.submit(task)
|
|
296
|
+
|
|
297
|
+
def shutdown(self, wait=True):
|
|
298
|
+
"""
|
|
299
|
+
Shutdown the thread pool executor.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
wait: If True, wait for all pending tasks to complete
|
|
303
|
+
"""
|
|
304
|
+
if self._executor is not None:
|
|
305
|
+
self._logger.info(f"Shutting down ThreadPoolExecutor (wait={wait})")
|
|
306
|
+
self._executor.shutdown(wait=wait)
|
|
307
|
+
self._executor = None
|
|
308
|
+
self._logger.debug("ThreadPoolExecutor shutdown complete")
|
|
309
|
+
|
|
310
|
+
def __del__(self):
|
|
311
|
+
"""Cleanup executor on garbage collection"""
|
|
312
|
+
if self._logger:
|
|
313
|
+
self._logger.debug("PostwingSdk instance being destroyed, cleaning up executor")
|
|
314
|
+
self.shutdown(wait=False)
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: postwing
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Python SDK for Postwing email service API (api.postwing.app)
|
|
5
|
+
Author-email: Andrey Fanyagin <skymanrm@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/skymanrm/postwing-python-sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/skymanrm/postwing-python-sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/skymanrm/postwing-python-sdk/issues
|
|
10
|
+
Keywords: email,postwing,sdk,api,email-service
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Communications :: Email
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.7
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: requests>=2.25.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: faker>=8.0.0; extra == "dev"
|
|
27
|
+
|
|
28
|
+
# PostwingSDK
|
|
29
|
+
|
|
30
|
+
A Python SDK for the [Postwing](https://postwing.ru) email service API. Provides both synchronous and asynchronous methods for sending emails via templates or simple HTML content.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- **Synchronous and Asynchronous API** - Choose between blocking and non-blocking email sending
|
|
35
|
+
- **Template Support** - Send emails using pre-configured templates with parameters
|
|
36
|
+
- **Simple HTML Emails** - Send plain HTML emails directly
|
|
37
|
+
- **Idempotency Keys** - Prevent duplicate email sends with unique keys
|
|
38
|
+
- **Multi-language Support** - Send templated emails in different languages
|
|
39
|
+
- **Thread Pool Execution** - Efficient concurrent email sending with configurable worker threads
|
|
40
|
+
- **Callback Support** - Handle async results with callbacks for fire-and-forget patterns
|
|
41
|
+
- **Fail Silently Mode** - Option to suppress exceptions for graceful degradation
|
|
42
|
+
- **Comprehensive Logging** - Configurable logging levels with detailed request/response information for debugging
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
Install the required dependencies:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install -r requirements.txt
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Dependencies
|
|
53
|
+
|
|
54
|
+
- `requests` - For making HTTP API calls
|
|
55
|
+
- `faker` - For running tests (development only)
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from postwing.sdk import PostwingSdk
|
|
61
|
+
|
|
62
|
+
# Initialize the SDK
|
|
63
|
+
sdk = PostwingSdk(
|
|
64
|
+
username="your-domain@example.com",
|
|
65
|
+
password="your-api-token"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Send a simple HTML email
|
|
69
|
+
sdk.send_simple(
|
|
70
|
+
recipient="user@example.com",
|
|
71
|
+
sender="noreply@example.com",
|
|
72
|
+
subject="Welcome!",
|
|
73
|
+
body="<h1>Hello World</h1>"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Send a templated email
|
|
77
|
+
sdk.send(
|
|
78
|
+
tpl="welcome-template",
|
|
79
|
+
recipient="user@example.com",
|
|
80
|
+
sender="noreply@example.com",
|
|
81
|
+
lang="en",
|
|
82
|
+
params={"name": "John", "code": "123456"}
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Usage
|
|
87
|
+
|
|
88
|
+
### Synchronous Methods
|
|
89
|
+
|
|
90
|
+
#### Send Simple HTML Email
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
sdk.send_simple(
|
|
94
|
+
recipient="user@example.com",
|
|
95
|
+
sender="noreply@example.com",
|
|
96
|
+
subject="Important Notice",
|
|
97
|
+
body="<p>This is an important message.</p>",
|
|
98
|
+
idempotency_key="unique-key-123" # Optional: prevent duplicates
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### Send Templated Email
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
sdk.send(
|
|
106
|
+
tpl="password-reset",
|
|
107
|
+
recipient="user@example.com",
|
|
108
|
+
sender="noreply@example.com",
|
|
109
|
+
lang="en", # Optional: language code
|
|
110
|
+
params={"reset_link": "https://example.com/reset/token"}, # Template variables
|
|
111
|
+
idempotency_key="reset-user-123" # Optional: prevent duplicates
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Asynchronous Methods
|
|
116
|
+
|
|
117
|
+
Async methods use a thread pool executor for non-blocking operation. They return `Future` objects that can be used in various ways:
|
|
118
|
+
|
|
119
|
+
#### Fire and Forget
|
|
120
|
+
|
|
121
|
+
Send emails without waiting for responses:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
sdk.send_simple_async(
|
|
125
|
+
recipient="user@example.com",
|
|
126
|
+
sender="noreply@example.com",
|
|
127
|
+
subject="Newsletter",
|
|
128
|
+
body="<h1>Latest Updates</h1>"
|
|
129
|
+
)
|
|
130
|
+
# Continues immediately without blocking
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### Using Callbacks
|
|
134
|
+
|
|
135
|
+
Handle results with callback functions:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
def on_complete(success, error):
|
|
139
|
+
if error:
|
|
140
|
+
print(f"Failed to send email: {error}")
|
|
141
|
+
else:
|
|
142
|
+
print("Email sent successfully!")
|
|
143
|
+
|
|
144
|
+
sdk.send_async(
|
|
145
|
+
tpl="notification",
|
|
146
|
+
recipient="user@example.com",
|
|
147
|
+
sender="noreply@example.com",
|
|
148
|
+
params={"message": "You have a new notification"},
|
|
149
|
+
callback=on_complete
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### Wait for Results
|
|
154
|
+
|
|
155
|
+
Send async but wait for completion when needed:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
future = sdk.send_simple_async(
|
|
159
|
+
recipient="user@example.com",
|
|
160
|
+
sender="noreply@example.com",
|
|
161
|
+
subject="Confirmation",
|
|
162
|
+
body="<p>Please confirm your action</p>"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Do other work...
|
|
166
|
+
|
|
167
|
+
# Wait for result (with timeout)
|
|
168
|
+
try:
|
|
169
|
+
result = future.result(timeout=10) # Wait up to 10 seconds
|
|
170
|
+
print(f"Email sent: {result}")
|
|
171
|
+
except Exception as e:
|
|
172
|
+
print(f"Email failed: {e}")
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### Batch Sending
|
|
176
|
+
|
|
177
|
+
Send multiple emails concurrently:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
recipients = ["user1@example.com", "user2@example.com", "user3@example.com"]
|
|
181
|
+
futures = []
|
|
182
|
+
|
|
183
|
+
for recipient in recipients:
|
|
184
|
+
future = sdk.send_simple_async(
|
|
185
|
+
recipient=recipient,
|
|
186
|
+
sender="noreply@example.com",
|
|
187
|
+
subject="Batch Email",
|
|
188
|
+
body="<p>Hello!</p>",
|
|
189
|
+
idempotency_key=f"batch-{recipient}"
|
|
190
|
+
)
|
|
191
|
+
futures.append(future)
|
|
192
|
+
|
|
193
|
+
# Wait for all to complete
|
|
194
|
+
for future in futures:
|
|
195
|
+
try:
|
|
196
|
+
future.result(timeout=30)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
print(f"Failed: {e}")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
#### Check Status Without Blocking
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
future = sdk.send_simple_async(
|
|
205
|
+
recipient="user@example.com",
|
|
206
|
+
sender="noreply@example.com",
|
|
207
|
+
subject="Status Check",
|
|
208
|
+
body="<p>Testing</p>"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if future.done():
|
|
212
|
+
result = future.result()
|
|
213
|
+
print(f"Already completed: {result}")
|
|
214
|
+
else:
|
|
215
|
+
print("Still processing...")
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Configuration
|
|
219
|
+
|
|
220
|
+
### SDK Options
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
import logging
|
|
224
|
+
|
|
225
|
+
sdk = PostwingSdk(
|
|
226
|
+
username="your-domain@example.com",
|
|
227
|
+
password="your-api-token",
|
|
228
|
+
fail_silently=False, # If True, suppresses exceptions
|
|
229
|
+
max_workers=5, # Number of concurrent threads for async operations
|
|
230
|
+
log_level=logging.INFO # Logging level (default: INFO)
|
|
231
|
+
)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Logging
|
|
235
|
+
|
|
236
|
+
The SDK includes comprehensive logging capabilities to help with debugging and monitoring email operations.
|
|
237
|
+
|
|
238
|
+
#### Log Levels
|
|
239
|
+
|
|
240
|
+
The SDK supports standard Python logging levels:
|
|
241
|
+
|
|
242
|
+
- `logging.DEBUG` - Detailed information including request/response data (recommended for development)
|
|
243
|
+
- `logging.INFO` - General operational messages about SDK lifecycle and email sends (default)
|
|
244
|
+
- `logging.WARNING` - Warning messages
|
|
245
|
+
- `logging.ERROR` - Error messages only
|
|
246
|
+
|
|
247
|
+
#### Basic Logging Configuration
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
import logging
|
|
251
|
+
from postwing.sdk import PostwingSdk
|
|
252
|
+
|
|
253
|
+
# Enable DEBUG logging to see detailed request/response information
|
|
254
|
+
sdk = PostwingSdk(
|
|
255
|
+
username="your-domain@example.com",
|
|
256
|
+
password="your-api-token",
|
|
257
|
+
log_level=logging.DEBUG
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Send an email - you'll see detailed logs
|
|
261
|
+
sdk.send_simple(
|
|
262
|
+
recipient="user@example.com",
|
|
263
|
+
sender="noreply@example.com",
|
|
264
|
+
subject="Test Email",
|
|
265
|
+
body="<p>Testing with debug logs</p>"
|
|
266
|
+
)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
#### What Gets Logged
|
|
270
|
+
|
|
271
|
+
**INFO Level:**
|
|
272
|
+
- SDK initialization with configuration
|
|
273
|
+
- ThreadPoolExecutor creation and shutdown
|
|
274
|
+
- Successful email sends
|
|
275
|
+
|
|
276
|
+
**DEBUG Level:**
|
|
277
|
+
- All INFO level messages
|
|
278
|
+
- Full request details (URL, parameters, sanitized payload)
|
|
279
|
+
- Full response details (status codes, response body)
|
|
280
|
+
- Async task lifecycle (submission, start, completion)
|
|
281
|
+
- Callback execution
|
|
282
|
+
|
|
283
|
+
**ERROR Level:**
|
|
284
|
+
- API errors (non-2xx responses)
|
|
285
|
+
- Network/connection errors
|
|
286
|
+
- Async task failures
|
|
287
|
+
|
|
288
|
+
#### Example Output
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
2025-11-21 17:36:05,170 - postwing.sdk.4472389120 - INFO - PostwingSdk initialized with username=test, max_workers=5, log_level=INFO
|
|
292
|
+
2025-11-21 17:36:05,171 - postwing.sdk.4472389120 - INFO - Initializing ThreadPoolExecutor with 5 workers
|
|
293
|
+
2025-11-21 17:36:05,171 - postwing.sdk.4472389120 - DEBUG - Sending simple email - URL: https://api.postwing.app/external/send_email_simple/, recipient: user@example.com, sender: noreply@example.com, subject: Test, idempotency_key: None
|
|
294
|
+
2025-11-21 17:36:05,171 - postwing.sdk.4472389120 - DEBUG - Request payload: {"recipient": "user@example.com", "sender": "noreply@example.com", "subject": "Test", "body": "<p>Test</p>", "auth": "***"}
|
|
295
|
+
2025-11-21 17:36:05,171 - postwing.sdk.4472389120 - DEBUG - Response received - status_code: 200, response: {"success": true}
|
|
296
|
+
2025-11-21 17:36:05,171 - postwing.sdk.4472389120 - INFO - Simple email sent successfully to user@example.com
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
#### Disable Logging
|
|
300
|
+
|
|
301
|
+
To disable all logging output:
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
sdk = PostwingSdk(
|
|
305
|
+
username="your-domain@example.com",
|
|
306
|
+
password="your-api-token",
|
|
307
|
+
log_level=logging.CRITICAL # Only critical errors
|
|
308
|
+
)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### Production Recommendations
|
|
312
|
+
|
|
313
|
+
For production environments, we recommend:
|
|
314
|
+
|
|
315
|
+
1. Use `logging.INFO` or `logging.WARNING` to avoid logging sensitive data
|
|
316
|
+
2. Configure external log aggregation (e.g., CloudWatch, Datadog)
|
|
317
|
+
3. Monitor ERROR level logs for operational issues
|
|
318
|
+
4. Use DEBUG level only for troubleshooting specific issues
|
|
319
|
+
|
|
320
|
+
#### Custom Logging Configuration
|
|
321
|
+
|
|
322
|
+
If you need more control over logging format or handlers, you can configure Python's logging system before initializing the SDK:
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
import logging
|
|
326
|
+
|
|
327
|
+
# Configure global logging
|
|
328
|
+
logging.basicConfig(
|
|
329
|
+
level=logging.INFO,
|
|
330
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
331
|
+
handlers=[
|
|
332
|
+
logging.FileHandler('postwing.log'),
|
|
333
|
+
logging.StreamHandler()
|
|
334
|
+
]
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# SDK will use the configured logging system
|
|
338
|
+
sdk = PostwingSdk(
|
|
339
|
+
username="your-domain@example.com",
|
|
340
|
+
password="your-api-token",
|
|
341
|
+
log_level=logging.DEBUG
|
|
342
|
+
)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Cleanup
|
|
346
|
+
|
|
347
|
+
Properly shutdown the thread pool when done:
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
# Wait for all pending emails to complete before shutdown
|
|
351
|
+
sdk.shutdown(wait=True)
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Or use a try-finally pattern:
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
try:
|
|
358
|
+
sdk.send_simple_async(...)
|
|
359
|
+
# ... more operations
|
|
360
|
+
finally:
|
|
361
|
+
sdk.shutdown(wait=True)
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## API Reference
|
|
365
|
+
|
|
366
|
+
### `PostwingSdk`
|
|
367
|
+
|
|
368
|
+
#### Constructor
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
PostwingSdk(username: str, password: str, fail_silently=False, max_workers=5, log_level=logging.INFO)
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
- `username` - Your Postwing account username (typically your domain)
|
|
375
|
+
- `password` - Your Postwing API token
|
|
376
|
+
- `fail_silently` - If True, suppresses exceptions on errors
|
|
377
|
+
- `max_workers` - Number of threads for async operations (default: 5)
|
|
378
|
+
- `log_level` - Logging level using Python's logging constants (default: logging.INFO). Use logging.DEBUG for detailed request/response logs
|
|
379
|
+
|
|
380
|
+
#### Methods
|
|
381
|
+
|
|
382
|
+
##### `send_simple(recipient, sender, subject, body, idempotency_key=None) -> bool`
|
|
383
|
+
|
|
384
|
+
Send a simple HTML email synchronously.
|
|
385
|
+
|
|
386
|
+
##### `send(tpl, recipient, sender, lang=None, params=None, idempotency_key=None) -> bool`
|
|
387
|
+
|
|
388
|
+
Send a templated email synchronously.
|
|
389
|
+
|
|
390
|
+
##### `send_simple_async(recipient, sender, subject, body, idempotency_key=None, callback=None) -> Future`
|
|
391
|
+
|
|
392
|
+
Send a simple HTML email asynchronously.
|
|
393
|
+
|
|
394
|
+
##### `send_async(tpl, recipient, sender, lang=None, params=None, idempotency_key=None, callback=None) -> Future`
|
|
395
|
+
|
|
396
|
+
Send a templated email asynchronously.
|
|
397
|
+
|
|
398
|
+
##### `shutdown(wait=True)`
|
|
399
|
+
|
|
400
|
+
Shutdown the thread pool executor.
|
|
401
|
+
|
|
402
|
+
### Exceptions
|
|
403
|
+
|
|
404
|
+
#### `PostwingSdkException`
|
|
405
|
+
|
|
406
|
+
Raised when API requests fail or network errors occur. Can be suppressed with `fail_silently=True`.
|
|
407
|
+
|
|
408
|
+
## Development
|
|
409
|
+
|
|
410
|
+
### Using the Makefile
|
|
411
|
+
|
|
412
|
+
The project includes a Makefile for common development tasks:
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
# View all available commands
|
|
416
|
+
make help
|
|
417
|
+
|
|
418
|
+
# Install dependencies
|
|
419
|
+
make install
|
|
420
|
+
|
|
421
|
+
# Run all tests
|
|
422
|
+
make test
|
|
423
|
+
|
|
424
|
+
# Run synchronous tests only
|
|
425
|
+
make test-sync
|
|
426
|
+
|
|
427
|
+
# Run asynchronous tests only
|
|
428
|
+
make test-async
|
|
429
|
+
|
|
430
|
+
# Run a specific test
|
|
431
|
+
make test-specific TEST=tests.PostwingAsyncTestUtils.test_send_simple_async_success
|
|
432
|
+
|
|
433
|
+
# Build the package
|
|
434
|
+
make build
|
|
435
|
+
|
|
436
|
+
# Clean build artifacts
|
|
437
|
+
make clean
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Testing
|
|
441
|
+
|
|
442
|
+
The SDK includes a comprehensive test suite covering both synchronous and asynchronous operations.
|
|
443
|
+
|
|
444
|
+
#### Quick Test Commands (Using Makefile)
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
# Run all tests
|
|
448
|
+
make test
|
|
449
|
+
|
|
450
|
+
# Run specific test classes
|
|
451
|
+
make test-sync # Synchronous tests only
|
|
452
|
+
make test-async # Asynchronous tests only
|
|
453
|
+
|
|
454
|
+
# Run a specific test
|
|
455
|
+
make test-specific TEST=tests.PostwingAsyncTestUtils.test_send_simple_async_success
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
#### Manual Test Commands (Without Makefile)
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
# Run all tests
|
|
462
|
+
source .venv/bin/activate
|
|
463
|
+
PYTHONPATH=/Users/skyman/Documents/My/Python:$PYTHONPATH python -m unittest tests
|
|
464
|
+
|
|
465
|
+
# Run specific test class
|
|
466
|
+
PYTHONPATH=/Users/skyman/Documents/My/Python:$PYTHONPATH python -m unittest tests.PostwingTestUtils
|
|
467
|
+
|
|
468
|
+
# Run specific test
|
|
469
|
+
PYTHONPATH=/Users/skyman/Documents/My/Python:$PYTHONPATH python -m unittest tests.PostwingAsyncTestUtils.test_send_simple_async_success
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Building and Publishing
|
|
473
|
+
|
|
474
|
+
Build the package for distribution:
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
# Build the package
|
|
478
|
+
make build
|
|
479
|
+
|
|
480
|
+
# Publish to TestPyPI (for testing)
|
|
481
|
+
make publish-test
|
|
482
|
+
|
|
483
|
+
# Publish to PyPI (production)
|
|
484
|
+
make publish
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Or manually:
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
# Build
|
|
491
|
+
python -m build
|
|
492
|
+
|
|
493
|
+
# Check the distribution
|
|
494
|
+
twine check dist/*
|
|
495
|
+
|
|
496
|
+
# Upload to PyPI
|
|
497
|
+
twine upload dist/*
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
## Examples
|
|
501
|
+
|
|
502
|
+
See `async_example.py` for comprehensive examples of all async patterns including:
|
|
503
|
+
- Fire and forget
|
|
504
|
+
- Callback handling
|
|
505
|
+
- Waiting for results
|
|
506
|
+
- Batch sending
|
|
507
|
+
- Status checking
|
|
508
|
+
- Proper cleanup
|
|
509
|
+
|
|
510
|
+
## API Endpoints
|
|
511
|
+
|
|
512
|
+
The SDK communicates with the following Postwing API endpoints:
|
|
513
|
+
|
|
514
|
+
- **Base URL**: `https://api.postwing.app/external/`
|
|
515
|
+
- **Simple Send**: `POST /external/send_email_simple/`
|
|
516
|
+
- **Template Send**: `POST /external/send_email_tpl/`
|
|
517
|
+
|
|
518
|
+
## Contributing
|
|
519
|
+
|
|
520
|
+
Contributions are welcome! Please ensure all tests pass before submitting a pull request.
|
|
521
|
+
|
|
522
|
+
## License
|
|
523
|
+
|
|
524
|
+
[Add your license information here]
|
|
525
|
+
|
|
526
|
+
## Support
|
|
527
|
+
|
|
528
|
+
For issues, questions, or feature requests, please contact Postwing support or open an issue in this repository.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
postwing/__init__.py,sha256=x3d3lOraxp9KAZar8zG2wuANkwzFXoZmaP28tGFhBsY,211
|
|
2
|
+
postwing/exceptions.py,sha256=MYu58rUJ_WeXPiMNzDzD0zn1UgYSPdSeLAajUVA5Vpc,243
|
|
3
|
+
postwing/sdk.py,sha256=WLsEG-3dLG6Z32Ow68W9lTtuVk9-k-l7NztbOtzf-xE,11664
|
|
4
|
+
postwing-0.3.0.dist-info/METADATA,sha256=_KNkhG6zQ1QfBadIPZKj8Bi2Pwx-uVNjx2_TzE87VFw,13792
|
|
5
|
+
postwing-0.3.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
postwing-0.3.0.dist-info/top_level.txt,sha256=At-KaZlOibwogX4nh-ucNoXgYDTUjIOW3jVOYy_aAq8,9
|
|
7
|
+
postwing-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
postwing
|