avelto 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,36 @@
1
+ node_modules/
2
+ dist/
3
+ .next/
4
+ out/
5
+ coverage/
6
+ *.log
7
+ .DS_Store
8
+ .env
9
+ .env.*
10
+ !.env.example
11
+ !.env.*.example
12
+ backups/
13
+ .turbo/
14
+ deploy/nginx/*.rendered
15
+ .deploy-state/
16
+
17
+ # Credentials and certificates. Nothing matching these has ever been committed; the
18
+ # patterns are here so that the first time somebody drops a key in the tree, git does
19
+ # not offer to commit it.
20
+ *.pem
21
+ *.key
22
+ *.p12
23
+ *.pfx
24
+ .aws/
25
+
26
+ # Playwright, which the screenshot script drives. It writes its output straight into
27
+ # apps/web/public/screenshots (those are committed on purpose), but a failed or traced
28
+ # run leaves these behind.
29
+ test-results/
30
+ playwright-report/
31
+ .playwright/
32
+ blob-report/
33
+
34
+ # Build bookkeeping and editor/host state.
35
+ *.tsbuildinfo
36
+ .vercel/
avelto-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Avelto
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.
avelto-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,366 @@
1
+ Metadata-Version: 2.5
2
+ Name: avelto
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Avelto, the email API for developers who want it to just work
5
+ Project-URL: Homepage, https://avelto.dev
6
+ Project-URL: Documentation, https://avelto.dev/docs/sdk-python
7
+ Project-URL: Source, https://github.com/aveltohq/avelto/tree/master/packages/sdk-python
8
+ Author: Avelto
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,avelto,email,sdk,transactional,webhooks
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Communications :: Email
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.9
25
+ Requires-Dist: httpx<1,>=0.24
26
+ Requires-Dist: typing-extensions>=4.5; python_version < '3.11'
27
+ Provides-Extra: dev
28
+ Requires-Dist: mypy>=1.10; extra == 'dev'
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
30
+ Requires-Dist: pytest>=8; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # avelto
34
+
35
+ The official Python SDK for [Avelto](https://avelto.dev), the email API for developers who want it to just work.
36
+
37
+ - Python 3.9+
38
+ - Sync and asyncio clients with the same surface
39
+ - Typed: every response is a plain dict with a `TypedDict` describing its keys
40
+ - One dependency, `httpx`
41
+
42
+ ## Install
43
+
44
+ ```sh
45
+ pip install avelto
46
+ ```
47
+
48
+ ## Quickstart
49
+
50
+ ```python
51
+ import os
52
+ from avelto import Avelto
53
+
54
+ avelto = Avelto(os.environ["AVELTO_API_KEY"])
55
+
56
+ email = avelto.emails.send(
57
+ from_="Acme <hello@mail.acme.com>",
58
+ to="jane@example.com",
59
+ subject="Welcome to Acme",
60
+ html="<p>Thanks for signing up.</p>",
61
+ text="Thanks for signing up.",
62
+ )
63
+ print(email["id"]) # "5b3d…"
64
+ ```
65
+
66
+ `from_` is the sender, because `from` is a Python keyword. `to`, `cc` and `bcc` take one address or a list. Addresses can be `a@b.com` or `Name <a@b.com>`. Provide `html`, `text`, or both. You can also pass `reply_to`, `headers`, `tags` (up to 10), `attachments` (dicts with `filename` and base64 `content` or an http(s) `url`, up to 10 files and 7 MB in total), `scheduled_at` and `unsubscribe_url`. To send a stored template instead of a body, pass `template_id` or `template_slug` with `variables` and leave `subject`, `html` and `text` unset.
67
+
68
+ Every method returns the API's JSON as a dict, exactly as the [reference](https://avelto.dev/docs/api) documents it, so `email["id"]` rather than `email.id`.
69
+
70
+ ### asyncio
71
+
72
+ ```python
73
+ from avelto import AsyncAvelto
74
+
75
+ async with AsyncAvelto(os.environ["AVELTO_API_KEY"]) as avelto:
76
+ email = await avelto.emails.send(from_="hello@mail.acme.com", to="jane@example.com", subject="Hi", text="Hello")
77
+ ```
78
+
79
+ Same options, same methods, each one awaited.
80
+
81
+ ### Options
82
+
83
+ ```python
84
+ avelto = Avelto(
85
+ api_key,
86
+ base_url="https://api.avelto.dev", # also read from AVELTO_BASE_URL
87
+ timeout=30.0, # per request, seconds
88
+ max_attempts=3, base_delay=0.3, max_delay=5.0, # the retry defaults
89
+ )
90
+ ```
91
+
92
+ `Avelto` holds a connection pool. Use it as a context manager or call `close()` when you are done (`aclose()` on the async client).
93
+
94
+ ## Idempotency
95
+
96
+ Every `emails.send` carries an `Idempotency-Key` header: a fresh random UUID per call by default, so the SDK's own retries can never create two emails. Pass your own key (an order id, say) to make retries at your level safe too. A replay returns the id of the email that was already created and does not send again.
97
+
98
+ ```python
99
+ email = avelto.emails.send(
100
+ from_="hello@mail.acme.com", to="jane@example.com", subject="Receipt #1042", text="…",
101
+ idempotency_key="receipt-1042",
102
+ )
103
+ ```
104
+
105
+ ## Retries
106
+
107
+ The SDK makes up to three attempts at a failed request (the call plus two retries) with exponential backoff and jitter; a `429` waits for the server's `Retry-After` instead. `429`, `502` and `503` are retried for every request. Network errors, timeouts and `504` (where the request may have been processed) are retried only for `GET`, `DELETE`, `emails.send`, `emails.send_batch` and `emails.validate`, never for the other `POST`s or for `PATCH`. Pass `max_attempts=1` to turn retries off. After the last attempt the `AveltoError` is raised; on `429` it carries `retry_after_seconds`.
108
+
109
+ ## Scheduled send and cancel
110
+
111
+ `scheduled_at` is an ISO 8601 timestamp, in the future and at most 30 days ahead. A scheduled email can be cancelled until it is sent.
112
+
113
+ ```python
114
+ from datetime import datetime, timedelta, timezone
115
+
116
+ email = avelto.emails.send(
117
+ from_="hello@mail.acme.com",
118
+ to="jane@example.com",
119
+ subject="Your trial ends tomorrow",
120
+ text="…",
121
+ scheduled_at=(datetime.now(timezone.utc) + timedelta(days=1)).isoformat(),
122
+ )
123
+
124
+ cancelled = avelto.emails.cancel(email["id"]) # cancelled["status"] == "cancelled"
125
+ ```
126
+
127
+ Cancelling an email that is no longer `scheduled` raises an `AveltoError` with code `not_scheduled`.
128
+
129
+ ## Validate an address
130
+
131
+ Ask whether an address is worth a send before you spend one on it, for example on every submit of a signup form. Nothing is sent or stored.
132
+
133
+ ```python
134
+ check = avelto.emails.validate("jane@gmial.com")
135
+ check["result"] # "deliverable" | "risky" | "undeliverable"
136
+ check["reason"] # None | "invalid_syntax" | "suppressed" | "no_mail_server" | "disposable" | "possible_typo" | "role_address"
137
+ check["suggestion"] # "jane@gmail.com"
138
+ check["normalized"] # the lower-cased address to store, or None when the syntax is invalid
139
+ check["checks"] # {"syntax", "mail_server", "disposable", "role", "free_provider", "suppressed"}
140
+ ```
141
+
142
+ `undeliverable` means a send would fail or be refused: bad syntax, a domain with no mail server, or an address on your own suppression list. `risky` means it may well deliver but deserves a look: a throwaway domain, a role address such as `info@`, or a domain within a typo of a well-known one, in which case `suggestion` is the corrected address to offer the user. It cannot tell you whether the mailbox exists; nobody can without sending.
143
+
144
+ ## Batch send
145
+
146
+ `emails.send_batch` sends up to 100 messages in one call. Each message is a dict with the same keys as `send` (`"from"`, or `"from_"` if you prefer to avoid the keyword in a dict literal too). Each is accepted or refused on its own, so read `results` rather than assuming the call either worked or did not; `results[i]` lines up with `messages[i]`.
147
+
148
+ ```python
149
+ batch = avelto.emails.send_batch([
150
+ {"from": "billing@mail.acme.com", "to": "jane@example.com", "subject": "Receipt #1042", "text": "…"},
151
+ {"from": "billing@mail.acme.com", "to": "sam@example.com", "subject": "Receipt #1043", "text": "…"},
152
+ ], idempotency_key="receipts-2026-09-24")
153
+
154
+ for r in batch["results"]:
155
+ if r["ok"]:
156
+ print(r["index"], r["id"])
157
+ else:
158
+ print(r["index"], r["error"]["code"], r["error"]["message"])
159
+ ```
160
+
161
+ ## Fetch and list emails
162
+
163
+ ```python
164
+ email = avelto.emails.get(email_id)
165
+ print(email["status"]) # "queued" | "scheduled" | "sent" | "delivered" | "bounced" | "complained" | "failed" | "cancelled"
166
+ print([e["type"] for e in email["events"]]) # ["email.queued", "email.sent", "email.delivered"]
167
+ ```
168
+
169
+ Lists are newest first and cursor-paginated. Filter by `status`, `tag`, `mode` and `q`, a substring search over recipient, subject and message id. A live key defaults to live and may ask for test; a test key only ever sees test emails, whatever `mode` says, and a live email is a `404` to it.
170
+
171
+ ```python
172
+ cursor = None
173
+ while True:
174
+ page = avelto.emails.list(status="bounced", tag="onboarding", limit=100, cursor=cursor)
175
+ for e in page["data"]:
176
+ print(e["id"], e["to"], e["subject"])
177
+ cursor = page["next_cursor"]
178
+ if not cursor:
179
+ break
180
+ ```
181
+
182
+ ## Domains
183
+
184
+ Add a domain, publish the DNS records it returns, then poll `get` until it is verified. `domains.get` re-checks verification on every call.
185
+
186
+ ```python
187
+ import time
188
+
189
+ domain = avelto.domains.create("mail.acme.com")
190
+ for r in domain["dns_records"]:
191
+ print(r["type"], r["name"], r["value"], f"({r['purpose']})", sep="\t")
192
+
193
+ # Publish the records, then:
194
+ status = domain["status"]
195
+ while status == "pending":
196
+ time.sleep(30)
197
+ status = avelto.domains.get(domain["id"])["status"]
198
+ print(status) # "verified" (or "failed")
199
+
200
+ avelto.domains.list()
201
+ avelto.domains.delete(domain["id"])
202
+ ```
203
+
204
+ ## Templates
205
+
206
+ ```python
207
+ tpl = avelto.templates.create(name="Welcome", subject="Hi {{name}}", html="<p>Welcome, {{name}}.</p>")
208
+ avelto.emails.send(from_="hello@mail.acme.com", to="jane@example.com", template_slug=tpl["slug"], variables={"name": "Jane"})
209
+
210
+ avelto.templates.update(tpl["id"], subject="Hello {{name}}") # saves a new version
211
+ avelto.templates.update(tpl["id"], text=None) # None clears a body part; leaving it out keeps it
212
+ versions = avelto.templates.versions(tpl["id"])["data"]
213
+ avelto.templates.restore(tpl["id"], versions[-1]["version"])
214
+ ```
215
+
216
+ ## Webhooks
217
+
218
+ Create an endpoint. The signing `secret` is returned once, on creation. Store it.
219
+
220
+ ```python
221
+ endpoint = avelto.webhooks.create(
222
+ "https://acme.com/hooks/avelto",
223
+ events=["email.delivered", "email.bounced", "email.complained"], # optional; defaults to all eight
224
+ )
225
+ print(endpoint["secret"])
226
+ ```
227
+
228
+ Every delivery is a JSON POST with an `Avelto-Signature` header of the form `t=<unix seconds>,v1=<hex>`. Verify it against the raw body before you trust the payload. Use the event `id` to de-duplicate.
229
+
230
+ ### Flask
231
+
232
+ ```python
233
+ import json, os
234
+ from flask import Flask, request, abort
235
+ from avelto import verify_webhook_signature
236
+
237
+ app = Flask(__name__)
238
+
239
+ @app.post("/hooks/avelto")
240
+ def avelto_hook():
241
+ body = request.get_data() # raw bytes, before any parsing
242
+ if not verify_webhook_signature(os.environ["AVELTO_WEBHOOK_SECRET"], body, request.headers.get("Avelto-Signature")):
243
+ abort(400)
244
+ event = json.loads(body)
245
+ if event["type"] == "email.bounced":
246
+ print("bounced", event["data"]["email_id"], event["data"]["details"])
247
+ return "", 200
248
+ ```
249
+
250
+ ### Django
251
+
252
+ ```python
253
+ import json, os
254
+ from django.http import HttpResponse, HttpResponseBadRequest
255
+ from django.views.decorators.csrf import csrf_exempt
256
+ from django.views.decorators.http import require_POST
257
+ from avelto import verify_webhook_signature
258
+
259
+ @csrf_exempt
260
+ @require_POST
261
+ def avelto_hook(request):
262
+ if not verify_webhook_signature(os.environ["AVELTO_WEBHOOK_SECRET"], request.body, request.headers.get("Avelto-Signature")):
263
+ return HttpResponseBadRequest("invalid signature")
264
+ event = json.loads(request.body)
265
+ print(event["type"], event["data"]["email_id"])
266
+ return HttpResponse(status=200)
267
+ ```
268
+
269
+ Signatures older than five minutes are rejected; pass `tolerance_seconds=` to change that.
270
+
271
+ ### Deliveries
272
+
273
+ ```python
274
+ endpoints = avelto.webhooks.list()["data"]
275
+ endpoint = avelto.webhooks.get(endpoint_id)
276
+
277
+ page = avelto.webhooks.list_deliveries(endpoint_id, limit=50)
278
+ failed = next((d for d in page["data"] if d["status"] == "failed"), None)
279
+ if failed:
280
+ avelto.webhooks.retry_delivery(endpoint_id, failed["id"])
281
+
282
+ avelto.webhooks.delete(endpoint_id)
283
+ ```
284
+
285
+ ## Suppressions
286
+
287
+ Addresses that hard-bounce or complain are suppressed automatically and sends to them are rejected. You can manage the list yourself.
288
+
289
+ ```python
290
+ page = avelto.suppressions.list(limit=100)
291
+ for s in page["data"]:
292
+ print(s["email_address"], s["reason"]) # "bounce" | "complaint" | "manual"
293
+
294
+ avelto.suppressions.create("unsubscribed@example.com")
295
+ avelto.suppressions.delete("unsubscribed@example.com")
296
+ ```
297
+
298
+ ## Account
299
+
300
+ `account.get()` returns the key's mode and scopes, the sandbox domain with every recipient a sandbox send may reach, and the domains, webhook endpoints and templates that exist. Read it first when wiring up a new project.
301
+
302
+ ## Error handling
303
+
304
+ Every failed request raises `AveltoError` with the HTTP `status`, the API `code`, the `message`, optional `details`, the `request_id` the API answered with, and on a `429` the `retry_after_seconds`. A request that got no response at all has status `0` and code `network_error`, with the underlying exception as `__cause__`; `is_network_error` is true for exactly those.
305
+
306
+ ```python
307
+ from avelto import AveltoError
308
+
309
+ try:
310
+ avelto.emails.send(from_="hello@mail.acme.com", to="jane@example.com", subject="Hi", text="Hi")
311
+ except AveltoError as err:
312
+ if err.code == "domain_not_verified": # 403: verify mail.acme.com first
313
+ ...
314
+ elif err.code == "sandbox_recipient_not_allowed": # 403: the sandbox sender only delivers to your account's owner email
315
+ ...
316
+ elif err.code == "recipient_suppressed": # 422: address is on your suppression list
317
+ ...
318
+ elif err.code == "validation_error": # 400: err.details lists the failing fields
319
+ ...
320
+ elif err.is_network_error: # status 0: no response (DNS, TLS, timeout)
321
+ ...
322
+ else:
323
+ print(err.status, err.code, err.message, err.details)
324
+ ```
325
+
326
+ | Code | Status | When |
327
+ | --- | --- | --- |
328
+ | `validation_error` | 400 | Request body or query failed validation. `details` lists the issues. |
329
+ | `unauthorized` | 401 | Missing or invalid API key. |
330
+ | `forbidden` | 403 | Key is not allowed to do this. |
331
+ | `not_found` | 404 | No such email, domain, webhook or suppression. |
332
+ | `conflict` | 409 | Resource already exists (for example the domain). |
333
+ | `not_scheduled` | 409 | `emails.cancel` on an email that is not scheduled. |
334
+ | `sandbox_recipient_not_allowed` | 403 | Sending from the sandbox domain to anyone but your account's owner email. |
335
+ | `domain_not_verified` | 403 | `from_` uses a domain that is not added and verified on your account. |
336
+ | `recipient_suppressed` | 422 | A recipient is on your suppression list. |
337
+ | `plan_limit` | 429 / 403 | Monthly or first-week sending cap reached (429), or the plan does not allow another domain (403). |
338
+ | `account_paused` | 403 | Sending is paused on the account. Check the dashboard. |
339
+ | `account_suspended` | 403 | The account is suspended; the message says why. Nothing sends until it is lifted. |
340
+ | `rate_limited` | 429 | Too many requests for this key; honour `retry_after_seconds`. |
341
+ | `internal_error` | 5xx | Something went wrong on our side. |
342
+ | `network_error` | 0 | The request never got a response. `err.__cause__` holds the underlying exception. |
343
+
344
+ ## Test mode
345
+
346
+ Keys starting with `av_test_` never send real email. They run the full pipeline and emit the same events and webhooks, so you can build against them without touching an inbox.
347
+
348
+ Use these recipients on your sandbox domain to simulate outcomes, in both live and test mode:
349
+
350
+ - `delivered@<your sandbox domain>` - delivered
351
+ - `bounced@<your sandbox domain>` - hard bounce (never added to suppressions)
352
+ - `complained@<your sandbox domain>` - complaint (never added to suppressions)
353
+
354
+ Your sandbox domain is shown in the dashboard. Sending from it to any other address only works for your account's owner email; verify a domain to send to anyone.
355
+
356
+ ## Types
357
+
358
+ Every response shape is a `TypedDict` in `avelto.types`, so an editor knows the keys:
359
+
360
+ ```python
361
+ from avelto.types import Email, EmailSummary, Domain, WebhookEndpoint, WebhookPayload, Suppression, ValidateEmailResponse
362
+ ```
363
+
364
+ ## License
365
+
366
+ MIT