babelqueue 1.13.0__tar.gz → 1.15.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 (95) hide show
  1. babelqueue-1.15.0/.github/dependabot.yml +11 -0
  2. {babelqueue-1.13.0 → babelqueue-1.15.0}/.github/workflows/ci.yml +2 -1
  3. {babelqueue-1.13.0 → babelqueue-1.15.0}/CHANGELOG.md +108 -0
  4. {babelqueue-1.13.0 → babelqueue-1.15.0}/PKG-INFO +25 -4
  5. {babelqueue-1.13.0 → babelqueue-1.15.0}/README.md +21 -0
  6. {babelqueue-1.13.0 → babelqueue-1.15.0}/pyproject.toml +3 -3
  7. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/__init__.py +11 -2
  8. babelqueue-1.15.0/src/babelqueue/app.py +528 -0
  9. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/artemis_transport.py +5 -5
  10. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/asb_transport.py +2 -2
  11. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/codec.py +93 -8
  12. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/kafka_transport.py +3 -3
  13. babelqueue-1.15.0/src/babelqueue/pulsar_transport.py +383 -0
  14. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/redrive.py +13 -9
  15. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/sqs_transport.py +87 -11
  16. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/transport.py +49 -0
  17. babelqueue-1.15.0/tests/conformance/CONFORMANCE_VERSION +1 -0
  18. babelqueue-1.15.0/tests/conformance/fixtures/data-array-rejected.json +13 -0
  19. babelqueue-1.15.0/tests/conformance/fixtures/empty-data-roundtrip.json +13 -0
  20. babelqueue-1.15.0/tests/conformance/fixtures/forbidden-key-meta-attempts.json +16 -0
  21. babelqueue-1.15.0/tests/conformance/fixtures/forbidden-key-meta-max-retries.json +16 -0
  22. babelqueue-1.15.0/tests/conformance/fixtures/forbidden-key-meta-source.json +16 -0
  23. babelqueue-1.15.0/tests/conformance/fixtures/forbidden-key-meta-ts.json +16 -0
  24. babelqueue-1.15.0/tests/conformance/fixtures/forbidden-key-timestamp.json +16 -0
  25. babelqueue-1.15.0/tests/conformance/fixtures/unknown-lang.json +15 -0
  26. babelqueue-1.15.0/tests/conformance/fixtures/unknown-meta-roundtrip.json +20 -0
  27. babelqueue-1.15.0/tests/conformance/fixtures/unknown-toplevel-roundtrip.json +16 -0
  28. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/conformance/manifest.json +256 -0
  29. babelqueue-1.15.0/tests/test_app.py +303 -0
  30. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_artemis_transport.py +27 -0
  31. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_asb_transport.py +22 -0
  32. babelqueue-1.15.0/tests/test_behaviour_conformance.py +195 -0
  33. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_codec.py +27 -0
  34. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_kafka_transport.py +23 -0
  35. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_pulsar_conformance.py +1 -1
  36. babelqueue-1.15.0/tests/test_pulsar_transport.py +648 -0
  37. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_redrive.py +38 -0
  38. babelqueue-1.15.0/tests/test_shutdown.py +302 -0
  39. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_sqs_transport.py +245 -3
  40. babelqueue-1.13.0/src/babelqueue/app.py +0 -252
  41. babelqueue-1.13.0/src/babelqueue/pulsar_transport.py +0 -202
  42. babelqueue-1.13.0/tests/test_app.py +0 -125
  43. babelqueue-1.13.0/tests/test_pulsar_transport.py +0 -215
  44. {babelqueue-1.13.0 → babelqueue-1.15.0}/.github/FUNDING.yml +0 -0
  45. {babelqueue-1.13.0 → babelqueue-1.15.0}/.github/workflows/release.yml +0 -0
  46. {babelqueue-1.13.0 → babelqueue-1.15.0}/.gitignore +0 -0
  47. {babelqueue-1.13.0 → babelqueue-1.15.0}/LICENSE +0 -0
  48. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/celery.py +0 -0
  49. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/contracts.py +0 -0
  50. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/dead_letter.py +0 -0
  51. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/django/__init__.py +0 -0
  52. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/django/apps.py +0 -0
  53. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/django/management/__init__.py +0 -0
  54. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/django/management/commands/__init__.py +0 -0
  55. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/django/management/commands/babelqueue_worker.py +0 -0
  56. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/exceptions.py +0 -0
  57. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/gdpr.py +0 -0
  58. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/headers.py +0 -0
  59. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/idempotency.py +0 -0
  60. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/otel.py +0 -0
  61. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/outbox.py +0 -0
  62. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/pika_transport.py +0 -0
  63. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/py.typed +0 -0
  64. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/redis_transport.py +0 -0
  65. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/replay.py +0 -0
  66. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/routing.py +0 -0
  67. {babelqueue-1.13.0 → babelqueue-1.15.0}/src/babelqueue/schema.py +0 -0
  68. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/conformance/fixtures/dead-lettered.json +0 -0
  69. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/conformance/fixtures/invalid-missing-urn.json +0 -0
  70. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/conformance/fixtures/invalid-unknown-schema-version.json +0 -0
  71. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/conformance/fixtures/order-created.json +0 -0
  72. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/conformance/fixtures/unicode-and-numbers.json +0 -0
  73. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/conformance/fixtures/urn-alias.json +0 -0
  74. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/conformance/schema/message-envelope.schema.json +0 -0
  75. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/fixtures/dead-lettered.json +0 -0
  76. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/fixtures/order-created.json +0 -0
  77. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_artemis_conformance.py +0 -0
  78. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_asb_conformance.py +0 -0
  79. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_celery.py +0 -0
  80. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_conformance.py +0 -0
  81. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_dead_letter.py +0 -0
  82. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_django.py +0 -0
  83. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_gdpr.py +0 -0
  84. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_headers.py +0 -0
  85. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_idempotency.py +0 -0
  86. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_kafka_conformance.py +0 -0
  87. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_otel.py +0 -0
  88. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_outbox.py +0 -0
  89. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_overhead.py +0 -0
  90. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_pika_transport.py +0 -0
  91. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_redis_transport.py +0 -0
  92. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_replay.py +0 -0
  93. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_schema.py +0 -0
  94. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_schema_conformance.py +0 -0
  95. {babelqueue-1.13.0 → babelqueue-1.15.0}/tests/test_sqs_conformance.py +0 -0
