babelqueue 1.22.0__tar.gz → 1.24.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. {babelqueue-1.22.0 → babelqueue-1.24.0}/.github/workflows/ci.yml +1 -1
  2. {babelqueue-1.22.0 → babelqueue-1.24.0}/.github/workflows/release.yml +2 -2
  3. {babelqueue-1.22.0 → babelqueue-1.24.0}/CHANGELOG.md +135 -0
  4. {babelqueue-1.22.0 → babelqueue-1.24.0}/PKG-INFO +41 -7
  5. {babelqueue-1.22.0 → babelqueue-1.24.0}/README.md +40 -6
  6. {babelqueue-1.22.0 → babelqueue-1.24.0}/pyproject.toml +1 -1
  7. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/__init__.py +7 -1
  8. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/_envelope_shape.py +1 -1
  9. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/app.py +201 -24
  10. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/artemis_transport.py +2 -1
  11. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/asb_transport.py +37 -0
  12. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/codec.py +46 -1
  13. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/kafka_transport.py +8 -2
  14. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/pika_transport.py +3 -1
  15. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/pulsar_transport.py +35 -2
  16. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/redis_transport.py +23 -13
  17. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/sqs_transport.py +40 -0
  18. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/transport.py +67 -0
  19. babelqueue-1.24.0/tests/conformance/CONFORMANCE_VERSION +1 -0
  20. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/manifest.json +1522 -1
  21. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/schema/results.example.json +1 -1
  22. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_app.py +137 -4
  23. babelqueue-1.24.0/tests/test_asb_conformance.py +177 -0
  24. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_asb_transport.py +148 -1
  25. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_behaviour_conformance.py +281 -4
  26. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_conformance.py +6 -0
  27. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_kafka_retry.py +56 -0
  28. babelqueue-1.24.0/tests/test_poison_routing.py +581 -0
  29. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_pulsar_transport.py +4 -0
  30. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_redis_transport.py +92 -5
  31. babelqueue-1.24.0/tests/test_sqs_conformance.py +155 -0
  32. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_sqs_transport.py +99 -13
  33. babelqueue-1.22.0/tests/conformance/CONFORMANCE_VERSION +0 -1
  34. babelqueue-1.22.0/tests/test_asb_conformance.py +0 -84
  35. babelqueue-1.22.0/tests/test_sqs_conformance.py +0 -80
  36. {babelqueue-1.22.0 → babelqueue-1.24.0}/.github/FUNDING.yml +0 -0
  37. {babelqueue-1.22.0 → babelqueue-1.24.0}/.github/dependabot.yml +0 -0
  38. {babelqueue-1.22.0 → babelqueue-1.24.0}/.gitignore +0 -0
  39. {babelqueue-1.22.0 → babelqueue-1.24.0}/LICENSE +0 -0
  40. {babelqueue-1.22.0 → babelqueue-1.24.0}/conformance-known-gaps.json +0 -0
  41. {babelqueue-1.22.0 → babelqueue-1.24.0}/scripts/check_conformance_results.py +0 -0
  42. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/_poison_record.py +0 -0
  43. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/celery.py +0 -0
  44. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/contracts.py +0 -0
  45. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/dead_letter.py +0 -0
  46. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/django/__init__.py +0 -0
  47. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/django/apps.py +0 -0
  48. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/django/management/__init__.py +0 -0
  49. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/django/management/commands/__init__.py +0 -0
  50. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/django/management/commands/babelqueue_worker.py +0 -0
  51. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/exceptions.py +0 -0
  52. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/gdpr.py +0 -0
  53. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/headers.py +0 -0
  54. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/idempotency.py +0 -0
  55. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/kafka_retry.py +0 -0
  56. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/otel.py +0 -0
  57. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/outbox.py +0 -0
  58. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/py.typed +0 -0
  59. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/redrive.py +0 -0
  60. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/replay.py +0 -0
  61. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/routing.py +0 -0
  62. {babelqueue-1.22.0 → babelqueue-1.24.0}/src/babelqueue/schema.py +0 -0
  63. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/blank-ascii-job-urn-fallback.json +0 -0
  64. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/data-array-rejected.json +0 -0
  65. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/dead-letter-roundtrip.json +0 -0
  66. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/dead-lettered.json +0 -0
  67. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/duplicate-keys-last-wins.json +0 -0
  68. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/empty-data-roundtrip.json +0 -0
  69. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/forbidden-key-job-and-urn.json +0 -0
  70. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/forbidden-key-job-nonascii-space-and-urn.json +0 -0
  71. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/forbidden-key-meta-attempts.json +0 -0
  72. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/forbidden-key-meta-max-retries.json +0 -0
  73. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/forbidden-key-meta-source.json +0 -0
  74. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/forbidden-key-meta-ts.json +0 -0
  75. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/forbidden-key-timestamp.json +0 -0
  76. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/invalid-missing-urn.json +0 -0
  77. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/invalid-unknown-schema-version.json +0 -0
  78. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/invalid-wrong-case-job.json +0 -0
  79. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-array-urn-fallback.json +0 -0
  80. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-bool-urn-fallback.json +0 -0
  81. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-empty-string-urn-fallback.json +0 -0
  82. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-missing-job-array-urn.json +0 -0
  83. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-null-no-urn.json +0 -0
  84. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-null-urn-fallback.json +0 -0
  85. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-number-blank-urn.json +0 -0
  86. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-number-nonstring-urn.json +0 -0
  87. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-number-urn-fallback.json +0 -0
  88. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-object-urn-fallback.json +0 -0
  89. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/meta-ext-vendor-roundtrip.json +0 -0
  90. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/order-created.json +0 -0
  91. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-job-cr.json +0 -0
  92. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-job-ff.json +0 -0
  93. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-job-lf.json +0 -0
  94. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-job-space.json +0 -0
  95. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-job-tab.json +0 -0
  96. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-job-vt.json +0 -0
  97. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-trace-id-cr.json +0 -0
  98. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-trace-id-ff.json +0 -0
  99. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-trace-id-lf.json +0 -0
  100. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-trace-id-space.json +0 -0
  101. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-trace-id-tab.json +0 -0
  102. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-trace-id-vt.json +0 -0
  103. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-job-and-urn-both-blank.json +0 -0
  104. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-reemit-job-ascii-padded.json +0 -0
  105. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-reemit-job-unicode-padded.json +0 -0
  106. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-reemit-trace-id-padded.json +0 -0
  107. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-unicode-job-emsp.json +0 -0
  108. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-unicode-job-ideographic.json +0 -0
  109. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-unicode-job-nbsp.json +0 -0
  110. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-urn-only-ascii-blank.json +0 -0
  111. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-urn-only-unicode-space.json +0 -0
  112. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/unicode-and-numbers.json +0 -0
  113. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/unknown-lang.json +0 -0
  114. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/unknown-meta-roundtrip.json +0 -0
  115. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/unknown-toplevel-roundtrip.json +0 -0
  116. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/urn-alias.json +0 -0
  117. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/urn-only-roundtrip.json +0 -0
  118. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/fixtures/wrong-case-keys-roundtrip.json +0 -0
  119. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/schema/message-envelope.schema.json +0 -0
  120. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/conformance/schema/results.schema.json +0 -0
  121. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/fakes/confluent_kafka/__init__.py +0 -0
  122. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/fakes/confluent_kafka/admin.py +0 -0
  123. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/fixtures/dead-lettered.json +0 -0
  124. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/fixtures/order-created.json +0 -0
  125. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/redis_backend.py +0 -0
  126. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_artemis_conformance.py +0 -0
  127. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_artemis_transport.py +0 -0
  128. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_celery.py +0 -0
  129. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_codec.py +0 -0
  130. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_dead_letter.py +0 -0
  131. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_django.py +0 -0
  132. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_gdpr.py +0 -0
  133. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_headers.py +0 -0
  134. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_idempotency.py +0 -0
  135. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_kafka_conformance.py +0 -0
  136. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_kafka_integration.py +0 -0
  137. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_kafka_integration_logic.py +0 -0
  138. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_kafka_transport.py +0 -0
  139. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_otel.py +0 -0
  140. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_outbox.py +0 -0
  141. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_overhead.py +0 -0
  142. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_pika_transport.py +0 -0
  143. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_pulsar_conformance.py +0 -0
  144. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_redrive.py +0 -0
  145. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_reencode_regression.py +0 -0
  146. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_replay.py +0 -0
  147. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_schema.py +0 -0
  148. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_schema_conformance.py +0 -0
  149. {babelqueue-1.22.0 → babelqueue-1.24.0}/tests/test_shutdown.py +0 -0
