Skip to main content

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)

Source distribution for duva-mail 0.1.0
File Size Uploaded
duva_mail-0.1.0.tar.gz 72.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for duva-mail 0.1.0
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.0 This release

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page