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.
Files changed (95) hide show
  1. rabbitkit/__init__.py +201 -0
  2. rabbitkit/_version.py +3 -0
  3. rabbitkit/aio/__init__.py +31 -0
  4. rabbitkit/async_/__init__.py +9 -0
  5. rabbitkit/async_/batch.py +213 -0
  6. rabbitkit/async_/broker.py +1123 -0
  7. rabbitkit/async_/connection.py +274 -0
  8. rabbitkit/async_/pool.py +363 -0
  9. rabbitkit/async_/transport.py +877 -0
  10. rabbitkit/asyncapi/__init__.py +5 -0
  11. rabbitkit/asyncapi/generator.py +219 -0
  12. rabbitkit/asyncapi/schema.py +98 -0
  13. rabbitkit/cli/__init__.py +77 -0
  14. rabbitkit/cli/_utils.py +38 -0
  15. rabbitkit/cli/commands/__init__.py +0 -0
  16. rabbitkit/cli/commands/dlq.py +190 -0
  17. rabbitkit/cli/commands/health.py +34 -0
  18. rabbitkit/cli/commands/migrate.py +570 -0
  19. rabbitkit/cli/commands/routes.py +88 -0
  20. rabbitkit/cli/commands/run.py +144 -0
  21. rabbitkit/cli/commands/shell.py +72 -0
  22. rabbitkit/cli/commands/topology.py +346 -0
  23. rabbitkit/concurrency.py +451 -0
  24. rabbitkit/core/__init__.py +5 -0
  25. rabbitkit/core/app.py +323 -0
  26. rabbitkit/core/config.py +849 -0
  27. rabbitkit/core/env_config.py +251 -0
  28. rabbitkit/core/errors.py +199 -0
  29. rabbitkit/core/logging.py +261 -0
  30. rabbitkit/core/message.py +235 -0
  31. rabbitkit/core/path.py +53 -0
  32. rabbitkit/core/pipeline.py +1289 -0
  33. rabbitkit/core/protocols.py +349 -0
  34. rabbitkit/core/registry.py +284 -0
  35. rabbitkit/core/route.py +329 -0
  36. rabbitkit/core/router.py +142 -0
  37. rabbitkit/core/topology.py +261 -0
  38. rabbitkit/core/topology_dispatch.py +74 -0
  39. rabbitkit/core/types.py +324 -0
  40. rabbitkit/dashboard/__init__.py +5 -0
  41. rabbitkit/dashboard/app.py +212 -0
  42. rabbitkit/di/__init__.py +19 -0
  43. rabbitkit/di/context.py +193 -0
  44. rabbitkit/di/depends.py +42 -0
  45. rabbitkit/di/resolver.py +503 -0
  46. rabbitkit/dlq.py +320 -0
  47. rabbitkit/experimental/__init__.py +50 -0
  48. rabbitkit/fastapi.py +91 -0
  49. rabbitkit/health.py +654 -0
  50. rabbitkit/highload/__init__.py +10 -0
  51. rabbitkit/highload/backpressure.py +514 -0
  52. rabbitkit/highload/batch.py +448 -0
  53. rabbitkit/locking.py +277 -0
  54. rabbitkit/management.py +470 -0
  55. rabbitkit/middleware/__init__.py +27 -0
  56. rabbitkit/middleware/base.py +125 -0
  57. rabbitkit/middleware/circuit_breaker.py +131 -0
  58. rabbitkit/middleware/compression.py +267 -0
  59. rabbitkit/middleware/deduplication.py +651 -0
  60. rabbitkit/middleware/error_classifier.py +43 -0
  61. rabbitkit/middleware/exception.py +105 -0
  62. rabbitkit/middleware/metrics.py +440 -0
  63. rabbitkit/middleware/otel.py +203 -0
  64. rabbitkit/middleware/rate_limit.py +247 -0
  65. rabbitkit/middleware/retry.py +540 -0
  66. rabbitkit/middleware/signing.py +682 -0
  67. rabbitkit/middleware/timeout.py +291 -0
  68. rabbitkit/py.typed +0 -0
  69. rabbitkit/queue_metrics.py +174 -0
  70. rabbitkit/results/__init__.py +6 -0
  71. rabbitkit/results/backend.py +102 -0
  72. rabbitkit/results/middleware.py +123 -0
  73. rabbitkit/rpc.py +632 -0
  74. rabbitkit/serialization/__init__.py +25 -0
  75. rabbitkit/serialization/base.py +35 -0
  76. rabbitkit/serialization/json.py +122 -0
  77. rabbitkit/serialization/msgspec.py +136 -0
  78. rabbitkit/serialization/pipeline.py +255 -0
  79. rabbitkit/streams.py +139 -0
  80. rabbitkit/sync/__init__.py +11 -0
  81. rabbitkit/sync/batch.py +595 -0
  82. rabbitkit/sync/broker.py +996 -0
  83. rabbitkit/sync/connection.py +209 -0
  84. rabbitkit/sync/pool.py +262 -0
  85. rabbitkit/sync/transport.py +1085 -0
  86. rabbitkit/testing/__init__.py +20 -0
  87. rabbitkit/testing/app.py +99 -0
  88. rabbitkit/testing/broker.py +540 -0
  89. rabbitkit/testing/fixtures.py +56 -0
  90. rabbitkit-0.9.0.dist-info/METADATA +575 -0
  91. rabbitkit-0.9.0.dist-info/RECORD +95 -0
  92. rabbitkit-0.9.0.dist-info/WHEEL +5 -0
  93. rabbitkit-0.9.0.dist-info/entry_points.txt +2 -0
  94. rabbitkit-0.9.0.dist-info/licenses/LICENSE +21 -0
  95. rabbitkit-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,56 @@