@@ -177,7 +177,7 @@ jobs:
177
177
  --health-retries 10
178
178
  env:
179
179
  # The pinned conformance release (ADR-0032). Bump together with tests/conformance/.
180
- BQ_CONFORMANCE_VERSION: 1.6.0
180
+ BQ_CONFORMANCE_VERSION: 1.8.0
181
181
  # sha256 of that release's fetch.sh: the script runs on the runner, so it is pinned rather
182
182
  # than trusted from a mutable tag. Bump together with BQ_CONFORMANCE_VERSION.
183
183
  BQ_FETCH_SH_SHA256: 8bb6ba567b288fb0ac71a70fd17041b2503df119e01998d76fdad372570bf849
@@ -14,7 +14,7 @@ jobs:
14
14
  # Recommended for PyPI Trusted Publishing — configure a "pypi" environment +
15
15
  # a trusted publisher on PyPI (repo BabelQueue/babelqueue-python, this workflow).
16
16
  environment: pypi
17
- # redis_layout (conformance 1.6.0) drives the real Lua scripts: a real redis:7 is mandatory,
17
+ # redis_layout (conformance 1.7.0+) drives the real Lua scripts: a real redis:7 is mandatory,
18
18
  # exactly as in ci.yml's `conformance` job — without it the cases are reported as failures.
