tosend 1.0.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.
- tosend-1.0.0/LICENSE +21 -0
- tosend-1.0.0/PKG-INFO +239 -0
- tosend-1.0.0/README.md +211 -0
- tosend-1.0.0/pyproject.toml +42 -0
- tosend-1.0.0/setup.cfg +4 -0
- tosend-1.0.0/tosend/__init__.py +34 -0
- tosend-1.0.0/tosend/client.py +201 -0
- tosend-1.0.0/tosend/exceptions.py +47 -0
- tosend-1.0.0/tosend/py.typed +0 -0
- tosend-1.0.0/tosend/types.py +154 -0
- tosend-1.0.0/tosend.egg-info/PKG-INFO +239 -0
- tosend-1.0.0/tosend.egg-info/SOURCES.txt +12 -0
- tosend-1.0.0/tosend.egg-info/dependency_links.txt +1 -0
- tosend-1.0.0/tosend.egg-info/top_level.txt +1 -0
tosend-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 ToSend
|
|
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.
|
tosend-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tosend
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the ToSend email API
|
|
5
|
+
Author-email: ToSend <support@tosend.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://tosend.com
|
|
8
|
+
Project-URL: Documentation, https://tosend.com/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/tosend/tosend-python
|
|
10
|
+
Project-URL: Issues, https://github.com/tosend/tosend-python/issues
|
|
11
|
+
Keywords: email,api,tosend,transactional,mail
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
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: Topic :: Communications :: Email
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# ToSend Python SDK
|
|
30
|
+
|
|
31
|
+
Official Python SDK for the [ToSend](https://tosend.com) email API.
|
|
32
|
+
|
|
33
|
+
[](https://badge.fury.io/py/tosend)
|
|
34
|
+
[](https://pypi.org/project/tosend/)
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install tosend
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
|
|
44
|
+
See the [examples](examples) folder for complete working examples:
|
|
45
|
+
|
|
46
|
+
- [Send Email](examples/send_email.py) - Send a single email
|
|
47
|
+
- [Batch Send](examples/batch_send.py) - Send multiple emails in one request
|
|
48
|
+
- [Account Info](examples/account_info.py) - Get account information and domains
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
export TOSEND_API_KEY=tsend_your_api_key
|
|
52
|
+
python examples/send_email.py
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from tosend import ToSend
|
|
59
|
+
|
|
60
|
+
client = ToSend("tsend_your_api_key")
|
|
61
|
+
|
|
62
|
+
response = client.send(
|
|
63
|
+
from_address={"email": "hello@yourdomain.com", "name": "Your App"},
|
|
64
|
+
to=[{"email": "user@example.com"}],
|
|
65
|
+
subject="Welcome!",
|
|
66
|
+
html="<h1>Hello!</h1><p>Thanks for signing up.</p>",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
print(response.message_id)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
### Send an Email
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
response = client.send(
|
|
78
|
+
from_address={"email": "hello@yourdomain.com", "name": "Your App"},
|
|
79
|
+
to=[
|
|
80
|
+
{"email": "user@example.com"},
|
|
81
|
+
{"email": "another@example.com", "name": "John Doe"},
|
|
82
|
+
],
|
|
83
|
+
subject="Hello!",
|
|
84
|
+
html="<h1>Welcome</h1>",
|
|
85
|
+
text="Welcome", # optional
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### With CC, BCC, and Reply-To
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
response = client.send(
|
|
93
|
+
from_address={"email": "hello@yourdomain.com"},
|
|
94
|
+
to=[{"email": "user@example.com"}],
|
|
95
|
+
cc=[{"email": "cc@example.com"}],
|
|
96
|
+
bcc=[{"email": "bcc@example.com"}],
|
|
97
|
+
reply_to={"email": "support@yourdomain.com", "name": "Support"},
|
|
98
|
+
subject="Hello",
|
|
99
|
+
html="<p>Hello World</p>",
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### With Attachments
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import base64
|
|
107
|
+
|
|
108
|
+
with open("invoice.pdf", "rb") as f:
|
|
109
|
+
content = base64.b64encode(f.read()).decode()
|
|
110
|
+
|
|
111
|
+
response = client.send(
|
|
112
|
+
from_address={"email": "hello@yourdomain.com"},
|
|
113
|
+
to=[{"email": "user@example.com"}],
|
|
114
|
+
subject="Your Invoice",
|
|
115
|
+
html="<p>Please find your invoice attached.</p>",
|
|
116
|
+
attachments=[
|
|
117
|
+
{
|
|
118
|
+
"type": "application/pdf",
|
|
119
|
+
"name": "invoice.pdf",
|
|
120
|
+
"content": content,
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Batch Sending
|
|
127
|
+
|
|
128
|
+
Send multiple emails in a single request:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
response = client.batch([
|
|
132
|
+
{
|
|
133
|
+
"from": {"email": "hello@yourdomain.com"},
|
|
134
|
+
"to": [{"email": "user1@example.com"}],
|
|
135
|
+
"subject": "Hello User 1",
|
|
136
|
+
"html": "<p>Welcome!</p>",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"from": {"email": "hello@yourdomain.com"},
|
|
140
|
+
"to": [{"email": "user2@example.com"}],
|
|
141
|
+
"subject": "Hello User 2",
|
|
142
|
+
"html": "<p>Welcome!</p>",
|
|
143
|
+
},
|
|
144
|
+
])
|
|
145
|
+
|
|
146
|
+
for result in response.results:
|
|
147
|
+
if result.status == "success":
|
|
148
|
+
print(f"Sent: {result.message_id}")
|
|
149
|
+
else:
|
|
150
|
+
print(f"Failed: {result.message}")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Get Account Info
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
info = client.get_account_info()
|
|
157
|
+
|
|
158
|
+
print(info.account.title)
|
|
159
|
+
print(info.account.emails_usage_this_month)
|
|
160
|
+
print(info.account.emails_sent_last_24hrs)
|
|
161
|
+
|
|
162
|
+
for domain in info.domains:
|
|
163
|
+
print(f"{domain.domain_name}: {domain.verification_status}")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Configuration
|
|
167
|
+
|
|
168
|
+
### With Options
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
client = ToSend(
|
|
172
|
+
api_key="tsend_your_api_key",
|
|
173
|
+
base_url="https://api.tosend.com", # optional
|
|
174
|
+
timeout=60, # optional, in seconds
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Error Handling
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from tosend import ToSend, ToSendError
|
|
182
|
+
|
|
183
|
+
client = ToSend("tsend_your_api_key")
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
response = client.send(
|
|
187
|
+
from_address={"email": "hello@yourdomain.com"},
|
|
188
|
+
to=[{"email": "user@example.com"}],
|
|
189
|
+
subject="Hello",
|
|
190
|
+
html="<p>Hello</p>",
|
|
191
|
+
)
|
|
192
|
+
except ToSendError as e:
|
|
193
|
+
print(f"Error: {e.message}")
|
|
194
|
+
print(f"Status: {e.status_code}")
|
|
195
|
+
print(f"Type: {e.error_type}")
|
|
196
|
+
print(f"Details: {e.errors}")
|
|
197
|
+
|
|
198
|
+
if e.is_validation_error:
|
|
199
|
+
# Handle validation error (422)
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
if e.is_authentication_error:
|
|
203
|
+
# Handle auth error (401/403)
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
if e.is_rate_limit_error:
|
|
207
|
+
# Handle rate limit (429)
|
|
208
|
+
pass
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Type Hints
|
|
212
|
+
|
|
213
|
+
The SDK includes full type hints. You can also use the provided dataclasses:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from tosend import ToSend, Address, SendEmailRequest
|
|
217
|
+
|
|
218
|
+
client = ToSend("tsend_your_api_key")
|
|
219
|
+
|
|
220
|
+
# Using Address dataclass
|
|
221
|
+
from_addr = Address(email="hello@yourdomain.com", name="Your App")
|
|
222
|
+
to_addr = Address(email="user@example.com")
|
|
223
|
+
|
|
224
|
+
response = client.send(
|
|
225
|
+
from_address=from_addr,
|
|
226
|
+
to=[to_addr],
|
|
227
|
+
subject="Hello!",
|
|
228
|
+
html="<h1>Welcome!</h1>",
|
|
229
|
+
)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Requirements
|
|
233
|
+
|
|
234
|
+
- Python 3.8 or higher
|
|
235
|
+
- No external dependencies (uses only standard library)
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT
|
tosend-1.0.0/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# ToSend Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [ToSend](https://tosend.com) email API.
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/py/tosend)
|
|
6
|
+
[](https://pypi.org/project/tosend/)
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install tosend
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Examples
|
|
15
|
+
|
|
16
|
+
See the [examples](examples) folder for complete working examples:
|
|
17
|
+
|
|
18
|
+
- [Send Email](examples/send_email.py) - Send a single email
|
|
19
|
+
- [Batch Send](examples/batch_send.py) - Send multiple emails in one request
|
|
20
|
+
- [Account Info](examples/account_info.py) - Get account information and domains
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
export TOSEND_API_KEY=tsend_your_api_key
|
|
24
|
+
python examples/send_email.py
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from tosend import ToSend
|
|
31
|
+
|
|
32
|
+
client = ToSend("tsend_your_api_key")
|
|
33
|
+
|
|
34
|
+
response = client.send(
|
|
35
|
+
from_address={"email": "hello@yourdomain.com", "name": "Your App"},
|
|
36
|
+
to=[{"email": "user@example.com"}],
|
|
37
|
+
subject="Welcome!",
|
|
38
|
+
html="<h1>Hello!</h1><p>Thanks for signing up.</p>",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
print(response.message_id)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
### Send an Email
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
response = client.send(
|
|
50
|
+
from_address={"email": "hello@yourdomain.com", "name": "Your App"},
|
|
51
|
+
to=[
|
|
52
|
+
{"email": "user@example.com"},
|
|
53
|
+
{"email": "another@example.com", "name": "John Doe"},
|
|
54
|
+
],
|
|
55
|
+
subject="Hello!",
|
|
56
|
+
html="<h1>Welcome</h1>",
|
|
57
|
+
text="Welcome", # optional
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### With CC, BCC, and Reply-To
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
response = client.send(
|
|
65
|
+
from_address={"email": "hello@yourdomain.com"},
|
|
66
|
+
to=[{"email": "user@example.com"}],
|
|
67
|
+
cc=[{"email": "cc@example.com"}],
|
|
68
|
+
bcc=[{"email": "bcc@example.com"}],
|
|
69
|
+
reply_to={"email": "support@yourdomain.com", "name": "Support"},
|
|
70
|
+
subject="Hello",
|
|
71
|
+
html="<p>Hello World</p>",
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### With Attachments
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import base64
|
|
79
|
+
|
|
80
|
+
with open("invoice.pdf", "rb") as f:
|
|
81
|
+
content = base64.b64encode(f.read()).decode()
|
|
82
|
+
|
|
83
|
+
response = client.send(
|
|
84
|
+
from_address={"email": "hello@yourdomain.com"},
|
|
85
|
+
to=[{"email": "user@example.com"}],
|
|
86
|
+
subject="Your Invoice",
|
|
87
|
+
html="<p>Please find your invoice attached.</p>",
|
|
88
|
+
attachments=[
|
|
89
|
+
{
|
|
90
|
+
"type": "application/pdf",
|
|
91
|
+
"name": "invoice.pdf",
|
|
92
|
+
"content": content,
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Batch Sending
|
|
99
|
+
|
|
100
|
+
Send multiple emails in a single request:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
response = client.batch([
|
|
104
|
+
{
|
|
105
|
+
"from": {"email": "hello@yourdomain.com"},
|
|
106
|
+
"to": [{"email": "user1@example.com"}],
|
|
107
|
+
"subject": "Hello User 1",
|
|
108
|
+
"html": "<p>Welcome!</p>",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"from": {"email": "hello@yourdomain.com"},
|
|
112
|
+
"to": [{"email": "user2@example.com"}],
|
|
113
|
+
"subject": "Hello User 2",
|
|
114
|
+
"html": "<p>Welcome!</p>",
|
|
115
|
+
},
|
|
116
|
+
])
|
|
117
|
+
|
|
118
|
+
for result in response.results:
|
|
119
|
+
if result.status == "success":
|
|
120
|
+
print(f"Sent: {result.message_id}")
|
|
121
|
+
else:
|
|
122
|
+
print(f"Failed: {result.message}")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Get Account Info
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
info = client.get_account_info()
|
|
129
|
+
|
|
130
|
+
print(info.account.title)
|
|
131
|
+
print(info.account.emails_usage_this_month)
|
|
132
|
+
print(info.account.emails_sent_last_24hrs)
|
|
133
|
+
|
|
134
|
+
for domain in info.domains:
|
|
135
|
+
print(f"{domain.domain_name}: {domain.verification_status}")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Configuration
|
|
139
|
+
|
|
140
|
+
### With Options
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
client = ToSend(
|
|
144
|
+
api_key="tsend_your_api_key",
|
|
145
|
+
base_url="https://api.tosend.com", # optional
|
|
146
|
+
timeout=60, # optional, in seconds
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Error Handling
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from tosend import ToSend, ToSendError
|
|
154
|
+
|
|
155
|
+
client = ToSend("tsend_your_api_key")
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
response = client.send(
|
|
159
|
+
from_address={"email": "hello@yourdomain.com"},
|
|
160
|
+
to=[{"email": "user@example.com"}],
|
|
161
|
+
subject="Hello",
|
|
162
|
+
html="<p>Hello</p>",
|
|
163
|
+
)
|
|
164
|
+
except ToSendError as e:
|
|
165
|
+
print(f"Error: {e.message}")
|
|
166
|
+
print(f"Status: {e.status_code}")
|
|
167
|
+
print(f"Type: {e.error_type}")
|
|
168
|
+
print(f"Details: {e.errors}")
|
|
169
|
+
|
|
170
|
+
if e.is_validation_error:
|
|
171
|
+
# Handle validation error (422)
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
if e.is_authentication_error:
|
|
175
|
+
# Handle auth error (401/403)
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
if e.is_rate_limit_error:
|
|
179
|
+
# Handle rate limit (429)
|
|
180
|
+
pass
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Type Hints
|
|
184
|
+
|
|
185
|
+
The SDK includes full type hints. You can also use the provided dataclasses:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from tosend import ToSend, Address, SendEmailRequest
|
|
189
|
+
|
|
190
|
+
client = ToSend("tsend_your_api_key")
|
|
191
|
+
|
|
192
|
+
# Using Address dataclass
|
|
193
|
+
from_addr = Address(email="hello@yourdomain.com", name="Your App")
|
|
194
|
+
to_addr = Address(email="user@example.com")
|
|
195
|
+
|
|
196
|
+
response = client.send(
|
|
197
|
+
from_address=from_addr,
|
|
198
|
+
to=[to_addr],
|
|
199
|
+
subject="Hello!",
|
|
200
|
+
html="<h1>Welcome!</h1>",
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Requirements
|
|
205
|
+
|
|
206
|
+
- Python 3.8 or higher
|
|
207
|
+
- No external dependencies (uses only standard library)
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tosend"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official Python SDK for the ToSend email API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "ToSend", email = "support@tosend.com"}
|
|
13
|
+
]
|
|
14
|
+
keywords = ["email", "api", "tosend", "transactional", "mail"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Communications :: Email",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
requires-python = ">=3.8"
|
|
30
|
+
dependencies = []
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://tosend.com"
|
|
34
|
+
Documentation = "https://tosend.com/docs"
|
|
35
|
+
Repository = "https://github.com/tosend/tosend-python"
|
|
36
|
+
Issues = "https://github.com/tosend/tosend-python/issues"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
include = ["tosend*"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
tosend = ["py.typed"]
|
tosend-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ToSend Python SDK
|
|
3
|
+
|
|
4
|
+
Official Python SDK for the ToSend email API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .client import ToSend
|
|
8
|
+
from .exceptions import ToSendError
|
|
9
|
+
from .types import (
|
|
10
|
+
Address,
|
|
11
|
+
Attachment,
|
|
12
|
+
SendEmailRequest,
|
|
13
|
+
SendEmailResponse,
|
|
14
|
+
BatchEmailResult,
|
|
15
|
+
BatchEmailResponse,
|
|
16
|
+
Domain,
|
|
17
|
+
Account,
|
|
18
|
+
AccountInfo,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__version__ = "1.0.0"
|
|
22
|
+
__all__ = [
|
|
23
|
+
"ToSend",
|
|
24
|
+
"ToSendError",
|
|
25
|
+
"Address",
|
|
26
|
+
"Attachment",
|
|
27
|
+
"SendEmailRequest",
|
|
28
|
+
"SendEmailResponse",
|
|
29
|
+
"BatchEmailResult",
|
|
30
|
+
"BatchEmailResponse",
|
|
31
|
+
"Domain",
|
|
32
|
+
"Account",
|
|
33
|
+
"AccountInfo",
|
|
34
|
+
]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""ToSend API client."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional, Dict, Any, List, Union
|
|
5
|
+
from urllib.request import Request, urlopen
|
|
6
|
+
from urllib.error import HTTPError, URLError
|
|
7
|
+
|
|
8
|
+
from .types import (
|
|
9
|
+
Address,
|
|
10
|
+
SendEmailRequest,
|
|
11
|
+
SendEmailResponse,
|
|
12
|
+
BatchEmailResponse,
|
|
13
|
+
AccountInfo,
|
|
14
|
+
)
|
|
15
|
+
from .exceptions import ToSendError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ToSend:
|
|
19
|
+
"""ToSend API client."""
|
|
20
|
+
|
|
21
|
+
DEFAULT_BASE_URL = "https://api.tosend.com"
|
|
22
|
+
DEFAULT_TIMEOUT = 30
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
api_key: str,
|
|
27
|
+
base_url: Optional[str] = None,
|
|
28
|
+
timeout: Optional[int] = None,
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Initialize ToSend client.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
api_key: Your ToSend API key
|
|
35
|
+
base_url: Custom base URL (optional)
|
|
36
|
+
timeout: Request timeout in seconds (optional)
|
|
37
|
+
"""
|
|
38
|
+
if not api_key or not api_key.strip():
|
|
39
|
+
raise ToSendError("API key is required", 401, "unauthorized")
|
|
40
|
+
|
|
41
|
+
self.api_key = api_key.strip()
|
|
42
|
+
self.base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")
|
|
43
|
+
self.timeout = timeout or self.DEFAULT_TIMEOUT
|
|
44
|
+
|
|
45
|
+
def send(
|
|
46
|
+
self,
|
|
47
|
+
*,
|
|
48
|
+
from_address: Union[Address, Dict[str, str]],
|
|
49
|
+
to: List[Union[Address, Dict[str, str]]],
|
|
50
|
+
subject: str,
|
|
51
|
+
html: Optional[str] = None,
|
|
52
|
+
text: Optional[str] = None,
|
|
53
|
+
cc: Optional[List[Union[Address, Dict[str, str]]]] = None,
|
|
54
|
+
bcc: Optional[List[Union[Address, Dict[str, str]]]] = None,
|
|
55
|
+
reply_to: Optional[Union[Address, Dict[str, str]]] = None,
|
|
56
|
+
headers: Optional[Dict[str, str]] = None,
|
|
57
|
+
attachments: Optional[List[Dict[str, str]]] = None,
|
|
58
|
+
) -> SendEmailResponse:
|
|
59
|
+
"""
|
|
60
|
+
Send a single email.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
from_address: Sender address
|
|
64
|
+
to: List of recipient addresses
|
|
65
|
+
subject: Email subject
|
|
66
|
+
html: HTML content
|
|
67
|
+
text: Plain text content
|
|
68
|
+
cc: CC recipients
|
|
69
|
+
bcc: BCC recipients
|
|
70
|
+
reply_to: Reply-to address
|
|
71
|
+
headers: Custom headers
|
|
72
|
+
attachments: File attachments
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
SendEmailResponse with message_id
|
|
76
|
+
"""
|
|
77
|
+
data: Dict[str, Any] = {
|
|
78
|
+
"from": self._to_address_dict(from_address),
|
|
79
|
+
"to": [self._to_address_dict(addr) for addr in to],
|
|
80
|
+
"subject": subject,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if html:
|
|
84
|
+
data["html"] = html
|
|
85
|
+
if text:
|
|
86
|
+
data["text"] = text
|
|
87
|
+
if cc:
|
|
88
|
+
data["cc"] = [self._to_address_dict(addr) for addr in cc]
|
|
89
|
+
if bcc:
|
|
90
|
+
data["bcc"] = [self._to_address_dict(addr) for addr in bcc]
|
|
91
|
+
if reply_to:
|
|
92
|
+
data["reply_to"] = self._to_address_dict(reply_to)
|
|
93
|
+
if headers:
|
|
94
|
+
data["headers"] = headers
|
|
95
|
+
if attachments:
|
|
96
|
+
data["attachments"] = attachments
|
|
97
|
+
|
|
98
|
+
response = self._request("POST", "/v2/emails", data)
|
|
99
|
+
return SendEmailResponse.from_dict(response)
|
|
100
|
+
|
|
101
|
+
def batch(
|
|
102
|
+
self,
|
|
103
|
+
emails: List[Dict[str, Any]],
|
|
104
|
+
) -> BatchEmailResponse:
|
|
105
|
+
"""
|
|
106
|
+
Send multiple emails in a single request.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
emails: List of email objects (same format as send())
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
BatchEmailResponse with results for each email
|
|
113
|
+
"""
|
|
114
|
+
if not emails:
|
|
115
|
+
raise ToSendError(
|
|
116
|
+
"Emails list is required and cannot be empty",
|
|
117
|
+
422,
|
|
118
|
+
"validation_error",
|
|
119
|
+
{"emails": {"required": "At least one email is required"}},
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Process each email to ensure proper format
|
|
123
|
+
processed_emails = []
|
|
124
|
+
for email in emails:
|
|
125
|
+
processed = {}
|
|
126
|
+
if "from_address" in email:
|
|
127
|
+
processed["from"] = self._to_address_dict(email["from_address"])
|
|
128
|
+
elif "from" in email:
|
|
129
|
+
processed["from"] = self._to_address_dict(email["from"])
|
|
130
|
+
|
|
131
|
+
if "to" in email:
|
|
132
|
+
processed["to"] = [self._to_address_dict(addr) for addr in email["to"]]
|
|
133
|
+
|
|
134
|
+
for key in ("subject", "html", "text", "headers", "attachments"):
|
|
135
|
+
if key in email:
|
|
136
|
+
processed[key] = email[key]
|
|
137
|
+
|
|
138
|
+
for key in ("cc", "bcc"):
|
|
139
|
+
if key in email and email[key]:
|
|
140
|
+
processed[key] = [self._to_address_dict(addr) for addr in email[key]]
|
|
141
|
+
|
|
142
|
+
if "reply_to" in email and email["reply_to"]:
|
|
143
|
+
processed["reply_to"] = self._to_address_dict(email["reply_to"])
|
|
144
|
+
|
|
145
|
+
processed_emails.append(processed)
|
|
146
|
+
|
|
147
|
+
response = self._request("POST", "/v2/emails/batch", {"emails": processed_emails})
|
|
148
|
+
return BatchEmailResponse.from_dict(response)
|
|
149
|
+
|
|
150
|
+
def get_account_info(self) -> AccountInfo:
|
|
151
|
+
"""
|
|
152
|
+
Get account information and domains.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
AccountInfo with account details and domains
|
|
156
|
+
"""
|
|
157
|
+
response = self._request("GET", "/v2/info")
|
|
158
|
+
return AccountInfo.from_dict(response)
|
|
159
|
+
|
|
160
|
+
def _to_address_dict(self, address: Union[Address, Dict[str, str]]) -> Dict[str, str]:
|
|
161
|
+
"""Convert address to dict format."""
|
|
162
|
+
if isinstance(address, Address):
|
|
163
|
+
return address.to_dict()
|
|
164
|
+
return address
|
|
165
|
+
|
|
166
|
+
def _request(
|
|
167
|
+
self,
|
|
168
|
+
method: str,
|
|
169
|
+
path: str,
|
|
170
|
+
data: Optional[Dict[str, Any]] = None,
|
|
171
|
+
) -> Dict[str, Any]:
|
|
172
|
+
"""Make an HTTP request to the API."""
|
|
173
|
+
url = f"{self.base_url}{path}"
|
|
174
|
+
|
|
175
|
+
headers = {
|
|
176
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
177
|
+
"Content-Type": "application/json",
|
|
178
|
+
"Accept": "application/json",
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
body = json.dumps(data).encode("utf-8") if data else None
|
|
182
|
+
|
|
183
|
+
request = Request(url, data=body, headers=headers, method=method)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
with urlopen(request, timeout=self.timeout) as response:
|
|
187
|
+
response_body = response.read().decode("utf-8")
|
|
188
|
+
if response_body:
|
|
189
|
+
return json.loads(response_body)
|
|
190
|
+
return {}
|
|
191
|
+
except HTTPError as e:
|
|
192
|
+
try:
|
|
193
|
+
error_body = e.read().decode("utf-8")
|
|
194
|
+
error_data = json.loads(error_body)
|
|
195
|
+
raise ToSendError.from_response(error_data, e.code)
|
|
196
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
197
|
+
raise ToSendError(str(e), e.code)
|
|
198
|
+
except URLError as e:
|
|
199
|
+
raise ToSendError(f"Request failed: {e.reason}")
|
|
200
|
+
except Exception as e:
|
|
201
|
+
raise ToSendError(f"Request failed: {str(e)}")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Exceptions for ToSend SDK."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Dict, Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ToSendError(Exception):
|
|
7
|
+
"""ToSend API error."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
message: str,
|
|
12
|
+
status_code: int = 0,
|
|
13
|
+
error_type: str = "unknown_error",
|
|
14
|
+
errors: Optional[Dict[str, Dict[str, str]]] = None,
|
|
15
|
+
):
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
self.message = message
|
|
18
|
+
self.status_code = status_code
|
|
19
|
+
self.error_type = error_type
|
|
20
|
+
self.errors = errors or {}
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_response(cls, data: Dict[str, Any], status_code: int) -> "ToSendError":
|
|
24
|
+
return cls(
|
|
25
|
+
message=data.get("message", "Unknown error"),
|
|
26
|
+
status_code=status_code,
|
|
27
|
+
error_type=data.get("error_type", "unknown_error"),
|
|
28
|
+
errors=data.get("errors"),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def is_validation_error(self) -> bool:
|
|
33
|
+
return self.error_type == "validation_error" or self.status_code == 422
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def is_authentication_error(self) -> bool:
|
|
37
|
+
return (
|
|
38
|
+
self.error_type in ("unauthorized", "forbidden")
|
|
39
|
+
or self.status_code in (401, 403)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def is_rate_limit_error(self) -> bool:
|
|
44
|
+
return self.error_type == "rate_limit_exceeded" or self.status_code == 429
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
return f"ToSendError: {self.message} (status: {self.status_code}, type: {self.error_type})"
|
|
File without changes
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Type definitions for ToSend SDK."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field, asdict
|
|
4
|
+
from typing import Optional, List, Dict, Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Address:
|
|
9
|
+
"""Email address with optional name."""
|
|
10
|
+
email: str
|
|
11
|
+
name: Optional[str] = None
|
|
12
|
+
|
|
13
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
14
|
+
d = {"email": self.email}
|
|
15
|
+
if self.name:
|
|
16
|
+
d["name"] = self.name
|
|
17
|
+
return d
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Attachment:
|
|
22
|
+
"""Email attachment."""
|
|
23
|
+
type: str # MIME type
|
|
24
|
+
name: str # Filename
|
|
25
|
+
content: str # Base64 encoded content
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> Dict[str, str]:
|
|
28
|
+
return {"type": self.type, "name": self.name, "content": self.content}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class SendEmailRequest:
|
|
33
|
+
"""Request to send a single email."""
|
|
34
|
+
from_address: Address
|
|
35
|
+
to: List[Address]
|
|
36
|
+
subject: str
|
|
37
|
+
html: Optional[str] = None
|
|
38
|
+
text: Optional[str] = None
|
|
39
|
+
cc: Optional[List[Address]] = None
|
|
40
|
+
bcc: Optional[List[Address]] = None
|
|
41
|
+
reply_to: Optional[Address] = None
|
|
42
|
+
headers: Optional[Dict[str, str]] = None
|
|
43
|
+
attachments: Optional[List[Attachment]] = None
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
46
|
+
d: Dict[str, Any] = {
|
|
47
|
+
"from": self.from_address.to_dict(),
|
|
48
|
+
"to": [addr.to_dict() for addr in self.to],
|
|
49
|
+
"subject": self.subject,
|
|
50
|
+
}
|
|
51
|
+
if self.html:
|
|
52
|
+
d["html"] = self.html
|
|
53
|
+
if self.text:
|
|
54
|
+
d["text"] = self.text
|
|
55
|
+
if self.cc:
|
|
56
|
+
d["cc"] = [addr.to_dict() for addr in self.cc]
|
|
57
|
+
if self.bcc:
|
|
58
|
+
d["bcc"] = [addr.to_dict() for addr in self.bcc]
|
|
59
|
+
if self.reply_to:
|
|
60
|
+
d["reply_to"] = self.reply_to.to_dict()
|
|
61
|
+
if self.headers:
|
|
62
|
+
d["headers"] = self.headers
|
|
63
|
+
if self.attachments:
|
|
64
|
+
d["attachments"] = [att.to_dict() for att in self.attachments]
|
|
65
|
+
return d
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class SendEmailResponse:
|
|
70
|
+
"""Response from sending an email."""
|
|
71
|
+
message_id: str
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_dict(cls, data: Dict[str, Any]) -> "SendEmailResponse":
|
|
75
|
+
return cls(message_id=data.get("message_id", ""))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class BatchEmailResult:
|
|
80
|
+
"""Result of a single email in a batch."""
|
|
81
|
+
status: str
|
|
82
|
+
message_id: Optional[str] = None
|
|
83
|
+
message: Optional[str] = None
|
|
84
|
+
errors: Optional[Dict[str, Dict[str, str]]] = None
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def from_dict(cls, data: Dict[str, Any]) -> "BatchEmailResult":
|
|
88
|
+
return cls(
|
|
89
|
+
status=data.get("status", ""),
|
|
90
|
+
message_id=data.get("message_id"),
|
|
91
|
+
message=data.get("message"),
|
|
92
|
+
errors=data.get("errors"),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class BatchEmailResponse:
|
|
98
|
+
"""Response from sending batch emails."""
|
|
99
|
+
results: List[BatchEmailResult]
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def from_dict(cls, data: Dict[str, Any]) -> "BatchEmailResponse":
|
|
103
|
+
results = [BatchEmailResult.from_dict(r) for r in data.get("results", [])]
|
|
104
|
+
return cls(results=results)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class Domain:
|
|
109
|
+
"""Verified domain."""
|
|
110
|
+
domain_name: str
|
|
111
|
+
verification_status: str
|
|
112
|
+
created_at: str
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Domain":
|
|
116
|
+
return cls(
|
|
117
|
+
domain_name=data.get("domain_name", ""),
|
|
118
|
+
verification_status=data.get("verification_status", ""),
|
|
119
|
+
created_at=data.get("created_at", ""),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class Account:
|
|
125
|
+
"""Account information."""
|
|
126
|
+
title: str
|
|
127
|
+
plan_type: str
|
|
128
|
+
status: str
|
|
129
|
+
emails_usage_this_month: int
|
|
130
|
+
emails_sent_last_24hrs: int
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Account":
|
|
134
|
+
return cls(
|
|
135
|
+
title=data.get("title", ""),
|
|
136
|
+
plan_type=data.get("plan_type", ""),
|
|
137
|
+
status=data.get("status", ""),
|
|
138
|
+
emails_usage_this_month=data.get("emails_usage_this_month", 0),
|
|
139
|
+
emails_sent_last_24hrs=data.get("emails_sent_last_24hrs", 0),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class AccountInfo:
|
|
145
|
+
"""Account info response."""
|
|
146
|
+
account: Account
|
|
147
|
+
domains: List[Domain]
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def from_dict(cls, data: Dict[str, Any]) -> "AccountInfo":
|
|
151
|
+
return cls(
|
|
152
|
+
account=Account.from_dict(data.get("account", {})),
|
|
153
|
+
domains=[Domain.from_dict(d) for d in data.get("domains", [])],
|
|
154
|
+
)
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tosend
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the ToSend email API
|
|
5
|
+
Author-email: ToSend <support@tosend.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://tosend.com
|
|
8
|
+
Project-URL: Documentation, https://tosend.com/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/tosend/tosend-python
|
|
10
|
+
Project-URL: Issues, https://github.com/tosend/tosend-python/issues
|
|
11
|
+
Keywords: email,api,tosend,transactional,mail
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
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: Topic :: Communications :: Email
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# ToSend Python SDK
|
|
30
|
+
|
|
31
|
+
Official Python SDK for the [ToSend](https://tosend.com) email API.
|
|
32
|
+
|
|
33
|
+
[](https://badge.fury.io/py/tosend)
|
|
34
|
+
[](https://pypi.org/project/tosend/)
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install tosend
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
|
|
44
|
+
See the [examples](examples) folder for complete working examples:
|
|
45
|
+
|
|
46
|
+
- [Send Email](examples/send_email.py) - Send a single email
|
|
47
|
+
- [Batch Send](examples/batch_send.py) - Send multiple emails in one request
|
|
48
|
+
- [Account Info](examples/account_info.py) - Get account information and domains
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
export TOSEND_API_KEY=tsend_your_api_key
|
|
52
|
+
python examples/send_email.py
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from tosend import ToSend
|
|
59
|
+
|
|
60
|
+
client = ToSend("tsend_your_api_key")
|
|
61
|
+
|
|
62
|
+
response = client.send(
|
|
63
|
+
from_address={"email": "hello@yourdomain.com", "name": "Your App"},
|
|
64
|
+
to=[{"email": "user@example.com"}],
|
|
65
|
+
subject="Welcome!",
|
|
66
|
+
html="<h1>Hello!</h1><p>Thanks for signing up.</p>",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
print(response.message_id)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
### Send an Email
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
response = client.send(
|
|
78
|
+
from_address={"email": "hello@yourdomain.com", "name": "Your App"},
|
|
79
|
+
to=[
|
|
80
|
+
{"email": "user@example.com"},
|
|
81
|
+
{"email": "another@example.com", "name": "John Doe"},
|
|
82
|
+
],
|
|
83
|
+
subject="Hello!",
|
|
84
|
+
html="<h1>Welcome</h1>",
|
|
85
|
+
text="Welcome", # optional
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### With CC, BCC, and Reply-To
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
response = client.send(
|
|
93
|
+
from_address={"email": "hello@yourdomain.com"},
|
|
94
|
+
to=[{"email": "user@example.com"}],
|
|
95
|
+
cc=[{"email": "cc@example.com"}],
|
|
96
|
+
bcc=[{"email": "bcc@example.com"}],
|
|
97
|
+
reply_to={"email": "support@yourdomain.com", "name": "Support"},
|
|
98
|
+
subject="Hello",
|
|
99
|
+
html="<p>Hello World</p>",
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### With Attachments
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import base64
|
|
107
|
+
|
|
108
|
+
with open("invoice.pdf", "rb") as f:
|
|
109
|
+
content = base64.b64encode(f.read()).decode()
|
|
110
|
+
|
|
111
|
+
response = client.send(
|
|
112
|
+
from_address={"email": "hello@yourdomain.com"},
|
|
113
|
+
to=[{"email": "user@example.com"}],
|
|
114
|
+
subject="Your Invoice",
|
|
115
|
+
html="<p>Please find your invoice attached.</p>",
|
|
116
|
+
attachments=[
|
|
117
|
+
{
|
|
118
|
+
"type": "application/pdf",
|
|
119
|
+
"name": "invoice.pdf",
|
|
120
|
+
"content": content,
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Batch Sending
|
|
127
|
+
|
|
128
|
+
Send multiple emails in a single request:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
response = client.batch([
|
|
132
|
+
{
|
|
133
|
+
"from": {"email": "hello@yourdomain.com"},
|
|
134
|
+
"to": [{"email": "user1@example.com"}],
|
|
135
|
+
"subject": "Hello User 1",
|
|
136
|
+
"html": "<p>Welcome!</p>",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"from": {"email": "hello@yourdomain.com"},
|
|
140
|
+
"to": [{"email": "user2@example.com"}],
|
|
141
|
+
"subject": "Hello User 2",
|
|
142
|
+
"html": "<p>Welcome!</p>",
|
|
143
|
+
},
|
|
144
|
+
])
|
|
145
|
+
|
|
146
|
+
for result in response.results:
|
|
147
|
+
if result.status == "success":
|
|
148
|
+
print(f"Sent: {result.message_id}")
|
|
149
|
+
else:
|
|
150
|
+
print(f"Failed: {result.message}")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Get Account Info
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
info = client.get_account_info()
|
|
157
|
+
|
|
158
|
+
print(info.account.title)
|
|
159
|
+
print(info.account.emails_usage_this_month)
|
|
160
|
+
print(info.account.emails_sent_last_24hrs)
|
|
161
|
+
|
|
162
|
+
for domain in info.domains:
|
|
163
|
+
print(f"{domain.domain_name}: {domain.verification_status}")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Configuration
|
|
167
|
+
|
|
168
|
+
### With Options
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
client = ToSend(
|
|
172
|
+
api_key="tsend_your_api_key",
|
|
173
|
+
base_url="https://api.tosend.com", # optional
|
|
174
|
+
timeout=60, # optional, in seconds
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Error Handling
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from tosend import ToSend, ToSendError
|
|
182
|
+
|
|
183
|
+
client = ToSend("tsend_your_api_key")
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
response = client.send(
|
|
187
|
+
from_address={"email": "hello@yourdomain.com"},
|
|
188
|
+
to=[{"email": "user@example.com"}],
|
|
189
|
+
subject="Hello",
|
|
190
|
+
html="<p>Hello</p>",
|
|
191
|
+
)
|
|
192
|
+
except ToSendError as e:
|
|
193
|
+
print(f"Error: {e.message}")
|
|
194
|
+
print(f"Status: {e.status_code}")
|
|
195
|
+
print(f"Type: {e.error_type}")
|
|
196
|
+
print(f"Details: {e.errors}")
|
|
197
|
+
|
|
198
|
+
if e.is_validation_error:
|
|
199
|
+
# Handle validation error (422)
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
if e.is_authentication_error:
|
|
203
|
+
# Handle auth error (401/403)
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
if e.is_rate_limit_error:
|
|
207
|
+
# Handle rate limit (429)
|
|
208
|
+
pass
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Type Hints
|
|
212
|
+
|
|
213
|
+
The SDK includes full type hints. You can also use the provided dataclasses:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from tosend import ToSend, Address, SendEmailRequest
|
|
217
|
+
|
|
218
|
+
client = ToSend("tsend_your_api_key")
|
|
219
|
+
|
|
220
|
+
# Using Address dataclass
|
|
221
|
+
from_addr = Address(email="hello@yourdomain.com", name="Your App")
|
|
222
|
+
to_addr = Address(email="user@example.com")
|
|
223
|
+
|
|
224
|
+
response = client.send(
|
|
225
|
+
from_address=from_addr,
|
|
226
|
+
to=[to_addr],
|
|
227
|
+
subject="Hello!",
|
|
228
|
+
html="<h1>Welcome!</h1>",
|
|
229
|
+
)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Requirements
|
|
233
|
+
|
|
234
|
+
- Python 3.8 or higher
|
|
235
|
+
- No external dependencies (uses only standard library)
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
tosend/__init__.py
|
|
5
|
+
tosend/client.py
|
|
6
|
+
tosend/exceptions.py
|
|
7
|
+
tosend/py.typed
|
|
8
|
+
tosend/types.py
|
|
9
|
+
tosend.egg-info/PKG-INFO
|
|
10
|
+
tosend.egg-info/SOURCES.txt
|
|
11
|
+
tosend.egg-info/dependency_links.txt
|
|
12
|
+
tosend.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tosend
|