AceMQ for Python

A Python client for AceMQ messaging over AMQP, speaking the same wire contract as the Java, Go and .NET libraries: the same reserved headers, the same defaults, the same retry arithmetic. A Python consumer reads what a Java producer writes.

That claim is pinned rather than asserted. The fixtures under tests/fixtures/ were generated by the Java implementation and are committed byte for byte by all five libraries: one file for the headers a message carries, and one for the retry arithmetic, the queue names and the topology. A port that hand-copies either out of documentation acquires a difference nobody notices until two languages disagree in production — which has happened, four times in one afternoon, and is what the conformance suite is for.

pip install --extra-index-url https://acemq.org/pypi/simple/ "acemq-amqp[rabbitmq]==0.7.12"

A message, end to end

import asyncio
from datetime import timedelta

from acemq_amqp import (
    Ack, Message, Topology, accept, connect, exponential_retry, retry,
)


async def main() -> None:
    async with await connect(
        "amqp://guest:guest@localhost:5672/",
        origin="checkout@pod-7",
        retry=exponential_retry(5, timedelta(seconds=1), timedelta(minutes=1)),
    ) as mq:
        await mq.declare(
            Topology()
            .exchange("orders-events", "topic")
            .queue("shipping.orders", dead_letter=True)
            .binding("shipping.orders", "orders-events", "order.placed")
        )

        async def ship(message: Message) -> Ack:
            if not warehouse_is_up():
                return retry(RuntimeError("the warehouse is not answering"))
            place(message.payload)
            return accept()

        consumer = await mq.consume("shipping.orders", ship)

        await mq.publisher("orders-events", "order.placed").send({"id": "7"})
        await asyncio.sleep(5)
        await consumer.close()


asyncio.run(main())

Getting started takes this apart line by line, and the tutorials build it up from nothing in four steps.

What it does for you

An envelope on every message. Identity, type, correlation, causation, attempt count, origin and the time it was first published — the same fields the Java, Go, Ruby and .NET libraries write, so a message crossing between them keeps its history. See the envelope.

Retries that stop. A handler returns retry(), and a policy decides whether there is another attempt and how long to wait. A retry is republished rather than requeued, so the attempt count really advances and lives on the message rather than in the memory of the process that has been failing. Long waits are spent in the broker, on a rung queue whose time-to-live is the wait, so a consumer restart cannot shorten them. See retries, redelivery and shutdown.

Security you have to opt out of. The URL scheme decides whether the connection is encrypted, an amqps:// URL verifies the broker against the machine's trust store with no further configuration, and the way out is a function with a long name rather than a keyword argument somebody can paste. See security.

A core with no dependencies at all. pip install acemq-amqp installs the envelope, the codecs, the retry arithmetic and the naming, and nothing else — not even a UUID library. The broker client is the [rabbitmq] extra and the metrics client is [prometheus], because a service that only reads an AceMQ envelope somebody else delivered should not be made to install an AMQP client to do it.

A blocking API for programs that are not asyncio. acemq_amqp.sync is a facade over the same connection: one loop on a thread of its own, handlers on worker threads, and exactly one implementation of the retry rules underneath. See getting started.

Numbers an operator cannot get from outside. Throughput, failures, handler duration and in-flight count through a three-method Observer, and a health check that asks the broker a real question rather than looking at a socket. See metrics, health and tracing.

Typed throughout. The package ships py.typed, is checked under mypy --strict, and everything on the wire is a frozen dataclass.

The layout

One package, a module per concern:

acemq_amqp the whole public API, re-exported: envelopes, codecs, acknowledgements, retry policies, topology, security, telemetry
acemq_amqp.connection connect, Connection, Publisher, Consumer, Message
acemq_amqp.sync the same library, blocking, for programs with no event loop
acemq_amqp.transport what a broker has to provide, described without naming one
acemq_amqp.rabbitmq the RabbitMQ transport, on aio-pika. The only module that knows a broker client exists
acemq_amqp.security TLS modes, trusted authorities, credentials, and the refusal of development certificates
acemq_amqp.devcerts a development certificate authority, broker and client certificate, behind the [crypto] extra
acemq_amqp.topology exchanges, queues, bindings, and the retry rung arguments
acemq_amqp.interceptors the seam around publishing and handling
acemq_amqp.telemetry the Observer interface, in-memory Metrics, health reports
acemq_amqp.prometheus one Observer implementation, behind the [prometheus] extra
acemq_amqp.tracing OpenTelemetry spans for publishing and consuming, behind the [opentelemetry] extra
acemq_amqp.codecs YAML, TOML, XML, Protobuf and Avro — the five formats Java and Go ship, one module and one extra each. XML needs no extra — plus encrypted, which wraps any of them in AES-GCM
acemq_amqp.patterns request-reply, idempotency, outbox, ordering, pipelines, replay, routing slips, sagas, scheduled delivery, consumer groups, schema registry, streams, the claim check
acemq_amqp.patterns.sql the idempotency, outbox and schema-registry seams over any DB-API 2.0 database, with no driver dependency

Everything worth naming is re-exported from the top, so a program imports from one place:

from acemq_amqp import Ack, Envelope, Message, Topology, accept, connect, reject, retry

The exception is the two modules that are a choice rather than the default — acemq_amqp.sync and acemq_amqp.prometheus — and the patterns, which are from acemq_amqp.patterns import ... because a service uses two of the thirteen.

One difference from the other four libraries

Identical, because a message crosses languages: the reserved header names and their types, the defaults applied when they are absent, the retry schedule arithmetic, the {queue}.dlq / {queue}.parked / {queue}.retry.{delay} naming, and the rules for giving up.

Not identical, deliberately: the API shape. Go takes a ctx and returns an error; .NET has IAsyncEnumerable; Python gets async with, frozen dataclasses, keyword arguments and exceptions. Forcing a Java shape onto Python produces a library nobody enjoys using. The contract is portable; the ergonomics are native.

The one place that shows up most is the interceptor. Java has a pair of before-and-after hooks; here an interceptor is a single function handed the message and the rest of the work, because try/finally already runs the way out in the reverse of the way in. See interceptors.

Where to go next

Status

Pre-1.0 and in development. The API may change before 1.0. The contract layer, the transport and the pattern library are implemented and tested, against a real broker as well as a fake one. The current release is 0.7.12, from https://acemq.org/pypi/simple/ — getting started says how to install from it.