19
19
  services:
20
20
  redis:
@@ -28,7 +28,7 @@ jobs:
28
28
  --health-retries 10
29
29
  env:
30
30
  # The pinned conformance release (ADR-0032). Keep identical to ci.yml.
31
- BQ_CONFORMANCE_VERSION: 1.6.0
31
+ BQ_CONFORMANCE_VERSION: 1.8.0
32
32
  BQ_FETCH_SH_SHA256: 8bb6ba567b288fb0ac71a70fd17041b2503df119e01998d76fdad372570bf849
33
33
  permissions:
34
34
  id-token: write # PyPI Trusted Publishing (OIDC) — no API token needed
@@ -9,6 +9,141 @@ The envelope wire format is versioned separately by `meta.schema_version`
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [1.24.0] - 2026-10-26
13
+
14
+ Implements conformance **1.8.0** (vendored from the `v1.8.0` release asset, verified with
15
+ `fetch.sh --check`; `BQ_CONFORMANCE_VERSION` is now `1.8.0` in `ci.yml` and `release.yml`). The
16
+ envelope stays at `schema_version: 1`; nothing here touches the wire (ADR-0037). The new
17
+ `version_gate_pulsar`, `version_gate_artemis`, `version_gate_sqs` and `version_gate_asb` sections run
18
+ and pass (17 cases each; Python has the `on_unknown_urn` option, so no row is skipped). A MINOR.
19
+
20
+ ### Changed (behaviour)
21
+ - **With the DLQ off, SQS and Azure Service Bus no longer drop a version-gated message or retry a
22
+ poison body; they are left to the broker's native mechanism (ADR-0037 decision 1 rule 3 and
23
+ decision 2 "Go and Python"; broker-bindings 3.5, 4.5, 5.7; owner decision 2026-10-25).**
24
+ - *Version-gated message* (an unsupported `bq-schema-version`), `dead_letter` off and a strategy
25
+ other than `DELETE` or `DEAD_LETTER`: it used to be acknowledged and dropped. It is now **not**
26
+ acknowledged. SQS: no `DeleteMessage`; visibility expiry redelivers it (the gate rejects it
27
+ again) and the queue's `RedrivePolicy` moves it to the native DLQ after `maxReceiveCount`. Azure
28
+ Service Bus: an explicit native `DeadLetter` (reason `unsupported schema version`) into
29
+ `$DeadLetterQueue` instead of `Complete`.
30
+ - *Poison body* (invalid JSON or an identity-less object), `dead_letter` off and a strategy other
31
+ than `DELETE` or `DEAD_LETTER`: it used to take the bounded `FAIL` path (re-publish an empty
32
+ envelope up to `max_attempts`, then drop). On SQS and Azure Service Bus it is now settled
33
+ natively **on the first delivery** exactly as above (Azure Service Bus reason `poison`): no
34
+ re-publish, no retry, no ack, body unchanged, the event is logged. Every other transport
35
+ (Pulsar, Artemis, AMQP, Kafka, Redis, memory) keeps its behaviour; `DELETE` (ack and drop),
36
+ `dead_letter=True` and the `DEAD_LETTER` strategy (the raw record, then ack) are unchanged on
37
+ SQS and Azure Service Bus too.
38
+ - A deployment that relied on SQS/Azure Service Bus silently dropping these messages now needs a
39
+ `RedrivePolicy` (SQS) or accepts them in `$DeadLetterQueue` (Azure Service Bus), or sets
40
+ `dead_letter=True` / `on_unknown_urn=DELETE`.
41
+
42
+ ### Fixed
43
+ - **SQS: the default dead-letter queue name is now `<queue>-dlq` (FIFO `<queue>-dlq.fifo`) (ADR-0037
44
+ decision 7).** The old default `<queue>.dlq` was never a valid Amazon SQS queue name (a name may hold only
45
+ alphanumerics, hyphens and underscores, plus one `.fifo` suffix), so a DLQ write by default could not find a
46
+ queue on real SQS. `dead_letter_suffix` now defaults to `None` ("not set"); unless `dead_letter_queue` is set
47
+ the runtime asks the transport's new optional `DeadLetterNamer` capability
48
+ (`dead_letter_queue_name(queue, suffix)`, exported from `babelqueue`), which `SqsTransport` implements. The
49
+ runtime also hands it an explicit `dead_letter_suffix`; on a FIFO queue the `.fifo` suffix always stays last,
50
+ an explicit suffix included: `orders.fifo` with `dead_letter_suffix="-x"` is `orders-x.fifo`, never
51
+ `orders.fifo-x` (an SQS FIFO name must end in `.fifo`; before, that explicit suffix produced the uncreatable
52
+ `orders.fifo-x`). An explicit queue name behaves as before; every other transport keeps `<queue>.dlq` (the
53
+ attribute `App.dead_letter_suffix` is `None` instead of `".dlq"` until set). Deployments
54
+ that relied on the SQS default must name the DLQ `<queue>-dlq`, or pass `dead_letter_suffix=".dlq"` /
55
+ `dead_letter_queue=` to keep the old name. An explicitly empty `dead_letter_suffix=""` now counts as "no suffix" on SQS (`-dlq`); before, it made the DLQ the source queue itself.
56
+
57
+ ### Added
58
+ - **`babelqueue.transport.NativeSettler`**, an optional `Transport` capability
59
+ (`settle_native(message, reason, description)`). With the DLQ off the runtime calls it instead of
60
+ acknowledging a version-gated message (and, on the poison route, instead of the bounded retry) on
61
+ a transport that offers it; the decision follows the capability, never the transport's name.
62
+ `SqsTransport.settle_native` is a deliberate no-op on the wire; `AsbTransport.settle_native`
63
+ dead-letters natively (falling back to `abandon_message` for a receiver without
64
+ `dead_letter_message`). Transports without it are unchanged.
65
+ - **`babelqueue.transport.Abandoner` and `AsbTransport.abandon`.** When the dead-letter write of a
66
+ *version-gated* message fails, Azure Service Bus now `Abandon`s the peek-lock (broker-bindings 4.5,
67
+ "A failed write `Abandon`s it") so the broker redelivers it at once, instead of leaving it locked until
68
+ the lock lapses (up to 5 minutes). Only that branch abandons (owner decision 2026-10-25): `AsbTransport`
69
+ is deliberately **not** a `Rewinder`, so a graceful-shutdown release and a failed retry re-publish keep
70
+ their previous handling (a copy is re-published and the original completed), and a failed dead-letter
71
+ write of a poison body is still left to its lock. Tests pin all three. The `version_gate_asb` runner
72
+ asserts the `Abandon` on its `unsettled` rows.
73
+ - Conformance `version_gate_*` runners (`version_gate_pulsar` / `version_gate_artemis` through the
74
+ runtime, `version_gate_sqs` / `version_gate_asb` through the real transports) and their entries in
75
+ the results writer.
76
+
77
+ ## [1.23.0] - 2026-10-25
78
+
79
+ Implements ADR-0036 (post-R1 parity) decisions 2, 4, 5 and 6. A MINOR: every change is a
80
+ consumer-side settle decision, so the envelope stays at `schema_version: 1`. The vendored
81
+ conformance suite moves to v1.7.0 (new sections `identity_verdict` and `pulsar_exhaustion`, both
82
+ passing; `conformance-known-gaps.json` stays empty).
83
+
84
+ ### Changed (behaviour)
85
+ - **A body that does not decode, or has no identity, is poison on every transport (ADR-0036
86
+ decisions 4 and 6).** A JSON object that has neither a valid string `job` nor a valid string
87
+ `urn` (ADR-0032 section 6) used to be an *unknown URN*, and on SQS, AMQP, Pulsar, Azure Service
88
+ Bus, Artemis and the memory transport a body that did not decode was decoded leniently into an
89
+ empty envelope and retried as an unknown URN, finally dead-lettered as `reason: "unknown_urn"`
90
+ without the original body. Both are now **poison** and take the poison route of Go's
91
+ `routePoison`: they are never handed to a handler and never take the unknown-URN strategies
92
+ (those remain for a valid envelope whose URN has no handler). With the DLQ on (`dead_letter=True`
93
+ or `on_unknown_urn=DEAD_LETTER`) the message is written to the DLQ at once, with no retry, as the
94
+ ADR-0035 record (`dead_letter.reason: "poison"`, the original body preserved: a strict JSON
95
+ object spliced byte for byte, anything else as `raw`, base64 plus `raw_encoding` when it is not
96
+ valid UTF-8) and acknowledged after the write; `on_unknown_urn=DELETE` drops it (logged); with
97
+ the DLQ off `RELEASE` is replaced by the bounded `FAIL` path (re-published with `attempts + 1`
98
+ up to `max_attempts`, then dropped and logged). An operator who relied on a poison body being
99
+ retried through the unknown-URN strategy, or on a `reason: "unknown_urn"` record for it, sees the
100
+ new behaviour. The classification is `EnvelopeCodec.poison_cause(raw)` (new, pure): the typed
101
+ decode of the Go reference plus the identity rule; `decode` / `decode_lenient` are unchanged.
102
+ - **Pulsar poison records carry `reason: "poison"`** (they were `unknown_urn`). The Pulsar
103
+ transport also reports the reconciled `bq-attempts` / redelivery count to the runtime
104
+ (`ReceivedMessage.native_attempts`, new), as does SQS (`ApproximateReceiveCount - 1`), so a poison
105
+ body, which cannot carry `attempts`, is still bounded by `max_attempts`.
106
+ - **Redis: an identity-less body is dead-lettered unconditionally (ADR-0036 decision 4,
107
+ broker-bindings section 1.1 "Identity-less body").** In both layouts (legacy `:processing`
108
+ and Laravel-compatible zset) a body that decodes but names no identity (including a bare `null`,
109
+ which used to follow the bounded `FAIL` path) is handed over as transport poison: it is written
110
+ to `<queue>.dlq` as the ADR-0035 record with `reason: "poison"` in every configuration
111
+ (`on_unknown_urn=DELETE` and dead-lettering off change nothing), never handed to a handler and
112
+ never retried. Nothing else in the Redis transport changes.
113
+ - **Kafka without retry tiers: a poison record no longer blocks the partition (ADR-0036 decision
114
+ 5, broker-bindings section 6.5 degrade-to-delete).** With no `retry_tiers_ms`, dead-lettering off
115
+ and `on_unknown_urn` not `DELETE`, the bounded retry of a poison record (including an
116
+ identity-less body) needs a tier and raised, leaving the record uncommitted with `bq-attempts`
117
+ never advancing. It is now committed, dropped and logged, and the partition advances. A failing
118
+ handler and `RELEASE` of a valid envelope keep the section 6.5 rule (they still raise
119
+ `BabelQueueError` without tiers). The start-up warning for a tierless transport now says so; enable
120
+ `dead_letter` to keep such records in `<topic>.dlq`.
121
+ - A message dropped after its retries are exhausted with no DLQ configured is now logged
122
+ (`babelqueue.app`, WARNING) instead of being dropped silently.
123
+
124
+ - **Pulsar: a failed DLQ write or retry publish is negatively acknowledged (Go `Rewind`).**
125
+ `PulsarTransport` is now a `Rewinder` (`negative_acknowledge`), so the unsettled original is
126
+ redelivered by the broker instead of staying un-acknowledged for the life of the consumer. A
127
+ graceful-shutdown release of an in-flight Pulsar message is now a nack as well rather than a
128
+ re-publish (on a `Shared` subscription the native redelivery count advances; on `Exclusive` /
129
+ `Failover` it does not, which is harmless: `bq-attempts` carries the attempt limit).
130
+
131
+ ### Fixed
132
+ - **Bytes that are not valid UTF-8 no longer crash the AMQP, Pulsar and Artemis consumers.** They
133
+ were decoded strictly in `pop`; they are now kept losslessly (`surrogateescape`). Invalid UTF-8
134
+ is poison wherever it sits, **also inside a JSON string** (`EnvelopeCodec.poison_cause`,
135
+ error-handling section 7): it is never handed to a handler and is dead-lettered as the ADR-0035
136
+ base64 record, like on Redis and Kafka.
137
+
138
+ ### Added
139
+ - `EnvelopeCodec.poison_cause(raw, *, require_identity=True)`; `Transport.classifies_poison`
140
+ (Kafka and Redis classify poison themselves and opt out of the runtime's classification);
141
+ `ReceivedMessage.native_attempts`.
142
+ - Conformance runners for `identity_verdict` (the core verdict on all 97 bodies, plus the fate on
143
+ the memory transport) and `pulsar_exhaustion` (all 29 rows, driven through the real
144
+ `PulsarTransport` and runtime over a duck-typed client), and per-transport poison tests.
145
+ - `ci.yml` / `release.yml` pin conformance `1.7.0` (the `fetch.sh` SHA-256 is unchanged).
146
+
12
147
  ## [1.22.0] - 2026-10-24
13
148
 
14
149
  Implements ADR-0033 (Redis) and ADR-0034 (Kafka tiers, backoff, poison). The envelope stays at
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: babelqueue
3
- Version: 1.22.0
3
+ Version: 1.24.0
4
4
  Summary: Polyglot Queues, Simplified — the Python core: the canonical BabelQueue wire-envelope codec, contracts and dead-letter helpers.
5
5
  Project-URL: Homepage, https://babelqueue.com
6
6
  Project-URL: Source, https://github.com/BabelQueue/babelqueue-python
@@ -212,8 +212,13 @@ untrimmed; Azure Service Bus and Artemis also accept the integer `1`). A message
212
212
  anything else is not decoded: it reaches the runtime on the reserved route
