All notable changes to babelqueue (Python) are documented here.
The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning.
The envelope wire format is versioned separately by meta.schema_version
(currently 1) — see the contract at babelqueue.com.
- Runtime GDPR field encryption (ADR-0030) — the optional
babelqueue.gdprmodule ports the Go reference to Python: the SDK-enforcement half of governed PII. The babelqueue-registry only declares and auditsx-gdpr-sensitivefields (and can mask them for safe logging); this module enforces them on the wire — a producer encrypts each marked leaf before publish, a consumer decrypts it after decode. Strictly opt-in: a producer/consumer that never callsprotect/unprotectis unchanged. The envelope stays frozen (GR-1): only values insidedatachange — a sensitive leaf's value becomes a ciphertext string — sodatastays pure JSON (an SDK without the key still carries the envelope),meta.schema_versionstays1, andtrace_idis untouched (GR-4). The core stays stdlib-only (GR-7): Python's standard library has no AES-GCM, so the module ships no concrete cipher and pulls no crypto dependency —Cipheris a caller-providedtyping.Protocol(encrypt(bytes) -> str/decrypt(str) -> bytes) bound to a KMS / Vault / HSM / tokenisation service, or to a local AES-256-GCM via the optionalcryptographylibrary (newbabelqueue[gdpr]extra, documented in the README — still not a core dependency).protect(data, schema, cipher)canonically JSON-encodes each marked leaf and replaces it with the cipher's ciphertext string in place;unprotect(...)is the exact inverse, restoringdatabyte-for-byte. The marked paths come from the same per-URN schema the validator already loads —babelqueue.schema.sensitive_pathswalks nested objects (profile.full_name), array items (addresses[].line), container, and root marks, and thex-gdpr-sensitivekeyword is validation-neutral (annotating a schema is never breaking). An absent marked field is skipped (not an error); a re-rununprotecton already-cleartext data is a no-op (non-string leaves are left alone); a wrong key / tampered / non-ciphertext value raises the newbabelqueue.DecryptErrorso the consumer fails the message (retry / dead-letter) rather than handle unreadable PII. Validate cleartext beforeprotect/ afterunprotect, since a schema constraining a sensitive field would reject the ciphertext string. Unit-tested without any crypto dependency (a stdlib-only authenticated fake cipher) for round-trip exactness across nested/array/container/root marks, skips, idempotent re-runs, frozen-envelope decode/validate, and wrong-key failure. The envelope is unchanged (schema_version: 1); this is purely additive.
- Transactional outbox helper (ADR-0029) — the optional
babelqueue.outboxmodule ports the PHPBabelQueue\Outboxhelper to Python, removing the producer dual write: the message is persisted into your database, in the same transaction as the business data (so it commits or rolls back atomically with it), and a separate relay publishes the durable rows afterwards. No distributed transaction; exactly-once handoff into the broker, then at-least-once on the wire (the consumer dedupes onmeta.idvia the idempotency helper, the consumer-side mirror, ADR-0022). The core stays stdlib-only (GR-7):OutboxStoreis an abstracttyping.Protocolthe caller binds to their own DB — the module ships only the in-memoryInMemoryOutboxStorereference and pulls in no DB driver.Outbox.write(envelope)encodes via the frozenEnvelopeCodecand delegates toOutboxStore.saveinside the transaction the caller already opened — it does not begin/commit anything (the caller owns the transaction boundary).OutboxRelay.flush()publishes one batch through the existing publish-onlyTransport, marking each row published only after the transport accepts it, or failed (caught →mark_failed, row left pending, with a bounded linear backoff via an injectable sleeper) so one poison row never blocks the batch;OutboxRelay.drain()loops while a pass makes progress, with a safety ceiling. The relay publishes the stored bytes verbatim — it never decodes, rebuilds or re-encodes the envelope — sotrace_idis preserved end-to-end and the body is byte-compatible across SDKs (GR-1/GR-4/GR-5). Unit-tested without a broker (write stores the encoded envelope byte-identical; relay publishes via a fakeTransport+ marks published; a raising publish →mark_failed, row still pending, batch continues;drainloops to empty and stops on no-progress; backoff grows linearly and caps via the injected sleeper). The envelope is unchanged (schema_version: 1); this is purely additive.
- W3C
traceparentspan-context propagation (OpenTelemetry v0.2, ADR-0028) — the optionalbabelqueue.otelmodule now carries true cross-hop span parent-child linkage, not just shared-trace_idcorrelation. On publish,otel.publishinjects the active span context as a W3Ctraceparenttransport header (and still stampstrace_idfor the v0.1 fallback); on consume,otel.wrap_handlerextracts it and starts the CONSUMER span as a true child of the producer span. With notraceparentpresent it falls back to the v0.1trace_id-derived parent, so it is a strict, backward-compatible upgrade — no regression. The header rides out of band via a new dependency-free core seam —BabelQueue.publish_with_headers(urn, data, headers, …)(produce side) andbabelqueue.headers_from_context()(consume side, surfaced by the runtime) — so the wire envelope stays frozen (schema_version: 1, GR-1) and the core stays zero-dependency (OTel remains the optional[otel]extra, GR-7).traceparentis carried on the in-memory (reference), Redis (a transport-owned__bq_frameJSON frame with bare-value back-compat, so cross-version queues interoperate; degrades to a bare publish in Laravel-compat mode), RabbitMQ (native AMQP header table, beside the contractx-*headers) and SQS (nativeMessageAttributes, beside the contractbq-*attributes) transports; where a transport can't carry headers, propagation degrades cleanly to v0.1trace_idcorrelation with no error. A plainpublishis byte-identical to before. Unit-tested without a broker (frame round-trip + bare back-compat, header merge/extract per transport, and an in-memory producer→consumer parent-child end-to-end with the OTel SDK'sInMemorySpanExporter); broker-gated integration tests assert a publishedtraceparentarrives on the consumed message's headers beside the unchanged body. The envelope is unchanged; this is purely additive. Ships as a MINOR.
1.6.0 - 2026-06-14
- Redis/Laravel reservation parity — the Redis transport can now consume a
shared Laravel BabelQueue Redis queue using Laravel's reserved-set / reliable-queue
semantics, not just a Python-owned queue. Enable it with the
laravel=1URL flag (redis://host:6379/0?laravel=1) orRedisTransport(..., laravel_compat=True). In this mode the key layout is Laravel's stock Redis queue (§1 of the broker-bindings contract): aqueues:<name>ready list, aqueues:<name>:reservedsorted set scored by aretry_afterdeadline (default 60s), aqueues:<name>:delayedset, and aqueues:<name>:notifywake-up list. Reserve, ack and release run the byte-for-byte same Lua scripts Laravel uses, so the reserved member a Python worker writes is identical to a Laravel worker's — either side can ack (ZREM) the other's reservation, so a Python worker and a Laravel worker share one Redis queue without losing or double-processing messages. Before each pop, expired reserved/delayed jobs migrate back to the ready list, so a crashed worker's in-flight job is re-reserved exactly as Laravel does. Tunable via?prefix=/?retry_after=(or thekey_prefix/retry_afterconstructor args). The default Python-owned reliable-queue mode (BLMOVE+<queue>:processinglist) is unchanged and stays the default, so existing callers are unaffected. The reservation logic is fully unit-tested with an injected in-memory Redis double (noredispackage, no broker); a live cross-runtime PHP↔Python shared-queue round-trip is covered by the integration suite where a real Redis is present. The envelope is unchanged (schema_version: 1); this is purely additive. Ships as a MINOR.
- Apache ActiveMQ Artemis transport (
babelqueue[artemis],python-qpid-proton) —ArtemisTransport, selected by theartemis://(orartemis+ssl://) URL scheme (e.g.artemis://localhost:5672; or pass an injectedconnection). Artemis speaks AMQP 1.0 (not RabbitMQ's 0-9-1), so the transport uses thepython-qpid-protonblocking client. Implements §7 of the broker-bindings contract: the canonical envelope is the message body, projected onto the AMQP fields a JMS peer reads —correlation-id=trace_id(JMSCorrelationID),creation-time=meta.created_at(JMSTimestamp), thex-opt-jms-typeannotation = URN (JMSType, the AMQP-JMS mapping), plus thebq-schema-version/bq-source-lang/bq-attempts/bq-app-idapplication properties. Consume reserves one message at a time (receive→ process →accept);attemptsis reconciled tomax(body, delivery_count)— the AMQP delivery-count header is 0-based, so it maps directly with no −1 (the Java JMS binding reads the 1-basedJMSXDeliveryCountand subtracts 1, arriving at the same 0-basedattempts), and themaxnever lowers a higher body count carried by a republish-driven retry. The projection + reconciliation + pop/ack flow are unit-tested with no broker and nopython-qpid-proton(the proton import is lazy; the transport talks to an injected connection fake); the publish flow that builds a real protonMessageis exercised wherever proton is installed. The envelope is unchanged (schema_version: 1); Apache ActiveMQ Artemis is purely additive. Ships as a MINOR.
- Apache Kafka transport (
babelqueue[kafka],confluent-kafka) —KafkaTransport, selected by thekafka://URL scheme (e.g.kafka://host:9092; or pass an injectedproducer+consumer_factory). Implements §6 of the broker-bindings contract: the record value is the canonical envelope, projected onto native Kafka record headers (UTF-8 byte strings) —bq-job= URN,bq-trace-id,bq-message-id, plusbq-schema-version/bq-source-lang/bq-attempts— with the record timestamp mirroringmeta.created_at. Consume is process-then-commit (popreserves viapollwithenable.auto.commit=false,ackcommits the offset); thebq-attemptsheader is the authoritative attempt counter (the body'sattemptsis the fallback for non-BabelQueue producers). The projection + reconciliation + publish/pop/ack flow are unit-tested with no broker and noconfluent-kafka(the kafka import is lazy; the transport talks to injected producer/consumer fakes). The envelope is unchanged (schema_version: 1); Apache Kafka is purely additive. Ships as a MINOR.
- Apache Pulsar transport (
babelqueue[pulsar],pulsar-client) —PulsarTransport, selected by thepulsar://(orpulsar+ssl://) URL scheme (e.g.pulsar://localhost:6650; or pass a builtclient). Implements §5 of the broker-bindings contract: the canonical envelope is the message payload, projected onto native Pulsar message properties (string→string) —bq-job= URN,bq-trace-id=trace_id,bq-message-id=meta.id, plusbq-schema-version/bq-source-lang/bq-attempts; receive →acknowledge;attemptsreconciled tomax(bq-attempts, redelivery_count)(Pulsar's redelivery count is 0-based, so it maps directly with no −1, and themaxnever lowers a higher body count carried by a republish-driven retry). DefaultSharedsubscription namedbabelqueue; a client can be injected for tests/DI. The projection + reconciliation + publish/pop/ack flow are unit-tested with no broker and nopulsar-client(the pulsar import is lazy and publishing sends raw bytes). The envelope is unchanged (schema_version: 1); Apache Pulsar is purely additive. Ships as a MINOR.
1.2.0 - 2026-06-13
- Azure Service Bus transport (
babelqueue[azureservicebus],azure-servicebus) —AsbTransport, selected by thesb://URL scheme (e.g.sb://<namespace>.servicebus.windows.net, Azure AD viaDefaultAzureCredential; or passconnection_string=.../ a builtclient). Implements §4 of the broker-bindings contract: the canonical envelope is the message body, projected onto native Service Bus fields (Subject= URN,CorrelationId=trace_id,MessageId=meta.id, plus thebq-application properties); PeekLock reserve →complete_messageack;attemptsreconciled to the broker-authoritativeDeliveryCount − 1. A client can be injected for tests/DI. The projection + reconciliation are unit-tested with no broker and noazure-servicebus(the azure import is lazy); the publish flow is covered with a fake client. The envelope is unchanged (schema_version: 1); Azure Service Bus is purely additive. Ships as a MINOR.
1.1.0 - 2026-06-12
- Amazon SQS transport (
babelqueue[sqs],boto3) —SqsTransport, selected by thesqs://URL scheme (e.g.sqs://us-east-1?endpoint=http://localhost:4566for LocalStack). Implements §3 of the broker-bindings contract: the canonical envelope is theMessageBody, projected onto nativeMessageAttributes(bq-job/bq-trace-id/bq-message-id/bq-schema-version/bq-source-lang/bq-created-at); visibility-timeout reserve →delete_messageack;attemptsreconciled toApproximateReceiveCount − 1(never lowering a runtime-incremented count). Supports FIFO (MessageGroupId/MessageDeduplicationId=meta.id), content-based dedup, configurable wait/visibility, and aqueue_url_prefixthat skipsGetQueueUrl. A client can be injected for tests/DI. 16 unit tests run against a fake client (no boto3, no broker); a LocalStack integration test round-trips the realboto3path. The envelope is unchanged (schema_version: 1); SQS is purely additive. Ships as a MINOR.
1.0.0 - 2026-06-07
1.0.0 — the public API is now SemVer-stable: breaking changes require a MAJOR,
following the deprecation policy. The wire envelope is unchanged
(schema_version: 1); the core + Celery/Django adapters ship together. Full
reference at babelqueue.com.
- CI adds ruff + mypy static analysis and a >=90% coverage gate
(
pytest --cov --cov-fail-under=90, run in the broker-backed job so the Redis / RabbitMQ transports count). Type-safety fix inredis_transport(str-narrow the BLMOVE reply) surfaced by mypy — no behaviour change. - GR-8 latency benchmark (
tests/test_overhead.py) — asserts the envelope encode/decode path adds ≤2% over plain-JSON serialization vs a conservative 2ms broker round-trip (the pure-Python codec is slower than the compiled SDKs — ~16µs marginal on CPython 3.9/CI — so the reference is higher to stay robust).
0.5.0 - 2026-06-06
- Celery adapter (
babelqueue.celery,[celery]extra) —from_celery(app)builds aBabelQueueruntime on a Celery app's broker, andinstall_worker(app)registers a Celery worker bootstep that drains URN-routed polyglot messages in a background thread alongside Celery's own consumer. - Django adapter (
babelqueue.django,[django]extra) — settings-drivenBABELQUEUEconfig,get_app()/publish()shortcuts, and amanage.py babelqueue_workermanagement command. Add"babelqueue.django"toINSTALLED_APPS. - Both adapters lazy-import their framework, so the core stays dependency-free.
0.4.0 - 2026-06-06
EnvelopeCodec.urn()— resolve the URN (job, acceptingurnas an alias).EnvelopeCodec.accepts()— consumer-side envelope validation (rejects empty URN, unsupportedmeta.schema_version, blanktrace_id, non-objectdata).- Shared cross-SDK conformance suite under
tests/conformance/(vendored from the canonicalconformance/set) plus atest_conformance.pyrunner.
0.3.0 - 2026-06-06
- RabbitMQ transport (
PikaTransport,amqp://): durable queue, persistent delivery,basic_get+ manual ack, and the contract AMQP properties (type=URN,correlation_id=trace_id,x-schema-version/x-source-lang/x-attempts). Optional[amqp]extra (lazypikaimport) — the core stays zero-dep.
0.2.0 - 2026-06-06
- Runtime —
BabelQueue(broker_url=...)app with a@app.handler("urn:...")decorator,publish(), and aconsume()/run()loop. Routes by URN over the canonical envelope;attempts-based retry → opt-in dead-letter queue;on_unknown_urnstrategies (fail/delete/release/dead_letter). - Transports — a pluggable
Transportabstraction withInMemoryTransport(memory://, for tests/local) andRedisTransport(redis://, reliable-queue pattern viaBLMOVE+ a processing list). Redis client is an optional[redis]extra, imported lazily — the core stays zero-dep.
0.1.0 - 2026-06-06
EnvelopeCodec— builds (make,from_message), encodes and decodes the canonical{job, trace_id, data, meta, attempts}envelope (schema_version1). The single Python implementation of the wire format.- Contracts
PolyglotMessage/HasTraceId(typedProtocols). dead_letter.annotate()— additivedead_letterblock builder.UnknownUrnStrategy—fail/delete/release/dead_letter.BabelQueueError/UnknownUrnError.- Golden conformance fixtures under
tests/fixtures/(shared cross-SDK set). py.typed— ships inline type hints (PEP 561).
- Pre-1.0: the public API may change before the
1.0.0tag. - The core has zero runtime dependencies (standard library only); Python
>=3.9.