1
+ """Pytest fixtures for rabbitkit testing.
2
+
3
+ Provides standardized fixtures for TestBroker and TestApp.
4
+ Import these fixtures in your conftest.py.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Generator
10
+
11
+ import pytest
12
+
13
+ from rabbitkit.core.app import RabbitApp
14
+ from rabbitkit.testing.app import TestApp
15
+ from rabbitkit.testing.broker import TestBroker
16
+
17
+
18
+ @pytest.fixture # pragma: no cover
19
+ def test_broker() -> Generator[TestBroker, None, None]:
20
+ """Provide a fresh TestBroker instance.
21
+
22
+ Usage:
23
+ def test_my_handler(test_broker):
24
+ @test_broker.subscriber(queue="orders")
25
+ def handle(body: bytes) -> None:
26
+ ...
27
+
28
+ test_broker.start()
29
+ test_broker.publish("orders", b'hello')
30
+ handle.mock.assert_called_once()
31
+ """
32
+ broker = TestBroker()
33
+ yield broker
34
+ broker.stop()
35
+
36
+
37
+ @pytest.fixture # pragma: no cover
38
+ def test_app() -> Generator[TestApp, None, None]:
39
+ """Provide a fresh TestApp instance with RabbitApp + TestBroker.
40
+
41
+ Usage:
42
+ def test_lifecycle(test_app):
43
+ @test_app.broker.subscriber(queue="orders")
44
+ def handle(body: bytes) -> None:
45
+ ...
46
+
47
+ test_app.start()
48
+ test_app.broker.publish("orders", b'hello')
49
+ test_app.stop()
50
+ """
51
+ app = RabbitApp(title="test-app")
52
+ broker = TestBroker()
53
+ ta = TestApp(app, broker)
54
+ yield ta
55
+ if ta.state.value not in ("stopped", "idle"):
56
+ ta.stop()
@@ -0,0 +1,575 @@
1
+ Metadata-Version: 2.4
2
+ Name: rabbitkit
3
+ Version: 0.9.0
4
+ Summary: Production-grade RabbitMQ toolkit — sync/async, decorator routing, retry, compression, full configurability
5
+ Author-email: Talaat Magdy <talaatmagdy75@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/talaatmagdyx/rabbitkit
8
+ Project-URL: Documentation, https://talaatmagdyx.github.io/rabbitkit/
9
+ Project-URL: Repository, https://github.com/talaatmagdyx/rabbitkit
10
+ Project-URL: Changelog, https://github.com/talaatmagdyx/rabbitkit/blob/main/CHANGELOG.md
11
+ Project-URL: Issues, https://github.com/talaatmagdyx/rabbitkit/issues
12
+ Keywords: rabbitmq,amqp,messaging,pika,aio-pika,microservices,event-driven,message-queue
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: structlog<26.0.0,>=23.1.0
27
+ Provides-Extra: sync
28
+ Requires-Dist: pika<2.0.0,>=1.3.0; extra == "sync"
29
+ Provides-Extra: async
30
+ Requires-Dist: aio-pika<10.0.0,>=9.1.0; extra == "async"
31
+ Provides-Extra: all-brokers
32
+ Requires-Dist: rabbitkit[async,sync]; extra == "all-brokers"
33
+ Provides-Extra: redis
34
+ Requires-Dist: redis<6.0.0,>=5.0.0; extra == "redis"
35
+ Provides-Extra: pydantic
36
+ Requires-Dist: pydantic<3.0.0,>=2.0.0; extra == "pydantic"
37
+ Provides-Extra: msgspec
38
+ Requires-Dist: msgspec<1.0.0,>=0.18.0; extra == "msgspec"
39
+ Provides-Extra: fastapi
40
+ Requires-Dist: fastapi<1.0.0,>=0.111.0; extra == "fastapi"
41
+ Provides-Extra: compression
42
+ Requires-Dist: zstandard<1.0.0,>=0.22.0; extra == "compression"
43
+ Provides-Extra: management
44
+ Requires-Dist: aiohttp<4.0.0,>=3.9.0; extra == "management"
45
+ Provides-Extra: otel
46
+ Requires-Dist: opentelemetry-api<2,>=1.20; extra == "otel"
47
+ Provides-Extra: settings
48
+ Requires-Dist: pydantic-settings<3.0.0,>=2.0.0; extra == "settings"
49
+ Provides-Extra: cli
50
+ Requires-Dist: typer<1.0.0,>=0.12.0; extra == "cli"
51
+ Provides-Extra: dashboard
52
+ Requires-Dist: starlette<2.0.0,>=0.37.0; extra == "dashboard"
53
+ Requires-Dist: uvicorn<1.0.0,>=0.29.0; extra == "dashboard"
54
+ Provides-Extra: reload
55
+ Requires-Dist: watchfiles<2.0.0,>=0.21.0; extra == "reload"
56
+ Provides-Extra: all
57
+ Requires-Dist: rabbitkit[all-brokers,cli,compression,dashboard,fastapi,management,msgspec,otel,pydantic,redis,reload,settings]; extra == "all"
58
+ Provides-Extra: dev
59
+ Requires-Dist: rabbitkit[all]; extra == "dev"
60
+ Requires-Dist: pytest>=8.0; extra == "dev"
61
+ Requires-Dist: httpx<1.0,>=0.27; extra == "dev"
62
+ Requires-Dist: prometheus-client<2.0,>=0.20; extra == "dev"
63
+ Requires-Dist: mkdocs>=1.6.0; extra == "dev"
64
+ Requires-Dist: mkdocs-material>=9.5.0; extra == "dev"
65
+ Requires-Dist: mkdocstrings[python]>=0.26.0; extra == "dev"
66
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
67
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
68
+ Requires-Dist: pytest-timeout>=2.2; extra == "dev"
69
+ Requires-Dist: ruff>=0.4.0; extra == "dev"
70
+ Requires-Dist: mypy>=1.10; extra == "dev"
71
+ Requires-Dist: hypothesis>=6.100.0; extra == "dev"
72
+ Requires-Dist: pre-commit>=3.7.0; extra == "dev"
73
+ Provides-Extra: integration
74
+ Requires-Dist: rabbitkit[dev]; extra == "integration"
75
+ Requires-Dist: testcontainers[rabbitmq]>=4.0.0; extra == "integration"
76
+ Requires-Dist: docker>=7.0.0; extra == "integration"
77
+ Dynamic: license-file
78
+
79
+ <p align="center"><img src="assets/logo.svg" alt="rabbitkit" width="420"></p>
80
+
81
+ # rabbitkit
82
+
83
+ **RabbitMQ made enjoyable — less broker plumbing, more business logic.**
84
+
85
+ [![PyPI](https://img.shields.io/pypi/v/rabbitkit)](https://pypi.org/project/rabbitkit/)
86
+ [![CI](https://github.com/talaatmagdyx/rabbitkit/actions/workflows/ci.yml/badge.svg)](https://github.com/talaatmagdyx/rabbitkit/actions/workflows/ci.yml)
87
+ [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](pyproject.toml)
88
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
89
+ [![Typed](https://img.shields.io/badge/types-mypy%20--strict-blue)](pyproject.toml)
90
+ [![Style](https://img.shields.io/badge/style-ruff-261230)](pyproject.toml)
91
+
92
+ rabbitkit is a **RabbitMQ-first toolkit for Python services**. It gives you
93
+ clean decorators, safe retries, dead-letter queues, publisher confirms,
94
+ explicit acknowledgement policies, Kubernetes-ready lifecycle hooks,
95
+ structured logging, OpenTelemetry tracing, and real in-memory testing — so
96
+ your team can focus on what each message *means*, not how the broker behaves
97
+ when things fail.
98
+
99
+ RabbitMQ is powerful. Production RabbitMQ is full of sharp edges.
100
+ rabbitkit smooths those edges **without hiding the broker from you**.
101
+
102
+ ```python
103
+ from rabbitkit import AsyncBroker, RabbitConfig
104
+
105
+ broker = AsyncBroker(RabbitConfig())
106
+
107
+ @broker.subscriber(queue="orders.created")
108
+ async def handle_order(order: dict) -> None:
109
+ await fulfill_order(order)
110
+ ```
111
+
112
+ That should feel like application code. The retry topology,
113
+ acknowledgements, confirms, DLQs, shutdown behavior, and test harness should
114
+ not be rewritten in every service. **That is what rabbitkit is for.**
115
+
116
+ **Contents:**
117
+ - [Believes](#what-rabbitkit-believes)
118
+ - [Why](#why-rabbitkit-exists)
119
+ - [Install](#installation)
120
+ - [Quick start](#quick-start)
121
+ - [Safety model](#message-safety-model)
122
+ - [Failure table](#what-happens-when-things-fail)
123
+ - [Ack policies](#acknowledgement-policies)
124
+ - [Production profile](#production-profile)
125
+ - [Observability](#observability)
126
+ - [DI](#dependency-injection)
127
+ - [Middleware](#middleware-batteries-included)
128
+ - [CLI](#operate-it-from-the-terminal)
129
+ - [Where it fits](#where-rabbitkit-fits)
130
+ - [Architecture](#architecture)
131
+ - [Docs](#documentation)
132
+
133
+ ---
134
+
135
+ ## What rabbitkit believes
136
+
137
+ Most services need the same things:
138
+
139
+ - a clean way to register consumers
140
+ - safe retry behavior
141
+ - a place for poison messages to go
142
+ - publisher confirms that are **checked**
143
+ - explicit acknowledgement ownership
144
+ - graceful shutdown
145
+ - useful logs and traces
146
+ - health checks that behave correctly in Kubernetes
147
+ - tests that do not require a live broker
148
+
149
+ rabbitkit packages those concerns into one focused toolkit. The philosophy:
150
+
151
+ > **Make RabbitMQ pleasant for developers and predictable for operators.**
152
+
153
+ Developers get a clean programming model. Operators get visible message
154
+ outcomes. Production gets fewer silent failure paths.
155
+
156
+ ## Why rabbitkit exists
157
+
158
+ Starting with RabbitMQ is easy: `basic_publish(...)`, `basic_consume(...)`.
159
+ Then production asks better questions:
160
+
161
+ - What happens if a handler fails *forever*?
162
+ - Where does a malformed payload go?
163
+ - Can a rejected message disappear because the queue had no DLX?
164
+ - Did the retry publish **confirm** before the original was acknowledged?
165
+ - Can a DLQ replay remove a message before the republish is confirmed?
166
+ - Can a pod shut down without interrupting in-flight work?
167
+ - Can CI test real consumer behavior without starting RabbitMQ?
168
+
169
+ rabbitkit exists for those questions. Its goal is not to turn RabbitMQ into
170
+ something else — it is to make direct RabbitMQ usage feel like good
171
+ application code: clear routing, safe defaults, explicit outcomes, real
172
+ tests, production-ready lifecycle.
173
+
174
+ **rabbitkit is:**
175
+
176
+ - a RabbitMQ-first toolkit
177
+ - a clean consumer/publisher API
178
+ - a reliability layer over `pika` and `aio-pika`
179
+ - a testing layer for handlers
180
+ - a production lifecycle layer
181
+ - safety defaults for retry, DLQ, confirms, and acks
182
+
183
+ **rabbitkit is not:**
184
+
185
+ - a task queue
186
+ - a scheduler
187
+ - a generic event-streaming abstraction
188
+ - a replacement for understanding RabbitMQ
189
+ - an exactly-once delivery system
190
+
191
+ ---
192
+
193
+ ## Installation
194
+
195
+ ```bash
196
+ pip install rabbitkit[async] # AsyncBroker (aio-pika)
197
+ pip install rabbitkit[sync] # SyncBroker (pika)
198
+ pip install rabbitkit[all-brokers] # both transports
199
+ pip install rabbitkit[all] # everything optional
200
+ ```
201
+
202
+ Requires Python ≥ 3.11.
203
+
204
+ ## The 10-minute path
205
+
206
+ A durable, retrying, DLQ-backed consumer — tested without a broker:
207
+
208
+ 1. `pip install rabbitkit[async]`
209
+ 2. Create an `AsyncBroker(RabbitConfig())`
210
+ 3. Register a handler with `@broker.subscriber(queue=...)`
211
+ 4. Add `retry=RetryConfig(max_retries=3, delays=(5, 30, 120))`
212
+ 5. Run it: `rabbitkit run myapp.main:broker`
213
+ 6. Test it in CI with `TestBroker` — no RabbitMQ required
214
+
215
+ Each step is shown below.
216
+
217
+ ## Quick start
218
+
219
+ ### 1. Create a consumer
220
+
221
+ ```python
222
+ from rabbitkit import RabbitConfig, AsyncBroker
223
+
224
+ broker = AsyncBroker(RabbitConfig())
225
+
226
+ @broker.subscriber(queue="orders.created")
227
+ async def handle_order(body: dict) -> None:
228
+ print(f"order id={body['id']}")
229
+
230
+ async def main() -> None:
231
+ await broker.start()
232
+ ```
233
+
234
+ That is enough to consume messages. But production usually needs more than
235
+ "enough".
236
+
237
+ ### 2. Publish — and check the outcome
238
+
239
+ ```python
240
+ async def publish_order() -> None:
241
+ outcome = await broker.publish(
242
+ exchange="orders",
243
+ routing_key="orders.created",
244
+ body={"id": 42, "item": "widget"},
245
+ )
246
+ outcome.raise_for_status()
247
+ ```
248
+
249
+ A publish can be `CONFIRMED`, `SENT`, `RETURNED`, `NACKED`, `TIMEOUT`, or
250
+ `ERROR`. Application code can treat those as different states instead of
251
+ assuming "publish called" means "message safe".
252
+
253
+ ### 3. Add retry and DLQ handling
254
+
255
+ ```python
256
+ from rabbitkit import RetryConfig
257
+
258
+ @broker.subscriber(
259
+ queue="orders.created",
260
+ exchange="orders",
261
+ routing_key="orders.created",
262
+ retry=RetryConfig(max_retries=3, delays=(5, 30, 120)),
263
+ )
264
+ async def handle_order_with_retry(body: dict) -> None:
265
+ await fulfill_order(body)
266
+ ```
267
+
268
+ This wires the reliability path — **broker-side**, carried in hardened
269
+ headers, surviving crashes and reconnects:
270
+
271
+ ```
272
+ orders.created
273
+ → orders.created.retry.1 (5s)
274
+ → orders.created.retry.2 (30s)
275
+ → orders.created.retry.3 (120s)
276
+ → orders.created.dlq
277
+ ```
278
+
279
+ Transient failures retry with backoff. Permanent failures skip the ladder
280
+ and go straight to the DLQ. By default, rejected messages do not disappear
281
+ silently — every rejecting route gets a DLQ unless you explicitly opt into
282
+ discard behavior.
283
+
284
+ ### 4. Test it without RabbitMQ
285
+
286
+ ```python
287
+ from rabbitkit.testing import TestBroker
288
+
289
+ def test_order_handler():
290
+ broker = TestBroker()
291
+
292
+ @broker.subscriber(queue="orders.created")
293
+ def handle(body: dict) -> None:
294
+ assert body["id"] == 42
295
+
296
+ broker.start()
297
+ broker.publish("orders.created", b'{"id": 42}')
298
+ broker.stop()
299
+ ```
300
+
301
+ `TestBroker` is not a mock. It runs the real routing, middleware,
302
+ serialization, dependency resolution, settlement, and ack/nack pipeline in
303
+ memory. Your CI can test RabbitMQ behavior without running RabbitMQ.
304
+
305
+ ### 5. Run with FastAPI
306
+
307
+ ```python
308
+ from contextlib import asynccontextmanager
309
+
310
+ from fastapi import FastAPI
311
+
312
+ from rabbitkit import RabbitConfig, AsyncBroker
313
+ from rabbitkit.fastapi import rabbitkit_lifespan
314
+
315
+ api_broker = AsyncBroker(RabbitConfig())
316
+
317
+ @api_broker.subscriber(queue="orders.created")
318
+ async def handle_order_event(body: dict) -> None:
319
+ ...
320
+
321
+ @asynccontextmanager
322
+ async def lifespan(app: FastAPI):
323
+ async with rabbitkit_lifespan(api_broker):
324
+ yield
325
+
326
+ app = FastAPI(lifespan=lifespan)
327
+ ```
328
+
329
+ ### Sync example
330
+
331
+ ```python
332
+ from rabbitkit import RabbitConfig
333
+ from rabbitkit.sync import SyncBroker
334
+
335
+ sync_broker = SyncBroker(RabbitConfig())
336
+
337
+ @sync_broker.subscriber(queue="orders.created")
338
+ def handle_order_sync(body: bytes) -> None:
339
+ print(f"received order: {body!r}")
340
+
341
+ def main() -> None:
342
+ # Blocks until SIGINT/SIGTERM or stop(); reconnects on connection drops
343
+ # and drains in-flight work on pod termination.
344
+ sync_broker.run()
345
+ ```
346
+
347
+ The sync broker fits simple workers, scripts, legacy services, and teams
348
+ that do not want an asyncio runtime. **Throughput note:** sync confirmed
349
+ publishing waits one confirm per message (~0.9k msg/s measured);
350
+ `worker_count` does not raise it. For high-throughput confirmed publishing
351
+ use `AsyncBroker` + `AsyncBatchPublisher` (pipelined confirms, ~6.1k msg/s
352
+ measured) or `SyncBatchPublisher` (pipelined confirms for sync code on a
353
+ dedicated I/O thread), or scale out across processes.
354
+
355
+ ---
356
+
357
+ ## Message safety model
358
+
359
+ rabbitkit is an **at-least-once** toolkit: a handler may run more than once
360
+ (crash after work but before ack, connection death mid-handler, DLQ replay,
361
+ producer retry after a confirm timeout…). rabbitkit removes dangerous
362
+ ambiguity around those cases — it does not remove the need for idempotency.
363
+
364
+ For payments, emails, tickets, webhooks, external API calls: design the
365
+ handler so running it twice is safe (idempotency keys, unique constraints,
366
+ processed-event tables, outbox patterns — or rabbitkit's deduplication
367
+ middleware, whose `store_results` mode replays the original result to
368
+ duplicates). The rule is simple:
369
+
370
+ > rabbitkit can help you retry safely. Your business logic must still be
371
+ > safe to retry.
372
+
373
+ ## What happens when things fail?
374
+
375
+ | Failure mode | rabbitkit behavior |
376
+ |---|---|
377
+ | Handler raises forever | Retry ladder, then DLQ |
378
+ | Malformed payload | Classified permanent, preserved in DLQ |
379
+ | Reject with no DLX | Safe default auto-provisions a DLQ — no silent discard |
380
+ | Retry publish times out | Original is **not** acked as if the retry succeeded |
381
+ | DLQ replay publish fails | DLQ message is **not** removed as if replay succeeded |
382
+ | Message unroutable | Mandatory publishing returns a distinct `RETURNED` outcome |
383
+ | Broker blips | Readiness changes; liveness does **not** kill the pod |
384
+ | Pod gets SIGTERM | Consumers stop first, in-flight work drains |
385
+ | CI has no RabbitMQ | `TestBroker` runs the real pipeline in memory |
386
+
387
+ ## Acknowledgement policies
388
+
389
+ Settlement is a decision, not a side effect hidden in a callback.
390
+
391
+ | Policy | Behavior | Use case |
392
+ |---|---|---|
393
+ | `AUTO` | Ack on success, retry/reject on failure | Most consumers |
394
+ | `MANUAL` | Handler owns ack/nack/reject | Custom settlement flows |
395
+ | `NACK_ON_ERROR` | Ack on success, nack on failure | Never silently accept failed work |
396
+ | `ACK_FIRST` | Ack before the handler runs | At-most-once workloads |
397
+
398
+ `ACK_FIRST` can lose messages if the handler fails after the ack — use it
399
+ only when loss is acceptable.
400
+
401
+ ## Production profile
402
+
403
+ The recommended baseline (see the
404
+ [production checklist](docs/production/checklist.md)): quorum queues (+
405
+ `delivery_limit`), per-queue retry/DLQ topology, publisher confirms on,
406
+ mandatory publishing where routing matters, checked `PublishOutcome`s,
407
+ explicit ack policies, structured logs, split readiness/liveness probes,
408
+ management-API metrics for queue depth and consumer lag, idempotent
409
+ handlers. Migrating existing classic queues to quorum? There's a tool:
410
+ `rabbitkit topology migrate` ([guide](docs/quorum-migration.md)).
411
+
412
+ ## Observability
413
+
414
+ Structured logs carry message context (`message_id`, `correlation_id`,
415
+ routing, queue, handler, retry count, settlement, duration, error type) with
416
+ secret redaction on by default. Metrics cover consumed/acked/nacked/
417
+ retried/dead-lettered counts, publish outcomes, handler latency,
418
+ redeliveries, reconnects, and — via the management API poller — queue depth
419
+ and consumer lag. Tracing is standard OpenTelemetry
420
+ (`pip install rabbitkit[otel]`): W3C context propagation over AMQP headers,
421
+ one continuous trace from publish to consume.
422
+
423
+ ## Advanced & experimental
424
+
425
+ **Advanced stable** (enable deliberately): publish-side backpressure
426
+ (`FlowController`), batch publishing/acking, pipelined sync confirms
427
+ (`SyncBatchPublisher`), DLQ inspector + replay CLI, management API client,
428
+ topology validation/drift/migration CLI, health watcher, circuit-breaker
429
+ middleware (bring any `CircuitBreakerProtocol` implementation, e.g.
430
+ pybreaker).
431
+
432
+ **Experimental** (may change without a deprecation cycle — read the
433
+ [stability policy](docs/stability-policy.md)): RPC over direct reply-to,
434
+ distributed locking, message signing, result backends, stream queues, the
435
+ monitoring dashboard. Notable caveats: the default signing nonce cache is
436
+ per-process (use a shared cache for real replay protection), and never
437
+ expose the dashboard publicly without authentication.
438
+
439
+ ## Dependency injection
440
+
441
+ Handlers resolve request-like context declaratively — typed body, headers,
442
+ routing-key segments, and shared dependencies:
443
+
444
+ ```python
445
+ from rabbitkit import AsyncBroker, Context, Depends, Header, Path, RabbitConfig
446
+ from rabbitkit.core.message import RabbitMessage
447
+
448
+ di_broker = AsyncBroker(RabbitConfig())
449
+
450
+ def get_db() -> str:
451
+ return "db-connection"
452
+
453
+ @di_broker.subscriber(queue="tenants.{tenant_id}.orders")
454
+ async def handle_tenant_order(
455
+ body: dict,
456
+ tenant_id: str = Path(),
457
+ trace_id: str = Header("x-trace-id", default=""),
458
+ db: str = Depends(get_db),
459
+ message: RabbitMessage = Context(),
460
+ ) -> None:
461
+ ...
462
+ ```
463
+
464
+ Serialization is pluggable per route: raw bytes, JSON (default), Pydantic
465
+ models, msgspec structs, or a custom parser/decoder pipeline — annotate the
466
+ body parameter with the type you want and pick the serializer that
467
+ validates it.
468
+
469
+ ## Middleware, batteries included
470
+
471
+ | Middleware | Job |
472
+ |---|---|
473
+ | `RetryMiddleware` | Broker-side retry ladder (auto-wired with `retry=`) |
474
+ | `DeduplicationMiddleware` | Redis-backed duplicate suppression; `claim` policy is crash-safe; `store_results` replays the original answer to duplicates |
475
+ | `MetricsMiddleware` | Counters + latency histograms, cardinality-guarded labels |
476
+ | `OTelTracingMiddleware` | Standard OpenTelemetry spans + W3C propagation |
477
+ | `CompressionMiddleware` | gzip/zstd with streaming zip-bomb guards |
478
+ | `RateLimitMiddleware` | Token-bucket consume throttling (nack/drop/wait) |
479
+ | `TimeoutMiddleware` | Per-handler deadlines, retry-classified |
480
+ | `CircuitBreakerMiddleware` | Wraps any `CircuitBreakerProtocol` implementation |
481
+ | `SigningMiddleware` | HMAC signing + replay protection (experimental) |
482
+
483
+ ## Operate it from the terminal
484
+
485
+ ```bash
486
+ rabbitkit run myapp.main:broker # run consumers
487
+ rabbitkit dlq inspect orders.dlq # peek at poison messages
488
+ rabbitkit dlq replay orders.dlq orders --reset-retry-count
489
+ rabbitkit topology validate myapp.main:broker # declared vs live drift
490
+ rabbitkit topology migrate myapp.main:broker # classic -> quorum, planned & resumable
491
+ rabbitkit health myapp.main:broker
492
+ ```
493
+
494
+ The DLQ replay acks a message only after its republish is broker-confirmed —
495
+ the recovery tool cannot itself lose messages.
496
+
497
+ <p align="center"><img src="assets/demo.svg" alt="rabbitkit dlq inspect and replay demo" width="720"></p>
498
+
499
+ ## Where rabbitkit fits
500
+
501
+ rabbitkit sits *above* `pika` and `aio-pika` — a reliability layer, not a
502
+ replacement; drop to the underlying client any time. It is for teams that
503
+ use RabbitMQ directly and want production-safe messaging without rebuilding
504
+ retries, DLQs, publisher confirms, acknowledgements, lifecycle handling, and
505
+ test infrastructure in every service.
506
+
507
+ **A good fit when:**
508
+
509
+ - RabbitMQ is your primary broker
510
+ - message loss would be an incident
511
+ - retry and DLQ behavior must be explicit
512
+ - CI should test handlers without a real broker
513
+ - Kubernetes shutdown and readiness matter
514
+ - operators need visibility into message outcomes
515
+
516
+ **Probably not the right fit when:**
517
+
518
+ - you need a task queue or scheduler (use a task framework)
519
+ - you need a broker-agnostic framework or want to hide RabbitMQ semantics
520
+ - at-most-once behavior is acceptable and a raw client is enough
521
+
522
+ A detailed framework-by-framework comparison lives in
523
+ [docs/comparison.md](docs/comparison.md).
524
+
525
+ ## Architecture
526
+
527
+ ```
528
+ rabbitkit/
529
+ core/ # route registry, topology, pipeline, settlement, config
530
+ sync/ # pika adapter (+ SyncBatchPublisher)
531
+ async_/ # aio-pika adapter (+ AsyncBatchPublisher)
532
+ middleware/ # retry, dedup, metrics, otel, compression, rate limit…
533
+ serialization/ # JSON, msgspec, Pydantic, parser/decoder pipeline
534
+ di/ # Depends, Header, Path, Context
535
+ testing/ # TestBroker and friends
536
+ highload/ # FlowController, BatchPublisher, BatchAcker
537
+ cli/ # dlq, topology, migrate, health, run, shell
538
+ fastapi.py # FastAPI lifespan integration
539
+ ```
540
+
541
+ The shared core has **zero** imports from `pika` or `aio-pika` — both
542
+ transports are adapters over the same registry, pipeline, topology model,
543
+ and settlement rules.
544
+
545
+ ## Compatibility
546
+
547
+ Python ≥ 3.11 (tested: 3.11 / 3.12 / 3.13 / 3.14; 3.15 pre-release experimental) · RabbitMQ ≥ 3.12 recommended ·
548
+ `pika >= 1.3, < 2.0` · `aio-pika >= 9.1, < 10.0`
549
+
550
+ ## Documentation
551
+
552
+ **📚 Full rendered docs: [talaatmagdyx.github.io/rabbitkit](https://talaatmagdyx.github.io/rabbitkit/)**
553
+
554
+ - [Getting Started](docs/guide/getting-started.md)
555
+ - [Full Guide](docs/guide/full-guide.md)
556
+ - [Message Safety](docs/message-safety.md)
557
+ - [Retry & DLQ](docs/retry-and-dlq.md)
558
+ - [Production Checklist](docs/production/checklist.md)
559
+ - [Idempotency Contract](docs/production/idempotency.md)
560
+ - [Kubernetes](docs/kubernetes.md)
561
+ - [Quorum Migration](docs/quorum-migration.md)
562
+ - [Security](docs/security.md)
563
+ - [Stability Policy](docs/stability-policy.md)
564
+ - [Troubleshooting](docs/troubleshooting.md)
565
+
566
+ ## Contributing & security
567
+
568
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for local development and quality
569
+ gates (ruff, `mypy --strict`, near-total test coverage — the bar is real).
570
+ Found a vulnerability? Follow [SECURITY.md](SECURITY.md) and report it
571
+ privately.
572
+
573
+ ## License
574
+
575
+ [MIT](LICENSE)