213
213
  `babelqueue:unsupported-schema-version:<v>` (the version percent-escaped) and is **never
214
214
  retried** — it goes straight to the dead-letter queue (the incoming body, byte-identical,
215
- with no `dead_letter` block) when `dead_letter` is enabled, and is dropped otherwise, whatever `max_attempts` is.
216
- It does not depend on a broker `RedrivePolicy`. If the dead-letter write fails the message is
215
+ with no `dead_letter` block) when `dead_letter` is enabled (or `on_unknown_urn=DEAD_LETTER`), and
216
+ `on_unknown_urn=DELETE` drops it, whatever `max_attempts` is. With `dead_letter` off, **SQS and Azure
217
+ Service Bus no longer drop it** (ADR-0037): it is left to the broker's native mechanism and logged
218
+ (SQS: no `DeleteMessage`, so visibility expiry redelivers it — the gate rejects it again — and the
219
+ queue's `RedrivePolicy` moves it to the native DLQ after `maxReceiveCount`; Azure Service Bus: an
220
+ explicit native `DeadLetter` with reason `unsupported schema version` into `$DeadLetterQueue`
221
+ instead of `Complete`). Kafka, Artemis and Pulsar still acknowledge and drop it. If the dead-letter write fails the message is
217
222
  left unacknowledged (an error is logged), is never retried and never reaches a handler; the
