babelqueue 1.16.1__tar.gz → 1.18.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 (98) hide show
  1. {babelqueue-1.16.1 → babelqueue-1.18.0}/CHANGELOG.md +64 -0
  2. {babelqueue-1.16.1 → babelqueue-1.18.0}/PKG-INFO +1 -1
  3. {babelqueue-1.16.1 → babelqueue-1.18.0}/pyproject.toml +1 -1
  4. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/__init__.py +5 -1
  5. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/app.py +5 -3
  6. babelqueue-1.18.0/src/babelqueue/kafka_transport.py +598 -0
  7. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/redrive.py +43 -16
  8. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/transport.py +65 -3
  9. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/fakes/confluent_kafka/__init__.py +22 -6
  10. babelqueue-1.18.0/tests/test_kafka_integration.py +311 -0
  11. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_kafka_integration_logic.py +1 -1
  12. babelqueue-1.18.0/tests/test_kafka_transport.py +1408 -0
  13. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_overhead.py +26 -11
  14. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_redrive.py +40 -0
  15. babelqueue-1.16.1/src/babelqueue/kafka_transport.py +0 -310
  16. babelqueue-1.16.1/tests/test_kafka_integration.py +0 -169
  17. babelqueue-1.16.1/tests/test_kafka_transport.py +0 -592
  18. {babelqueue-1.16.1 → babelqueue-1.18.0}/.github/FUNDING.yml +0 -0
  19. {babelqueue-1.16.1 → babelqueue-1.18.0}/.github/dependabot.yml +0 -0
  20. {babelqueue-1.16.1 → babelqueue-1.18.0}/.github/workflows/ci.yml +0 -0
  21. {babelqueue-1.16.1 → babelqueue-1.18.0}/.github/workflows/release.yml +0 -0
  22. {babelqueue-1.16.1 → babelqueue-1.18.0}/.gitignore +0 -0
  23. {babelqueue-1.16.1 → babelqueue-1.18.0}/LICENSE +0 -0
  24. {babelqueue-1.16.1 → babelqueue-1.18.0}/README.md +0 -0
  25. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/artemis_transport.py +0 -0
  26. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/asb_transport.py +0 -0
  27. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/celery.py +0 -0
  28. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/codec.py +0 -0
  29. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/contracts.py +0 -0
  30. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/dead_letter.py +0 -0
  31. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/django/__init__.py +0 -0
  32. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/django/apps.py +0 -0
  33. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/django/management/__init__.py +0 -0
  34. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/django/management/commands/__init__.py +0 -0
  35. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/django/management/commands/babelqueue_worker.py +0 -0
  36. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/exceptions.py +0 -0
  37. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/gdpr.py +0 -0
  38. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/headers.py +0 -0
  39. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/idempotency.py +0 -0
  40. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/otel.py +0 -0
  41. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/outbox.py +0 -0
  42. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/pika_transport.py +0 -0
  43. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/pulsar_transport.py +0 -0
  44. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/py.typed +0 -0
  45. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/redis_transport.py +0 -0
  46. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/replay.py +0 -0
  47. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/routing.py +0 -0
  48. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/schema.py +0 -0
  49. {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/sqs_transport.py +0 -0
  50. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/CONFORMANCE_VERSION +0 -0
  51. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/data-array-rejected.json +0 -0
  52. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/dead-lettered.json +0 -0
  53. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/empty-data-roundtrip.json +0 -0
  54. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/forbidden-key-meta-attempts.json +0 -0
  55. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/forbidden-key-meta-max-retries.json +0 -0
  56. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/forbidden-key-meta-source.json +0 -0
  57. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/forbidden-key-meta-ts.json +0 -0
  58. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/forbidden-key-timestamp.json +0 -0
  59. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/invalid-missing-urn.json +0 -0
  60. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/invalid-unknown-schema-version.json +0 -0
  61. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/order-created.json +0 -0
  62. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/unicode-and-numbers.json +0 -0
  63. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/unknown-lang.json +0 -0
  64. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/unknown-meta-roundtrip.json +0 -0
  65. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/unknown-toplevel-roundtrip.json +0 -0
  66. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/urn-alias.json +0 -0
  67. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/manifest.json +0 -0
  68. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/schema/message-envelope.schema.json +0 -0
  69. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/fakes/confluent_kafka/admin.py +0 -0
  70. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/fixtures/dead-lettered.json +0 -0
  71. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/fixtures/order-created.json +0 -0
  72. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_app.py +0 -0
  73. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_artemis_conformance.py +0 -0
  74. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_artemis_transport.py +0 -0
  75. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_asb_conformance.py +0 -0
  76. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_asb_transport.py +0 -0
  77. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_behaviour_conformance.py +0 -0
  78. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_celery.py +0 -0
  79. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_codec.py +0 -0
  80. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_conformance.py +0 -0
  81. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_dead_letter.py +0 -0
  82. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_django.py +0 -0
  83. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_gdpr.py +0 -0
  84. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_headers.py +0 -0
  85. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_idempotency.py +0 -0
  86. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_kafka_conformance.py +0 -0
  87. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_otel.py +0 -0
  88. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_outbox.py +0 -0
  89. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_pika_transport.py +0 -0
  90. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_pulsar_conformance.py +0 -0
  91. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_pulsar_transport.py +0 -0
  92. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_redis_transport.py +0 -0
  93. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_replay.py +0 -0
  94. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_schema.py +0 -0
  95. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_schema_conformance.py +0 -0
  96. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_shutdown.py +0 -0
  97. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_sqs_conformance.py +0 -0
  98. {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_sqs_transport.py +0 -0
@@ -9,6 +9,70 @@ The envelope wire format is versioned separately by `meta.schema_version`
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [1.18.0] - 2026-10-11
13
+
14
+ ### Added
15
+ - `BatchPopper` (optional transport capability, exported from `babelqueue`) with
16
+ `pop_batch(queue, max=0, timeout=1.0)`: take several deliveries at once and hold them un-settled.
17
+ `KafkaTransport` implements it; `redrive` uses it when a transport offers it.
18
+
19
+ ### Fixed
20
+ - **`redrive` no longer loops forever (default `max=0`) or repeats records (`max>1`) on Kafka.**
21
+ `redrive` drains the DLQ before it processes it, but `KafkaTransport.pop` first seeks back to the
22
+ previous un-acked record, so every pop returned the first record again. The drain now takes the
23
+ batch through `pop_batch` (one rewind up front, none between the polls), and `KafkaTransport`
24
+ tracks several in-flight records: acks are committed in offset order, so the cumulative commit
25
+ never passes a record that is still unsettled, and an abandoned batch is rewound to its lowest
26
+ offset by the next `pop`. The restore writes are not re-read in the same call. Other transports
27
+ and the single-record `pop`/`ack`/`rewind` behaviour are unchanged. No wire change.
28
+ A record whose envelope carries an `attempts` the runtime cannot read as an integer (a foreign
29
+ producer: `null`, `""`, a list/object, a bool, `"three"`, `NaN`/`Infinity`; an absent key is just 0) no longer raises out
30
+ of `pop` or the restore publish. `KafkaTransport.pop` hands it over under a reserved poison route
31
+ (like a non-UTF-8 record), so the runtime dead-letters it raw without running a handler (a failing
32
+ handler can no longer wedge the partition) and `redrive` restores it to the DLQ instead of
33
+ replaying it. The redundant `bq-attempts` header is left out of such a write.
34
+ - **`redrive` now carries the DLQ record key onto the redriven record.** The restore writes back to
35
+ the DLQ already kept the key; the redrive write to the work topic did not, so a replayed Kafka
36
+ record lost its key (and per-key partition affinity). It now goes through
37
+ `publish_keeping_route(..., key=message.key)`, byte-for-byte (null stays null, empty stays empty),
38
+ per contract §6.5 "Record key on republish". Non-keyed transports behave exactly as before; the
39
+ `bypass=True` header path still carries no key. No wire change.
40
+
41
+ ## [1.17.0] - 2026-10-11
42
+
43
+ ### Added
44
+ - `SettlePublisher` (optional transport capability, exported from `babelqueue`) with
45
+ `publish_settle(queue, body, route_urn="")`: the runtime's settle writes (retry republish,
46
+ envelope / poison / gate-rejection DLQ, `redrive` restore) go through it when a transport
47
+ implements it. `KafkaTransport` implements it.
48
+ - `KafkaDeliveryError` (a `BabelQueueError`) and the optional `KafkaTransport(delivery_timeout=30.0)`
49
+ constructor argument.
50
+ - `ReceivedMessage.key` (`Optional[bytes]`, `None` = null key) and an optional `key=` argument on
51
+ `SettlePublisher.publish_settle()` / `publish_keeping_route()`. `KafkaTransport.pop()` fills the
52
+ key from the consumed record.
53
+
54
+ ### Fixed
55
+ - **Kafka: the original record key now survives the retry republish, the envelope DLQ and the
56
+ raw (poison / gate-rejection) DLQ.** The key was dropped on every settle write, so a keyed record
57
+ lost its key (and with it per-key partition affinity) after the first retry. A null key stays
58
+ null, an empty key stays empty, and a key is never synthesised. `redrive` restores to `<queue>.dlq`
59
+ also keep it. Partition equality is not guaranteed. The envelope and the `dead_letter` block are
60
+ unchanged.
61
+ - **Kafka: retry / dead-letter writes are now broker-confirmed before the offset is committed.**
62
+ `KafkaTransport.publish_settle()` waits for this write's own delivery report
63
+ (`flush(delivery_timeout)`). A delivery error or timeout raises `KafkaDeliveryError`, nothing is
64
+ committed, and the record is re-read through the existing rewind/seek path — so such a write can
65
+ no longer be lost to a commit that raced ahead of it. `publish()` (and so `app.publish()`) is
66
+ unchanged: still fire-and-forget and strict (a body that is not valid UTF-8 still raises
67
+ `UnicodeEncodeError`), also from inside a handler.
68
+ - **Kafka: a record whose value is not valid UTF-8 is dead-lettered raw instead of raising.** The
69
+ original bytes are written to `<queue>.dlq` unchanged, with no projected headers (as
70
+ Java/Go/Node do for poison records), and the offset is committed only once that write is
71
+ confirmed; with the DLQ off, or `UnknownUrnStrategy.DELETE`, the record is dropped and
72
+ committed. It reuses the version-gate settle path, so a failed write logs as a "version-gated"
73
+ message — the logged route (`...:non-utf8-payload`) tells them apart. A non-UTF-8 `bq-attempts`
74
+ header no longer raises. No wire change.
75
+
12
76
  ## [1.16.1] - 2026-10-11
13
77
 
14
78
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: babelqueue
3
- Version: 1.16.1
3
+ Version: 1.18.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "babelqueue"
7
- version = "1.16.1"
7
+ version = "1.18.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,16 +30,18 @@ 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
+ BatchPopper,
33
34
  HeaderPublisher,
34
35
  InMemoryTransport,
35
36
  ReceivedMessage,
36
37
  Redeliverer,
37
38
  Rewinder,
38
39
  RoutedPublisher,
40
+ SettlePublisher,
39
41
  Transport,
40
42
  )