@@ -0,0 +1,11 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "pip"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+
8
+ - package-ecosystem: "github-actions"
9
+ directory: "/"
10
+ schedule:
11
+ interval: "weekly"
@@ -118,7 +118,8 @@ jobs:
118
118
  - uses: actions/checkout@v5
119
119
  - name: Verify vendored conformance matches the canonical suite
120
120
  run: |
121
- git clone --depth 1 https://github.com/BabelQueue/conformance.git "$RUNNER_TEMP/conformance"
121
+ git clone --depth 1 --branch v1.0.0 https://github.com/BabelQueue/conformance.git "$RUNNER_TEMP/conformance"
122
+ diff -u "$RUNNER_TEMP/conformance/CONFORMANCE_VERSION" "tests/conformance/CONFORMANCE_VERSION"
122
123
  diff -ru "$RUNNER_TEMP/conformance/manifest.json" "tests/conformance/manifest.json"
123
124
  diff -ru "$RUNNER_TEMP/conformance/fixtures" "tests/conformance/fixtures"
124
125
  diff -ru "$RUNNER_TEMP/conformance/schema" "tests/conformance/schema"
@@ -9,6 +9,114 @@ The envelope wire format is versioned separately by `meta.schema_version`
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [1.15.0] - 2026-10-10
13
+
14
+ ### Added
15
+ - **`ReceivedMessage.route_urn` routing seam** (MINOR). An optional field a transport can set
16
+ when its broker carries the URN out of band. When non-empty `BabelQueue.dispatch()` looks the
17
+ handler up by it instead of the decoded body's URN; empty (the default for every existing
18
+ transport) keeps routing on the body. The body is never rewritten, so the handler, the retry
19
+ re-publish and the dead-letter record carry the producer's original body (including a `urn`
20
+ alias). No wire change.
21
+ - **`RoutedPublisher` optional transport capability** (MINOR). `publish_routed(queue, body,
22
+ route_urn)`. When a delivery has a `route_urn` and the transport implements it, the retry, release and dead-letter
23
+ writes, and `redrive`, use it so the broker's routing property keeps the original
24
+ value across attempts (no flip to the body URN on the second delivery). `PulsarTransport`
25
+ implements it (`bq-job` pinned; blank derives from the body). Others keep plain `publish`.
26
+ No wire change.
27
+
28
+ ### Changed
29
+ - `BabelQueue.dispatch()` applies `route_urn` only to a body that decodes; an undecodable body
30
+ stays on the poison / unknown-URN path instead of being routed to a handler.
31
+
32
+ ### Fixed
33
+ - **`on_unknown_urn=RELEASE` no longer loops a version-gated or poison message forever.**
34
+ `BabelQueue.dispatch()` re-published such a message unchanged on every delivery (`attempts` never
35
+ grew, so it never reached the DLQ). A delivery a transport version-gated (reserved
36
+ `unsupported-schema-version` `route_urn`, e.g. Pulsar `bq-schema-version`), an undecodable body
37
+ and a body naming no URN now take the bounded `FAIL` path under `RELEASE` (retry up to
38
+ `max_attempts`, then dead-letter or drop), matching the Go core (GR-5). A genuinely unknown URN
39
+ still releases verbatim. No public API or wire change. Note: as on the existing `FAIL` path, a
40
+ body that is not valid JSON is not carried verbatim through the retry / dead-letter re-publish
41
+ (the runtime re-encodes the envelope it could build, so the producer's original bytes are lost);
42
+ previously `RELEASE` re-published those bytes unchanged.
43
+ - **Pulsar consumer version-gates on `bq-schema-version` (broker-bindings §5.7 step 3).**
44
+ `PulsarTransport.pop()` now reads the `bq-schema-version` message property before decoding the
45
+ body. A missing or blank value changes nothing; "Blank" is the same set in every SDK: the empty string or only ASCII whitespace (space, `\t`, `\n`, `\v`, `\f`, `\r`); anything else (NBSP, U+001C-U+001F, U+0085, U+FEFF, U+3000, ...) is not blank and is rejected unless it is exactly the supported version. (`str.strip()` is not used: it also strips U+001C-U+001F, U+0085, NBSP, ...) a value other than the supported
46
+ `SCHEMA_VERSION` (`"2"`, `"x"`, `"01"`, ...) is not decoded: the delivery carries the reserved
47
+ `route_urn` `babelqueue:unsupported-schema-version:<value>`, which no handler owns, so the
48
+ runtime quarantines it through the existing unknown-URN retry / dead-letter path (the error
49
+ names the rejected version). The body is passed through untouched. No wire change. The route
50
+ is an in-process marker only: it also carries the delivery's original `bq-job`, and the retry /
51
+ dead-letter re-publish (`publish_routed`) writes that original `bq-job` (or, when the delivery
52
+ had none, the body's job) and the original raw `bq-schema-version` back, never the sentinel, so
53
+ a redrive meets the same gate. The gate also logs a `babelqueue.pulsar` warning naming the
54
+ rejected version and queue, including for a body that is not valid JSON (which the runtime
55
+ quarantines as a poison message whose error names the reserved route).
56
+ - **Pulsar consumer now follows broker-bindings §5.2 / §5.7.** `PulsarTransport.pop()` routes on
57
+ the `bq-job` message property (set on `ReceivedMessage.route_urn`; the body's URN is used only
58
+ when the property is absent or blank) and takes `attempts` from the authoritative
59
+ `bq-attempts` property (the body's value is used only when the property is absent or not a
60
+ plain ASCII-decimal integer that fits a 32-bit int), then applies `max(.., redelivery_count)`.
61
+ Previously only the body was read. The body's `job` / `urn` is **not** rewritten, so a
62
+ property/body mismatch no longer leaks into retries or the DLQ; `attempts` is written into the
63
+ body only when the body already holds an integer `attempts` and the value changes (a missing
64
+ or non-integer `attempts` is still rejected by `EnvelopeCodec.accepts`). When the property and
65
+ the body name different URNs the property wins and the new optional
66
+ `on_routing_mismatch(property_urn, body_urn, envelope)` constructor hook is called for an
67
+ accepted envelope, after attempts are reconciled (its exceptions are swallowed; it is separate
68
+ from error handling). A body without a URN is still rejected. No wire change
69
+ (`schema_version: 1`).
70
+
71
+ ## [1.14.0] - 2026-10-03
72
+
73
+ ### Added
74
+ - **Graceful shutdown** of `BabelQueue.consume()`: on SIGTERM/SIGINT the loop sets a stop flag,
75
+ takes no new message, lets the in-flight handler finish (or releases the message unchanged once
76
+ the new `shutdown_timeout` — default 30 s — expires), restores the previous signal handlers and
77
+ closes the transport. Handlers are installed only on the main thread (`handle_signals=False`
78
+ opts out); a second signal forces an immediate exit with the message released. A plain
79
+ `KeyboardInterrupt` keeps its previous behaviour. New `BabelQueue.stop()` ends the loop from any
80
+ thread after the in-flight message.
81
+ - `Redeliverer` protocol (`redeliver(message, body, delay)`) — a transport's native
82
+ release; the runtime uses it for retries, unknown-URN `RELEASE` and shutdown releases, and
83
+ falls back to publish + ack otherwise. New `retry_backoff` / `unknown_urn_release_delay`
84
+ settings feed its delay.
85
+ - Conformance runners for the `roundtrip`, `data_shape`, `forbidden_keys` and
86
+ `payload_schema_unicode` sections (none skipped).
87
+ - `.github/dependabot.yml` (pip + GitHub Actions, weekly).
88
+
89
+ ### Changed
90
+ - **SQS release** (broker-bindings §3.5): retries, unknown-URN `RELEASE` and shutdown releases
91
+ **always** call `ChangeMessageVisibility` instead of re-sending, so the broker's
92
+ `ApproximateReceiveCount` carries the attempt count. There is no send + delete fallback. The
93
+ default delay for `retry_backoff` and `unknown_urn_release_delay` is **0 s** (immediate
94
+ redelivery); a delay outside 0..43200 s (including `inf`/`nan`) is clamped with a warning on the
95
+ `babelqueue.sqs` logger. A message without a receipt handle is not released (warning); one
96
+ without a receive count is released but its attempts cannot advance (warning).
97
+ **Poison-loop risk:** a message that always fails is redelivered immediately until
98
+ `max_attempts` — configure a native SQS `RedrivePolicy` (`maxReceiveCount` ≥ `max_attempts`) as
99
+ the backstop and a non-zero `retry_backoff` where needed. A shutdown release also counts as a
100
+ receive on SQS, so keep `shutdown_timeout` above the longest handler time.
101
+ - Delivery semantics are documented as **at-least-once**: a handler that finishes right as the
102
+ shutdown deadline fires may be released and run again.
103
+ - The SQS, Kafka, Pulsar, Azure Service Bus and Artemis transports' header/property projections
104
+ read the envelope without logging a false forbidden-key drop (the body is sent unchanged).
105
+
106
+ ### Fixed
107
+ - The five forbidden envelope keys (`timestamp`, `meta.max_retries`, `meta.attempts`,
108
+ `meta.source`, `meta.ts` — message-envelope §10) are dropped on decode with a warning on the
109
+ `babelqueue.codec` logger, and never written on encode. **Transitional:** encode currently drops
110
+ them with a warning; a future MINOR (K-15 / R1-E0) will reject them instead. A raw-body release
111
+ republish now strips them too (a clean body is re-sent byte for byte); DLQ redrive still restores
112
+ the original bytes unchanged.
113
+ - `BabelQueue.stop()` called before `consume()` starts is now honoured instead of being cleared.
114
+ - A failed acknowledgement of an already-processed message (e.g. SQS `DeleteMessage` after a
115
+ successful handler, an unknown-URN `DELETE`, or a dead-letter publish) is no longer treated as a
116
+ handler failure: the message is **not** released or retried; the failure is logged at `ERROR`
117
+ on the new `babelqueue.app` logger and the broker redelivers it on its own (SQS: after the
118
+ visibility timeout). The consume loop keeps running.
119
+
12
120
  ## [1.13.0] - 2026-06-21
13
121
 
14
122
  ### Added
@@ -1,12 +1,12 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: babelqueue
3
- Version: 1.13.0
3
+ Version: 1.15.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
7
7
  Project-URL: Issues, https://github.com/BabelQueue/babelqueue-python/issues
8
8
  Project-URL: Changelog, https://github.com/BabelQueue/babelqueue-python/blob/main/CHANGELOG.md
9
- Author-email: Muhammet Şafak <info@muhammetsafak.com.tr>
9
+ Author-email: Muhammet Şafak <info@muhammetsafak.com>
10
10
  License-Expression: MIT
11
11
  License-File: LICENSE
12
12
  Keywords: envelope,json,messaging,microservices,polyglot,queue
@@ -35,7 +35,7 @@ Provides-Extra: dev
35
35
  Requires-Dist: mypy>=1.8; extra == 'dev'
36
36
  Requires-Dist: pytest-cov>=4; extra == 'dev'
37
37
  Requires-Dist: pytest>=7; extra == 'dev'
38
- Requires-Dist: ruff>=0.5; extra == 'dev'
38
+ Requires-Dist: ruff<0.16,>=0.5; extra == 'dev'
39
39
  Provides-Extra: django
40
40
  Requires-Dist: django>=4.2; extra == 'django'
41
41
  Provides-Extra: gdpr
@@ -174,6 +174,27 @@ app.run() # consume forever (Ctrl-C to stop)
174
174
  and `memory://` (in-process, great for tests/local). Bring your own by passing
175
175
  `transport=...`.
176
176
 
177
+ - **Graceful shutdown:** SIGTERM/SIGINT finish the in-flight handler, or release
178
+ the message unchanged once `shutdown_timeout` expires. Delivery is
179
+ **at-least-once** — keep handlers idempotent (see the idempotency helper).
180
+
181
+ ### SQS release and poison messages
182
+
183
+ On a handler failure, unknown-URN `release` or shutdown release, the SQS
184
+ transport **always** releases with `ChangeMessageVisibility` (broker-bindings
185
+ §3.5) — it never re-sends a copy. The broker's `ApproximateReceiveCount` is the
186
+ attempt counter, so a shutdown release also consumes an attempt. The default
187
+ delay is **0 s** (`retry_backoff` / `unknown_urn_release_delay`), i.e. the
188
+ message is visible again immediately; delays outside 0..43200 s are clamped with
189
+ a warning.
190
+
191
+ **Poison-loop risk:** a message that always fails is redelivered at once until
192
+ `max_attempts` is reached. If the receive count is unavailable the SDK cannot
193
+ advance attempts at all. **Configure a native `RedrivePolicy`**
194
+ (`maxReceiveCount` ≥ `max_attempts`, pointing at `<queue>.dlq`) on every SQS
195
+ queue as the broker-side backstop, and set a non-zero `retry_backoff` if the
196
+ handler's dependencies need time to recover.
197
+
177
198
  ### Sharing a Redis queue with Laravel
178
199
 
179
200
  By default the Redis transport owns its queue end-to-end (`RPUSH` to produce;
@@ -120,6 +120,27 @@ app.run() # consume forever (Ctrl-C to stop)
120
120
  and `memory://` (in-process, great for tests/local). Bring your own by passing
121
121
  `transport=...`.
122
122
 
123
+ - **Graceful shutdown:** SIGTERM/SIGINT finish the in-flight handler, or release
124
+ the message unchanged once `shutdown_timeout` expires. Delivery is
125
+ **at-least-once** — keep handlers idempotent (see the idempotency helper).
126
+
127
+ ### SQS release and poison messages
128
+
129
+ On a handler failure, unknown-URN `release` or shutdown release, the SQS
130
+ transport **always** releases with `ChangeMessageVisibility` (broker-bindings
131
+ §3.5) — it never re-sends a copy. The broker's `ApproximateReceiveCount` is the
132
+ attempt counter, so a shutdown release also consumes an attempt. The default
133
+ delay is **0 s** (`retry_backoff` / `unknown_urn_release_delay`), i.e. the
134
+ message is visible again immediately; delays outside 0..43200 s are clamped with
135
+ a warning.
136
+
137
+ **Poison-loop risk:** a message that always fails is redelivered at once until
138
+ `max_attempts` is reached. If the receive count is unavailable the SDK cannot
139
+ advance attempts at all. **Configure a native `RedrivePolicy`**
140
+ (`maxReceiveCount` ≥ `max_attempts`, pointing at `<queue>.dlq`) on every SQS
141
+ queue as the broker-side backstop, and set a non-zero `retry_backoff` if the
142
+ handler's dependencies need time to recover.
143
+
123
144
  ### Sharing a Redis queue with Laravel
124
145
 
125
146
  By default the Redis transport owns its queue end-to-end (`RPUSH` to produce;
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "babelqueue"
7
- version = "1.13.0"
7
+ version = "1.15.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"
11
11
  license = "MIT"
12
12
  license-files = ["LICENSE"]
13
- authors = [{ name = "Muhammet Şafak", email = "info@muhammetsafak.com.tr" }]
13
+ authors = [{ name = "Muhammet Şafak", email = "info@muhammetsafak.com" }]
14
14
  keywords = ["queue", "polyglot", "microservices", "json", "envelope", "messaging"]
15
15
  classifiers = [
16
16
  "Development Status :: 4 - Beta",
@@ -42,7 +42,7 @@ otel = ["opentelemetry-api>=1.20"]
42
42
  gdpr = ["cryptography>=42"]
43
43
  celery = ["celery>=5"]
44
44
  django = ["django>=4.2"]
45
- dev = ["pytest>=7", "pytest-cov>=4", "mypy>=1.8", "ruff>=0.5"]
45
+ dev = ["pytest>=7", "pytest-cov>=4", "mypy>=1.8", "ruff>=0.5,<0.16"]
46
46
 
47
47
  [project.urls]
48
48
  Homepage = "https://babelqueue.com"
@@ -29,9 +29,16 @@ from .outbox import (
29
29
  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
- from .transport import HeaderPublisher, InMemoryTransport, ReceivedMessage, Transport
32
+ from .transport import (
33
+ HeaderPublisher,
34
+ InMemoryTransport,
35
+ ReceivedMessage,
36
+ Redeliverer,
37
+ RoutedPublisher,
38
+ Transport,
39
+ )
33
40
 
34
- __version__ = "1.13.0"
41
+ __version__ = "1.15.0"
35
42
 
36
43
  __all__ = [
37
44
  "BabelQueue",
@@ -45,6 +52,8 @@ __all__ = [
45
52
  "InMemoryTransport",
46
53
  "ReceivedMessage",
47
54
  "HeaderPublisher",
55
+ "Redeliverer",
56
+ "RoutedPublisher",
48
57
  "BabelQueueError",
49
58
  "UnknownUrnError",
50
59
  "DecryptError",