zasend 0.1.0__tar.gz
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.
- zasend-0.1.0/LICENSE +21 -0
- zasend-0.1.0/PKG-INFO +204 -0
- zasend-0.1.0/README.md +172 -0
- zasend-0.1.0/pyproject.toml +56 -0
- zasend-0.1.0/setup.cfg +4 -0
- zasend-0.1.0/tests/test_client.py +91 -0
- zasend-0.1.0/zasend/__init__.py +12 -0
- zasend-0.1.0/zasend/client.py +171 -0
- zasend-0.1.0/zasend.egg-info/PKG-INFO +204 -0
- zasend-0.1.0/zasend.egg-info/SOURCES.txt +11 -0
- zasend-0.1.0/zasend.egg-info/dependency_links.txt +1 -0
- zasend-0.1.0/zasend.egg-info/requires.txt +1 -0
- zasend-0.1.0/zasend.egg-info/top_level.txt +1 -0
zasend-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zaSend
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
zasend-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zasend
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python transactional email API client for zaSend
|
|
5
|
+
Author-email: zaSend <support@zasend.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://zasend.com
|
|
8
|
+
Project-URL: Documentation, https://zasend.com/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/lr2bmail/firemail-api/tree/master/clients/python
|
|
10
|
+
Project-URL: Issues, https://github.com/lr2bmail/firemail-api/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/lr2bmail/firemail-api/commits/master/clients/python
|
|
12
|
+
Keywords: email,transactional-email,email-api,smtp,python-email,django-email,flask-email,fastapi-email,webhooks,dkim,zasend
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Communications :: Email
|
|
24
|
+
Classifier: Topic :: Communications :: Email :: Mail Transport Agents
|
|
25
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Requires-Python: >=3.8
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: requests>=2.25
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# zasend
|
|
34
|
+
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
[](pyproject.toml)
|
|
37
|
+
|
|
38
|
+
Official Python client for the [zaSend](https://zasend.com) transactional email API.
|
|
39
|
+
|
|
40
|
+
zaSend is a developer-focused email delivery service for sending product, auth, billing, notification, and lifecycle emails from your application. This package is a small wrapper around the zaSend REST API for Python apps, Django, Flask, FastAPI, background jobs, and scripts.
|
|
41
|
+
|
|
42
|
+
Use it to:
|
|
43
|
+
|
|
44
|
+
- Send transactional email from verified domains
|
|
45
|
+
- Send template-based email with variables
|
|
46
|
+
- Check message status and events
|
|
47
|
+
- Manage suppressions, webhooks, templates, and domains
|
|
48
|
+
- Verify zaSend webhook signatures
|
|
49
|
+
|
|
50
|
+
The client stays close to the raw API. It has no heavy framework dependencies and uses `requests`.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install zasend
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from zasend import ZaSend
|
|
62
|
+
|
|
63
|
+
client = ZaSend(api_key="sk_live_...")
|
|
64
|
+
|
|
65
|
+
result = client.send_email(
|
|
66
|
+
from_email="zaSend <noreply@zasend.com>",
|
|
67
|
+
to="user@example.com",
|
|
68
|
+
subject="Welcome",
|
|
69
|
+
text="Thanks for signing up.",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
print(result["message_id"])
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
You can use an Account API Key (`sk_live_...`) for all methods. A Domain Sending Key (`dsk_live_...`) is restricted to sending email from one verified domain.
|
|
76
|
+
|
|
77
|
+
## Send HTML Email
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
client.send_email(
|
|
81
|
+
from_email="Acme <noreply@acme.com>",
|
|
82
|
+
to="customer@example.com",
|
|
83
|
+
subject="Your receipt",
|
|
84
|
+
html="<h1>Thanks for your order</h1><p>Your receipt is attached.</p>",
|
|
85
|
+
text="Thanks for your order. Your receipt is attached.",
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Send to Multiple Recipients
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
client.send_email(
|
|
93
|
+
from_email="Acme <noreply@acme.com>",
|
|
94
|
+
to=["one@example.com", "two@example.com"],
|
|
95
|
+
subject="Product update",
|
|
96
|
+
text="A new update is available.",
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
zaSend returns one queued message per `to` recipient. Suppressed recipients reject the whole request so you can fix the list before sending.
|
|
101
|
+
|
|
102
|
+
## Template Send
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
client.send_template_email(
|
|
106
|
+
from_email="Acme <noreply@acme.com>",
|
|
107
|
+
to="customer@example.com",
|
|
108
|
+
template="welcome",
|
|
109
|
+
variables={"name": "Ada"},
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Django Example
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
# settings.py
|
|
117
|
+
ZASEND_API_KEY = "sk_live_..."
|
|
118
|
+
|
|
119
|
+
# anywhere in your app
|
|
120
|
+
from django.conf import settings
|
|
121
|
+
from zasend import ZaSend
|
|
122
|
+
|
|
123
|
+
zasend = ZaSend(settings.ZASEND_API_KEY)
|
|
124
|
+
zasend.send_email(
|
|
125
|
+
from_email="Acme <noreply@acme.com>",
|
|
126
|
+
to=user.email,
|
|
127
|
+
subject="Reset your password",
|
|
128
|
+
text="Use this link to reset your password.",
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Flask or FastAPI Example
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import os
|
|
136
|
+
from zasend import ZaSend
|
|
137
|
+
|
|
138
|
+
zasend = ZaSend(os.environ["ZASEND_API_KEY"])
|
|
139
|
+
|
|
140
|
+
def send_signup_email(email):
|
|
141
|
+
return zasend.send_template_email(
|
|
142
|
+
from_email="Acme <noreply@acme.com>",
|
|
143
|
+
to=email,
|
|
144
|
+
template="welcome",
|
|
145
|
+
variables={"product": "Acme"},
|
|
146
|
+
)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Webhook Verification
|
|
150
|
+
|
|
151
|
+
zaSend signs webhook payloads with HMAC-SHA256. Verify the raw request body before trusting webhook data.
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from zasend import verify_webhook_signature
|
|
155
|
+
|
|
156
|
+
ok = verify_webhook_signature(
|
|
157
|
+
request.get_data(),
|
|
158
|
+
"whsec_...",
|
|
159
|
+
request.headers.get("X-zaSend-Signature"),
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## API Methods
|
|
164
|
+
|
|
165
|
+
| Method | Description |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| `send_email(**message)` | Send direct content or a template email |
|
|
168
|
+
| `send_template_email(...)` | Convenience wrapper for template sends |
|
|
169
|
+
| `get_email(message_id)` | Fetch email status and events |
|
|
170
|
+
| `get_rate_limits()` | Fetch daily usage and limits |
|
|
171
|
+
| `list_domains()` / `add_domain(domain)` / `verify_domain(id)` / `delete_domain(id)` | Manage sending domains |
|
|
172
|
+
| `list_suppressions(...)` / `add_suppression(email, ...)` / `delete_suppression(id)` | Manage suppression list entries |
|
|
173
|
+
| `list_webhooks()` / `create_webhook(url, events)` / `delete_webhook(id)` | Manage signed delivery webhooks |
|
|
174
|
+
| `list_templates()` / `create_template(...)` / `get_template(id)` / `update_template(id, ...)` / `delete_template(id)` | Manage email templates |
|
|
175
|
+
|
|
176
|
+
## Error Handling
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from zasend import APIError, RateLimitError, ValidationError, ZaSend
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
ZaSend("sk_live_...").send_email(
|
|
183
|
+
from_email="Acme <noreply@acme.com>",
|
|
184
|
+
to="user@example.com",
|
|
185
|
+
subject="Hello",
|
|
186
|
+
text="Body",
|
|
187
|
+
)
|
|
188
|
+
except RateLimitError as exc:
|
|
189
|
+
print("Rate limited:", exc)
|
|
190
|
+
except ValidationError as exc:
|
|
191
|
+
print("Invalid request:", exc)
|
|
192
|
+
except APIError as exc:
|
|
193
|
+
print("zaSend API error:", exc)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Links
|
|
197
|
+
|
|
198
|
+
- Website: [zasend.com](https://zasend.com)
|
|
199
|
+
- API documentation: [zasend.com/docs](https://zasend.com/docs)
|
|
200
|
+
- Source: [GitHub](https://github.com/lr2bmail/firemail-api/tree/master/clients/python)
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT
|
zasend-0.1.0/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# zasend
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](pyproject.toml)
|
|
5
|
+
|
|
6
|
+
Official Python client for the [zaSend](https://zasend.com) transactional email API.
|
|
7
|
+
|
|
8
|
+
zaSend is a developer-focused email delivery service for sending product, auth, billing, notification, and lifecycle emails from your application. This package is a small wrapper around the zaSend REST API for Python apps, Django, Flask, FastAPI, background jobs, and scripts.
|
|
9
|
+
|
|
10
|
+
Use it to:
|
|
11
|
+
|
|
12
|
+
- Send transactional email from verified domains
|
|
13
|
+
- Send template-based email with variables
|
|
14
|
+
- Check message status and events
|
|
15
|
+
- Manage suppressions, webhooks, templates, and domains
|
|
16
|
+
- Verify zaSend webhook signatures
|
|
17
|
+
|
|
18
|
+
The client stays close to the raw API. It has no heavy framework dependencies and uses `requests`.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install zasend
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from zasend import ZaSend
|
|
30
|
+
|
|
31
|
+
client = ZaSend(api_key="sk_live_...")
|
|
32
|
+
|
|
33
|
+
result = client.send_email(
|
|
34
|
+
from_email="zaSend <noreply@zasend.com>",
|
|
35
|
+
to="user@example.com",
|
|
36
|
+
subject="Welcome",
|
|
37
|
+
text="Thanks for signing up.",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
print(result["message_id"])
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
You can use an Account API Key (`sk_live_...`) for all methods. A Domain Sending Key (`dsk_live_...`) is restricted to sending email from one verified domain.
|
|
44
|
+
|
|
45
|
+
## Send HTML Email
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
client.send_email(
|
|
49
|
+
from_email="Acme <noreply@acme.com>",
|
|
50
|
+
to="customer@example.com",
|
|
51
|
+
subject="Your receipt",
|
|
52
|
+
html="<h1>Thanks for your order</h1><p>Your receipt is attached.</p>",
|
|
53
|
+
text="Thanks for your order. Your receipt is attached.",
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Send to Multiple Recipients
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
client.send_email(
|
|
61
|
+
from_email="Acme <noreply@acme.com>",
|
|
62
|
+
to=["one@example.com", "two@example.com"],
|
|
63
|
+
subject="Product update",
|
|
64
|
+
text="A new update is available.",
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
zaSend returns one queued message per `to` recipient. Suppressed recipients reject the whole request so you can fix the list before sending.
|
|
69
|
+
|
|
70
|
+
## Template Send
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
client.send_template_email(
|
|
74
|
+
from_email="Acme <noreply@acme.com>",
|
|
75
|
+
to="customer@example.com",
|
|
76
|
+
template="welcome",
|
|
77
|
+
variables={"name": "Ada"},
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Django Example
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
# settings.py
|
|
85
|
+
ZASEND_API_KEY = "sk_live_..."
|
|
86
|
+
|
|
87
|
+
# anywhere in your app
|
|
88
|
+
from django.conf import settings
|
|
89
|
+
from zasend import ZaSend
|
|
90
|
+
|
|
91
|
+
zasend = ZaSend(settings.ZASEND_API_KEY)
|
|
92
|
+
zasend.send_email(
|
|
93
|
+
from_email="Acme <noreply@acme.com>",
|
|
94
|
+
to=user.email,
|
|
95
|
+
subject="Reset your password",
|
|
96
|
+
text="Use this link to reset your password.",
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Flask or FastAPI Example
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
import os
|
|
104
|
+
from zasend import ZaSend
|
|
105
|
+
|
|
106
|
+
zasend = ZaSend(os.environ["ZASEND_API_KEY"])
|
|
107
|
+
|
|
108
|
+
def send_signup_email(email):
|
|
109
|
+
return zasend.send_template_email(
|
|
110
|
+
from_email="Acme <noreply@acme.com>",
|
|
111
|
+
to=email,
|
|
112
|
+
template="welcome",
|
|
113
|
+
variables={"product": "Acme"},
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Webhook Verification
|
|
118
|
+
|
|
119
|
+
zaSend signs webhook payloads with HMAC-SHA256. Verify the raw request body before trusting webhook data.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from zasend import verify_webhook_signature
|
|
123
|
+
|
|
124
|
+
ok = verify_webhook_signature(
|
|
125
|
+
request.get_data(),
|
|
126
|
+
"whsec_...",
|
|
127
|
+
request.headers.get("X-zaSend-Signature"),
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## API Methods
|
|
132
|
+
|
|
133
|
+
| Method | Description |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| `send_email(**message)` | Send direct content or a template email |
|
|
136
|
+
| `send_template_email(...)` | Convenience wrapper for template sends |
|
|
137
|
+
| `get_email(message_id)` | Fetch email status and events |
|
|
138
|
+
| `get_rate_limits()` | Fetch daily usage and limits |
|
|
139
|
+
| `list_domains()` / `add_domain(domain)` / `verify_domain(id)` / `delete_domain(id)` | Manage sending domains |
|
|
140
|
+
| `list_suppressions(...)` / `add_suppression(email, ...)` / `delete_suppression(id)` | Manage suppression list entries |
|
|
141
|
+
| `list_webhooks()` / `create_webhook(url, events)` / `delete_webhook(id)` | Manage signed delivery webhooks |
|
|
142
|
+
| `list_templates()` / `create_template(...)` / `get_template(id)` / `update_template(id, ...)` / `delete_template(id)` | Manage email templates |
|
|
143
|
+
|
|
144
|
+
## Error Handling
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from zasend import APIError, RateLimitError, ValidationError, ZaSend
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
ZaSend("sk_live_...").send_email(
|
|
151
|
+
from_email="Acme <noreply@acme.com>",
|
|
152
|
+
to="user@example.com",
|
|
153
|
+
subject="Hello",
|
|
154
|
+
text="Body",
|
|
155
|
+
)
|
|
156
|
+
except RateLimitError as exc:
|
|
157
|
+
print("Rate limited:", exc)
|
|
158
|
+
except ValidationError as exc:
|
|
159
|
+
print("Invalid request:", exc)
|
|
160
|
+
except APIError as exc:
|
|
161
|
+
print("zaSend API error:", exc)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Links
|
|
165
|
+
|
|
166
|
+
- Website: [zasend.com](https://zasend.com)
|
|
167
|
+
- API documentation: [zasend.com/docs](https://zasend.com/docs)
|
|
168
|
+
- Source: [GitHub](https://github.com/lr2bmail/firemail-api/tree/master/clients/python)
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zasend"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python transactional email API client for zaSend"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "zaSend", email = "support@zasend.com"},
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"email",
|
|
17
|
+
"transactional-email",
|
|
18
|
+
"email-api",
|
|
19
|
+
"smtp",
|
|
20
|
+
"python-email",
|
|
21
|
+
"django-email",
|
|
22
|
+
"flask-email",
|
|
23
|
+
"fastapi-email",
|
|
24
|
+
"webhooks",
|
|
25
|
+
"dkim",
|
|
26
|
+
"zasend",
|
|
27
|
+
]
|
|
28
|
+
classifiers = [
|
|
29
|
+
"Development Status :: 3 - Alpha",
|
|
30
|
+
"Intended Audience :: Developers",
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
33
|
+
"Programming Language :: Python :: 3.8",
|
|
34
|
+
"Programming Language :: Python :: 3.9",
|
|
35
|
+
"Programming Language :: Python :: 3.10",
|
|
36
|
+
"Programming Language :: Python :: 3.11",
|
|
37
|
+
"Programming Language :: Python :: 3.12",
|
|
38
|
+
"Programming Language :: Python :: 3.13",
|
|
39
|
+
"Topic :: Communications :: Email",
|
|
40
|
+
"Topic :: Communications :: Email :: Mail Transport Agents",
|
|
41
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
42
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
43
|
+
]
|
|
44
|
+
dependencies = [
|
|
45
|
+
"requests>=2.25",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[project.urls]
|
|
49
|
+
Homepage = "https://zasend.com"
|
|
50
|
+
Documentation = "https://zasend.com/docs"
|
|
51
|
+
Repository = "https://github.com/lr2bmail/firemail-api/tree/master/clients/python"
|
|
52
|
+
Issues = "https://github.com/lr2bmail/firemail-api/issues"
|
|
53
|
+
Changelog = "https://github.com/lr2bmail/firemail-api/commits/master/clients/python"
|
|
54
|
+
|
|
55
|
+
[tool.setuptools.packages.find]
|
|
56
|
+
include = ["zasend*"]
|
zasend-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from zasend import APIError, RateLimitError, ValidationError, ZaSend, ZaSendError, verify_webhook_signature
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Response:
|
|
10
|
+
def __init__(self, body=None, status_code=200, ok=True, reason="OK"):
|
|
11
|
+
self._body = body
|
|
12
|
+
self.status_code = status_code
|
|
13
|
+
self.ok = ok
|
|
14
|
+
self.reason = reason
|
|
15
|
+
|
|
16
|
+
def json(self):
|
|
17
|
+
if isinstance(self._body, Exception):
|
|
18
|
+
raise self._body
|
|
19
|
+
return self._body
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Session:
|
|
23
|
+
def __init__(self, response):
|
|
24
|
+
self.response = response
|
|
25
|
+
self.calls = []
|
|
26
|
+
|
|
27
|
+
def request(self, *args, **kwargs):
|
|
28
|
+
self.calls.append((args, kwargs))
|
|
29
|
+
return self.response
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_send_email_posts_json_with_bearer_auth():
|
|
33
|
+
session = Session(Response({"success": True, "message_id": "msg_123"}))
|
|
34
|
+
client = ZaSend("key", base_url="https://example.test/api/v1", session=session)
|
|
35
|
+
|
|
36
|
+
result = client.send_email(
|
|
37
|
+
**{
|
|
38
|
+
"from": "noreply@example.com",
|
|
39
|
+
"to": "user@example.com",
|
|
40
|
+
"subject": "Hello",
|
|
41
|
+
"text": "Body",
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
args, kwargs = session.calls[0]
|
|
46
|
+
assert args == ("POST", "https://example.test/api/v1/emails/send")
|
|
47
|
+
assert kwargs["headers"]["Authorization"] == "Bearer key"
|
|
48
|
+
assert kwargs["json"]["subject"] == "Hello"
|
|
49
|
+
assert result["message_id"] == "msg_123"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_list_suppressions_sends_query_parameters():
|
|
53
|
+
session = Session(Response({"suppressions": []}))
|
|
54
|
+
client = ZaSend("key", base_url="https://example.test/api/v1", session=session)
|
|
55
|
+
|
|
56
|
+
client.list_suppressions(page=2, per_page=100, reason="bounce")
|
|
57
|
+
|
|
58
|
+
_, kwargs = session.calls[0]
|
|
59
|
+
assert kwargs["params"] == {"page": 2, "per_page": 100, "reason": "bounce"}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_typed_errors():
|
|
63
|
+
with pytest.raises(RateLimitError):
|
|
64
|
+
ZaSend("key", session=Session(Response({"message": "slow down"}, 429, False))).get_rate_limits()
|
|
65
|
+
|
|
66
|
+
with pytest.raises(ValidationError):
|
|
67
|
+
ZaSend("key", session=Session(Response({"message": "bad email"}, 422, False))).send_email()
|
|
68
|
+
|
|
69
|
+
with pytest.raises(APIError):
|
|
70
|
+
ZaSend("key", session=Session(Response({"message": "missing"}, 404, False))).get_email("missing")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_delete_methods_return_none_for_204():
|
|
74
|
+
client = ZaSend("key", session=Session(Response(None, 204, True)))
|
|
75
|
+
|
|
76
|
+
assert client.delete_webhook("hook_123") is None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_webhook_signature_verification_accepts_valid_signature():
|
|
80
|
+
body = b'{"event":"accepted_by_postfix"}'
|
|
81
|
+
secret = "secret"
|
|
82
|
+
signature = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
|
|
83
|
+
|
|
84
|
+
assert verify_webhook_signature(body, secret, "sha256={0}".format(signature))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_client_errors_still_behave_like_value_error():
|
|
88
|
+
assert issubclass(APIError, ZaSendError)
|
|
89
|
+
assert issubclass(RateLimitError, APIError)
|
|
90
|
+
assert issubclass(ValidationError, APIError)
|
|
91
|
+
assert issubclass(ZaSendError, ValueError)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .client import APIError, RateLimitError, ValidationError, ZaSend, ZaSendError, verify_webhook_signature
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"APIError",
|
|
5
|
+
"RateLimitError",
|
|
6
|
+
"ValidationError",
|
|
7
|
+
"ZaSend",
|
|
8
|
+
"ZaSendError",
|
|
9
|
+
"verify_webhook_signature",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
DEFAULT_BASE_URL = "https://zasend.com/api/v1"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ZaSendError(ValueError):
|
|
11
|
+
"""Base exception for zaSend client errors."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class APIError(ZaSendError):
|
|
15
|
+
"""Raised when the zaSend API returns an error response."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, message, status_code=None, body=None):
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.status_code = status_code
|
|
20
|
+
self.body = body
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RateLimitError(APIError):
|
|
24
|
+
"""Raised when the API returns 429."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ValidationError(APIError):
|
|
28
|
+
"""Raised when the API returns a validation-style error."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ZaSend:
|
|
32
|
+
"""Lightweight client for the zaSend API."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, api_key, base_url=None, timeout=10, session=None):
|
|
35
|
+
if not api_key:
|
|
36
|
+
raise ZaSendError("API key is required")
|
|
37
|
+
self.api_key = api_key
|
|
38
|
+
self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
|
|
39
|
+
self.timeout = timeout
|
|
40
|
+
self._session = session or requests.Session()
|
|
41
|
+
|
|
42
|
+
def send_email(self, **message):
|
|
43
|
+
if "from_email" in message and "from" not in message:
|
|
44
|
+
message["from"] = message.pop("from_email")
|
|
45
|
+
return self._request("POST", "/emails/send", json=message)
|
|
46
|
+
|
|
47
|
+
def send_template_email(
|
|
48
|
+
self,
|
|
49
|
+
from_email,
|
|
50
|
+
to,
|
|
51
|
+
template,
|
|
52
|
+
variables=None,
|
|
53
|
+
cc=None,
|
|
54
|
+
bcc=None,
|
|
55
|
+
list_unsubscribe=None,
|
|
56
|
+
list_unsubscribe_post=None,
|
|
57
|
+
):
|
|
58
|
+
return self.send_email(
|
|
59
|
+
**{
|
|
60
|
+
"from": from_email,
|
|
61
|
+
"to": to,
|
|
62
|
+
"template": template,
|
|
63
|
+
"variables": variables or {},
|
|
64
|
+
"cc": cc,
|
|
65
|
+
"bcc": bcc,
|
|
66
|
+
"list_unsubscribe": list_unsubscribe,
|
|
67
|
+
"list_unsubscribe_post": list_unsubscribe_post,
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def get_email(self, message_id):
|
|
72
|
+
return self._request("GET", "/emails/{0}".format(message_id))
|
|
73
|
+
|
|
74
|
+
def get_rate_limits(self):
|
|
75
|
+
return self._request("GET", "/rate-limits")
|
|
76
|
+
|
|
77
|
+
def list_domains(self):
|
|
78
|
+
return self._request("GET", "/domains")
|
|
79
|
+
|
|
80
|
+
def add_domain(self, domain):
|
|
81
|
+
return self._request("POST", "/domains", json={"domain": domain})
|
|
82
|
+
|
|
83
|
+
def verify_domain(self, domain_id):
|
|
84
|
+
return self._request("POST", "/domains/{0}/verify".format(domain_id))
|
|
85
|
+
|
|
86
|
+
def delete_domain(self, domain_id):
|
|
87
|
+
return self._request("DELETE", "/domains/{0}".format(domain_id))
|
|
88
|
+
|
|
89
|
+
def list_suppressions(self, page=None, per_page=None, reason=None):
|
|
90
|
+
return self._request(
|
|
91
|
+
"GET",
|
|
92
|
+
"/suppressions",
|
|
93
|
+
params={"page": page, "per_page": per_page, "reason": reason},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def add_suppression(self, email, reason="manual", details=None):
|
|
97
|
+
return self._request(
|
|
98
|
+
"POST",
|
|
99
|
+
"/suppressions",
|
|
100
|
+
json={"email": email, "reason": reason, "details": details},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def delete_suppression(self, suppression_id):
|
|
104
|
+
return self._request("DELETE", "/suppressions/{0}".format(suppression_id))
|
|
105
|
+
|
|
106
|
+
def list_webhooks(self):
|
|
107
|
+
return self._request("GET", "/webhooks")
|
|
108
|
+
|
|
109
|
+
def create_webhook(self, url, events):
|
|
110
|
+
return self._request("POST", "/webhooks", json={"url": url, "events": events})
|
|
111
|
+
|
|
112
|
+
def delete_webhook(self, webhook_id):
|
|
113
|
+
return self._request("DELETE", "/webhooks/{0}".format(webhook_id))
|
|
114
|
+
|
|
115
|
+
def list_templates(self):
|
|
116
|
+
return self._request("GET", "/templates")
|
|
117
|
+
|
|
118
|
+
def create_template(self, **template):
|
|
119
|
+
return self._request("POST", "/templates", json=template)
|
|
120
|
+
|
|
121
|
+
def get_template(self, template_id):
|
|
122
|
+
return self._request("GET", "/templates/{0}".format(template_id))
|
|
123
|
+
|
|
124
|
+
def update_template(self, template_id, **template):
|
|
125
|
+
return self._request("PUT", "/templates/{0}".format(template_id), json=template)
|
|
126
|
+
|
|
127
|
+
def delete_template(self, template_id):
|
|
128
|
+
return self._request("DELETE", "/templates/{0}".format(template_id))
|
|
129
|
+
|
|
130
|
+
def _request(self, method, path, json=None, params=None):
|
|
131
|
+
headers = {
|
|
132
|
+
"Authorization": "Bearer {0}".format(self.api_key),
|
|
133
|
+
"Accept": "application/json",
|
|
134
|
+
}
|
|
135
|
+
clean_params = {k: v for k, v in (params or {}).items() if v is not None}
|
|
136
|
+
response = self._session.request(
|
|
137
|
+
method,
|
|
138
|
+
"{0}{1}".format(self.base_url, path),
|
|
139
|
+
headers=headers,
|
|
140
|
+
json={k: v for k, v in (json or {}).items() if v is not None} if json is not None else None,
|
|
141
|
+
params=clean_params or None,
|
|
142
|
+
timeout=self.timeout,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if response.status_code == 204:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
data = response.json()
|
|
150
|
+
except ValueError as exc:
|
|
151
|
+
if response.ok:
|
|
152
|
+
raise APIError("API returned invalid JSON", response.status_code) from exc
|
|
153
|
+
data = None
|
|
154
|
+
|
|
155
|
+
if not response.ok:
|
|
156
|
+
message = data.get("message") if isinstance(data, dict) else response.reason
|
|
157
|
+
if response.status_code == 429:
|
|
158
|
+
raise RateLimitError(message, response.status_code, data)
|
|
159
|
+
if response.status_code in (400, 422):
|
|
160
|
+
raise ValidationError(message, response.status_code, data)
|
|
161
|
+
raise APIError("API error {0}: {1}".format(response.status_code, message), response.status_code, data)
|
|
162
|
+
|
|
163
|
+
return data
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def verify_webhook_signature(payload_body, secret, signature_header):
|
|
167
|
+
received = (signature_header or "").replace("sha256=", "", 1)
|
|
168
|
+
if isinstance(payload_body, str):
|
|
169
|
+
payload_body = payload_body.encode()
|
|
170
|
+
expected = hmac.new(secret.encode(), payload_body, hashlib.sha256).hexdigest()
|
|
171
|
+
return hmac.compare_digest(expected, received)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zasend
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python transactional email API client for zaSend
|
|
5
|
+
Author-email: zaSend <support@zasend.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://zasend.com
|
|
8
|
+
Project-URL: Documentation, https://zasend.com/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/lr2bmail/firemail-api/tree/master/clients/python
|
|
10
|
+
Project-URL: Issues, https://github.com/lr2bmail/firemail-api/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/lr2bmail/firemail-api/commits/master/clients/python
|
|
12
|
+
Keywords: email,transactional-email,email-api,smtp,python-email,django-email,flask-email,fastapi-email,webhooks,dkim,zasend
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Communications :: Email
|
|
24
|
+
Classifier: Topic :: Communications :: Email :: Mail Transport Agents
|
|
25
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Requires-Python: >=3.8
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: requests>=2.25
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# zasend
|
|
34
|
+
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
[](pyproject.toml)
|
|
37
|
+
|
|
38
|
+
Official Python client for the [zaSend](https://zasend.com) transactional email API.
|
|
39
|
+
|
|
40
|
+
zaSend is a developer-focused email delivery service for sending product, auth, billing, notification, and lifecycle emails from your application. This package is a small wrapper around the zaSend REST API for Python apps, Django, Flask, FastAPI, background jobs, and scripts.
|
|
41
|
+
|
|
42
|
+
Use it to:
|
|
43
|
+
|
|
44
|
+
- Send transactional email from verified domains
|
|
45
|
+
- Send template-based email with variables
|
|
46
|
+
- Check message status and events
|
|
47
|
+
- Manage suppressions, webhooks, templates, and domains
|
|
48
|
+
- Verify zaSend webhook signatures
|
|
49
|
+
|
|
50
|
+
The client stays close to the raw API. It has no heavy framework dependencies and uses `requests`.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install zasend
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from zasend import ZaSend
|
|
62
|
+
|
|
63
|
+
client = ZaSend(api_key="sk_live_...")
|
|
64
|
+
|
|
65
|
+
result = client.send_email(
|
|
66
|
+
from_email="zaSend <noreply@zasend.com>",
|
|
67
|
+
to="user@example.com",
|
|
68
|
+
subject="Welcome",
|
|
69
|
+
text="Thanks for signing up.",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
print(result["message_id"])
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
You can use an Account API Key (`sk_live_...`) for all methods. A Domain Sending Key (`dsk_live_...`) is restricted to sending email from one verified domain.
|
|
76
|
+
|
|
77
|
+
## Send HTML Email
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
client.send_email(
|
|
81
|
+
from_email="Acme <noreply@acme.com>",
|
|
82
|
+
to="customer@example.com",
|
|
83
|
+
subject="Your receipt",
|
|
84
|
+
html="<h1>Thanks for your order</h1><p>Your receipt is attached.</p>",
|
|
85
|
+
text="Thanks for your order. Your receipt is attached.",
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Send to Multiple Recipients
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
client.send_email(
|
|
93
|
+
from_email="Acme <noreply@acme.com>",
|
|
94
|
+
to=["one@example.com", "two@example.com"],
|
|
95
|
+
subject="Product update",
|
|
96
|
+
text="A new update is available.",
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
zaSend returns one queued message per `to` recipient. Suppressed recipients reject the whole request so you can fix the list before sending.
|
|
101
|
+
|
|
102
|
+
## Template Send
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
client.send_template_email(
|
|
106
|
+
from_email="Acme <noreply@acme.com>",
|
|
107
|
+
to="customer@example.com",
|
|
108
|
+
template="welcome",
|
|
109
|
+
variables={"name": "Ada"},
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Django Example
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
# settings.py
|
|
117
|
+
ZASEND_API_KEY = "sk_live_..."
|
|
118
|
+
|
|
119
|
+
# anywhere in your app
|
|
120
|
+
from django.conf import settings
|
|
121
|
+
from zasend import ZaSend
|
|
122
|
+
|
|
123
|
+
zasend = ZaSend(settings.ZASEND_API_KEY)
|
|
124
|
+
zasend.send_email(
|
|
125
|
+
from_email="Acme <noreply@acme.com>",
|
|
126
|
+
to=user.email,
|
|
127
|
+
subject="Reset your password",
|
|
128
|
+
text="Use this link to reset your password.",
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Flask or FastAPI Example
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import os
|
|
136
|
+
from zasend import ZaSend
|
|
137
|
+
|
|
138
|
+
zasend = ZaSend(os.environ["ZASEND_API_KEY"])
|
|
139
|
+
|
|
140
|
+
def send_signup_email(email):
|
|
141
|
+
return zasend.send_template_email(
|
|
142
|
+
from_email="Acme <noreply@acme.com>",
|
|
143
|
+
to=email,
|
|
144
|
+
template="welcome",
|
|
145
|
+
variables={"product": "Acme"},
|
|
146
|
+
)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Webhook Verification
|
|
150
|
+
|
|
151
|
+
zaSend signs webhook payloads with HMAC-SHA256. Verify the raw request body before trusting webhook data.
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from zasend import verify_webhook_signature
|
|
155
|
+
|
|
156
|
+
ok = verify_webhook_signature(
|
|
157
|
+
request.get_data(),
|
|
158
|
+
"whsec_...",
|
|
159
|
+
request.headers.get("X-zaSend-Signature"),
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## API Methods
|
|
164
|
+
|
|
165
|
+
| Method | Description |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| `send_email(**message)` | Send direct content or a template email |
|
|
168
|
+
| `send_template_email(...)` | Convenience wrapper for template sends |
|
|
169
|
+
| `get_email(message_id)` | Fetch email status and events |
|
|
170
|
+
| `get_rate_limits()` | Fetch daily usage and limits |
|
|
171
|
+
| `list_domains()` / `add_domain(domain)` / `verify_domain(id)` / `delete_domain(id)` | Manage sending domains |
|
|
172
|
+
| `list_suppressions(...)` / `add_suppression(email, ...)` / `delete_suppression(id)` | Manage suppression list entries |
|
|
173
|
+
| `list_webhooks()` / `create_webhook(url, events)` / `delete_webhook(id)` | Manage signed delivery webhooks |
|
|
174
|
+
| `list_templates()` / `create_template(...)` / `get_template(id)` / `update_template(id, ...)` / `delete_template(id)` | Manage email templates |
|
|
175
|
+
|
|
176
|
+
## Error Handling
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from zasend import APIError, RateLimitError, ValidationError, ZaSend
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
ZaSend("sk_live_...").send_email(
|
|
183
|
+
from_email="Acme <noreply@acme.com>",
|
|
184
|
+
to="user@example.com",
|
|
185
|
+
subject="Hello",
|
|
186
|
+
text="Body",
|
|
187
|
+
)
|
|
188
|
+
except RateLimitError as exc:
|
|
189
|
+
print("Rate limited:", exc)
|
|
190
|
+
except ValidationError as exc:
|
|
191
|
+
print("Invalid request:", exc)
|
|
192
|
+
except APIError as exc:
|
|
193
|
+
print("zaSend API error:", exc)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Links
|
|
197
|
+
|
|
198
|
+
- Website: [zasend.com](https://zasend.com)
|
|
199
|
+
- API documentation: [zasend.com/docs](https://zasend.com/docs)
|
|
200
|
+
- Source: [GitHub](https://github.com/lr2bmail/firemail-api/tree/master/clients/python)
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
tests/test_client.py
|
|
5
|
+
zasend/__init__.py
|
|
6
|
+
zasend/client.py
|
|
7
|
+
zasend.egg-info/PKG-INFO
|
|
8
|
+
zasend.egg-info/SOURCES.txt
|
|
9
|
+
zasend.egg-info/dependency_links.txt
|
|
10
|
+
zasend.egg-info/requires.txt
|
|
11
|
+
zasend.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.25
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
zasend
|