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.
- {babelqueue-1.16.1 → babelqueue-1.18.0}/CHANGELOG.md +64 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/PKG-INFO +1 -1
- {babelqueue-1.16.1 → babelqueue-1.18.0}/pyproject.toml +1 -1
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/__init__.py +5 -1
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/app.py +5 -3
- babelqueue-1.18.0/src/babelqueue/kafka_transport.py +598 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/redrive.py +43 -16
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/transport.py +65 -3
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/fakes/confluent_kafka/__init__.py +22 -6
- babelqueue-1.18.0/tests/test_kafka_integration.py +311 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_kafka_integration_logic.py +1 -1
- babelqueue-1.18.0/tests/test_kafka_transport.py +1408 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_overhead.py +26 -11
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_redrive.py +40 -0
- babelqueue-1.16.1/src/babelqueue/kafka_transport.py +0 -310
- babelqueue-1.16.1/tests/test_kafka_integration.py +0 -169
- babelqueue-1.16.1/tests/test_kafka_transport.py +0 -592
- {babelqueue-1.16.1 → babelqueue-1.18.0}/.github/FUNDING.yml +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/.github/dependabot.yml +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/.github/workflows/ci.yml +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/.github/workflows/release.yml +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/.gitignore +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/LICENSE +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/README.md +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/artemis_transport.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/asb_transport.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/celery.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/codec.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/contracts.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/dead_letter.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/django/__init__.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/django/apps.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/django/management/__init__.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/django/management/commands/__init__.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/django/management/commands/babelqueue_worker.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/exceptions.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/gdpr.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/headers.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/idempotency.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/otel.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/outbox.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/pika_transport.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/pulsar_transport.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/py.typed +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/redis_transport.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/replay.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/routing.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/schema.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/src/babelqueue/sqs_transport.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/CONFORMANCE_VERSION +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/data-array-rejected.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/dead-lettered.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/empty-data-roundtrip.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/forbidden-key-meta-attempts.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/forbidden-key-meta-max-retries.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/forbidden-key-meta-source.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/forbidden-key-meta-ts.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/forbidden-key-timestamp.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/invalid-missing-urn.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/invalid-unknown-schema-version.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/order-created.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/unicode-and-numbers.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/unknown-lang.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/unknown-meta-roundtrip.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/unknown-toplevel-roundtrip.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/fixtures/urn-alias.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/manifest.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/conformance/schema/message-envelope.schema.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/fakes/confluent_kafka/admin.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/fixtures/dead-lettered.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/fixtures/order-created.json +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_app.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_artemis_conformance.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_artemis_transport.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_asb_conformance.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_asb_transport.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_behaviour_conformance.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_celery.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_codec.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_conformance.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_dead_letter.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_django.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_gdpr.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_headers.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_idempotency.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_kafka_conformance.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_otel.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_outbox.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_pika_transport.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_pulsar_conformance.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_pulsar_transport.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_redis_transport.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_replay.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_schema.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_schema_conformance.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_shutdown.py +0 -0
- {babelqueue-1.16.1 → babelqueue-1.18.0}/tests/test_sqs_conformance.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
)
|