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 ADDED
@@ -0,0 +1,7 @@
1
+ """PostwingSDK - Python SDK for Postwing email service API."""
2
+
3
+ from .sdk import PostwingSdk
4
+ from .exceptions import PostwingSdkException
5
+
6
+ __version__ = "1.0.0"
7
+ __all__ = ["PostwingSdk", "PostwingSdkException"]
postwing/exceptions.py ADDED
@@ -0,0 +1,9 @@
1
+ class PostwingSdkException(Exception):
2
+ msg = "Unhandled exception"
3
+
4
+ def __init__(self, msg: str, *args):
5
+ super().__init__(*args)
6
+ self.msg = msg
7
+
8
+ def __str__(self):
9
+ return f"PostwingSdkException: {self.msg}"
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ postwing