218
223
  dead-letter write is attempted again when the broker brings it back, which depends on the
219
224
  broker:
@@ -253,10 +258,17 @@ a warning. A **per-message delay** at publish time is
253
258
  standard queue; a larger value, or any per-message delay on a FIFO queue, raises
254
259
  `BabelQueueError` instead of being truncated or dropped).
255
260
 
261
+ **Poison bodies.** A body that does not decode, or has no identity, is never retried by the SDK
262
+ (ADR-0037): `dead_letter=True` writes the ADR-0035 record to the DLQ (`<queue>-dlq` on SQS, `<queue>.dlq` elsewhere) and then deletes it,
263
+ `on_unknown_urn=DELETE` deletes it, and with the DLQ off it is **not deleted and not re-sent**: the
264
+ transport's `settle_native` capability leaves it to SQS on the first delivery (visibility expiry,
265
+ then the queue's `RedrivePolicy`; the event is logged). Azure Service Bus does the same with a native
266
+ `DeadLetter` (reason `poison`). Every other transport keeps the bounded retry up to `max_attempts`.
267
+
256
268
  **Poison-loop risk:** a message that always fails is redelivered at once until
257
269
  `max_attempts` is reached. If the receive count is unavailable the SDK cannot
258
270
  advance attempts at all. **Configure a native `RedrivePolicy`**
