Duva for Python
The official Duva client library for Python. Duva is a transactional email API hosted in Canada.
pip install duva-mail
# or: uv add duva-mail
Requires Python 3.10 or later. Fully typed (py.typed); both a synchronous and an asynchronous
client are provided.
Sending a message
from duva import Duva
duva = Duva(api_key="dv_...", domain="example.com") # or DUVA_API_KEY / DUVA_DOMAIN
message = duva.messages.send(
from_="Example <notifications@example.com>",
to=["client@example.org"],
subject="Your order",
text="Thank you for your order.",
)
print(message.id, message.status) # "queued": always asynchronous
The same call, asynchronously:
from duva import AsyncDuva
async with AsyncDuva(api_key="dv_...", domain="example.com") as duva:
message = await duva.messages.send(
from_="Example <notifications@example.com>",
to=["client@example.org"],
subject="Your order",
text="Thank you for your order.",
)
Reading events and pagination
for event in duva.events.list_all(type="bounced"):
print(event.type, event.detail.get("recipient"))
events.list() and suppressions.list() return one page (.data, .next_cursor);
events.list_all() and suppressions.list_all() are generators that follow next_cursor for
you, optionally bounded with max_items=. AsyncDuva's equivalents (list_all) are async
generators (async for).
Verifying a webhook
from duva import WebhookSignatureError, construct_event
try:
event = construct_event(secret, request.headers, raw_body)
print(event.type, event.data.get("message_id"))
except WebhookSignatureError:
# respond 400
...
raw_body must be the exact bytes Duva sent (your framework's raw-body option, not a
re-serialized parsed body): re-encoding it changes the bytes and invalidates the signature.
Rotating your webhook secret? Pass a list — construct_event([old_secret, new_secret], ...) —
while both are active.
Errors
Every error Duva answers with is a DuvaError subclass; rely on .code (the contract), never on
the exception message (its wording can change):
from duva import NotFoundError, QuotaExceededError, ValidationError
try:
duva.messages.send(...)
except ValidationError as error:
print(error.fields) # [{"field": "to[0]", "message": "..."}]
except QuotaExceededError as error:
print(f"retry in {error.retry_after}s")
except NotFoundError:
... # the API key, domain or resource could not be found
Network failures and timeouts raise DuvaConnectionError / DuvaTimeoutError instead (no HTTP
response was ever received). Reads and messages.send (idempotency-key protected) are retried
automatically on a transient failure; suppressions.add/remove and webhooks.create/delete
are not, because the outcome of a timed-out first attempt is unknown. A 429 quota_exceeded is
never retried automatically (its retry_after can be hours); a 429 rate_limited is, as long as
the wait fits within max_retry_wait_seconds (30s by default).
Attachments
from duva import Attachment
attachment = Attachment.from_file("./invoice.pdf")
duva.messages.send(..., attachments=[attachment])
Attachment.from_bytes(filename, content, content_type=None, content_id=None) works from data
already in memory; content_id turns the attachment into an inline image the HTML references
with cid:.
Configuration
| Argument | Default | |
|---|---|---|
api_key |
DUVA_API_KEY |
Required. |
domain |
DUVA_DOMAIN |
Required: the domain this key was created for. |
base_url |
https://api.duva.ca |
|
timeout |
10 (seconds) |
|
max_retries |
2 |
Network failures / 5xx on a safe-to-retry call. |
max_retry_wait_seconds |
30 |
A 429 rate_limited with a longer wait is not retried. |
language |
unset | "en" or "fr": the language of error.message. |
http_client |
a new httpx.Client/AsyncClient |
Inject your own (proxying, tests). |
Full reference
The complete API surface and the OpenAPI specification this library is generated from: https://duva.ca/en/docs and https://duva.ca/openapi.json.
Development
uv sync --extra dev
uv run --extra dev python scripts/generate.py --local # regenerate _generated/models.py from a local ../duva checkout
uv run mypy
uv run pytest # unit tests
uv run --extra dev python scripts/fetch_conformance.py && uv run pytest tests/conformance
uv build
This library's request/response models are generated from Duva's OpenAPI specification
(src/duva/_generated/, never edited by hand); the client itself (retries, pagination, errors,
webhooks) is hand-written and checked against the shared fixtures published in
duva-mail/duva-conformance.
License
MIT, see LICENSE.
Metadata
Release files for duva-mail 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| duva_mail-0.1.0.tar.gz | 72.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| duva_mail-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 95.5 kB
Release files / duva_mail-0.1.0.tar.gz
| Download URL | duva_mail-0.1.0.tar.gz |
|---|---|
| Size | 72.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ad755a78fa67c60115b5b7808bc7d5f39fa17c3282f898d35b09a0d8842984d6
|
|
BLAKE2b-256 checksum How to use checksums |
9e120e7bbd225169fe65cdd8439fd184c71601ecc9e12cab5a6caa25f28ee227
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / duva_mail-0.1.0-py3-none-any.whl
| Download URL | duva_mail-0.1.0-py3-none-any.whl |
|---|---|
| Size | 22.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f5330ee70855574efa862632687be2c4d46298dc4093fddc935665e5d38b6098
|
|
BLAKE2b-256 checksum How to use checksums |
91882baa28b17fc643a1315d7d3b8722bf02fec1018dfec97a9f8b681e820a86
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|