ankusa
The client SDK for Ankusa deployments: one PyPI package meant to bundle everything a non-Elixir consumer needs to talk to an Ankusa deployment. Today that's the claim-check gateway client and a webhook-receiving header helper; more clients (ingest, admin) land here as they're built.
Install
Not published yet. Until the first release, depend on it as a local path,
the same way the Elixir packages in this monorepo depend on ankusa core
before their first Hex release, and packages/sdk-typescript depends on
itself via file:.
With uv:
[project]
dependencies = ["ankusa"]
[tool.uv.sources]
ankusa = { path = "../../packages/sdk-python" }
Or with plain pip:
pip install -e ../../packages/sdk-python
Claim-check client
Redeem a claim-check ref, verify the bytes it returns against the message's
sha256, and classify failures into dead-letter vs. retry,
without holding any object-store credentials. Conforms to the framework's
own contract, priv/openapi/claim_check.v1.yaml:
the spec is the source of truth, this package conforms to it, not the
reverse.
import os
from ankusa import ClaimCheckClient, ClaimCheckError
claim_check = ClaimCheckClient(os.environ.get("CLAIM_CHECK_URL", "http://localhost:4001"))
# A queue message that carries a claim also carries its sha256:
# "claim": "urn:ankusa:claim:v1:<tenant>:<claim_id>" (claim_id: uppercase ULID)
# "sha256": 64-char lowercase hex of the claim's bytes
def resolve_body(message: dict) -> bytes:
try:
return claim_check.redeem(message["claim"], message["sha256"])
except ClaimCheckError as err:
if not err.retryable:
# bad ref/sha256, 404, or an integrity mismatch: dead-letter, don't requeue
raise
# gateway unreachable or 5xx: safe to retry
raise
redeem() does three things GET /v1/claims/... alone doesn't:
- Parses the ref into its tenant id, claim id, and gateway path
(
GET /v1/claims/{tenant_id}/{claim_id}) (parse_claim_ref, also exported standalone). - Fetches the bytes.
- Verifies them against the message's
sha256(the gateway itself does not check this, see "Redeem a claim" indocs/claim-check.md) before ever returning them to you.
Every failure is a ClaimCheckError subclass with a retryable attribute,
so a consumer needs exactly one bit to decide dead-letter vs. retry:
| Class | retryable |
Cause |
|---|---|---|
InvalidClaimRefError |
False |
ref isn't a well-formed claim-check URN, or sha256 isn't 64-char lowercase hex |
ClaimNotFoundError |
False |
gateway 404: expired by retention, or never written |
ClaimRejectedError |
False |
gateway 4xx other than 404 (.status, .body) |
ClaimIntegrityError |
False |
sha256 of the returned bytes doesn't match |
ClaimCheckUnavailableError |
True |
gateway 5xx/503, or unreachable |
health() hits GET /health for a liveness probe.
ClaimCheckClient owns an httpx.Client; use it as a context manager
(with ClaimCheckClient(...) as claim_check:) or call .close() yourself
when done.
Webhook receiver helper
Every receiver of Ankusa's HTTP sink needs the same handful of headers off
each request; parse_headers replaces the hand-rolled self.headers.get(...)
calls with one call and a typed result:
from ankusa import MissingHookIdError, parse_headers
def do_POST(self):
try:
hook = parse_headers(self.headers)
except MissingHookIdError:
self.send_response(400)
self.end_headers()
return
body = self.rfile.read(int(self.headers.get("content-length", "0")))
# hook.id, hook.source, hook.seq, hook.tenant, hook.content_type
...
HookHeaders.id is what a receiver dedupes on: delivery is at-least-once
(see "HTTP handoff" in
docs/integrations.md),
so the same hook can arrive twice after a retry. Header lookup is always
case-insensitive, regardless of whether the mapping passed in already is.
examples/quickstart/worker.py
uses this.
Routes client
Manage route definitions and the global IP rules on the route-management
listener (routes.admin.port, default 4003) — the routes tag of
priv/openapi/admin.v1.yaml.
import os
from ankusa import RoutesClient
routes = RoutesClient(os.environ.get("ROUTES_URL", "http://localhost:4003"))
routes.create_route({"id": "stripe", "path": "/webhooks/stripe"})
routes.get_ip_rules() # {"default": "allow", "rules": []}
routes.test_route({"method": "POST", "path": "/webhooks/stripe", "ip": "203.0.113.7"})
Methods: health(), list_routes(), create_route(), get_route(),
replace_route(), update_route(), delete_route(), get_ip_rules(),
put_ip_rules(), test_route(). Failures are RoutesError subclasses with a
retryable attribute: RouteNotFoundError (404), RoutesRejectedError
(400/409, carrying code, field, message, conflicting_id,
max_routes), and RoutesUnavailableError (5xx/unreachable, retryable).
Admin client
The operator API on admin.port (default 4002): health, Prometheus metrics,
the redacted config, the DLQ, and the quarantine list — the operations,
dlq, and quarantine tags of admin.v1.yaml.
import os
from ankusa import AdminClient
admin = AdminClient(os.environ.get("ADMIN_URL", "http://localhost:4002"))
admin.health() # {"status": "ok", "instance": ..., "roles": [...]}
admin.list_dead_letters(limit=10)
admin.replay_dead_letters({"source_id": "demo"})
admin.list_quarantined()
Methods: health(), metrics() (Prometheus text), config(),
list_dead_letters(), replay_dead_letters(), list_quarantined(). Failures
are AdminError subclasses: RoleNotEnabledError (409, carrying role),
AdminRejectedError (400, carrying code), and AdminUnavailableError
(5xx/unreachable, retryable).
Layout
src/ankusa/
__init__.py # umbrella barrel: re-exports every client this package bundles
py.typed
webhook.py # x-ankusa-* header parsing for HTTP-sink receivers
claim_check/ # the claim-check gateway client
__init__.py # barrel for this client
client.py
ref.py
errors.py
routes/ # the route-management client (routes.admin.port)
__init__.py
client.py
errors.py
admin/ # the operator client (admin.port)
__init__.py
client.py
errors.py
tests/
test_client.py
test_ref.py
test_webhook.py
test_routes.py
test_admin.py
A future client (say, an ingest helper) gets its own src/ankusa/<name>/
directory with the same shape, re-exported from src/ankusa/__init__.py.
Develop
uv sync
uv run pytest
uv run mypy
Metadata
Release files for ankusa 0.2.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ankusa-0.2.4.tar.gz | 47.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ankusa-0.2.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 67.7 kB
Release files / ankusa-0.2.4.tar.gz
| Download URL | ankusa-0.2.4.tar.gz |
|---|---|
| Size | 47.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
98354c7fdef37f5063269e9c0af4421a141c5579289b4f6ec6de46cdd1e9dc56
|
|
BLAKE2b-256 checksum How to use checksums |
2ac07c3a1386491950cca07dd6b16a8553228177c1a64a98eb02a5ca7ab91522
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.
Transparency logRelease files / ankusa-0.2.4-py3-none-any.whl
| Download URL | ankusa-0.2.4-py3-none-any.whl |
|---|---|
| Size | 20.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2cdc72851158003413807e17c0ada3a76123f2d6288724a1d403084011dcd3f2
|
|
BLAKE2b-256 checksum How to use checksums |
758a83c35fdac114fe735537bf8e1939a76e8e2b1df9fca290416fbaa76824ab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.
Transparency log