babelqueue 1.23.0__tar.gz → 1.25.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 (155) hide show
  1. {babelqueue-1.23.0 → babelqueue-1.25.0}/.github/workflows/ci.yml +1 -1
  2. {babelqueue-1.23.0 → babelqueue-1.25.0}/.github/workflows/release.yml +2 -2
  3. {babelqueue-1.23.0 → babelqueue-1.25.0}/CHANGELOG.md +152 -0
  4. {babelqueue-1.23.0 → babelqueue-1.25.0}/PKG-INFO +16 -4
  5. {babelqueue-1.23.0 → babelqueue-1.25.0}/README.md +15 -3
  6. {babelqueue-1.23.0 → babelqueue-1.25.0}/pyproject.toml +1 -1
  7. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/__init__.py +13 -1
  8. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/app.py +230 -31
  9. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/artemis_transport.py +26 -4
  10. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/asb_transport.py +77 -3
  11. babelqueue-1.25.0/src/babelqueue/dead_letter.py +87 -0
  12. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/headers.py +71 -1
  13. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/kafka_transport.py +68 -2
  14. babelqueue-1.25.0/src/babelqueue/otel.py +677 -0
  15. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/pulsar_transport.py +31 -0
  16. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/redrive.py +11 -3
  17. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/sqs_transport.py +29 -0
  18. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/transport.py +77 -0
  19. babelqueue-1.25.0/tests/conformance/CONFORMANCE_VERSION +1 -0
  20. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/manifest.json +3039 -2
  21. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/schema/results.example.json +25 -1
  22. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_app.py +114 -0
  23. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_artemis_transport.py +68 -0
  24. babelqueue-1.25.0/tests/test_asb_conformance.py +177 -0
  25. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_asb_transport.py +283 -1
  26. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_behaviour_conformance.py +108 -4
  27. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_conformance.py +7 -0
  28. babelqueue-1.25.0/tests/test_dead_letter_observer.py +298 -0
  29. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_headers.py +18 -0
  30. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_kafka_retry.py +58 -0
  31. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_kafka_transport.py +140 -0
  32. babelqueue-1.25.0/tests/test_otel.py +553 -0
  33. babelqueue-1.25.0/tests/test_otel_conformance.py +480 -0
  34. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_poison_routing.py +85 -15
  35. babelqueue-1.25.0/tests/test_publish_envelope.py +181 -0
  36. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_pulsar_transport.py +101 -0
  37. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_redrive.py +8 -0
  38. babelqueue-1.25.0/tests/test_sqs_conformance.py +155 -0
  39. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_sqs_transport.py +99 -13
  40. babelqueue-1.23.0/src/babelqueue/dead_letter.py +0 -39
  41. babelqueue-1.23.0/src/babelqueue/otel.py +0 -265
  42. babelqueue-1.23.0/tests/conformance/CONFORMANCE_VERSION +0 -1
  43. babelqueue-1.23.0/tests/test_asb_conformance.py +0 -84
  44. babelqueue-1.23.0/tests/test_otel.py +0 -263
  45. babelqueue-1.23.0/tests/test_sqs_conformance.py +0 -80
  46. {babelqueue-1.23.0 → babelqueue-1.25.0}/.github/FUNDING.yml +0 -0
  47. {babelqueue-1.23.0 → babelqueue-1.25.0}/.github/dependabot.yml +0 -0
  48. {babelqueue-1.23.0 → babelqueue-1.25.0}/.gitignore +0 -0
  49. {babelqueue-1.23.0 → babelqueue-1.25.0}/LICENSE +0 -0
  50. {babelqueue-1.23.0 → babelqueue-1.25.0}/conformance-known-gaps.json +0 -0
  51. {babelqueue-1.23.0 → babelqueue-1.25.0}/scripts/check_conformance_results.py +0 -0
  52. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/_envelope_shape.py +0 -0
  53. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/_poison_record.py +0 -0
  54. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/celery.py +0 -0
  55. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/codec.py +0 -0
  56. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/contracts.py +0 -0
  57. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/django/__init__.py +0 -0
  58. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/django/apps.py +0 -0
  59. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/django/management/__init__.py +0 -0
  60. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/django/management/commands/__init__.py +0 -0
  61. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/django/management/commands/babelqueue_worker.py +0 -0
  62. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/exceptions.py +0 -0
  63. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/gdpr.py +0 -0
  64. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/idempotency.py +0 -0
  65. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/kafka_retry.py +0 -0
  66. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/outbox.py +0 -0
  67. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/pika_transport.py +0 -0
  68. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/py.typed +0 -0
  69. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/redis_transport.py +0 -0
  70. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/replay.py +0 -0
  71. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/routing.py +0 -0
  72. {babelqueue-1.23.0 → babelqueue-1.25.0}/src/babelqueue/schema.py +0 -0
  73. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/blank-ascii-job-urn-fallback.json +0 -0
  74. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/data-array-rejected.json +0 -0
  75. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/dead-letter-roundtrip.json +0 -0
  76. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/dead-lettered.json +0 -0
  77. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/duplicate-keys-last-wins.json +0 -0
  78. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/empty-data-roundtrip.json +0 -0
  79. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/forbidden-key-job-and-urn.json +0 -0
  80. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/forbidden-key-job-nonascii-space-and-urn.json +0 -0
  81. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/forbidden-key-meta-attempts.json +0 -0
  82. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/forbidden-key-meta-max-retries.json +0 -0
  83. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/forbidden-key-meta-source.json +0 -0
  84. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/forbidden-key-meta-ts.json +0 -0
  85. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/forbidden-key-timestamp.json +0 -0
  86. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/invalid-missing-urn.json +0 -0
  87. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/invalid-unknown-schema-version.json +0 -0
  88. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/invalid-wrong-case-job.json +0 -0
  89. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/job-type-array-urn-fallback.json +0 -0
  90. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/job-type-bool-urn-fallback.json +0 -0
  91. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/job-type-empty-string-urn-fallback.json +0 -0
  92. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/job-type-missing-job-array-urn.json +0 -0
  93. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/job-type-null-no-urn.json +0 -0
  94. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/job-type-null-urn-fallback.json +0 -0
  95. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/job-type-number-blank-urn.json +0 -0
  96. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/job-type-number-nonstring-urn.json +0 -0
  97. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/job-type-number-urn-fallback.json +0 -0
  98. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/job-type-object-urn-fallback.json +0 -0
  99. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/meta-ext-vendor-roundtrip.json +0 -0
  100. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/order-created.json +0 -0
  101. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-blank-job-cr.json +0 -0
  102. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-blank-job-ff.json +0 -0
  103. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-blank-job-lf.json +0 -0
  104. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-blank-job-space.json +0 -0
  105. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-blank-job-tab.json +0 -0
  106. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-blank-job-vt.json +0 -0
  107. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-blank-trace-id-cr.json +0 -0
  108. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-blank-trace-id-ff.json +0 -0
  109. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-blank-trace-id-lf.json +0 -0
  110. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-blank-trace-id-space.json +0 -0
  111. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-blank-trace-id-tab.json +0 -0
  112. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-blank-trace-id-vt.json +0 -0
  113. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-job-and-urn-both-blank.json +0 -0
  114. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-reemit-job-ascii-padded.json +0 -0
  115. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-reemit-job-unicode-padded.json +0 -0
  116. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-reemit-trace-id-padded.json +0 -0
  117. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-unicode-job-emsp.json +0 -0
  118. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-unicode-job-ideographic.json +0 -0
  119. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-unicode-job-nbsp.json +0 -0
  120. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-urn-only-ascii-blank.json +0 -0
  121. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/trim-urn-only-unicode-space.json +0 -0
  122. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/unicode-and-numbers.json +0 -0
  123. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/unknown-lang.json +0 -0
  124. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/unknown-meta-roundtrip.json +0 -0
  125. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/unknown-toplevel-roundtrip.json +0 -0
  126. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/urn-alias.json +0 -0
  127. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/urn-only-roundtrip.json +0 -0
  128. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/fixtures/wrong-case-keys-roundtrip.json +0 -0
  129. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/schema/message-envelope.schema.json +0 -0
  130. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/conformance/schema/results.schema.json +0 -0
  131. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/fakes/confluent_kafka/__init__.py +0 -0
  132. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/fakes/confluent_kafka/admin.py +0 -0
  133. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/fixtures/dead-lettered.json +0 -0
  134. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/fixtures/order-created.json +0 -0
  135. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/redis_backend.py +0 -0
  136. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_artemis_conformance.py +0 -0
  137. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_celery.py +0 -0
  138. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_codec.py +0 -0
  139. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_dead_letter.py +0 -0
  140. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_django.py +0 -0
  141. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_gdpr.py +0 -0
  142. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_idempotency.py +0 -0
  143. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_kafka_conformance.py +0 -0
  144. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_kafka_integration.py +0 -0
  145. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_kafka_integration_logic.py +0 -0
  146. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_outbox.py +0 -0
  147. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_overhead.py +0 -0
  148. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_pika_transport.py +0 -0
  149. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_pulsar_conformance.py +0 -0
  150. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_redis_transport.py +0 -0
  151. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_reencode_regression.py +0 -0
  152. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_replay.py +0 -0
  153. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_schema.py +0 -0
  154. {babelqueue-1.23.0 → babelqueue-1.25.0}/tests/test_schema_conformance.py +0 -0
  155. {babelqueue-1.23.0 → babelqueue-1.25.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.7.0
180
+ BQ_CONFORMANCE_VERSION: 1.9.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.7.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.7.0
31
+ BQ_CONFORMANCE_VERSION: 1.9.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,158 @@ The envelope wire format is versioned separately by `meta.schema_version`
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [1.25.0] - 2026-11-01
13
+
14
+ ### Changed (dashboard-breaking) — `babelqueue.otel` follows the OpenTelemetry messaging semantic conventions v1.44.0 (ADR-0038)
15
+ - **Span names change with no overlap:** `publish <urn>` becomes `send <queue>` and
16
+ `process <urn>` becomes `process <queue>` (the bare `send` / `process` when there is no queue).
17
+ The URN is never in the span name now (it is unbounded); it is the `babelqueue.urn` attribute.
18
+ The producer's `messaging.destination.name` is the queue the message is published to (it was the
19
+ URN), so a producer span and its consumer span share one destination. **Saved queries and
20
+ dashboards that filter on the old span names or on the producer destination must be updated.**
21
+ - **`messaging.system` is now the broker when you pass it.** `wrap_handler(..., system="kafka")` and
22
+ `publish(..., system="kafka")` (`redis`, `rabbitmq`, `aws_sqs`, `servicebus`, `pulsar`, `kafka`,
23
+ `activemq`; any other value is written as given). **Omitting `system` keeps `babelqueue`, which is
24
+ deprecated** (a one-time warning is logged); what an absent `system` means after the deprecation
25
+ window is not decided yet, so pass it now.
26
+ - New span attributes: `messaging.operation.type` / `messaging.operation.name` (`send` or
27
+ `process`), `babelqueue.urn`, `babelqueue.schema_version`, `babelqueue.attempts` and, on a failure,
28
+ `error.type` (`module.QualName` of the exception, the bare name for builtins) with an explicit
29
+ `ERROR` status whose description is the exception message. `consumer_group="..."` on
30
+ `wrap_handler` writes `messaging.consumer.group.name`.
31
+ - **Dual-emit for this MINOR only:** the deprecated `messaging.operation` (`publish` / `process`) and
32
+ `messaging.babelqueue.attempts` are still written next to their replacements. **The next MINOR
33
+ removes both.**
34
+ - `babelqueue.otel.SEMCONV_VERSION` (`1.44.0`) and `SEMCONV_SCHEMA_URL` (`https://opentelemetry.io/schemas/1.44.0`)
35
+ are public; create the tracer and meter with the schema URL
36
+ (`trace.get_tracer("orders", schema_url=otel.SEMCONV_SCHEMA_URL)`).
37
+ - A `trace_id` that is not a UUID is still preserved byte for byte and hashed for the OTel TraceID
38
+ (`trace_id_of` is unchanged); `wrap_handler` now logs a warning once per distinct value
39
+ (message-envelope.md 5.1, ADR-0038 section 7). Nothing is rejected or regenerated.
40
+
41
+ ### Added
42
+ - **Standard metrics (ADR-0039), opt-in.** Pass `meter=` to `wrap_handler` and `publish` to record
43
+ `messaging.process.duration` (histogram, `s`, semconv bucket advice where the API supports it),
44
+ `messaging.client.consumed.messages`, `messaging.client.sent.messages` and `babelqueue.retries`
45
+ (counters, `{message}`; `retries` counts a delivery with `attempts > 0`). The attributes are
46
+ `messaging.system`, `messaging.operation.name`, `messaging.destination.name` (when known) and
47
+ `error.type` (on failure); never the URN, an id, the attempt count or a dead-letter reason. With no
48
+ `meter` nothing is recorded.
49
+ - **`babelqueue.dlq.messages` and the dead-letter observer seam (ADR-0039 section 5).**
50
+ `BabelQueue(..., dead_letter_observer=callback)` calls `callback(DeadLetterEvent(queue,
51
+ dead_letter_queue, reason))` once per dead-letter the runtime carried out, after the DLQ write is
52
+ confirmed (`BabelQueue._write_dead_letter`, the one chokepoint for exhausted retries, an unknown
53
+ URN, a poison body and a version-gated raw body) or after an SDK-initiated native dead-letter. A
54
+ failed write, a native no-op (SQS) and a broker-side dead-letter are not reported; an exception
55
+ raised by the observer is logged and never changes how the message is settled.
56
+ `otel.dead_letter_observer(meter, system=...)` is the observer that counts the metric.
57
+ New exports: `DeadLetterEvent`, `DeadLetterObserver`, and the `NativeDeadLetterSettler` protocol.
58
+ - **`NativeDeadLetterSettler`**: the optional native-settle variant
59
+ `settle_native_dead_letter(message, reason, description) -> bool` next to `NativeSettler`
60
+ (unchanged). When a transport implements it the runtime prefers it and reports a dead-letter only
61
+ on `True`; `AsbTransport` implements it, the SQS no-op does not.
62
+ - **`BabelQueue.publish_envelope(urn, data, *, queue=None, trace_id=None, headers=None)`** returns a
63
+ `PublishResult(envelope, error)` pair instead of raising. After a transport failure it still
64
+ carries the envelope that was handed to the transport (so `otel.publish` can put `meta.id` and
65
+ `trace_id` on a failed span); after a `make` or `encode` failure `envelope` is `None`. `publish`
66
+ and `publish_with_headers` delegate to it and behave exactly as before (an empty `queue` still
67
+ resolves to the default queue). `otel.publish` falls back to a duck-typed `app.publish(...)` for
68
+ objects that do not have it.
69
+ - Conformance **1.9.0** is vendored; the `span-shape`, `trace-id` and `metrics` sections (and the six
70
+ new `traceparent` cases) run and pass.
71
+ - **W3C `traceparent` / `tracestate` on the Kafka, Pulsar, Artemis and Azure Service Bus
72
+ transports (ADR-0028; broker-bindings 4.3, 5.2, 6.3, 7.2).** Each transport now implements the
73
+ `HeaderPublisher` capability: `publish_with_headers(queue, body, headers)` carries the headers
74
+ out of band beside the `bq-*` / `bq_*` projection (Kafka record headers, Pulsar message
75
+ properties, Artemis AMQP 1.0 application-properties, Azure Service Bus application properties).
76
+ The projection wins a key collision, and the body is byte-identical to a plain `publish`.
77
+ `pop` surfaces every non-projection header on `ReceivedMessage.headers` (so
78
+ `babelqueue.otel.wrap_handler` links the consumer span to the producer's across these brokers);
79
+ the projection keys themselves are never leaked, and `bq-replay-bypass` (ADR-0027) passes
80
+ through. A Kafka retry parks the consumed record's `traceparent` / `tracestate` on the tier topic
81
+ (the re-inject consumer already keeps every header except `bq-delay` / `bq-original-topic`), so
82
+ a retried message stays in the original trace. Retry tier names, `retry_tiers_ms` and the
83
+ poison / DLQ shapes are unchanged. `babelqueue.headers.out_of_band_headers` is the shared helper.
84
+ - **`RedrivePublisher` capability and `redrive(bypass=True)` on the header-capable transports.**
85
+ Kafka, Pulsar, Artemis and Azure Service Bus becoming `HeaderPublisher`s means
86
+ `redrive(..., bypass=True)` now really stamps the `bq-replay-bypass` marker on them
87
+ (`RedriveItem.bypassed` is `True`; it was `False` before, because the transports could not carry
88
+ headers), and the write goes through the transport's header-carrying path. So that this path
89
+ loses nothing the ordinary redrive write keeps, `Kafka` and `Pulsar` implement the new optional
90
+ `publish_redriven(queue, body, headers, route_urn, key)`: Kafka's is the broker-confirmed settle
91
+ write (a delivery failure raises `KafkaDeliveryError`, the DLQ record is restored and not
92
+ committed past) and carries the DLQ record key unchanged; Pulsar's pins `bq-job` to the
93
+ dead-lettered delivery's routing property. `redrive` uses it when the transport has it.
94
+ - **`AsbTransport.settle_native_dead_letter(message, reason, description) -> bool`** (ADR-0039
95
+ section 5.1): the optional native-settle variant that reports what it did. It performs the same
96
+ settle as `settle_native` (which is unchanged) and returns `True` when the receiver's
97
+ `dead_letter_message` ran, `False` when it only abandoned the lock or had no handle.
98
+
99
+ ## [1.24.0] - 2026-10-26
100
+
101
+ Implements conformance **1.8.0** (vendored from the `v1.8.0` release asset, verified with
102
+ `fetch.sh --check`; `BQ_CONFORMANCE_VERSION` is now `1.8.0` in `ci.yml` and `release.yml`). The
103
+ envelope stays at `schema_version: 1`; nothing here touches the wire (ADR-0037). The new
104
+ `version_gate_pulsar`, `version_gate_artemis`, `version_gate_sqs` and `version_gate_asb` sections run
105
+ and pass (17 cases each; Python has the `on_unknown_urn` option, so no row is skipped). A MINOR.
106
+
107
+ ### Changed (behaviour)
108
+ - **With the DLQ off, SQS and Azure Service Bus no longer drop a version-gated message or retry a
109
+ poison body; they are left to the broker's native mechanism (ADR-0037 decision 1 rule 3 and
110
+ decision 2 "Go and Python"; broker-bindings 3.5, 4.5, 5.7; owner decision 2026-10-25).**
111
+ - *Version-gated message* (an unsupported `bq-schema-version`), `dead_letter` off and a strategy
112
+ other than `DELETE` or `DEAD_LETTER`: it used to be acknowledged and dropped. It is now **not**
113
+ acknowledged. SQS: no `DeleteMessage`; visibility expiry redelivers it (the gate rejects it
114
+ again) and the queue's `RedrivePolicy` moves it to the native DLQ after `maxReceiveCount`. Azure
115
+ Service Bus: an explicit native `DeadLetter` (reason `unsupported schema version`) into
116
+ `$DeadLetterQueue` instead of `Complete`.
117
+ - *Poison body* (invalid JSON or an identity-less object), `dead_letter` off and a strategy other
118
+ than `DELETE` or `DEAD_LETTER`: it used to take the bounded `FAIL` path (re-publish an empty
119
+ envelope up to `max_attempts`, then drop). On SQS and Azure Service Bus it is now settled
120
+ natively **on the first delivery** exactly as above (Azure Service Bus reason `poison`): no
121
+ re-publish, no retry, no ack, body unchanged, the event is logged. Every other transport
122
+ (Pulsar, Artemis, AMQP, Kafka, Redis, memory) keeps its behaviour; `DELETE` (ack and drop),
123
+ `dead_letter=True` and the `DEAD_LETTER` strategy (the raw record, then ack) are unchanged on
124
+ SQS and Azure Service Bus too.
125
+ - A deployment that relied on SQS/Azure Service Bus silently dropping these messages now needs a
126
+ `RedrivePolicy` (SQS) or accepts them in `$DeadLetterQueue` (Azure Service Bus), or sets
127
+ `dead_letter=True` / `on_unknown_urn=DELETE`.
128
+
129
+ ### Fixed
130
+ - **SQS: the default dead-letter queue name is now `<queue>-dlq` (FIFO `<queue>-dlq.fifo`) (ADR-0037
131
+ decision 7).** The old default `<queue>.dlq` was never a valid Amazon SQS queue name (a name may hold only
132
+ alphanumerics, hyphens and underscores, plus one `.fifo` suffix), so a DLQ write by default could not find a
133
+ queue on real SQS. `dead_letter_suffix` now defaults to `None` ("not set"); unless `dead_letter_queue` is set
134
+ the runtime asks the transport's new optional `DeadLetterNamer` capability
135
+ (`dead_letter_queue_name(queue, suffix)`, exported from `babelqueue`), which `SqsTransport` implements. The
136
+ runtime also hands it an explicit `dead_letter_suffix`; on a FIFO queue the `.fifo` suffix always stays last,
137
+ an explicit suffix included: `orders.fifo` with `dead_letter_suffix="-x"` is `orders-x.fifo`, never
138
+ `orders.fifo-x` (an SQS FIFO name must end in `.fifo`; before, that explicit suffix produced the uncreatable
139
+ `orders.fifo-x`). An explicit queue name behaves as before; every other transport keeps `<queue>.dlq` (the
140
+ attribute `App.dead_letter_suffix` is `None` instead of `".dlq"` until set). Deployments
141
+ that relied on the SQS default must name the DLQ `<queue>-dlq`, or pass `dead_letter_suffix=".dlq"` /
142
+ `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.
143
+
144
+ ### Added
145
+ - **`babelqueue.transport.NativeSettler`**, an optional `Transport` capability
146
+ (`settle_native(message, reason, description)`). With the DLQ off the runtime calls it instead of
147
+ acknowledging a version-gated message (and, on the poison route, instead of the bounded retry) on
148
+ a transport that offers it; the decision follows the capability, never the transport's name.
149
+ `SqsTransport.settle_native` is a deliberate no-op on the wire; `AsbTransport.settle_native`
150
+ dead-letters natively (falling back to `abandon_message` for a receiver without
151
+ `dead_letter_message`). Transports without it are unchanged.
152
+ - **`babelqueue.transport.Abandoner` and `AsbTransport.abandon`.** When the dead-letter write of a
153
+ *version-gated* message fails, Azure Service Bus now `Abandon`s the peek-lock (broker-bindings 4.5,
154
+ "A failed write `Abandon`s it") so the broker redelivers it at once, instead of leaving it locked until
155
+ the lock lapses (up to 5 minutes). Only that branch abandons (owner decision 2026-10-25): `AsbTransport`
156
+ is deliberately **not** a `Rewinder`, so a graceful-shutdown release and a failed retry re-publish keep
157
+ their previous handling (a copy is re-published and the original completed), and a failed dead-letter
158
+ write of a poison body is still left to its lock. Tests pin all three. The `version_gate_asb` runner
159
+ asserts the `Abandon` on its `unsettled` rows.
160
+ - Conformance `version_gate_*` runners (`version_gate_pulsar` / `version_gate_artemis` through the
161
+ runtime, `version_gate_sqs` / `version_gate_asb` through the real transports) and their entries in
162
+ the results writer.
163
+
12
164
  ## [1.23.0] - 2026-10-25
13
165
 
14
166
  Implements ADR-0036 (post-R1 parity) decisions 2, 4, 5 and 6. A MINOR: every change is a
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: babelqueue
3
- Version: 1.23.0
3
+ Version: 1.25.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
 
@@ -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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "babelqueue"
7
- version = "1.23.0"
7
+ version = "1.25.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"
@@ -15,6 +15,7 @@ from . import dead_letter, gdpr, headers, idempotency, outbox, redrive, replay
15
15
  from .app import BabelQueue
16
16
  from .codec import SCHEMA_VERSION, SOURCE_LANG, EnvelopeCodec
17
17
  from .contracts import HasTraceId, PolyglotMessage
18
+ from .dead_letter import DeadLetterEvent, DeadLetterObserver, NativeDeadLetterSettler
18
19
  from .gdpr import Cipher, protect, unprotect
19
20
  from .headers import headers_from_context
20
21
  from .idempotency import IdempotencyStore, InMemoryStore
@@ -30,19 +31,23 @@ from .exceptions import BabelQueueError, DecryptError, UnknownUrnError
30
31
  from .replay import HEADER_REPLAY_BYPASS, bypass_external_effects, is_replay
31
32
  from .routing import UnknownUrnStrategy
32
33
  from .transport import (
34
+ Abandoner,
33
35
  BatchPopper,
36
+ DeadLetterNamer,
34
37
  DeadLetterWriter,
35
38
  HeaderPublisher,
36
39
  InMemoryTransport,
40
+ NativeSettler,
37
41
  ReceivedMessage,
38
42
  Redeliverer,
43
+ RedrivePublisher,
39
44
  Rewinder,
40
45
  RoutedPublisher,
41
46
  SettlePublisher,
42
47
  Transport,
43
48
  )
44
49
 
45
- __version__ = "1.23.0"
50
+ __version__ = "1.25.0"
46
51
 
47
52
  __all__ = [
48
53
  "BabelQueue",
@@ -55,10 +60,17 @@ __all__ = [
55
60
  "Transport",
56
61
  "InMemoryTransport",
57
62
  "ReceivedMessage",
63
+ "Abandoner",
58
64
  "BatchPopper",
65
+ "DeadLetterNamer",
59
66
  "DeadLetterWriter",
60
67
  "HeaderPublisher",
68
+ "NativeSettler",
69
+ "NativeDeadLetterSettler",
70
+ "DeadLetterEvent",
71
+ "DeadLetterObserver",
61
72
  "Redeliverer",
73
+ "RedrivePublisher",
62
74
  "Rewinder",
63
75
  "RoutedPublisher",
64
76
  "SettlePublisher",