eusend 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.
- eusend-0.1.0/.gitignore +6 -0
- eusend-0.1.0/LICENSE +21 -0
- eusend-0.1.0/PKG-INFO +364 -0
- eusend-0.1.0/README.md +344 -0
- eusend-0.1.0/eusend/__init__.py +72 -0
- eusend-0.1.0/eusend/_compat.py +15 -0
- eusend-0.1.0/eusend/_response.py +19 -0
- eusend-0.1.0/eusend/_util.py +58 -0
- eusend-0.1.0/eusend/api_keys.py +47 -0
- eusend-0.1.0/eusend/audiences.py +111 -0
- eusend-0.1.0/eusend/batch.py +35 -0
- eusend-0.1.0/eusend/broadcasts.py +94 -0
- eusend-0.1.0/eusend/domains.py +45 -0
- eusend-0.1.0/eusend/emails.py +116 -0
- eusend-0.1.0/eusend/exceptions.py +158 -0
- eusend-0.1.0/eusend/models.py +118 -0
- eusend-0.1.0/eusend/py.typed +0 -0
- eusend-0.1.0/eusend/request.py +95 -0
- eusend-0.1.0/eusend/templates.py +52 -0
- eusend-0.1.0/eusend/version.py +2 -0
- eusend-0.1.0/eusend/webhooks.py +52 -0
- eusend-0.1.0/pyproject.toml +33 -0
eusend-0.1.0/.gitignore
ADDED
eusend-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eusend
|
|
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.
|
eusend-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eusend
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Eusend API — the EU-native transactional email platform.
|
|
5
|
+
Project-URL: Homepage, https://eusend.dev
|
|
6
|
+
Project-URL: Documentation, https://eusend.dev/docs
|
|
7
|
+
Project-URL: Source, https://github.com/eusend-dev/eusend-python
|
|
8
|
+
Project-URL: Issues, https://github.com/eusend-dev/eusend-python/issues
|
|
9
|
+
Author: Eusend
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,email,eusend,smtp,transactional-email
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Requires-Dist: typing-extensions>=4.0
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# eusend
|
|
22
|
+
|
|
23
|
+
Official Python SDK for the [Eusend](https://eusend.dev) API — the EU-native transactional email platform.
|
|
24
|
+
|
|
25
|
+
Its shape mirrors [`resend-python`](https://github.com/resend/resend-python), so migrating from Resend is largely a `resend` → `eusend` rename.
|
|
26
|
+
|
|
27
|
+
- **Module-level config** — `eusend.api_key = "..."`, then call `eusend.Emails.send(...)`.
|
|
28
|
+
- **Zero HTTP dependencies** — the transport is built on the standard library.
|
|
29
|
+
- **Typed** — `TypedDict` params and responses; ships `py.typed`.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install eusend
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires Python 3.8+.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Getting started
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import eusend
|
|
43
|
+
|
|
44
|
+
eusend.api_key = "eu_live_..."
|
|
45
|
+
|
|
46
|
+
params: eusend.Emails.SendParams = {
|
|
47
|
+
# `from` accepts a bare email or a display-name form: "Acme <you@yourdomain.com>"
|
|
48
|
+
"from": "Acme <you@yourdomain.com>",
|
|
49
|
+
"to": ["user@example.com"],
|
|
50
|
+
"subject": "Hello",
|
|
51
|
+
"html": "<p>Hello world</p>",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
email = eusend.Emails.send(params)
|
|
55
|
+
print(email["id"]) # 9a8b7c6d-... (UUID)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The key can also come from the `EUSEND_API_KEY` environment variable, in which
|
|
59
|
+
case you can skip setting `eusend.api_key`.
|
|
60
|
+
|
|
61
|
+
Responses are dicts with **snake_case** keys — access fields with `email["id"]`.
|
|
62
|
+
On failure, methods raise `eusend.EusendError` (see [Error handling](#error-handling)).
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Emails
|
|
67
|
+
|
|
68
|
+
### Send
|
|
69
|
+
|
|
70
|
+
`from` and `to` are required; provide at least one of `html`, `text`, or `template_id`.
|
|
71
|
+
|
|
72
|
+
| Key | Type | Notes |
|
|
73
|
+
|-----|------|-------|
|
|
74
|
+
| `from` | `str` | Verified domain; bare or display-name form. |
|
|
75
|
+
| `to` `cc` `bcc` `reply_to` | `str \| list[str]` | Max 50 each. |
|
|
76
|
+
| `subject` | `str` | |
|
|
77
|
+
| `html` / `text` | `str` | |
|
|
78
|
+
| `template_id` | `str` | Saved template. |
|
|
79
|
+
| `variables` | `dict` | Template substitutions (HTML-escaped). |
|
|
80
|
+
| `headers` | `dict[str, str]` | No line breaks in names or values. |
|
|
81
|
+
| `track_opens` / `track_clicks` | `bool` | Default `True`. |
|
|
82
|
+
| `attachments` | `list[Attachment]` | See below. Up to 20, 10 MB combined. |
|
|
83
|
+
| `scheduled_at` | `str` | Future send, ≤ 30 days. |
|
|
84
|
+
|
|
85
|
+
### Attachments
|
|
86
|
+
|
|
87
|
+
Each attachment is a dict. `content` accepts raw `bytes` (base64-encoded for you)
|
|
88
|
+
or an already-base64 `str`; alternatively pass `path` (a public URL fetched at
|
|
89
|
+
send time). Set `content_id` for an inline `<img src="cid:...">`.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
with open("invoice.pdf", "rb") as f:
|
|
93
|
+
eusend.Emails.send({
|
|
94
|
+
"from": "you@yourdomain.com",
|
|
95
|
+
"to": "user@example.com",
|
|
96
|
+
"subject": "Your invoice",
|
|
97
|
+
"html": "<p>Attached.</p>",
|
|
98
|
+
"attachments": [
|
|
99
|
+
{"filename": "invoice.pdf", "content": f.read(), "content_type": "application/pdf"},
|
|
100
|
+
],
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Idempotent sends
|
|
105
|
+
|
|
106
|
+
Pass an `options` dict with an `idempotency_key` to safely retry without duplicating:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
eusend.Emails.send(
|
|
110
|
+
{"from": "you@yourdomain.com", "to": "user@example.com",
|
|
111
|
+
"subject": "Your receipt", "html": "<p>Thanks!</p>"},
|
|
112
|
+
options={"idempotency_key": f"receipt-{order_id}"},
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Scheduled sends
|
|
117
|
+
|
|
118
|
+
`scheduled_at` accepts an ISO 8601 string or natural language (`"in 1 hour"`,
|
|
119
|
+
`"tomorrow at 9am"`), parsed server-side in UTC.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
sent = eusend.Emails.send({
|
|
123
|
+
"from": "you@yourdomain.com", "to": "user@example.com",
|
|
124
|
+
"subject": "Reminder", "html": "<p>Soon.</p>",
|
|
125
|
+
"scheduled_at": "in 1 hour",
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
eusend.Emails.update({"id": sent["id"], "scheduled_at": "in 2 hours"}) # reschedule
|
|
129
|
+
eusend.Emails.cancel(sent["id"]) # cancel before it sends
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Batch
|
|
133
|
+
|
|
134
|
+
Up to 100 emails in one request. Attachments and scheduling are stripped (not
|
|
135
|
+
supported on the batch endpoint). The result maps positionally to the input:
|
|
136
|
+
queued items carry `id`, rejected items carry `error` and `code`.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
res = eusend.Batch.send([
|
|
140
|
+
{"from": "you@yourdomain.com", "to": "alice@example.com", "subject": "Hi", "html": "<p>Hi</p>"},
|
|
141
|
+
{"from": "you@yourdomain.com", "to": "bob@example.com", "subject": "Hi", "html": "<p>Hi</p>"},
|
|
142
|
+
])
|
|
143
|
+
for item in res["data"]:
|
|
144
|
+
print(item.get("id") or f"{item['code']}: {item['error']}")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Retrieve & list
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
email = eusend.Emails.get("9a8b7c6d-...")
|
|
151
|
+
print(email["status"], email["events"][0]["type"])
|
|
152
|
+
|
|
153
|
+
page = eusend.Emails.list({"limit": 20, "status": "delivered"})
|
|
154
|
+
for e in page["data"]:
|
|
155
|
+
print(e["id"], e["subject"])
|
|
156
|
+
if page["next_cursor"]:
|
|
157
|
+
page = eusend.Emails.list({"cursor": page["next_cursor"]})
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Filter by `status`, `from`, `to`. Statuses: `queued` `scheduled` `sending` `sent`
|
|
161
|
+
`delivered` `bounced` `complained` `suppressed` `failed`.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Domains
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
created = eusend.Domains.create("yourdomain.com")
|
|
169
|
+
print(created["dkim"]["name"], created["dkim"]["value"]) # DNS records to add
|
|
170
|
+
print(created["spf"], created["dmarc"])
|
|
171
|
+
|
|
172
|
+
eusend.Domains.verify(created["id"]) # after publishing the DNS records
|
|
173
|
+
eusend.Domains.list()
|
|
174
|
+
eusend.Domains.get(created["id"])
|
|
175
|
+
eusend.Domains.remove(created["id"])
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## API keys
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
key = eusend.ApiKeys.create({"name": "Production"})
|
|
184
|
+
print(key["key"]) # eu_live_... — returned only once
|
|
185
|
+
|
|
186
|
+
eusend.ApiKeys.create({"name": "Sandbox", "test_mode": True}) # eu_test_... key
|
|
187
|
+
eusend.ApiKeys.list() # prefixes only
|
|
188
|
+
eusend.ApiKeys.remove(key["id"])
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Emails sent with a test key are accepted and tracked but never delivered.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Audiences & contacts
|
|
196
|
+
|
|
197
|
+
Contact operations are grouped under `Audiences` (they live under a specific audience).
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
audience = eusend.Audiences.create("Newsletter")
|
|
201
|
+
|
|
202
|
+
eusend.Audiences.create_contact(audience["id"], {"email": "user@example.com", "first_name": "Jane"})
|
|
203
|
+
|
|
204
|
+
# Bulk upsert (up to 1,000) → {"count": N}
|
|
205
|
+
eusend.Audiences.batch_create_contacts(audience["id"], [
|
|
206
|
+
{"email": "alice@example.com", "first_name": "Alice"},
|
|
207
|
+
{"email": "bob@example.com", "first_name": "Bob"},
|
|
208
|
+
])
|
|
209
|
+
|
|
210
|
+
page = eusend.Audiences.list_contacts(audience["id"], {"subscribed": True, "search": "gmail.com"})
|
|
211
|
+
contact = page["data"][0]
|
|
212
|
+
|
|
213
|
+
eusend.Audiences.update_contact(audience["id"], contact["id"], {"unsubscribed": True})
|
|
214
|
+
eusend.Audiences.get_contact(audience["id"], contact["id"])
|
|
215
|
+
eusend.Audiences.remove_contact(audience["id"], contact["id"])
|
|
216
|
+
|
|
217
|
+
eusend.Audiences.list()
|
|
218
|
+
eusend.Audiences.remove(audience["id"])
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Templates
|
|
224
|
+
|
|
225
|
+
`{{variable}}` placeholders are substituted at send time; values are HTML-escaped.
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
tpl = eusend.Templates.create({
|
|
229
|
+
"name": "Welcome email",
|
|
230
|
+
"subject": "Welcome, {{name}}!",
|
|
231
|
+
"html": "<h1>Hi {{name}}</h1><p>Welcome to {{product}}.</p>",
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
eusend.Emails.send({
|
|
235
|
+
"from": "you@yourdomain.com", "to": "user@example.com",
|
|
236
|
+
"template_id": tpl["id"],
|
|
237
|
+
"variables": {"name": "Jane", "product": "Acme"},
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
eusend.Templates.list()
|
|
241
|
+
eusend.Templates.get(tpl["id"])
|
|
242
|
+
eusend.Templates.update(tpl["id"], {"subject": "New subject"})
|
|
243
|
+
eusend.Templates.remove(tpl["id"])
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Webhooks
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
hook = eusend.Webhooks.create({
|
|
252
|
+
"url": "https://yourapp.com/webhooks/eusend",
|
|
253
|
+
"events": ["email.delivered", "email.bounced", "email.complained"], # or ["*"]
|
|
254
|
+
})
|
|
255
|
+
print(hook["secret"]) # signing secret — returned only once
|
|
256
|
+
|
|
257
|
+
eusend.Webhooks.list()
|
|
258
|
+
eusend.Webhooks.get(hook["id"]) # includes recent deliveries
|
|
259
|
+
eusend.Webhooks.update(hook["id"], {"events": ["email.bounced"]})
|
|
260
|
+
eusend.Webhooks.remove(hook["id"])
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Events: `email.sent` `email.delivered` `email.bounced` `email.complained`
|
|
264
|
+
`email.opened` `email.clicked`. The endpoint must be a public `http(s)` URL
|
|
265
|
+
returning `2xx` directly (redirects count as failures).
|
|
266
|
+
|
|
267
|
+
### Verifying signatures
|
|
268
|
+
|
|
269
|
+
Every delivery is signed with HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{body}`:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
import base64
|
|
273
|
+
import hashlib
|
|
274
|
+
import hmac
|
|
275
|
+
|
|
276
|
+
def verify(headers, body: bytes, secret: str) -> bool:
|
|
277
|
+
signed = f"{headers['webhook-id']}.{headers['webhook-timestamp']}.{body.decode()}"
|
|
278
|
+
mac = hmac.new(secret.encode(), signed.encode(), hashlib.sha256)
|
|
279
|
+
expected = "v1," + base64.b64encode(mac.digest()).decode()
|
|
280
|
+
return hmac.compare_digest(headers["webhook-signature"], expected)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Broadcasts
|
|
286
|
+
|
|
287
|
+
Send one email to every contact in an audience. `{{first_name}}`, `{{last_name}}`,
|
|
288
|
+
`{{full_name}}`, and `{{email}}` are available per recipient, and RFC 8058
|
|
289
|
+
one-click unsubscribe headers are added automatically.
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
bc = eusend.Broadcasts.create({
|
|
293
|
+
"name": "May newsletter",
|
|
294
|
+
"audience_id": audience["id"],
|
|
295
|
+
"from": "Sivert <hello@yourdomain.com>",
|
|
296
|
+
"subject": "May update",
|
|
297
|
+
"html": "<p>Hi {{first_name}}, your monthly update is here...</p>",
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
eusend.Broadcasts.send(bc["id"]) # send now
|
|
301
|
+
eusend.Broadcasts.send(bc["id"], {"scheduled_at": "2026-06-01T09:00:00Z"}) # or schedule
|
|
302
|
+
eusend.Broadcasts.cancel(bc["id"])
|
|
303
|
+
|
|
304
|
+
eusend.Broadcasts.list()
|
|
305
|
+
eusend.Broadcasts.get(bc["id"]) # includes delivery stats
|
|
306
|
+
eusend.Broadcasts.update(bc["id"], {"subject": "Updated subject"})
|
|
307
|
+
eusend.Broadcasts.remove(bc["id"])
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Calling `send` on a paused broadcast resumes it from where it stopped.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Error handling
|
|
315
|
+
|
|
316
|
+
Any non-2xx response raises a subclass of `eusend.EusendError`. Network failures
|
|
317
|
+
that never reach the server raise `ApplicationError` (with `status_code == None`).
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
import eusend
|
|
321
|
+
from eusend import EusendError, RateLimitError
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
eusend.Emails.send({"from": "you@yourdomain.com", "to": "user@example.com",
|
|
325
|
+
"subject": "Hi", "html": "<p>Hi</p>"})
|
|
326
|
+
except RateLimitError as e:
|
|
327
|
+
print(e.code) # "MONTHLY_LIMIT_EXCEEDED"
|
|
328
|
+
print(e.status_code) # 429
|
|
329
|
+
except EusendError as e:
|
|
330
|
+
print(e.code, e.message, e.status_code)
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Exception classes: `MissingApiKeyError`, `InvalidApiKeyError`, `ValidationError`,
|
|
334
|
+
`NotFoundError`, `RateLimitError`, `ApplicationError` — all subclasses of
|
|
335
|
+
`EusendError`. Branch on `e.code`:
|
|
336
|
+
|
|
337
|
+
| Code | Status | Exception |
|
|
338
|
+
|------|--------|-----------|
|
|
339
|
+
| `UNAUTHORIZED` | 401 | `InvalidApiKeyError` |
|
|
340
|
+
| `FORBIDDEN` | 403 | `EusendError` |
|
|
341
|
+
| `NOT_FOUND` | 404 | `NotFoundError` |
|
|
342
|
+
| `VALIDATION_ERROR` / `BAD_REQUEST` | 400 | `ValidationError` |
|
|
343
|
+
| `CONFLICT` | 409 | `EusendError` |
|
|
344
|
+
| `RATE_LIMITED` | 429 | `RateLimitError` |
|
|
345
|
+
| `MONTHLY_LIMIT_EXCEEDED` | 429 | `RateLimitError` |
|
|
346
|
+
| `DAILY_LIMIT_EXCEEDED` | 429 | `RateLimitError` |
|
|
347
|
+
| `PLAN_LIMIT_EXCEEDED` | 403 | `EusendError` |
|
|
348
|
+
| `DOMAIN_NOT_VERIFIED` | 403 | `EusendError` |
|
|
349
|
+
| `SENDING_SUSPENDED` | 403 | `EusendError` |
|
|
350
|
+
| `ALL_SUPPRESSED` | 422 | `EusendError` |
|
|
351
|
+
| `SERVICE_PAUSED` | 503 | `EusendError` |
|
|
352
|
+
| `INTERNAL_ERROR` | 500 | `ApplicationError` |
|
|
353
|
+
| `application_error` | — | `ApplicationError` (network failure) |
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Configuration
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
import eusend
|
|
361
|
+
|
|
362
|
+
eusend.api_key = "eu_live_..." # or EUSEND_API_KEY
|
|
363
|
+
eusend.api_url = "https://api.eusend.dev" # override for testing
|
|
364
|
+
```
|