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.
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
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
+ ```