259
- (`maxReceiveCount` ≥ `max_attempts`, pointing at `<queue>.dlq`) on every SQS
271
+ (`maxReceiveCount` ≥ `max_attempts`, pointing at `<queue>-dlq`, the SQS default DLQ name: FIFO `<queue>-dlq.fifo`, because an SQS queue name cannot contain a dot; `dead_letter_queue` overrides it, and `dead_letter_suffix` replaces `-dlq`, placed before `.fifo` on a FIFO queue) on every SQS
260
272
  queue as the broker-side backstop, and set a non-zero `retry_backoff` if the
261
273
  handler's dependencies need time to recover.
262
274
 
@@ -313,9 +325,31 @@ the DLQ on (`dead_letter` or `on_unknown_urn=DEAD_LETTER`) it is dead-lettered o
313
325
  written to `<topic>.dlq` (which must exist) and its offset is committed once that write
314
326
  is confirmed; a failed write leaves it uncommitted and rewound. With the DLQ off nothing is written:
315
327
  `on_unknown_urn=DELETE` drops it, any other strategy retries it through the retry tiers up to
316
- `max_attempts` and then drops it (without tiers the record is redelivered in place, as above). On
317
- the Redis legacy (`<queue>:processing`) layout a body that Go's typed decode rejects (neither a JSON
318
- object nor `null`, or wrong member types) is dead-lettered raw on its first
328
+ `max_attempts` and then drops it. **Without retry tiers** that bounded retry has no tier to use, so
329
+ (with the DLQ off and `on_unknown_urn` not `DELETE`) the poison record is committed, dropped and logged
330
+ instead of blocking the partition (section 6.5 degrade-to-delete, ADR-0036 decision 5); enable
331
+ `dead_letter` to keep it in `<topic>.dlq`. A failing handler without tiers still cannot be retried, as above.
332
+
333
+ ### Poison messages on every other transport
334
+
335
+ A message body that does not decode into an envelope (not a JSON object, malformed, a wrongly typed
336
+ member, or bytes that are not valid UTF-8, even inside a JSON string) or that has neither a valid
337
+ string `job` nor a valid string `urn` is **poison** on every transport (ADR-0036 decisions 2, 4 and
338
+ 6; conformance `identity_verdict`). It is never handed to a handler and never takes the
339
+ unknown-URN strategies, which are for a valid envelope whose URN has no handler:
340
+
341
+ - DLQ on (`dead_letter` or `on_unknown_urn=DEAD_LETTER`): written to `<queue>.dlq` at once, with no
342
+ retry, as the ADR-0035 record (`dead_letter.reason: "poison"`, the original body preserved,
343
+ base64 plus `raw_encoding` when it is not UTF-8), then acknowledged. A failed write leaves it
344
+ unacknowledged.
345
+ - `on_unknown_urn=DELETE`: acknowledged and dropped (logged).
346
+ - DLQ off: the bounded `FAIL` path (`RELEASE` is replaced by it): retried up to `max_attempts`, then
347
+ dropped and logged. The count comes from the broker where it has one (SQS receive count, Pulsar
348
+ `bq-attempts` / redelivery count), otherwise from a bare `{"attempts": n}` body the retry leaves.
349
+
350
+ On Redis (both layouts) an identity-less body is dead-lettered as poison in **every** configuration,
351
+ including `on_unknown_urn=DELETE` and dead-lettering off (broker-bindings section 1.1). On the legacy
352
+ (`<queue>:processing`) layout a body that Go's typed decode rejects is dead-lettered raw on its first
319
353
  delivery and removed from `:processing`.
