babelqueue 0.2.0__tar.gz → 0.4.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-0.2.0 → babelqueue-0.4.0}/.github/workflows/ci.yml +13 -3
- {babelqueue-0.2.0 → babelqueue-0.4.0}/CHANGELOG.md +25 -3
- {babelqueue-0.2.0 → babelqueue-0.4.0}/PKG-INFO +12 -9
- {babelqueue-0.2.0 → babelqueue-0.4.0}/README.md +11 -8
- {babelqueue-0.2.0 → babelqueue-0.4.0}/pyproject.toml +1 -1
- {babelqueue-0.2.0 → babelqueue-0.4.0}/src/babelqueue/__init__.py +1 -1
- {babelqueue-0.2.0 → babelqueue-0.4.0}/src/babelqueue/codec.py +33 -0
- babelqueue-0.4.0/src/babelqueue/pika_transport.py +109 -0
- {babelqueue-0.2.0 → babelqueue-0.4.0}/src/babelqueue/transport.py +6 -2
- babelqueue-0.4.0/tests/conformance/fixtures/invalid-missing-urn.json +14 -0
- babelqueue-0.4.0/tests/conformance/fixtures/invalid-unknown-schema-version.json +15 -0
- babelqueue-0.4.0/tests/conformance/fixtures/unicode-and-numbers.json +20 -0
- babelqueue-0.4.0/tests/conformance/fixtures/urn-alias.json +15 -0
- babelqueue-0.4.0/tests/conformance/manifest.json +71 -0
- babelqueue-0.4.0/tests/conformance/schema/message-envelope.schema.json +110 -0
- babelqueue-0.4.0/tests/fixtures/dead-lettered.json +24 -0
- babelqueue-0.4.0/tests/fixtures/order-created.json +15 -0
- babelqueue-0.4.0/tests/test_conformance.py +58 -0
- babelqueue-0.4.0/tests/test_pika_transport.py +98 -0
- {babelqueue-0.2.0 → babelqueue-0.4.0}/.github/workflows/release.yml +0 -0
- {babelqueue-0.2.0 → babelqueue-0.4.0}/.gitignore +0 -0
- {babelqueue-0.2.0 → babelqueue-0.4.0}/LICENSE +0 -0
- {babelqueue-0.2.0 → babelqueue-0.4.0}/src/babelqueue/app.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.4.0}/src/babelqueue/contracts.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.4.0}/src/babelqueue/dead_letter.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.4.0}/src/babelqueue/exceptions.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.4.0}/src/babelqueue/py.typed +0 -0
- {babelqueue-0.2.0 → babelqueue-0.4.0}/src/babelqueue/redis_transport.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.4.0}/src/babelqueue/routing.py +0 -0
- {babelqueue-0.2.0/tests → babelqueue-0.4.0/tests/conformance}/fixtures/dead-lettered.json +0 -0
- {babelqueue-0.2.0/tests → babelqueue-0.4.0/tests/conformance}/fixtures/order-created.json +0 -0
- {babelqueue-0.2.0 → babelqueue-0.4.0}/tests/test_app.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.4.0}/tests/test_codec.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.4.0}/tests/test_dead_letter.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.4.0}/tests/test_redis_transport.py +0 -0
|
@@ -45,6 +45,15 @@ jobs:
|
|
|
45
45
|
--health-interval 5s
|
|
46
46
|
--health-timeout 3s
|
|
47
47
|
--health-retries 10
|
|
48
|
+
rabbitmq:
|
|
49
|
+
image: rabbitmq:3
|
|
50
|
+
ports:
|
|
51
|
+
- 5672:5672
|
|
52
|
+
options: >-
|
|
53
|
+
--health-cmd "rabbitmq-diagnostics -q ping"
|
|
54
|
+
--health-interval 10s
|
|
55
|
+
--health-timeout 5s
|
|
56
|
+
--health-retries 15
|
|
48
57
|
steps:
|
|
49
58
|
- uses: actions/checkout@v4
|
|
50
59
|
|
|
@@ -53,12 +62,13 @@ jobs:
|
|
|
53
62
|
with:
|
|
54
63
|
python-version: '3.12'
|
|
55
64
|
|
|
56
|
-
- name: Install (with redis
|
|
65
|
+
- name: Install (with redis + amqp extras)
|
|
57
66
|
run: |
|
|
58
67
|
python -m pip install --upgrade pip
|
|
59
|
-
pip install -e ".[redis,dev]"
|
|
68
|
+
pip install -e ".[redis,amqp,dev]"
|
|
60
69
|
|
|
61
|
-
- name: Run tests (Redis
|
|
70
|
+
- name: Run tests (Redis + RabbitMQ transports included)
|
|
62
71
|
env:
|
|
63
72
|
BABELQUEUE_TEST_REDIS: redis://localhost:6379/0
|
|
73
|
+
BABELQUEUE_TEST_AMQP: amqp://guest:guest@localhost:5672/
|
|
64
74
|
run: pytest
|
|
@@ -9,6 +9,25 @@ The envelope wire format is versioned separately by `meta.schema_version`
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.4.0] - 2026-06-06
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `EnvelopeCodec.urn()` — resolve the URN (`job`, accepting `urn` as an alias).
|
|
16
|
+
- `EnvelopeCodec.accepts()` — consumer-side envelope validation (rejects empty URN,
|
|
17
|
+
unsupported `meta.schema_version`, blank `trace_id`, non-object `data`).
|
|
18
|
+
- Shared **cross-SDK conformance suite** under `tests/conformance/` (vendored from
|
|
19
|
+
the canonical `conformance/` set) plus a `test_conformance.py` runner.
|
|
20
|
+
|
|
21
|
+
## [0.3.0] - 2026-06-06
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **RabbitMQ transport** (`PikaTransport`, `amqp://`): durable queue, persistent
|
|
25
|
+
delivery, `basic_get` + manual ack, and the contract AMQP properties (`type`=URN,
|
|
26
|
+
`correlation_id`=trace_id, `x-schema-version`/`x-source-lang`/`x-attempts`).
|
|
27
|
+
Optional `[amqp]` extra (lazy `pika` import) — the core stays zero-dep.
|
|
28
|
+
|
|
29
|
+
## [0.2.0] - 2026-06-06
|
|
30
|
+
|
|
12
31
|
### Added
|
|
13
32
|
- **Runtime** — `BabelQueue(broker_url=...)` app with a `@app.handler("urn:...")`
|
|
14
33
|
decorator, `publish()`, and a `consume()` / `run()` loop. Routes by URN over the
|
|
@@ -16,8 +35,8 @@ The envelope wire format is versioned separately by `meta.schema_version`
|
|
|
16
35
|
`on_unknown_urn` strategies (`fail`/`delete`/`release`/`dead_letter`).
|
|
17
36
|
- **Transports** — a pluggable `Transport` abstraction with `InMemoryTransport`
|
|
18
37
|
(`memory://`, for tests/local) and `RedisTransport` (`redis://`, reliable-queue
|
|
19
|
-
pattern via `BLMOVE` + a processing list). Redis client is an optional
|
|
20
|
-
|
|
38
|
+
pattern via `BLMOVE` + a processing list). Redis client is an optional `[redis]`
|
|
39
|
+
extra, imported lazily — the core stays zero-dep.
|
|
21
40
|
|
|
22
41
|
## [0.1.0] - 2026-06-06
|
|
23
42
|
|
|
@@ -36,5 +55,8 @@ The envelope wire format is versioned separately by `meta.schema_version`
|
|
|
36
55
|
- Pre-1.0: the public API may change before the `1.0.0` tag.
|
|
37
56
|
- The core has **zero runtime dependencies** (standard library only); Python `>=3.9`.
|
|
38
57
|
|
|
39
|
-
[Unreleased]: https://github.com/BabelQueue/babelqueue-python/compare/v0.
|
|
58
|
+
[Unreleased]: https://github.com/BabelQueue/babelqueue-python/compare/v0.4.0...HEAD
|
|
59
|
+
[0.4.0]: https://github.com/BabelQueue/babelqueue-python/compare/v0.3.0...v0.4.0
|
|
60
|
+
[0.3.0]: https://github.com/BabelQueue/babelqueue-python/compare/v0.2.0...v0.3.0
|
|
61
|
+
[0.2.0]: https://github.com/BabelQueue/babelqueue-python/compare/v0.1.0...v0.2.0
|
|
40
62
|
[0.1.0]: https://github.com/BabelQueue/babelqueue-python/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: babelqueue
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -114,17 +114,19 @@ dlq = dead_letter.annotate(envelope, "failed", "orders", attempts=3, error="boom
|
|
|
114
114
|
|
|
115
115
|
## Runtime — produce & consume
|
|
116
116
|
|
|
117
|
-
For an end-to-end app, use `BabelQueue` with a broker.
|
|
118
|
-
|
|
117
|
+
For an end-to-end app, use `BabelQueue` with a broker. Broker clients come via
|
|
118
|
+
extras:
|
|
119
119
|
|
|
120
120
|
```bash
|
|
121
|
-
pip install "babelqueue[redis]"
|
|
121
|
+
pip install "babelqueue[redis]" # redis://
|
|
122
|
+
pip install "babelqueue[amqp]" # amqp:// (RabbitMQ)
|
|
122
123
|
```
|
|
123
124
|
|
|
124
125
|
```python
|
|
125
126
|
from babelqueue import BabelQueue
|
|
126
127
|
|
|
127
128
|
app = BabelQueue("redis://localhost:6379/0", queue="orders")
|
|
129
|
+
# or: BabelQueue("amqp://guest:guest@localhost:5672/", queue="orders")
|
|
128
130
|
|
|
129
131
|
@app.handler("urn:babel:orders:created")
|
|
130
132
|
def on_order_created(data, meta): # AI/ML, data processing, anything
|
|
@@ -144,16 +146,17 @@ app.run() # consume forever (Ctrl-C to stop)
|
|
|
144
146
|
- **Retry & dead-letter:** failures are retried up to `max_attempts` (bumping the
|
|
145
147
|
envelope's `attempts`); enable `dead_letter=True` to quarantine exhausted
|
|
146
148
|
messages on `<queue>.dlq`. `on_unknown_urn` = `fail` | `delete` | `release` | `dead_letter`.
|
|
147
|
-
- **Transports:** `redis://` (reliable-queue pattern)
|
|
148
|
-
|
|
149
|
+
- **Transports:** `redis://` (reliable-queue pattern), `amqp://` (RabbitMQ via
|
|
150
|
+
`pika`, with the contract AMQP properties) and `memory://` (in-process, great for
|
|
151
|
+
tests/local). Bring your own by passing `transport=...`.
|
|
149
152
|
|
|
150
|
-
>
|
|
153
|
+
> **Celery** / **Django** adapters are the next iterations.
|
|
151
154
|
|
|
152
155
|
## What's here
|
|
153
156
|
|
|
154
157
|
The codec/contracts/dead-letter (zero-dep core) **and** the `BabelQueue` runtime
|
|
155
|
-
above (in-memory built in; Redis via
|
|
156
|
-
integration, the Celery and Django adapters are planned.
|
|
158
|
+
above (in-memory built in; Redis via `[redis]`, RabbitMQ via `[amqp]`). For
|
|
159
|
+
framework integration, the Celery and Django adapters are planned.
|
|
157
160
|
|
|
158
161
|
## Testing
|
|
159
162
|
|
|
@@ -82,17 +82,19 @@ dlq = dead_letter.annotate(envelope, "failed", "orders", attempts=3, error="boom
|
|
|
82
82
|
|
|
83
83
|
## Runtime — produce & consume
|
|
84
84
|
|
|
85
|
-
For an end-to-end app, use `BabelQueue` with a broker.
|
|
86
|
-
|
|
85
|
+
For an end-to-end app, use `BabelQueue` with a broker. Broker clients come via
|
|
86
|
+
extras:
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
|
-
pip install "babelqueue[redis]"
|
|
89
|
+
pip install "babelqueue[redis]" # redis://
|
|
90
|
+
pip install "babelqueue[amqp]" # amqp:// (RabbitMQ)
|
|
90
91
|
```
|
|
91
92
|
|
|
92
93
|
```python
|
|
93
94
|
from babelqueue import BabelQueue
|
|
94
95
|
|
|
95
96
|
app = BabelQueue("redis://localhost:6379/0", queue="orders")
|
|
97
|
+
# or: BabelQueue("amqp://guest:guest@localhost:5672/", queue="orders")
|
|
96
98
|
|
|
97
99
|
@app.handler("urn:babel:orders:created")
|
|
98
100
|
def on_order_created(data, meta): # AI/ML, data processing, anything
|
|
@@ -112,16 +114,17 @@ app.run() # consume forever (Ctrl-C to stop)
|
|
|
112
114
|
- **Retry & dead-letter:** failures are retried up to `max_attempts` (bumping the
|
|
113
115
|
envelope's `attempts`); enable `dead_letter=True` to quarantine exhausted
|
|
114
116
|
messages on `<queue>.dlq`. `on_unknown_urn` = `fail` | `delete` | `release` | `dead_letter`.
|
|
115
|
-
- **Transports:** `redis://` (reliable-queue pattern)
|
|
116
|
-
|
|
117
|
+
- **Transports:** `redis://` (reliable-queue pattern), `amqp://` (RabbitMQ via
|
|
118
|
+
`pika`, with the contract AMQP properties) and `memory://` (in-process, great for
|
|
119
|
+
tests/local). Bring your own by passing `transport=...`.
|
|
117
120
|
|
|
118
|
-
>
|
|
121
|
+
> **Celery** / **Django** adapters are the next iterations.
|
|
119
122
|
|
|
120
123
|
## What's here
|
|
121
124
|
|
|
122
125
|
The codec/contracts/dead-letter (zero-dep core) **and** the `BabelQueue` runtime
|
|
123
|
-
above (in-memory built in; Redis via
|
|
124
|
-
integration, the Celery and Django adapters are planned.
|
|
126
|
+
above (in-memory built in; Redis via `[redis]`, RabbitMQ via `[amqp]`). For
|
|
127
|
+
framework integration, the Celery and Django adapters are planned.
|
|
125
128
|
|
|
126
129
|
## Testing
|
|
127
130
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "babelqueue"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.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"
|
|
@@ -94,3 +94,36 @@ class EnvelopeCodec:
|
|
|
94
94
|
except (ValueError, TypeError):
|
|
95
95
|
return {}
|
|
96
96
|
return decoded if isinstance(decoded, dict) else {}
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def urn(envelope: Mapping[str, Any]) -> str:
|
|
100
|
+
"""The message URN: canonical ``job``, with ``urn`` accepted as an alias."""
|
|
101
|
+
return str(envelope.get("job") or envelope.get("urn") or "")
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def accepts(envelope: Mapping[str, Any]) -> bool:
|
|
105
|
+
"""Whether a consumer should accept this envelope (consumer-side validation).
|
|
106
|
+
|
|
107
|
+
Rejects messages with no URN, an unsupported ``meta.schema_version``, a
|
|
108
|
+
missing/blank ``trace_id``, or a non-object ``data`` / non-integer
|
|
109
|
+
``attempts``. (Accepts the ``urn`` alias, unlike the producer JSON Schema.)
|
|
110
|
+
"""
|
|
111
|
+
if EnvelopeCodec.urn(envelope) == "":
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
meta = envelope.get("meta")
|
|
115
|
+
if not isinstance(meta, dict) or meta.get("schema_version") != SCHEMA_VERSION:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
if not isinstance(envelope.get("data"), dict):
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
attempts = envelope.get("attempts")
|
|
122
|
+
if not isinstance(attempts, int) or isinstance(attempts, bool):
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
trace_id = envelope.get("trace_id")
|
|
126
|
+
if not isinstance(trace_id, str) or trace_id == "":
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
return True
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""RabbitMQ transport over AMQP 0-9-1. Requires the ``amqp`` extra:
|
|
2
|
+
|
|
3
|
+
pip install "babelqueue[amqp]"
|
|
4
|
+
|
|
5
|
+
Producing publishes the envelope to a durable queue with persistent delivery and
|
|
6
|
+
the AMQP properties that are part of the cross-language contract (``type`` = URN,
|
|
7
|
+
``correlation_id`` = trace_id, ``message_id`` = meta.id, ``x-schema-version`` /
|
|
8
|
+
``x-source-lang`` / ``x-attempts`` headers) — so a Go/PHP consumer can route on
|
|
9
|
+
``properties.type`` without parsing the body. Consuming uses ``basic_get`` + manual
|
|
10
|
+
ack (at-least-once), matching the PHP RabbitMQ driver.
|
|
11
|
+
|
|
12
|
+
Connection is lazy; it (re)connects on first use and after a drop.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from typing import Any, Dict, Optional
|
|
19
|
+
|
|
20
|
+
from .transport import ReceivedMessage, Transport
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PikaTransport(Transport):
|
|
24
|
+
def __init__(self, url: str) -> None:
|
|
25
|
+
try:
|
|
26
|
+
import pika
|
|
27
|
+
except ImportError as exc: # pragma: no cover - import guard
|
|
28
|
+
raise ImportError(
|
|
29
|
+
"PikaTransport requires the 'pika' package. Install with "
|
|
30
|
+
'pip install "babelqueue[amqp]".'
|
|
31
|
+
) from exc
|
|
32
|
+
|
|
33
|
+
self._pika = pika
|
|
34
|
+
self._url = url
|
|
35
|
+
self._connection: Any = None
|
|
36
|
+
self._channel: Any = None
|
|
37
|
+
self._declared: set[str] = set()
|
|
38
|
+
|
|
39
|
+
# -- connection / topology ---------------------------------------------
|
|
40
|
+
|
|
41
|
+
def _chan(self) -> Any:
|
|
42
|
+
if self._connection is None or self._connection.is_closed:
|
|
43
|
+
self._connection = self._pika.BlockingConnection(self._pika.URLParameters(self._url))
|
|
44
|
+
self._channel = None
|
|
45
|
+
self._declared.clear()
|
|
46
|
+
if self._channel is None or self._channel.is_closed:
|
|
47
|
+
self._channel = self._connection.channel()
|
|
48
|
+
return self._channel
|
|
49
|
+
|
|
50
|
+
def _declare(self, queue: str) -> None:
|
|
51
|
+
if queue not in self._declared:
|
|
52
|
+
self._chan().queue_declare(queue=queue, durable=True)
|
|
53
|
+
self._declared.add(queue)
|
|
54
|
+
|
|
55
|
+
def _properties(self, body: str) -> Any:
|
|
56
|
+
"""AMQP properties derived from the envelope (part of the wire contract)."""
|
|
57
|
+
try:
|
|
58
|
+
envelope: Dict[str, Any] = json.loads(body)
|
|
59
|
+
except (ValueError, TypeError):
|
|
60
|
+
return self._pika.BasicProperties(content_type="application/json", delivery_mode=2)
|
|
61
|
+
|
|
62
|
+
meta = envelope.get("meta") or {}
|
|
63
|
+
headers = {
|
|
64
|
+
"x-schema-version": meta.get("schema_version"),
|
|
65
|
+
"x-source-lang": meta.get("lang"),
|
|
66
|
+
"x-attempts": envelope.get("attempts", 0),
|
|
67
|
+
}
|
|
68
|
+
return self._pika.BasicProperties(
|
|
69
|
+
content_type="application/json",
|
|
70
|
+
content_encoding="utf-8",
|
|
71
|
+
delivery_mode=2, # persistent
|
|
72
|
+
message_id=meta.get("id"),
|
|
73
|
+
correlation_id=envelope.get("trace_id"),
|
|
74
|
+
type=envelope.get("job"),
|
|
75
|
+
app_id="babelqueue",
|
|
76
|
+
headers={k: v for k, v in headers.items() if v is not None},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# -- Transport ----------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def publish(self, queue: str, body: str) -> None:
|
|
82
|
+
self._declare(queue)
|
|
83
|
+
self._chan().basic_publish(
|
|
84
|
+
exchange="",
|
|
85
|
+
routing_key=queue,
|
|
86
|
+
body=body.encode("utf-8"),
|
|
87
|
+
properties=self._properties(body),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def pop(self, queue: str, timeout: float = 1.0) -> Optional[ReceivedMessage]:
|
|
91
|
+
self._declare(queue)
|
|
92
|
+
method, _props, body = self._chan().basic_get(queue=queue, auto_ack=False)
|
|
93
|
+
if method is None:
|
|
94
|
+
# Nothing ready — sleep (heartbeat-safe) so the caller doesn't busy-loop.
|
|
95
|
+
if timeout and timeout > 0:
|
|
96
|
+
self._connection.sleep(timeout)
|
|
97
|
+
return None
|
|
98
|
+
text = body.decode("utf-8") if isinstance(body, (bytes, bytearray)) else str(body)
|
|
99
|
+
return ReceivedMessage(body=text, queue=queue, handle=method.delivery_tag)
|
|
100
|
+
|
|
101
|
+
def ack(self, message: ReceivedMessage) -> None:
|
|
102
|
+
self._chan().basic_ack(delivery_tag=message.handle)
|
|
103
|
+
|
|
104
|
+
def close(self) -> None: # pragma: no cover
|
|
105
|
+
try:
|
|
106
|
+
if self._connection is not None and self._connection.is_open:
|
|
107
|
+
self._connection.close()
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
@@ -75,8 +75,12 @@ def make_transport(broker_url: str) -> Transport:
|
|
|
75
75
|
from .redis_transport import RedisTransport
|
|
76
76
|
|
|
77
77
|
return RedisTransport(broker_url)
|
|
78
|
+
if scheme in ("amqp", "amqps"):
|
|
79
|
+
from .pika_transport import PikaTransport
|
|
80
|
+
|
|
81
|
+
return PikaTransport(broker_url)
|
|
78
82
|
|
|
79
83
|
raise BabelQueueError(
|
|
80
|
-
f"Unsupported broker scheme {scheme!r}. Use 'memory://'
|
|
81
|
-
"or pass your own Transport via BabelQueue(transport=...)."
|
|
84
|
+
f"Unsupported broker scheme {scheme!r}. Use 'memory://', 'redis://' or "
|
|
85
|
+
"'amqp://', or pass your own Transport via BabelQueue(transport=...)."
|
|
82
86
|
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
|
|
3
|
+
"data": {
|
|
4
|
+
"order_id": 1042
|
|
5
|
+
},
|
|
6
|
+
"meta": {
|
|
7
|
+
"id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
|
|
8
|
+
"queue": "orders",
|
|
9
|
+
"lang": "php",
|
|
10
|
+
"schema_version": 1,
|
|
11
|
+
"created_at": 1749132727000
|
|
12
|
+
},
|
|
13
|
+
"attempts": 0
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"job": "urn:babel:orders:created",
|
|
3
|
+
"trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
|
|
4
|
+
"data": {
|
|
5
|
+
"order_id": 1042
|
|
6
|
+
},
|
|
7
|
+
"meta": {
|
|
8
|
+
"id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
|
|
9
|
+
"queue": "orders",
|
|
10
|
+
"lang": "php",
|
|
11
|
+
"schema_version": 2,
|
|
12
|
+
"created_at": 1749132727000
|
|
13
|
+
},
|
|
14
|
+
"attempts": 0
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"job": "urn:babel:catalog:item.indexed",
|
|
3
|
+
"trace_id": "3f7a1d2e-9b4c-4a8d-bc1e-0f5a6b7c8d90",
|
|
4
|
+
"data": {
|
|
5
|
+
"title": "Café — naïve ☕",
|
|
6
|
+
"qty": 7,
|
|
7
|
+
"price_cents": 1299,
|
|
8
|
+
"ratio": 0.5,
|
|
9
|
+
"active": true,
|
|
10
|
+
"note": null
|
|
11
|
+
},
|
|
12
|
+
"meta": {
|
|
13
|
+
"id": "b2c3d4e5-f607-4890-a1b2-c3d4e5f60718",
|
|
14
|
+
"queue": "catalog",
|
|
15
|
+
"lang": "python",
|
|
16
|
+
"schema_version": 1,
|
|
17
|
+
"created_at": 1749132727000
|
|
18
|
+
},
|
|
19
|
+
"attempts": 2
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"urn": "urn:babel:orders:created",
|
|
3
|
+
"trace_id": "9c1e0b44-7a2d-4e6f-8a10-2b3c4d5e6f70",
|
|
4
|
+
"data": {
|
|
5
|
+
"order_id": 1042
|
|
6
|
+
},
|
|
7
|
+
"meta": {
|
|
8
|
+
"id": "a1b2c3d4-e5f6-4789-90ab-cdef01234567",
|
|
9
|
+
"queue": "orders",
|
|
10
|
+
"lang": "go",
|
|
11
|
+
"schema_version": 1,
|
|
12
|
+
"created_at": 1749132727000
|
|
13
|
+
},
|
|
14
|
+
"attempts": 0
|
|
15
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"description": "Cross-SDK conformance cases. Every BabelQueue SDK core must satisfy these against the canonical wire envelope. Per-message fields (meta.id, trace_id, meta.created_at) are intrinsically unique and are NOT asserted by value.",
|
|
4
|
+
"cases": [
|
|
5
|
+
{
|
|
6
|
+
"name": "order-created",
|
|
7
|
+
"file": "fixtures/order-created.json",
|
|
8
|
+
"valid": true,
|
|
9
|
+
"description": "A normal produced envelope.",
|
|
10
|
+
"expect": {
|
|
11
|
+
"urn": "urn:babel:orders:created",
|
|
12
|
+
"data": { "order_id": 1042 },
|
|
13
|
+
"attempts": 0,
|
|
14
|
+
"lang": "php",
|
|
15
|
+
"schema_version": 1
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "urn-alias",
|
|
20
|
+
"file": "fixtures/urn-alias.json",
|
|
21
|
+
"valid": true,
|
|
22
|
+
"description": "Consumers MUST accept 'urn' as an inbound alias for 'job'.",
|
|
23
|
+
"expect": {
|
|
24
|
+
"urn": "urn:babel:orders:created",
|
|
25
|
+
"data": { "order_id": 1042 },
|
|
26
|
+
"attempts": 0,
|
|
27
|
+
"lang": "go",
|
|
28
|
+
"schema_version": 1
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "dead-lettered",
|
|
33
|
+
"file": "fixtures/dead-lettered.json",
|
|
34
|
+
"valid": true,
|
|
35
|
+
"description": "A dead-lettered message: original preserved + additive dead_letter block.",
|
|
36
|
+
"expect": {
|
|
37
|
+
"urn": "urn:babel:orders:created",
|
|
38
|
+
"data": { "order_id": 1042 },
|
|
39
|
+
"attempts": 3,
|
|
40
|
+
"lang": "php",
|
|
41
|
+
"schema_version": 1,
|
|
42
|
+
"dead_letter": { "reason": "failed", "original_queue": "orders" }
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "unicode-and-numbers",
|
|
47
|
+
"file": "fixtures/unicode-and-numbers.json",
|
|
48
|
+
"valid": true,
|
|
49
|
+
"description": "UTF-8 strings, integers, an exact float, boolean and null round-trip identically.",
|
|
50
|
+
"expect": {
|
|
51
|
+
"urn": "urn:babel:catalog:item.indexed",
|
|
52
|
+
"data": { "title": "Café — naïve ☕", "qty": 7, "price_cents": 1299, "ratio": 0.5, "active": true, "note": null },
|
|
53
|
+
"attempts": 2,
|
|
54
|
+
"lang": "python",
|
|
55
|
+
"schema_version": 1
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "invalid-unknown-schema-version",
|
|
60
|
+
"file": "fixtures/invalid-unknown-schema-version.json",
|
|
61
|
+
"valid": false,
|
|
62
|
+
"reason": "meta.schema_version is not a version this SDK supports"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "invalid-missing-urn",
|
|
66
|
+
"file": "fixtures/invalid-missing-urn.json",
|
|
67
|
+
"valid": false,
|
|
68
|
+
"reason": "no 'job' or 'urn' — the message has no identity"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://babelqueue.com/contracts/message-envelope.schema.json",
|
|
4
|
+
"title": "BabelQueueMessageEnvelope",
|
|
5
|
+
"description": "The canonical, language-agnostic BabelQueue wire envelope (schema_version 1). This schema is authoritative alongside contracts/message-envelope.md.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": true,
|
|
8
|
+
"required": ["job", "trace_id", "data", "meta", "attempts"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"job": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"minLength": 1,
|
|
13
|
+
"description": "The message URN — language-agnostic identity. Canonical producer field name. Consumers also accept 'urn' as an inbound alias. Never a class name.",
|
|
14
|
+
"examples": ["urn:babel:orders:created", "urn:babel:orders:invoice.requested"]
|
|
15
|
+
},
|
|
16
|
+
"urn": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"minLength": 1,
|
|
19
|
+
"description": "Inbound alias for 'job'. Accepted by consumers for interoperability; producers SHOULD emit 'job'."
|
|
20
|
+
},
|
|
21
|
+
"trace_id": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"format": "uuid",
|
|
24
|
+
"description": "Cross-service correlation id. Generated by the first producer, preserved and forwarded unchanged by every SDK across every hop."
|
|
25
|
+
},
|
|
26
|
+
"data": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"description": "Pure, JSON-encodable business payload. No language-specific types. Numbers/time/binary per contracts/message-envelope.md section 6.",
|
|
29
|
+
"additionalProperties": true
|
|
30
|
+
},
|
|
31
|
+
"meta": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"description": "Producer-set, immutable descriptive metadata.",
|
|
34
|
+
"additionalProperties": true,
|
|
35
|
+
"required": ["id", "queue", "lang", "schema_version", "created_at"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"id": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"format": "uuid",
|
|
40
|
+
"description": "Unique id for THIS message (distinct from trace_id)."
|
|
41
|
+
},
|
|
42
|
+
"queue": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"minLength": 1,
|
|
45
|
+
"description": "Logical queue name (not the broker key)."
|
|
46
|
+
},
|
|
47
|
+
"lang": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"enum": ["php", "go", "python", "java", "dotnet", "node"],
|
|
50
|
+
"description": "Producer language tag."
|
|
51
|
+
},
|
|
52
|
+
"schema_version": {
|
|
53
|
+
"type": "integer",
|
|
54
|
+
"const": 1,
|
|
55
|
+
"description": "Envelope schema version. Consumers MUST reject versions they do not support."
|
|
56
|
+
},
|
|
57
|
+
"created_at": {
|
|
58
|
+
"type": "integer",
|
|
59
|
+
"minimum": 0,
|
|
60
|
+
"description": "Production time as Unix epoch MILLISECONDS, UTC."
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"attempts": {
|
|
65
|
+
"type": "integer",
|
|
66
|
+
"minimum": 0,
|
|
67
|
+
"description": "Top-level transport retry counter, mutated by the broker/worker. Deliberately kept OUT of the immutable meta block."
|
|
68
|
+
},
|
|
69
|
+
"dead_letter": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"description": "Optional. Present ONLY on messages sitting on a dead-letter queue (ADR-0009). Additive, so it does not change schema_version. Normal consumers ignore it.",
|
|
72
|
+
"additionalProperties": true,
|
|
73
|
+
"required": ["reason", "failed_at", "original_queue", "attempts"],
|
|
74
|
+
"properties": {
|
|
75
|
+
"reason": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"enum": ["failed", "unknown_urn", "poison"],
|
|
78
|
+
"description": "Why the message was dead-lettered."
|
|
79
|
+
},
|
|
80
|
+
"error": {
|
|
81
|
+
"type": ["string", "null"],
|
|
82
|
+
"description": "Human-readable error message, if any."
|
|
83
|
+
},
|
|
84
|
+
"exception": {
|
|
85
|
+
"type": ["string", "null"],
|
|
86
|
+
"description": "Producer-language exception/type name, if any (informational, language-specific)."
|
|
87
|
+
},
|
|
88
|
+
"failed_at": {
|
|
89
|
+
"type": "integer",
|
|
90
|
+
"minimum": 0,
|
|
91
|
+
"description": "Dead-letter time as Unix epoch milliseconds, UTC."
|
|
92
|
+
},
|
|
93
|
+
"original_queue": {
|
|
94
|
+
"type": "string",
|
|
95
|
+
"description": "The queue the message was consumed from before dead-lettering."
|
|
96
|
+
},
|
|
97
|
+
"attempts": {
|
|
98
|
+
"type": "integer",
|
|
99
|
+
"minimum": 0,
|
|
100
|
+
"description": "Delivery attempts made before dead-lettering."
|
|
101
|
+
},
|
|
102
|
+
"lang": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"enum": ["php", "go", "python", "java", "dotnet", "node"],
|
|
105
|
+
"description": "Language of the SDK that dead-lettered the message."
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"job": "urn:babel:orders:created",
|
|
3
|
+
"trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
|
|
4
|
+
"data": {
|
|
5
|
+
"order_id": 1042
|
|
6
|
+
},
|
|
7
|
+
"meta": {
|
|
8
|
+
"id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
|
|
9
|
+
"queue": "orders",
|
|
10
|
+
"lang": "php",
|
|
11
|
+
"schema_version": 1,
|
|
12
|
+
"created_at": 1749132727000
|
|
13
|
+
},
|
|
14
|
+
"attempts": 3,
|
|
15
|
+
"dead_letter": {
|
|
16
|
+
"reason": "failed",
|
|
17
|
+
"error": "Payment gateway timeout",
|
|
18
|
+
"exception": "App\\Exceptions\\GatewayTimeout",
|
|
19
|
+
"failed_at": 1749132730000,
|
|
20
|
+
"original_queue": "orders",
|
|
21
|
+
"attempts": 3,
|
|
22
|
+
"lang": "php"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"job": "urn:babel:orders:created",
|
|
3
|
+
"trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
|
|
4
|
+
"data": {
|
|
5
|
+
"order_id": 1042
|
|
6
|
+
},
|
|
7
|
+
"meta": {
|
|
8
|
+
"id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
|
|
9
|
+
"queue": "orders",
|
|
10
|
+
"lang": "php",
|
|
11
|
+
"schema_version": 1,
|
|
12
|
+
"created_at": 1749132727000
|
|
13
|
+
},
|
|
14
|
+
"attempts": 0
|
|
15
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Runs the shared cross-SDK conformance suite (vendored under tests/conformance/).
|
|
2
|
+
|
|
3
|
+
The same manifest + fixtures are run by every BabelQueue SDK; passing here proves
|
|
4
|
+
this SDK reads/writes the canonical envelope identically to the others.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import unittest
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from babelqueue import EnvelopeCodec
|
|
14
|
+
|
|
15
|
+
SUITE = Path(__file__).parent / "conformance"
|
|
16
|
+
MANIFEST = json.loads((SUITE / "manifest.json").read_text(encoding="utf-8"))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConformanceTest(unittest.TestCase):
|
|
20
|
+
def test_suite_is_present(self) -> None:
|
|
21
|
+
self.assertEqual(MANIFEST["schema_version"], 1)
|
|
22
|
+
self.assertGreaterEqual(len(MANIFEST["cases"]), 6)
|
|
23
|
+
|
|
24
|
+
def test_cases(self) -> None:
|
|
25
|
+
for case in MANIFEST["cases"]:
|
|
26
|
+
with self.subTest(case=case["name"]):
|
|
27
|
+
raw = (SUITE / case["file"]).read_text(encoding="utf-8")
|
|
28
|
+
env = EnvelopeCodec.decode(raw)
|
|
29
|
+
self.assertNotEqual(env, {}, "fixture must decode")
|
|
30
|
+
|
|
31
|
+
if not case["valid"]:
|
|
32
|
+
self.assertFalse(
|
|
33
|
+
EnvelopeCodec.accepts(env),
|
|
34
|
+
f"{case['name']} must be rejected: {case.get('reason')}",
|
|
35
|
+
)
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
expect = case["expect"]
|
|
39
|
+
self.assertTrue(EnvelopeCodec.accepts(env), f"{case['name']} must be accepted")
|
|
40
|
+
self.assertEqual(EnvelopeCodec.urn(env), expect["urn"])
|
|
41
|
+
self.assertEqual(env["attempts"], expect["attempts"])
|
|
42
|
+
self.assertEqual(env["meta"]["lang"], expect["lang"])
|
|
43
|
+
self.assertEqual(env["meta"]["schema_version"], expect["schema_version"])
|
|
44
|
+
|
|
45
|
+
if "data" in expect:
|
|
46
|
+
self.assertEqual(env["data"], expect["data"])
|
|
47
|
+
|
|
48
|
+
if "dead_letter" in expect:
|
|
49
|
+
for key, value in expect["dead_letter"].items():
|
|
50
|
+
self.assertEqual(env["dead_letter"][key], value)
|
|
51
|
+
|
|
52
|
+
# Per-message fields must be present (not asserted by value).
|
|
53
|
+
self.assertIn("id", env["meta"])
|
|
54
|
+
self.assertTrue(env["trace_id"])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
unittest.main()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Integration tests for the RabbitMQ (pika) transport.
|
|
2
|
+
|
|
3
|
+
Skipped unless a broker is reachable (the `pika` package installed and a broker at
|
|
4
|
+
``BABELQUEUE_TEST_AMQP`` / localhost). The CI ``integration`` job runs these
|
|
5
|
+
against a RabbitMQ service; locally they skip cleanly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import unittest
|
|
12
|
+
import uuid
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import pika as _pika
|
|
16
|
+
except ImportError: # pragma: no cover
|
|
17
|
+
_pika = None
|
|
18
|
+
|
|
19
|
+
from babelqueue import BabelQueue, EnvelopeCodec
|
|
20
|
+
|
|
21
|
+
AMQP_URL = os.environ.get("BABELQUEUE_TEST_AMQP", "amqp://guest:guest@localhost:5672/")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _amqp_available() -> bool:
|
|
25
|
+
if _pika is None:
|
|
26
|
+
return False
|
|
27
|
+
try:
|
|
28
|
+
conn = _pika.BlockingConnection(_pika.URLParameters(AMQP_URL))
|
|
29
|
+
conn.close()
|
|
30
|
+
return True
|
|
31
|
+
except Exception: # pragma: no cover - connection failure
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@unittest.skipUnless(_amqp_available(), f"no reachable RabbitMQ at {AMQP_URL}")
|
|
36
|
+
class PikaTransportTest(unittest.TestCase):
|
|
37
|
+
def setUp(self) -> None:
|
|
38
|
+
self.queue = f"bqtest-{uuid.uuid4().hex}"
|
|
39
|
+
self.conn = _pika.BlockingConnection(_pika.URLParameters(AMQP_URL))
|
|
40
|
+
self.ctl = self.conn.channel()
|
|
41
|
+
|
|
42
|
+
def tearDown(self) -> None:
|
|
43
|
+
for q in (self.queue, f"{self.queue}.dlq"):
|
|
44
|
+
try:
|
|
45
|
+
self.ctl.queue_delete(queue=q)
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
self.conn.close()
|
|
49
|
+
|
|
50
|
+
def _depth(self, queue: str) -> int:
|
|
51
|
+
method = self.ctl.queue_declare(queue=queue, durable=True, passive=True)
|
|
52
|
+
return method.method.message_count
|
|
53
|
+
|
|
54
|
+
def test_publish_consume_round_trip_and_ack(self) -> None:
|
|
55
|
+
app = BabelQueue(AMQP_URL, queue=self.queue)
|
|
56
|
+
seen = {}
|
|
57
|
+
|
|
58
|
+
@app.handler("urn:babel:orders:created")
|
|
59
|
+
def handle(data, meta): # noqa: ANN001
|
|
60
|
+
seen.update(data)
|
|
61
|
+
|
|
62
|
+
app.publish("urn:babel:orders:created", {"order_id": 42})
|
|
63
|
+
processed = app.consume(max_messages=1, timeout=3)
|
|
64
|
+
|
|
65
|
+
self.assertEqual(processed, 1)
|
|
66
|
+
self.assertEqual(seen, {"order_id": 42})
|
|
67
|
+
self.assertEqual(self._depth(self.queue), 0) # acked
|
|
68
|
+
|
|
69
|
+
def test_publish_sets_contract_amqp_properties(self) -> None:
|
|
70
|
+
app = BabelQueue(AMQP_URL, queue=self.queue)
|
|
71
|
+
app.publish("urn:babel:orders:created", {"order_id": 1}, trace_id="trace-amqp")
|
|
72
|
+
|
|
73
|
+
method, props, body = self.ctl.basic_get(queue=self.queue, auto_ack=True)
|
|
74
|
+
self.assertIsNotNone(method)
|
|
75
|
+
self.assertEqual(props.type, "urn:babel:orders:created") # route on properties.type
|
|
76
|
+
self.assertEqual(props.correlation_id, "trace-amqp") # trace_id
|
|
77
|
+
self.assertEqual(props.content_type, "application/json")
|
|
78
|
+
self.assertEqual(props.delivery_mode, 2) # persistent
|
|
79
|
+
self.assertEqual(props.app_id, "babelqueue")
|
|
80
|
+
|
|
81
|
+
def test_failure_dead_letters(self) -> None:
|
|
82
|
+
app = BabelQueue(AMQP_URL, queue=self.queue, max_attempts=1, dead_letter=True)
|
|
83
|
+
|
|
84
|
+
@app.handler("urn:babel:orders:created")
|
|
85
|
+
def handle(data, meta): # noqa: ANN001
|
|
86
|
+
raise RuntimeError("boom")
|
|
87
|
+
|
|
88
|
+
app.publish("urn:babel:orders:created", {"order_id": 1})
|
|
89
|
+
app.consume(max_messages=2, timeout=3)
|
|
90
|
+
|
|
91
|
+
self.assertEqual(self._depth(f"{self.queue}.dlq"), 1)
|
|
92
|
+
_m, _p, body = self.ctl.basic_get(queue=f"{self.queue}.dlq", auto_ack=True)
|
|
93
|
+
env = EnvelopeCodec.decode(body.decode("utf-8"))
|
|
94
|
+
self.assertEqual(env["dead_letter"]["reason"], "failed")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|