babelqueue 1.13.0__tar.gz → 1.14.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 (92) hide show
  1. babelqueue-1.14.0/.github/dependabot.yml +11 -0
  2. {babelqueue-1.13.0 → babelqueue-1.14.0}/.github/workflows/ci.yml +2 -1
  3. {babelqueue-1.13.0 → babelqueue-1.14.0}/CHANGELOG.md +49 -0
  4. {babelqueue-1.13.0 → babelqueue-1.14.0}/PKG-INFO +24 -3
  5. {babelqueue-1.13.0 → babelqueue-1.14.0}/README.md +21 -0
  6. {babelqueue-1.13.0 → babelqueue-1.14.0}/pyproject.toml +2 -2
  7. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/__init__.py +3 -2
  8. babelqueue-1.14.0/src/babelqueue/app.py +502 -0
  9. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/artemis_transport.py +5 -5
  10. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/asb_transport.py +2 -2
  11. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/codec.py +93 -8
  12. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/kafka_transport.py +3 -3
  13. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/pulsar_transport.py +2 -2
  14. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/sqs_transport.py +87 -11
  15. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/transport.py +19 -0
  16. babelqueue-1.14.0/tests/conformance/CONFORMANCE_VERSION +1 -0
  17. babelqueue-1.14.0/tests/conformance/fixtures/data-array-rejected.json +13 -0
  18. babelqueue-1.14.0/tests/conformance/fixtures/empty-data-roundtrip.json +13 -0
  19. babelqueue-1.14.0/tests/conformance/fixtures/forbidden-key-meta-attempts.json +16 -0
  20. babelqueue-1.14.0/tests/conformance/fixtures/forbidden-key-meta-max-retries.json +16 -0
  21. babelqueue-1.14.0/tests/conformance/fixtures/forbidden-key-meta-source.json +16 -0
  22. babelqueue-1.14.0/tests/conformance/fixtures/forbidden-key-meta-ts.json +16 -0
  23. babelqueue-1.14.0/tests/conformance/fixtures/forbidden-key-timestamp.json +16 -0
  24. babelqueue-1.14.0/tests/conformance/fixtures/unknown-lang.json +15 -0
  25. babelqueue-1.14.0/tests/conformance/fixtures/unknown-meta-roundtrip.json +20 -0
  26. babelqueue-1.14.0/tests/conformance/fixtures/unknown-toplevel-roundtrip.json +16 -0
  27. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/conformance/manifest.json +256 -0
  28. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_artemis_transport.py +27 -0
  29. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_asb_transport.py +22 -0
  30. babelqueue-1.14.0/tests/test_behaviour_conformance.py +195 -0
  31. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_codec.py +27 -0
  32. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_kafka_transport.py +23 -0
  33. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_pulsar_transport.py +25 -0
  34. babelqueue-1.14.0/tests/test_shutdown.py +302 -0
  35. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_sqs_transport.py +245 -3
  36. babelqueue-1.13.0/src/babelqueue/app.py +0 -252
  37. {babelqueue-1.13.0 → babelqueue-1.14.0}/.github/FUNDING.yml +0 -0
  38. {babelqueue-1.13.0 → babelqueue-1.14.0}/.github/workflows/release.yml +0 -0
  39. {babelqueue-1.13.0 → babelqueue-1.14.0}/.gitignore +0 -0
  40. {babelqueue-1.13.0 → babelqueue-1.14.0}/LICENSE +0 -0
  41. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/celery.py +0 -0
  42. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/contracts.py +0 -0
  43. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/dead_letter.py +0 -0
  44. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/django/__init__.py +0 -0
  45. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/django/apps.py +0 -0
  46. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/django/management/__init__.py +0 -0
  47. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/django/management/commands/__init__.py +0 -0
  48. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/django/management/commands/babelqueue_worker.py +0 -0
  49. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/exceptions.py +0 -0
  50. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/gdpr.py +0 -0
  51. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/headers.py +0 -0
  52. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/idempotency.py +0 -0
  53. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/otel.py +0 -0
  54. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/outbox.py +0 -0
  55. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/pika_transport.py +0 -0
  56. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/py.typed +0 -0
  57. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/redis_transport.py +0 -0
  58. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/redrive.py +0 -0
  59. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/replay.py +0 -0
  60. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/routing.py +0 -0
  61. {babelqueue-1.13.0 → babelqueue-1.14.0}/src/babelqueue/schema.py +0 -0
  62. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/conformance/fixtures/dead-lettered.json +0 -0
  63. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/conformance/fixtures/invalid-missing-urn.json +0 -0
  64. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/conformance/fixtures/invalid-unknown-schema-version.json +0 -0
  65. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/conformance/fixtures/order-created.json +0 -0
  66. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/conformance/fixtures/unicode-and-numbers.json +0 -0
  67. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/conformance/fixtures/urn-alias.json +0 -0
  68. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/conformance/schema/message-envelope.schema.json +0 -0
  69. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/fixtures/dead-lettered.json +0 -0
  70. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/fixtures/order-created.json +0 -0
  71. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_app.py +0 -0
  72. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_artemis_conformance.py +0 -0
  73. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_asb_conformance.py +0 -0
  74. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_celery.py +0 -0
  75. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_conformance.py +0 -0
  76. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_dead_letter.py +0 -0
  77. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_django.py +0 -0
  78. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_gdpr.py +0 -0
  79. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_headers.py +0 -0
  80. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_idempotency.py +0 -0
  81. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_kafka_conformance.py +0 -0
  82. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_otel.py +0 -0
  83. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_outbox.py +0 -0
  84. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_overhead.py +0 -0
  85. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_pika_transport.py +0 -0
  86. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_pulsar_conformance.py +0 -0
  87. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_redis_transport.py +0 -0
  88. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_redrive.py +0 -0
  89. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_replay.py +0 -0
  90. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_schema.py +0 -0
  91. {babelqueue-1.13.0 → babelqueue-1.14.0}/tests/test_schema_conformance.py +0 -0
  92. {babelqueue-1.13.0 → babelqueue-1.14.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,55 @@ The envelope wire format is versioned separately by `meta.schema_version`
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [1.14.0] - 2026-10-03
13
+
14
+ ### Added
15
+ - **Graceful shutdown** of `BabelQueue.consume()`: on SIGTERM/SIGINT the loop sets a stop flag,
16
+ takes no new message, lets the in-flight handler finish (or releases the message unchanged once
17
+ the new `shutdown_timeout` — default 30 s — expires), restores the previous signal handlers and
18
+ closes the transport. Handlers are installed only on the main thread (`handle_signals=False`
19
+ opts out); a second signal forces an immediate exit with the message released. A plain
20
+ `KeyboardInterrupt` keeps its previous behaviour. New `BabelQueue.stop()` ends the loop from any
21
+ thread after the in-flight message.
22
+ - `Redeliverer` protocol (`redeliver(message, body, delay)`) — a transport's native
23
+ release; the runtime uses it for retries, unknown-URN `RELEASE` and shutdown releases, and
24
+ falls back to publish + ack otherwise. New `retry_backoff` / `unknown_urn_release_delay`
25
+ settings feed its delay.
26
+ - Conformance runners for the `roundtrip`, `data_shape`, `forbidden_keys` and
27
+ `payload_schema_unicode` sections (none skipped).
28
+ - `.github/dependabot.yml` (pip + GitHub Actions, weekly).
29
+
30
+ ### Changed
31
+ - **SQS release** (broker-bindings §3.5): retries, unknown-URN `RELEASE` and shutdown releases
32
+ **always** call `ChangeMessageVisibility` instead of re-sending, so the broker's
33
+ `ApproximateReceiveCount` carries the attempt count. There is no send + delete fallback. The
34
+ default delay for `retry_backoff` and `unknown_urn_release_delay` is **0 s** (immediate
35
+ redelivery); a delay outside 0..43200 s (including `inf`/`nan`) is clamped with a warning on the
36
+ `babelqueue.sqs` logger. A message without a receipt handle is not released (warning); one
37
+ without a receive count is released but its attempts cannot advance (warning).
38
+ **Poison-loop risk:** a message that always fails is redelivered immediately until
39
+ `max_attempts` — configure a native SQS `RedrivePolicy` (`maxReceiveCount` ≥ `max_attempts`) as
40
+ the backstop and a non-zero `retry_backoff` where needed. A shutdown release also counts as a
41
+ receive on SQS, so keep `shutdown_timeout` above the longest handler time.
42
+ - Delivery semantics are documented as **at-least-once**: a handler that finishes right as the
43
+ shutdown deadline fires may be released and run again.
44
+ - The SQS, Kafka, Pulsar, Azure Service Bus and Artemis transports' header/property projections
45
+ read the envelope without logging a false forbidden-key drop (the body is sent unchanged).
46
+
47
+ ### Fixed
48
+ - The five forbidden envelope keys (`timestamp`, `meta.max_retries`, `meta.attempts`,
49
+ `meta.source`, `meta.ts` — message-envelope §10) are dropped on decode with a warning on the
50
+ `babelqueue.codec` logger, and never written on encode. **Transitional:** encode currently drops
51
+ them with a warning; a future MINOR (K-15 / R1-E0) will reject them instead. A raw-body release
52
+ republish now strips them too (a clean body is re-sent byte for byte); DLQ redrive still restores
53
+ the original bytes unchanged.
54
+ - `BabelQueue.stop()` called before `consume()` starts is now honoured instead of being cleared.
55
+ - A failed acknowledgement of an already-processed message (e.g. SQS `DeleteMessage` after a
56
+ successful handler, an unknown-URN `DELETE`, or a dead-letter publish) is no longer treated as a
57
+ handler failure: the message is **not** released or retried; the failure is logged at `ERROR`
58
+ on the new `babelqueue.app` logger and the broker redelivers it on its own (SQS: after the
59
+ visibility timeout). The consume loop keeps running.
60
+
12
61
  ## [1.13.0] - 2026-06-21
13
62
 
14
63
  ### Added
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: babelqueue
3
- Version: 1.13.0
3
+ Version: 1.14.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
@@ -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,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "babelqueue"
7
- version = "1.13.0"
7
+ version = "1.14.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"
@@ -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,9 @@ 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 HeaderPublisher, InMemoryTransport, ReceivedMessage, Redeliverer, Transport
33
33
 
34
- __version__ = "1.13.0"
34
+ __version__ = "1.14.0"
35
35
 
36
36
  __all__ = [
37
37
  "BabelQueue",
@@ -45,6 +45,7 @@ __all__ = [
45
45
  "InMemoryTransport",
46
46
  "ReceivedMessage",
47
47
  "HeaderPublisher",
48
+ "Redeliverer",
48
49
  "BabelQueueError",
49
50
  "UnknownUrnError",
50
51
  "DecryptError",