320
354
 
321
355
  ### Sharing a Redis queue with Laravel
@@ -158,8 +158,13 @@ untrimmed; Azure Service Bus and Artemis also accept the integer `1`). A message
158
158
  anything else is not decoded: it reaches the runtime on the reserved route
159
159
  `babelqueue:unsupported-schema-version:<v>` (the version percent-escaped) and is **never
160
160
  retried** — it goes straight to the dead-letter queue (the incoming body, byte-identical,
161
- with no `dead_letter` block) when `dead_letter` is enabled, and is dropped otherwise, whatever `max_attempts` is.
162
- It does not depend on a broker `RedrivePolicy`. If the dead-letter write fails the message is
161
+ with no `dead_letter` block) when `dead_letter` is enabled (or `on_unknown_urn=DEAD_LETTER`), and
162
+ `on_unknown_urn=DELETE` drops it, whatever `max_attempts` is. With `dead_letter` off, **SQS and Azure
163
+ Service Bus no longer drop it** (ADR-0037): it is left to the broker's native mechanism and logged
164
+ (SQS: no `DeleteMessage`, so visibility expiry redelivers it — the gate rejects it again — and the
165
+ queue's `RedrivePolicy` moves it to the native DLQ after `maxReceiveCount`; Azure Service Bus: an
166
+ explicit native `DeadLetter` with reason `unsupported schema version` into `$DeadLetterQueue`
167
+ instead of `Complete`). Kafka, Artemis and Pulsar still acknowledge and drop it. If the dead-letter write fails the message is
163
168
  left unacknowledged (an error is logged), is never retried and never reaches a handler; the
164
169
  dead-letter write is attempted again when the broker brings it back, which depends on the
165
170
  broker:
@@ -199,10 +204,17 @@ a warning. A **per-message delay** at publish time is
199
204
  standard queue; a larger value, or any per-message delay on a FIFO queue, raises
200
205
  `BabelQueueError` instead of being truncated or dropped).
201
206
 
207
+ **Poison bodies.** A body that does not decode, or has no identity, is never retried by the SDK
208
+ (ADR-0037): `dead_letter=True` writes the ADR-0035 record to the DLQ (`<queue>-dlq` on SQS, `<queue>.dlq` elsewhere) and then deletes it,
209
+ `on_unknown_urn=DELETE` deletes it, and with the DLQ off it is **not deleted and not re-sent**: the
210
+ transport's `settle_native` capability leaves it to SQS on the first delivery (visibility expiry,
211
+ then the queue's `RedrivePolicy`; the event is logged). Azure Service Bus does the same with a native
212
+ `DeadLetter` (reason `poison`). Every other transport keeps the bounded retry up to `max_attempts`.
213
+
202
214
  **Poison-loop risk:** a message that always fails is redelivered at once until
203
215
  `max_attempts` is reached. If the receive count is unavailable the SDK cannot
204
216
  advance attempts at all. **Configure a native `RedrivePolicy`**
205
- (`maxReceiveCount` ≥ `max_attempts`, pointing at `<queue>.dlq`) on every SQS
217
+ (`maxReceiveCount` ≥ `max_attempts`, pointing at `<queue>-dlq`, the SQS default DLQ name: FIFO `<queue>-dlq.fifo`, because an SQS queue name cannot contain a dot; `dead_letter_queue` overrides it, and `dead_letter_suffix` replaces `-dlq`, placed before `.fifo` on a FIFO queue) on every SQS
206
218
  queue as the broker-side backstop, and set a non-zero `retry_backoff` if the
207
219
  handler's dependencies need time to recover.
208
220
 
@@ -259,9 +271,31 @@ the DLQ on (`dead_letter` or `on_unknown_urn=DEAD_LETTER`) it is dead-lettered o
259
271
  written to `<topic>.dlq` (which must exist) and its offset is committed once that write
260
272
  is confirmed; a failed write leaves it uncommitted and rewound. With the DLQ off nothing is written:
261
273
  `on_unknown_urn=DELETE` drops it, any other strategy retries it through the retry tiers up to