41
43
 
42
- __version__ = "1.16.1"
44
+ __version__ = "1.18.0"
43
45
 
44
46
  __all__ = [
45
47
  "BabelQueue",
@@ -52,10 +54,12 @@ __all__ = [
52
54
  "Transport",
53
55
  "InMemoryTransport",
54
56
  "ReceivedMessage",
57
+ "BatchPopper",
55
58
  "HeaderPublisher",
56
59
  "Redeliverer",
57
60
  "Rewinder",
58
61
  "RoutedPublisher",
62
+ "SettlePublisher",
59
63
  "BabelQueueError",
60
64
  "UnknownUrnError",
61
65
  "DecryptError",
@@ -308,8 +308,9 @@ class BabelQueue:
308
308
  # It is left unacked; the broker (or a rewind, for cumulative-offset brokers
309
309
  # such as Kafka) brings it back and the gate rejects it again.
310
310
  logger.error(
311
- "Dead-lettering a version-gated message on queue %r failed; it was NOT "
311
+ "Dead-lettering a version-gated message (route %r) on queue %r failed; it was NOT "
312
312
  "retried or acknowledged; it will come back and the dead-letter write will be retried.",
313
+ received.route_urn,
313
314
  received.queue,
314
315
  exc_info=exc,
315
316
  )
@@ -414,7 +415,7 @@ class BabelQueue:
414
415
  or its dead-letter record). Keeps the routing URN the broker property carried on this
415
416
  delivery when the transport can pin it (:class:`RoutedPublisher`), so a property/body
416
417
  mismatch does not flip to the body URN on the next attempt or in the DLQ."""
417
- publish_keeping_route(self.transport, queue, body, received.route_urn)
418
+ publish_keeping_route(self.transport, queue, body, received.route_urn, received.key)
418
419
 
419
420
  def _ack_settled(self, received: ReceivedMessage) -> None:
420
421
  """Acknowledge a message whose outcome is already final (handled, dead-lettered,
@@ -447,7 +448,8 @@ class BabelQueue:
447
448
  self.transport.rewind(received)
448
449
  except Exception: # noqa: BLE001 - reported, never kills the loop
449
450
  logger.error(
450
- "Failed to rewind an unsettled version-gated message on queue %r.",
451
+ "Failed to rewind an unsettled version-gated message (route %r) on queue %r.",
452
+ received.route_urn,
451
453
  received.queue,
452
454
  exc_info=True,
453
455
  )