Metadata-Version: 2.5
Name: ankow
Version: 1.0.0
Summary: Python client for the Ankow email API
Project-URL: Homepage, https://ankow.com
Project-URL: Documentation, https://docs.ankow.com
Project-URL: Organization, https://akcesssible.com
Author: Akcessible Technologies Limited
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Communications :: Email
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# ankow (Python)

The Python client for the Ankow email API — **Feature 027**, specified in
`planning/features/027-python-sdk.md`.

```
pip install ankow
```

Requires Python 3.10 or newer. No dependencies.

## Usage

```python
import os
from ankow import Ankow

ankow = Ankow(os.environ["ANKOW_API_KEY"])

result = ankow.emails.send(
    from_="hello@yourdomain.com",
    to="someone@example.com",
    template_id="tpl_welcome",
    variables={"name": "Ada"},
)

message = ankow.emails.get(result.id)
```

## Three things worth knowing

**Retries reuse one idempotency key.** A retrying client without one sends the
email twice — the first attempt can succeed and still look like a failure. One
key is generated per call and every retry of that call reuses it, so a retry
asks "did this land?" rather than instructing a second send. Pass
`idempotency_key` yourself to deduplicate across process restarts, the only case
the client cannot cover. Retries apply to connection failures and to
429/500/502/503/504; a 4xx below 429 is never retried.

**`from_` carries a trailing underscore, because `from` is a keyword.** It is
the only name in this package that does not match the wire, and the wire name is
what goes out. Everything else — `template_id`, `reply_to`, `failure_reason` —
is spelled exactly as the API spells it, because Python and the API are both
snake_case. **The Node client needs a two-way casing translation to keep its
error messages honest; this one needs none**, which is why it is the smaller of
the two.

**`status`, `failure_reason`, event `type` and `bounce_kind` are open sets.**
New values arrive without a major version — `queued` the day a queue exists,
`opened` the day tracking does. None of them is an `Enum` or a `Literal`, on
purpose: either would turn adding a value into a breaking change for every
caller who matched on it. They are `str`.

## Zero dependencies

A client for two endpoints does not justify pinning `requests` or `httpx` into
every project that installs it — that is a version conflict waiting in someone
else's dependency tree — and `urllib.request` is in the standard library. The
transport is a single callable and is injectable, so tests never open a socket
and a caller who wants connection pooling can supply their own.

## Errors

Every failure the API answered raises an `AnkowError` subclass carrying `type`,
`code`, `status` and **`request_id`** — quote that id in any support thread.

A request that never reached the API raises `AnkowConnectionError` instead,
which is **not** an `AnkowError`: it has no `request_id`, because none exists.

```python
from ankow import AnkowError, RateLimitError

try:
    ankow.emails.send(from_="...", to="...", template_id="...")
except RateLimitError as error:
    print("retry after", error.retry_after_seconds)
except AnkowError as error:
    print(error.code, error.request_id)
```

## Versioning and support

**Semantic versioning** (`MAJOR.MINOR.PATCH`), starting at `1.0.0` (**D84**).

A **breaking change** — anything that bumps `MAJOR` — is a change to a method
signature, a request or response field name, an error class name or its place in
the hierarchy, the default retry behaviour, or the minimum supported Python
version. Everything else is `MINOR` (additive) or `PATCH` (fixes).

**Open sets are not a breaking change.** `status`, `failure_reason`, event
`type` and `bounce_kind` gain values in a `MINOR` release — none of them is an
`Enum` or a `Literal`, on purpose, so adding a value never breaks a caller who
matched on it. They are `str`.

When the next `MAJOR` ships, the previous line receives security and
critical-correctness fixes for **six months**.

## Development

```
uv sync
uv run pytest
uv run ruff check .
uv run mypy
```

These also run in `verify` and in `scripts/local-ci.sh` (stage 8/14), via a
`uv` step added when **TD-079** closed on 2026-09-01.