262
- `max_attempts` and then drops it (without tiers the record is redelivered in place, as above). On
263
- the Redis legacy (`<queue>:processing`) layout a body that Go's typed decode rejects (neither a JSON
264
- object nor `null`, or wrong member types) is dead-lettered raw on its first
274
+ `max_attempts` and then drops it. **Without retry tiers** that bounded retry has no tier to use, so
275
+ (with the DLQ off and `on_unknown_urn` not `DELETE`) the poison record is committed, dropped and logged
276
+ instead of blocking the partition (section 6.5 degrade-to-delete, ADR-0036 decision 5); enable
277
+ `dead_letter` to keep it in `<topic>.dlq`. A failing handler without tiers still cannot be retried, as above.
278
+
279
+ ### Poison messages on every other transport
280
+
281
+ A message body that does not decode into an envelope (not a JSON object, malformed, a wrongly typed
282
+ member, or bytes that are not valid UTF-8, even inside a JSON string) or that has neither a valid
283
+ string `job` nor a valid string `urn` is **poison** on every transport (ADR-0036 decisions 2, 4 and
284
+ 6; conformance `identity_verdict`). It is never handed to a handler and never takes the
285
+ unknown-URN strategies, which are for a valid envelope whose URN has no handler:
286
+
287
+ - DLQ on (`dead_letter` or `on_unknown_urn=DEAD_LETTER`): written to `<queue>.dlq` at once, with no
288
+ retry, as the ADR-0035 record (`dead_letter.reason: "poison"`, the original body preserved,
289
+ base64 plus `raw_encoding` when it is not UTF-8), then acknowledged. A failed write leaves it
290
+ unacknowledged.
291
+ - `on_unknown_urn=DELETE`: acknowledged and dropped (logged).
292
+ - DLQ off: the bounded `FAIL` path (`RELEASE` is replaced by it): retried up to `max_attempts`, then
293
+ dropped and logged. The count comes from the broker where it has one (SQS receive count, Pulsar
294
+ `bq-attempts` / redelivery count), otherwise from a bare `{"attempts": n}` body the retry leaves.
295
+
296
+ On Redis (both layouts) an identity-less body is dead-lettered as poison in **every** configuration,
297
+ including `on_unknown_urn=DELETE` and dead-lettering off (broker-bindings section 1.1). On the legacy
298
+ (`<queue>:processing`) layout a body that Go's typed decode rejects is dead-lettered raw on its first
265
299
  delivery and removed from `:processing`.
266
300
 
267
301
  ### Sharing a Redis queue with Laravel
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "babelqueue"
7
- version = "1.22.0"
7
+ version = "1.24.0"
8
8
  description = "Polyglot Queues, Simplified — the Python core: the canonical BabelQueue wire-envelope codec, contracts and dead-letter helpers."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -30,10 +30,13 @@ from .exceptions import BabelQueueError, DecryptError, UnknownUrnError
30
30
  from .replay import HEADER_REPLAY_BYPASS, bypass_external_effects, is_replay
31
31
  from .routing import UnknownUrnStrategy
32
32
  from .transport import (
33
+ Abandoner,
33
34
  BatchPopper,
35
+ DeadLetterNamer,
34
36
  DeadLetterWriter,
35
37
  HeaderPublisher,
36
38
  InMemoryTransport,
39
+ NativeSettler,
37
40
  ReceivedMessage,
38
41
  Redeliverer,
39
42
  Rewinder,
@@ -42,7 +45,7 @@ from .transport import (
42
45
  Transport,
43
46
  )
44
47
 
45
- __version__ = "1.22.0"
48
+ __version__ = "1.24.0"
46
49
 
47
50
  __all__ = [
48
51
  "BabelQueue",
@@ -55,9 +58,12 @@ __all__ = [
55
58
  "Transport",
56
59
  "InMemoryTransport",
57
60
  "ReceivedMessage",
61
+ "Abandoner",
58
62
  "BatchPopper",
63
+ "DeadLetterNamer",
59
64
  "DeadLetterWriter",
60
65
  "HeaderPublisher",
66
+ "NativeSettler",
61
67
  "Redeliverer",
62
68
  "Rewinder",
63
69
  "RoutedPublisher",
@@ -15,7 +15,7 @@ Go reference's ``babelqueue.Decode``, except that member names are case-sensitiv
15
15
  decoder read them) and every occurrence of a duplicated key is checked; any other member (a
16
16
  differently-cased name or a non-string ``urn`` alias included) is only checked for syntax.
17
17
  A non-string ``job`` is **not** poison either: the identity resolver ignores it (ADR-0032 section 6),
18
- so the body is readable and simply carries no identity (conformance ``kafka_poison``, v1.6.0).
18
+ so the body is readable and simply carries no identity (conformance ``kafka_poison``, v1.6.0, and ``identity_verdict``, v1.7.0).
19
19
 
20
20
  A well-typed but non-conformant envelope is not poison: Go's consume path never calls ``Accepts``.
21
21
  """