rabbitkit 0.9.0__py3-none-any.whl
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.
- rabbitkit/__init__.py +201 -0
- rabbitkit/_version.py +3 -0
- rabbitkit/aio/__init__.py +31 -0
- rabbitkit/async_/__init__.py +9 -0
- rabbitkit/async_/batch.py +213 -0
- rabbitkit/async_/broker.py +1123 -0
- rabbitkit/async_/connection.py +274 -0
- rabbitkit/async_/pool.py +363 -0
- rabbitkit/async_/transport.py +877 -0
- rabbitkit/asyncapi/__init__.py +5 -0
- rabbitkit/asyncapi/generator.py +219 -0
- rabbitkit/asyncapi/schema.py +98 -0
- rabbitkit/cli/__init__.py +77 -0
- rabbitkit/cli/_utils.py +38 -0
- rabbitkit/cli/commands/__init__.py +0 -0
- rabbitkit/cli/commands/dlq.py +190 -0
- rabbitkit/cli/commands/health.py +34 -0
- rabbitkit/cli/commands/migrate.py +570 -0
- rabbitkit/cli/commands/routes.py +88 -0
- rabbitkit/cli/commands/run.py +144 -0
- rabbitkit/cli/commands/shell.py +72 -0
- rabbitkit/cli/commands/topology.py +346 -0
- rabbitkit/concurrency.py +451 -0
- rabbitkit/core/__init__.py +5 -0
- rabbitkit/core/app.py +323 -0
- rabbitkit/core/config.py +849 -0
- rabbitkit/core/env_config.py +251 -0
- rabbitkit/core/errors.py +199 -0
- rabbitkit/core/logging.py +261 -0
- rabbitkit/core/message.py +235 -0
- rabbitkit/core/path.py +53 -0
- rabbitkit/core/pipeline.py +1289 -0
- rabbitkit/core/protocols.py +349 -0
- rabbitkit/core/registry.py +284 -0
- rabbitkit/core/route.py +329 -0
- rabbitkit/core/router.py +142 -0
- rabbitkit/core/topology.py +261 -0
- rabbitkit/core/topology_dispatch.py +74 -0
- rabbitkit/core/types.py +324 -0
- rabbitkit/dashboard/__init__.py +5 -0
- rabbitkit/dashboard/app.py +212 -0
- rabbitkit/di/__init__.py +19 -0
- rabbitkit/di/context.py +193 -0
- rabbitkit/di/depends.py +42 -0
- rabbitkit/di/resolver.py +503 -0
- rabbitkit/dlq.py +320 -0
- rabbitkit/experimental/__init__.py +50 -0
- rabbitkit/fastapi.py +91 -0
- rabbitkit/health.py +654 -0
- rabbitkit/highload/__init__.py +10 -0
- rabbitkit/highload/backpressure.py +514 -0
- rabbitkit/highload/batch.py +448 -0
- rabbitkit/locking.py +277 -0
- rabbitkit/management.py +470 -0
- rabbitkit/middleware/__init__.py +27 -0
- rabbitkit/middleware/base.py +125 -0
- rabbitkit/middleware/circuit_breaker.py +131 -0
- rabbitkit/middleware/compression.py +267 -0
- rabbitkit/middleware/deduplication.py +651 -0
- rabbitkit/middleware/error_classifier.py +43 -0
- rabbitkit/middleware/exception.py +105 -0
- rabbitkit/middleware/metrics.py +440 -0
- rabbitkit/middleware/otel.py +203 -0
- rabbitkit/middleware/rate_limit.py +247 -0
- rabbitkit/middleware/retry.py +540 -0
- rabbitkit/middleware/signing.py +682 -0
- rabbitkit/middleware/timeout.py +291 -0
- rabbitkit/py.typed +0 -0
- rabbitkit/queue_metrics.py +174 -0
- rabbitkit/results/__init__.py +6 -0
- rabbitkit/results/backend.py +102 -0
- rabbitkit/results/middleware.py +123 -0
- rabbitkit/rpc.py +632 -0
- rabbitkit/serialization/__init__.py +25 -0
- rabbitkit/serialization/base.py +35 -0
- rabbitkit/serialization/json.py +122 -0
- rabbitkit/serialization/msgspec.py +136 -0
- rabbitkit/serialization/pipeline.py +255 -0
- rabbitkit/streams.py +139 -0
- rabbitkit/sync/__init__.py +11 -0
- rabbitkit/sync/batch.py +595 -0
- rabbitkit/sync/broker.py +996 -0
- rabbitkit/sync/connection.py +209 -0
- rabbitkit/sync/pool.py +262 -0
- rabbitkit/sync/transport.py +1085 -0
- rabbitkit/testing/__init__.py +20 -0
- rabbitkit/testing/app.py +99 -0
- rabbitkit/testing/broker.py +540 -0
- rabbitkit/testing/fixtures.py +56 -0
- rabbitkit-0.9.0.dist-info/METADATA +575 -0
- rabbitkit-0.9.0.dist-info/RECORD +95 -0
- rabbitkit-0.9.0.dist-info/WHEEL +5 -0
- rabbitkit-0.9.0.dist-info/entry_points.txt +2 -0
- rabbitkit-0.9.0.dist-info/licenses/LICENSE +21 -0
- rabbitkit-0.9.0.dist-info/top_level.txt +1 -0
rabbitkit/dlq.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""DLQ Inspector — peek, replay, and purge dead-letter queues.
|
|
2
|
+
|
|
3
|
+
Provides inspection and recovery tools for messages stuck in DLQs.
|
|
4
|
+
|
|
5
|
+
**Operational realism:**
|
|
6
|
+
- ``peek()`` returns materialized snapshots, not live references
|
|
7
|
+
- ``peek()`` may affect message ordering (basic.get + requeue changes position)
|
|
8
|
+
- ``replay()`` preserves original headers (pass ``reset_retry_count=True`` to
|
|
9
|
+
grant the replayed message a fresh retry ladder)
|
|
10
|
+
- ``replay()`` acks a DLQ original only after the republish outcome is OK;
|
|
11
|
+
failed republishes are nack-requeued so they stay on the DLQ
|
|
12
|
+
- ``purge()`` is immediate and unfiltered — use ``replay()`` for selective recovery
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from rabbitkit.core.message import RabbitMessage
|
|
22
|
+
from rabbitkit.core.types import MessageEnvelope
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
# Matches RetryConfig.retry_header's default. If you customized retry_header,
|
|
27
|
+
# strip your header via a predicate/pre-processing step instead.
|
|
28
|
+
_RETRY_COUNT_HEADER = "x-rabbitkit-retry-count"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ReplayResult(int):
|
|
32
|
+
"""Replay report that IS the replayed count (int-compatible, so existing
|
|
33
|
+
``count = inspector.replay(...)`` callers keep working), with extras:
|
|
34
|
+
|
|
35
|
+
- ``failed``: messages whose republish outcome was not OK — they were
|
|
36
|
+
nack-requeued and REMAIN ON THE DLQ.
|
|
37
|
+
- ``requeued``: non-matching messages returned to the DLQ (predicate
|
|
38
|
+
returned False).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
failed: int
|
|
42
|
+
requeued: int
|
|
43
|
+
|
|
44
|
+
def __new__(cls, replayed: int, failed: int = 0, requeued: int = 0) -> ReplayResult:
|
|
45
|
+
obj = super().__new__(cls, replayed)
|
|
46
|
+
obj.failed = failed
|
|
47
|
+
obj.requeued = requeued
|
|
48
|
+
return obj
|
|
49
|
+
|
|
50
|
+
def __repr__(self) -> str:
|
|
51
|
+
return f"ReplayResult(replayed={int(self)}, failed={self.failed}, requeued={self.requeued})"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DLQInspector:
|
|
55
|
+
"""Dead-letter queue inspection and replay.
|
|
56
|
+
|
|
57
|
+
Accepts a transport that supports ``basic_get``, ``publish``,
|
|
58
|
+
``purge_queue`` methods. Works with both sync and async transports.
|
|
59
|
+
|
|
60
|
+
Usage::
|
|
61
|
+
|
|
62
|
+
inspector = DLQInspector(transport)
|
|
63
|
+
|
|
64
|
+
# Peek at messages without consuming them
|
|
65
|
+
messages = inspector.peek("orders-queue.dlq", limit=5)
|
|
66
|
+
|
|
67
|
+
# Replay matching messages back to source queue
|
|
68
|
+
count = inspector.replay(
|
|
69
|
+
"orders-queue.dlq",
|
|
70
|
+
predicate=lambda msg: msg.headers.get("x-error") == "timeout",
|
|
71
|
+
target_queue="orders-queue",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Purge entire DLQ
|
|
75
|
+
count = inspector.purge("orders-queue.dlq")
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, transport: Any) -> None:
|
|
79
|
+
self._transport = transport
|
|
80
|
+
|
|
81
|
+
# ── Sync methods ─────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
def peek(self, queue: str, limit: int = 10) -> list[RabbitMessage]:
|
|
84
|
+
"""Fetch up to ``limit`` messages from the queue, then requeue them.
|
|
85
|
+
|
|
86
|
+
Messages are nacked with ``requeue=True`` so they return to the queue.
|
|
87
|
+
Ordering may change after this operation.
|
|
88
|
+
|
|
89
|
+
Returns a list of message snapshots.
|
|
90
|
+
"""
|
|
91
|
+
messages: list[RabbitMessage] = []
|
|
92
|
+
|
|
93
|
+
for _ in range(limit):
|
|
94
|
+
msg = self._transport.basic_get(queue)
|
|
95
|
+
if msg is None:
|
|
96
|
+
break
|
|
97
|
+
messages.append(msg)
|
|
98
|
+
|
|
99
|
+
# Requeue all peeked messages
|
|
100
|
+
for msg in messages:
|
|
101
|
+
if not msg.is_settled:
|
|
102
|
+
msg.nack(requeue=True)
|
|
103
|
+
|
|
104
|
+
return messages
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _build_replay_envelope(
|
|
108
|
+
msg: RabbitMessage,
|
|
109
|
+
target_queue: str | None,
|
|
110
|
+
target_exchange: str | None,
|
|
111
|
+
reset_retry_count: bool,
|
|
112
|
+
) -> MessageEnvelope:
|
|
113
|
+
"""Build the republish envelope for one DLQ message.
|
|
114
|
+
|
|
115
|
+
``mandatory=True`` so an unroutable target comes back as a
|
|
116
|
+
``RETURNED`` outcome instead of being broker-confirmed into the void.
|
|
117
|
+
"""
|
|
118
|
+
rk = target_queue or msg.headers.get("x-rabbitkit-original-queue", msg.routing_key)
|
|
119
|
+
headers = dict(msg.headers)
|
|
120
|
+
if reset_retry_count:
|
|
121
|
+
headers.pop(_RETRY_COUNT_HEADER, None)
|
|
122
|
+
return MessageEnvelope(
|
|
123
|
+
routing_key=rk,
|
|
124
|
+
body=msg.body,
|
|
125
|
+
exchange=target_exchange if target_exchange is not None else "",
|
|
126
|
+
headers=headers,
|
|
127
|
+
message_id=msg.message_id or "",
|
|
128
|
+
correlation_id=msg.correlation_id,
|
|
129
|
+
content_type=msg.content_type or "application/octet-stream",
|
|
130
|
+
content_encoding=msg.content_encoding,
|
|
131
|
+
# Preserve the remaining original message properties -- these used
|
|
132
|
+
# to be silently dropped on replay, e.g. a priority-queue message
|
|
133
|
+
# lost its priority, and an RPC request's reply_to/type/app_id/
|
|
134
|
+
# user_id never survived the replay for the reply to route back.
|
|
135
|
+
reply_to=msg.reply_to,
|
|
136
|
+
priority=msg.priority,
|
|
137
|
+
expiration=msg.expiration,
|
|
138
|
+
type=msg.type,
|
|
139
|
+
app_id=msg.app_id,
|
|
140
|
+
user_id=msg.user_id,
|
|
141
|
+
mandatory=True,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def replay(
|
|
145
|
+
self,
|
|
146
|
+
queue: str,
|
|
147
|
+
predicate: Callable[[RabbitMessage], bool] | None = None,
|
|
148
|
+
target_queue: str | None = None,
|
|
149
|
+
target_exchange: str | None = None,
|
|
150
|
+
*,
|
|
151
|
+
reset_retry_count: bool = False,
|
|
152
|
+
) -> ReplayResult:
|
|
153
|
+
"""Replay messages from the DLQ.
|
|
154
|
+
|
|
155
|
+
Fetches messages, applies optional predicate filter, publishes
|
|
156
|
+
matching messages to the target, and acks each original **only after
|
|
157
|
+
its republish outcome is OK**. A failed republish (NACKED / TIMEOUT /
|
|
158
|
+
RETURNED / ERROR) is nack-requeued, so the message stays on the DLQ
|
|
159
|
+
instead of being lost.
|
|
160
|
+
|
|
161
|
+
Non-matching messages are nacked with ``requeue=True``.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
queue: Source DLQ to replay from.
|
|
165
|
+
predicate: Optional filter — only replay messages where
|
|
166
|
+
predicate returns True. All messages replayed if None.
|
|
167
|
+
target_queue: Target queue routing key. Defaults to the
|
|
168
|
+
original queue from message headers.
|
|
169
|
+
target_exchange: Target exchange. Defaults to "".
|
|
170
|
+
reset_retry_count: Strip the ``x-rabbitkit-retry-count`` header
|
|
171
|
+
so the replayed message gets a fresh retry ladder. Default
|
|
172
|
+
False preserves headers verbatim — meaning a previously
|
|
173
|
+
max-retried message is terminal after ONE failed attempt and
|
|
174
|
+
returns to the DLQ.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
:class:`ReplayResult` — int-compatible replayed count, with
|
|
178
|
+
``.failed`` (left on the DLQ) and ``.requeued`` (non-matching).
|
|
179
|
+
|
|
180
|
+
Loop Engineering Review, Reliability: a non-matching or
|
|
181
|
+
failed-publish message is **not** nacked (requeued) until after this
|
|
182
|
+
method's fetch loop has fully exhausted the queue. ``basic_get`` has
|
|
183
|
+
no natural "already seen this delivery" tracking of its own -- if
|
|
184
|
+
such a message were requeued immediately, and nothing else is
|
|
185
|
+
consuming from this queue, the very next ``basic_get`` call in this
|
|
186
|
+
same loop could immediately re-fetch that exact message, forever.
|
|
187
|
+
Held-but-unsettled messages are invisible to further ``basic_get``
|
|
188
|
+
calls (the broker still considers them delivered-but-unacked), so
|
|
189
|
+
deferring the nack until after the loop truly exits guarantees
|
|
190
|
+
termination regardless of how many messages the predicate rejects or
|
|
191
|
+
the publisher fails.
|
|
192
|
+
"""
|
|
193
|
+
replayed = 0
|
|
194
|
+
held_for_requeue: list[RabbitMessage] = []
|
|
195
|
+
failed = 0
|
|
196
|
+
requeued = 0
|
|
197
|
+
|
|
198
|
+
while True:
|
|
199
|
+
msg = self._transport.basic_get(queue)
|
|
200
|
+
if msg is None:
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
# Apply predicate filter -- hold, don't nack yet (see docstring).
|
|
204
|
+
if predicate is not None and not predicate(msg):
|
|
205
|
+
held_for_requeue.append(msg)
|
|
206
|
+
requeued += 1
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
envelope = self._build_replay_envelope(msg, target_queue, target_exchange, reset_retry_count)
|
|
210
|
+
outcome = self._transport.publish(envelope)
|
|
211
|
+
if outcome is not None and not outcome.ok:
|
|
212
|
+
# Republish failed — DO NOT ack, or the message is lost
|
|
213
|
+
# forever. Hold for nack-requeue so it stays on the DLQ.
|
|
214
|
+
logger.error(
|
|
215
|
+
"DLQ replay publish failed (status=%s); message stays on %r: routing_key=%s message_id=%s",
|
|
216
|
+
getattr(outcome, "status", "unknown"),
|
|
217
|
+
queue,
|
|
218
|
+
envelope.routing_key,
|
|
219
|
+
envelope.message_id,
|
|
220
|
+
)
|
|
221
|
+
held_for_requeue.append(msg)
|
|
222
|
+
failed += 1
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
# Ack the original — it is safely republished now
|
|
226
|
+
if not msg.is_settled:
|
|
227
|
+
msg.ack()
|
|
228
|
+
|
|
229
|
+
replayed += 1
|
|
230
|
+
|
|
231
|
+
# The fetch loop is done (basic_get returned None) -- now it's safe
|
|
232
|
+
# to requeue held messages; this loop can no longer re-fetch them.
|
|
233
|
+
for msg in held_for_requeue:
|
|
234
|
+
if not msg.is_settled:
|
|
235
|
+
msg.nack(requeue=True)
|
|
236
|
+
|
|
237
|
+
return ReplayResult(replayed, failed=failed, requeued=requeued)
|
|
238
|
+
|
|
239
|
+
def purge(self, queue: str) -> int:
|
|
240
|
+
"""Purge all messages from the queue.
|
|
241
|
+
|
|
242
|
+
Returns the number of messages purged.
|
|
243
|
+
"""
|
|
244
|
+
return int(self._transport.purge_queue(queue))
|
|
245
|
+
|
|
246
|
+
# ── Async methods ────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
async def peek_async(self, queue: str, limit: int = 10) -> list[RabbitMessage]:
|
|
249
|
+
"""Async variant of ``peek``."""
|
|
250
|
+
messages: list[RabbitMessage] = []
|
|
251
|
+
|
|
252
|
+
for _ in range(limit):
|
|
253
|
+
msg = await self._transport.basic_get(queue)
|
|
254
|
+
if msg is None:
|
|
255
|
+
break
|
|
256
|
+
messages.append(msg)
|
|
257
|
+
|
|
258
|
+
# Requeue all peeked messages
|
|
259
|
+
for msg in messages:
|
|
260
|
+
if not msg.is_settled:
|
|
261
|
+
await msg.nack_async(requeue=True)
|
|
262
|
+
|
|
263
|
+
return messages
|
|
264
|
+
|
|
265
|
+
async def replay_async(
|
|
266
|
+
self,
|
|
267
|
+
queue: str,
|
|
268
|
+
predicate: Callable[[RabbitMessage], bool] | None = None,
|
|
269
|
+
target_queue: str | None = None,
|
|
270
|
+
target_exchange: str | None = None,
|
|
271
|
+
*,
|
|
272
|
+
reset_retry_count: bool = False,
|
|
273
|
+
) -> ReplayResult:
|
|
274
|
+
"""Async variant of ``replay`` -- see its docstring for the
|
|
275
|
+
outcome-checked ack, ``reset_retry_count``, and why non-matching /
|
|
276
|
+
failed messages are held, not nacked, until the fetch loop has fully
|
|
277
|
+
exhausted the queue (termination guarantee)."""
|
|
278
|
+
replayed = 0
|
|
279
|
+
held_for_requeue: list[RabbitMessage] = []
|
|
280
|
+
failed = 0
|
|
281
|
+
requeued = 0
|
|
282
|
+
|
|
283
|
+
while True:
|
|
284
|
+
msg = await self._transport.basic_get(queue)
|
|
285
|
+
if msg is None:
|
|
286
|
+
break
|
|
287
|
+
|
|
288
|
+
if predicate is not None and not predicate(msg):
|
|
289
|
+
held_for_requeue.append(msg)
|
|
290
|
+
requeued += 1
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
envelope = self._build_replay_envelope(msg, target_queue, target_exchange, reset_retry_count)
|
|
294
|
+
outcome = await self._transport.publish(envelope)
|
|
295
|
+
if outcome is not None and not outcome.ok:
|
|
296
|
+
logger.error(
|
|
297
|
+
"DLQ replay publish failed (status=%s); message stays on %r: routing_key=%s message_id=%s",
|
|
298
|
+
getattr(outcome, "status", "unknown"),
|
|
299
|
+
queue,
|
|
300
|
+
envelope.routing_key,
|
|
301
|
+
envelope.message_id,
|
|
302
|
+
)
|
|
303
|
+
held_for_requeue.append(msg)
|
|
304
|
+
failed += 1
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
if not msg.is_settled:
|
|
308
|
+
await msg.ack_async()
|
|
309
|
+
|
|
310
|
+
replayed += 1
|
|
311
|
+
|
|
312
|
+
for msg in held_for_requeue:
|
|
313
|
+
if not msg.is_settled:
|
|
314
|
+
await msg.nack_async(requeue=True)
|
|
315
|
+
|
|
316
|
+
return ReplayResult(replayed, failed=failed, requeued=requeued)
|
|
317
|
+
|
|
318
|
+
async def purge_async(self, queue: str) -> int:
|
|
319
|
+
"""Async variant of ``purge``."""
|
|
320
|
+
return int(await self._transport.purge_queue(queue))
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""rabbitkit.experimental — features under active development.
|
|
2
|
+
|
|
3
|
+
These APIs are NOT covered by the stability guarantee. They may change or
|
|
4
|
+
be removed in any release without a deprecation period.
|
|
5
|
+
|
|
6
|
+
Stable APIs are in the top-level ``rabbitkit`` package.
|
|
7
|
+
|
|
8
|
+
Experimental features:
|
|
9
|
+
- RPC (tight coupling, use with care)
|
|
10
|
+
- Dashboard (web UI for local development)
|
|
11
|
+
- Stream queues
|
|
12
|
+
- Distributed locking
|
|
13
|
+
- Message signing
|
|
14
|
+
- Result backends
|
|
15
|
+
|
|
16
|
+
Usage::
|
|
17
|
+
|
|
18
|
+
from rabbitkit.experimental import AsyncRPCClient, RPCClient
|
|
19
|
+
from rabbitkit.experimental import create_dashboard_app
|
|
20
|
+
from rabbitkit.experimental import DistributedLock, RedisLock
|
|
21
|
+
from rabbitkit.experimental import SigningMiddleware
|
|
22
|
+
from rabbitkit.experimental import RedisResultBackend
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from rabbitkit.dashboard.app import create_dashboard_app
|
|
26
|
+
from rabbitkit.locking import DistributedLock, LockMiddleware, RedisLock
|
|
27
|
+
from rabbitkit.middleware.signing import InvalidSignatureError, SigningConfig, SigningMiddleware
|
|
28
|
+
from rabbitkit.results.backend import RedisResultBackend, ResultBackend
|
|
29
|
+
from rabbitkit.results.middleware import ResultMiddleware
|
|
30
|
+
from rabbitkit.rpc import AsyncRPCClient, RPCClient, RPCTimeoutError
|
|
31
|
+
from rabbitkit.streams import StreamConsumerConfig, StreamOffset, StreamOffsetType
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"AsyncRPCClient",
|
|
35
|
+
"DistributedLock",
|
|
36
|
+
"InvalidSignatureError",
|
|
37
|
+
"LockMiddleware",
|
|
38
|
+
"RPCClient",
|
|
39
|
+
"RPCTimeoutError",
|
|
40
|
+
"RedisLock",
|
|
41
|
+
"RedisResultBackend",
|
|
42
|
+
"ResultBackend",
|
|
43
|
+
"ResultMiddleware",
|
|
44
|
+
"SigningConfig",
|
|
45
|
+
"SigningMiddleware",
|
|
46
|
+
"StreamConsumerConfig",
|
|
47
|
+
"StreamOffset",
|
|
48
|
+
"StreamOffsetType",
|
|
49
|
+
"create_dashboard_app",
|
|
50
|
+
]
|
rabbitkit/fastapi.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""FastAPI integration — lifespan context manager for rabbitkit.
|
|
2
|
+
|
|
3
|
+
Thin module that wires broker startup/shutdown to FastAPI's lifespan.
|
|
4
|
+
|
|
5
|
+
Usage::
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from rabbitkit.fastapi import rabbitkit_lifespan
|
|
9
|
+
|
|
10
|
+
app = FastAPI(lifespan=rabbitkit_lifespan(broker=broker, rabbit_app=rabbit_app))
|
|
11
|
+
|
|
12
|
+
Or as a decorator-style::
|
|
13
|
+
|
|
14
|
+
@asynccontextmanager
|
|
15
|
+
async def lifespan(app):
|
|
16
|
+
async with rabbitkit_lifespan(broker=broker):
|
|
17
|
+
yield
|
|
18
|
+
|
|
19
|
+
app = FastAPI(lifespan=lifespan)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import logging
|
|
26
|
+
from collections.abc import AsyncIterator
|
|
27
|
+
from contextlib import asynccontextmanager
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@asynccontextmanager
|
|
34
|
+
async def rabbitkit_lifespan(
|
|
35
|
+
app: Any = None,
|
|
36
|
+
*,
|
|
37
|
+
broker: Any | None = None,
|
|
38
|
+
rabbit_app: Any | None = None,
|
|
39
|
+
) -> AsyncIterator[None]:
|
|
40
|
+
"""Async context manager that starts/stops rabbitkit components.
|
|
41
|
+
|
|
42
|
+
Suitable for use as FastAPI's ``lifespan`` parameter or standalone.
|
|
43
|
+
|
|
44
|
+
Start order: rabbit_app.start_async() → broker.start() / broker.start_async()
|
|
45
|
+
Stop order (in finally): broker.stop() / broker.stop_async() → rabbit_app.stop_async()
|
|
46
|
+
|
|
47
|
+
Duck-types sync vs async: if the method is a coroutine, it is awaited.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
app: FastAPI app instance (passed by FastAPI lifespan protocol, may be None).
|
|
51
|
+
broker: Optional rabbitkit broker (SyncBroker or AsyncBroker).
|
|
52
|
+
rabbit_app: Optional RabbitApp for lifecycle hooks.
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
# Start rabbit_app first (startup hooks)
|
|
56
|
+
if rabbit_app is not None:
|
|
57
|
+
if hasattr(rabbit_app, "start_async"):
|
|
58
|
+
await rabbit_app.start_async()
|
|
59
|
+
elif hasattr(rabbit_app, "start"):
|
|
60
|
+
result = rabbit_app.start()
|
|
61
|
+
if asyncio.iscoroutine(result):
|
|
62
|
+
await result
|
|
63
|
+
|
|
64
|
+
# Start broker
|
|
65
|
+
if broker is not None:
|
|
66
|
+
if asyncio.iscoroutinefunction(getattr(broker, "start", None)):
|
|
67
|
+
await broker.start()
|
|
68
|
+
elif hasattr(broker, "start"):
|
|
69
|
+
broker.start()
|
|
70
|
+
|
|
71
|
+
logger.info("rabbitkit lifespan started")
|
|
72
|
+
yield
|
|
73
|
+
|
|
74
|
+
finally:
|
|
75
|
+
# Stop broker first
|
|
76
|
+
if broker is not None:
|
|
77
|
+
if asyncio.iscoroutinefunction(getattr(broker, "stop", None)):
|
|
78
|
+
await broker.stop()
|
|
79
|
+
elif hasattr(broker, "stop"):
|
|
80
|
+
broker.stop()
|
|
81
|
+
|
|
82
|
+
# Stop rabbit_app (shutdown hooks)
|
|
83
|
+
if rabbit_app is not None:
|
|
84
|
+
if hasattr(rabbit_app, "stop_async"):
|
|
85
|
+
await rabbit_app.stop_async()
|
|
86
|
+
elif hasattr(rabbit_app, "stop"):
|
|
87
|
+
result = rabbit_app.stop()
|
|
88
|
+
if asyncio.iscoroutine(result):
|
|
89
|
+
await result
|
|
90
|
+
|
|
91
|
+
logger.info("rabbitkit lifespan stopped")
|