babelqueue 0.2.0__tar.gz → 0.3.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.3.0}/.github/workflows/ci.yml +13 -3
- {babelqueue-0.2.0 → babelqueue-0.3.0}/CHANGELOG.md +12 -3
- {babelqueue-0.2.0 → babelqueue-0.3.0}/PKG-INFO +12 -9
- {babelqueue-0.2.0 → babelqueue-0.3.0}/README.md +11 -8
- {babelqueue-0.2.0 → babelqueue-0.3.0}/pyproject.toml +1 -1
- {babelqueue-0.2.0 → babelqueue-0.3.0}/src/babelqueue/__init__.py +1 -1
- babelqueue-0.3.0/src/babelqueue/pika_transport.py +109 -0
- {babelqueue-0.2.0 → babelqueue-0.3.0}/src/babelqueue/transport.py +6 -2
- babelqueue-0.3.0/tests/test_pika_transport.py +98 -0
- {babelqueue-0.2.0 → babelqueue-0.3.0}/.github/workflows/release.yml +0 -0
- {babelqueue-0.2.0 → babelqueue-0.3.0}/.gitignore +0 -0
- {babelqueue-0.2.0 → babelqueue-0.3.0}/LICENSE +0 -0
- {babelqueue-0.2.0 → babelqueue-0.3.0}/src/babelqueue/app.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.3.0}/src/babelqueue/codec.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.3.0}/src/babelqueue/contracts.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.3.0}/src/babelqueue/dead_letter.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.3.0}/src/babelqueue/exceptions.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.3.0}/src/babelqueue/py.typed +0 -0
- {babelqueue-0.2.0 → babelqueue-0.3.0}/src/babelqueue/redis_transport.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.3.0}/src/babelqueue/routing.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.3.0}/tests/fixtures/dead-lettered.json +0 -0
- {babelqueue-0.2.0 → babelqueue-0.3.0}/tests/fixtures/order-created.json +0 -0
- {babelqueue-0.2.0 → babelqueue-0.3.0}/tests/test_app.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.3.0}/tests/test_codec.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.3.0}/tests/test_dead_letter.py +0 -0
- {babelqueue-0.2.0 → babelqueue-0.3.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,14 @@ The envelope wire format is versioned separately by `meta.schema_version`
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
### Added
|
|
13
|
+
- **RabbitMQ transport** (`PikaTransport`, `amqp://`): durable queue, persistent
|
|
14
|
+
delivery, `basic_get` + manual ack, and the contract AMQP properties (`type`=URN,
|
|
15
|
+
`correlation_id`=trace_id, `x-schema-version`/`x-source-lang`/`x-attempts`).
|
|
16
|
+
Optional `[amqp]` extra (lazy `pika` import) — the core stays zero-dep.
|
|
17
|
+
|
|
18
|
+
## [0.2.0] - 2026-06-06
|
|
19
|
+
|
|
12
20
|
### Added
|
|
13
21
|
- **Runtime** — `BabelQueue(broker_url=...)` app with a `@app.handler("urn:...")`
|
|
14
22
|
decorator, `publish()`, and a `consume()` / `run()` loop. Routes by URN over the
|
|
@@ -16,8 +24,8 @@ The envelope wire format is versioned separately by `meta.schema_version`
|
|
|
16
24
|
`on_unknown_urn` strategies (`fail`/`delete`/`release`/`dead_letter`).
|
|
17
25
|
- **Transports** — a pluggable `Transport` abstraction with `InMemoryTransport`
|
|
18
26
|
(`memory://`, for tests/local) and `RedisTransport` (`redis://`, reliable-queue
|
|
19
|
-
pattern via `BLMOVE` + a processing list). Redis client is an optional
|
|
20
|
-
|
|
27
|
+
pattern via `BLMOVE` + a processing list). Redis client is an optional `[redis]`
|
|
28
|
+
extra, imported lazily — the core stays zero-dep.
|
|
21
29
|
|
|
22
30
|
## [0.1.0] - 2026-06-06
|
|
23
31
|
|
|
@@ -36,5 +44,6 @@ The envelope wire format is versioned separately by `meta.schema_version`
|
|
|
36
44
|
- Pre-1.0: the public API may change before the `1.0.0` tag.
|
|
37
45
|
- The core has **zero runtime dependencies** (standard library only); Python `>=3.9`.
|
|
38
46
|
|
|
39
|
-
[Unreleased]: https://github.com/BabelQueue/babelqueue-python/compare/v0.
|
|
47
|
+
[Unreleased]: https://github.com/BabelQueue/babelqueue-python/compare/v0.2.0...HEAD
|
|
48
|
+
[0.2.0]: https://github.com/BabelQueue/babelqueue-python/compare/v0.1.0...v0.2.0
|
|
40
49
|
[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.3.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.3.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"
|
|
@@ -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,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
|
|
File without changes
|