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,251 @@
1
+ """Environment-variable driven RabbitMQ configuration.
2
+
3
+ Requires: ``pip install rabbitkit[settings]``
4
+
5
+ Maps ``RABBITMQ_*`` environment variables to ``RabbitConfig`` using
6
+ pydantic-settings ``BaseSettings``. Supports ``.env`` files, environment
7
+ overrides, and type coercion out of the box.
8
+
9
+ Lazy import — no ``ImportError`` at import time when pydantic-settings is
10
+ absent; a placeholder class is installed instead and raises on instantiation.
11
+
12
+ Quick start
13
+ -----------
14
+ # .env (or set as real environment variables)
15
+ RABBITMQ_HOST=rabbitmq.prod.internal
16
+ RABBITMQ_PORT=5672
17
+ RABBITMQ_USER=myapp
18
+ RABBITMQ_PASSWORD=secret
19
+ RABBITMQ_VHOST=/production
20
+ RABBITMQ_PREFETCH_COUNT=20
21
+ RABBITMQ_TOPOLOGY_MODE=AUTO_DECLARE
22
+
23
+ # application code
24
+ from rabbitkit.core.env_config import RabbitSettings
25
+ from rabbitkit.async_ import AsyncBroker
26
+
27
+ settings = RabbitSettings() # reads from env / .env automatically
28
+ config = settings.to_rabbit_config()
29
+ broker = AsyncBroker(config)
30
+
31
+ Supported environment variables
32
+ --------------------------------
33
+ All variables are prefixed with ``RABBITMQ_`` and case-insensitive:
34
+
35
+ Connection
36
+ RABBITMQ_HOST (str, default "localhost")
37
+ RABBITMQ_PORT (int, default 5672)
38
+ RABBITMQ_USER (str, default "guest")
39
+ RABBITMQ_PASSWORD (str, default "guest")
40
+ RABBITMQ_VHOST (str, default "/")
41
+ RABBITMQ_HEARTBEAT (int, default 30)
42
+ RABBITMQ_SOCKET_TIMEOUT (float, default 10.0)
43
+ RABBITMQ_BLOCKED_CONNECTION_TIMEOUT (float, default 60.0)
44
+ RABBITMQ_CONNECTION_NAME (str | None, default None)
45
+ RABBITMQ_RECONNECT_BACKOFF_BASE (float, default 1.0)
46
+ RABBITMQ_RECONNECT_BACKOFF_MAX (float, default 30.0)
47
+
48
+ Consumer
49
+ RABBITMQ_PREFETCH_COUNT (int, default 10)
50
+ RABBITMQ_GRACEFUL_TIMEOUT (float, default 30.0)
51
+
52
+ Publisher
53
+ RABBITMQ_CONFIRM_DELIVERY (bool, default True)
54
+ RABBITMQ_CONFIRM_TIMEOUT (float, default 5.0)
55
+ RABBITMQ_MANDATORY (bool, default False)
56
+ RABBITMQ_PERSISTENT (bool, default True)
57
+ RABBITMQ_DEFAULT_EXCHANGE (str, default "")
58
+
59
+ Pool
60
+ RABBITMQ_CHANNEL_POOL_SIZE (int, default 10)
61
+ RABBITMQ_PUBLISHER_CONNECTIONS (int, default 1)
62
+ RABBITMQ_CONSUMER_CONNECTIONS (int, default 1)
63
+ RABBITMQ_CHANNEL_ACQUIRE_TIMEOUT (float, default 10.0)
64
+
65
+ SSL
66
+ RABBITMQ_SSL_ENABLED (bool, default False)
67
+ RABBITMQ_SSL_CERTFILE (str | None, default None)
68
+ RABBITMQ_SSL_KEYFILE (str | None, default None)
69
+ RABBITMQ_SSL_CA_CERTS (str | None, default None)
70
+ RABBITMQ_SSL_SERVER_HOSTNAME (str | None, default None)
71
+
72
+ Retry (set RABBITMQ_RETRY_MAX_RETRIES > 0 to enable)
73
+ RABBITMQ_RETRY_MAX_RETRIES (int, default 0 — disabled)
74
+ RABBITMQ_RETRY_DELAYS (str, default "5,30,120,600" — comma-separated ints)
75
+ RABBITMQ_RETRY_JITTER_FACTOR (float, default 0.1)
76
+
77
+ Topology
78
+ RABBITMQ_TOPOLOGY_MODE (str, default "AUTO_DECLARE")
79
+ valid: AUTO_DECLARE, PASSIVE_ONLY, MANUAL
80
+
81
+ Override at runtime
82
+ -------------------
83
+ pydantic-settings respects constructor keyword arguments, so you can mix
84
+ env-file defaults with runtime overrides:
85
+
86
+ settings = RabbitSettings(host="staging-rabbit", prefetch_count=5)
87
+ config = settings.to_rabbit_config()
88
+
89
+ Checking availability
90
+ ---------------------
91
+ from rabbitkit.core.env_config import _PYDANTIC_SETTINGS_AVAILABLE
92
+
93
+ if _PYDANTIC_SETTINGS_AVAILABLE:
94
+ settings = RabbitSettings()
95
+ else:
96
+ config = RabbitConfig(connection=ConnectionConfig(...))
97
+ """
98
+
99
+ from __future__ import annotations
100
+
101
+ from typing import Any
102
+
103
+ _PYDANTIC_SETTINGS_AVAILABLE = False
104
+
105
+ try:
106
+ from pydantic import Field, SecretStr
107
+ from pydantic_settings import BaseSettings, SettingsConfigDict
108
+
109
+ _PYDANTIC_SETTINGS_AVAILABLE = True
110
+ except ImportError: # pragma: no cover
111
+ pass # pragma: no cover
112
+
113
+ if _PYDANTIC_SETTINGS_AVAILABLE:
114
+ from rabbitkit.core.config import (
115
+ ConnectionConfig,
116
+ ConsumerConfig,
117
+ PoolConfig,
118
+ PublisherConfig,
119
+ RabbitConfig,
120
+ RetryConfig,
121
+ SecurityConfig,
122
+ SSLConfig,
123
+ )
124
+ from rabbitkit.core.types import TopologyMode
125
+
126
+ class RabbitSettings(BaseSettings):
127
+ """Load RabbitMQ config from RABBITMQ_* environment variables.
128
+
129
+ Example::
130
+
131
+ settings = RabbitSettings() # reads from env / .env
132
+ config = settings.to_rabbit_config()
133
+ broker = AsyncBroker(config)
134
+ """
135
+
136
+ model_config = SettingsConfigDict(
137
+ env_prefix="RABBITMQ_",
138
+ case_sensitive=False,
139
+ )
140
+
141
+ # Connection
142
+ host: str = "localhost"
143
+ port: int = 5672
144
+ user: str = Field(default="guest")
145
+ password: SecretStr = SecretStr("guest")
146
+ vhost: str = "/"
147
+ heartbeat: int = 30
148
+ socket_timeout: float = 10.0
149
+ blocked_connection_timeout: float = 60.0
150
+ connection_name: str | None = None
151
+ reconnect_backoff_base: float = 1.0
152
+ reconnect_backoff_max: float = 30.0
153
+
154
+ # Consumer
155
+ prefetch_count: int = 10
156
+ graceful_timeout: float = 30.0
157
+
158
+ # Publisher
159
+ confirm_delivery: bool = True
160
+ confirm_timeout: float = 5.0
161
+ mandatory: bool = False
162
+ persistent: bool = True
163
+ default_exchange: str = ""
164
+
165
+ # Pool
166
+ channel_pool_size: int = 10
167
+ publisher_connections: int = 1
168
+ consumer_connections: int = 1
169
+ channel_acquire_timeout: float = 10.0
170
+
171
+ # SSL
172
+ ssl_enabled: bool = False
173
+ ssl_certfile: str | None = None
174
+ ssl_keyfile: str | None = None
175
+ ssl_ca_certs: str | None = None
176
+ ssl_server_hostname: str | None = None
177
+
178
+ # Retry (0 = disabled, no RetryConfig created)
179
+ retry_max_retries: int = 0
180
+ retry_delays: str = "5,30,120,600"
181
+ retry_jitter_factor: float = 0.1
182
+
183
+ # Topology
184
+ topology_mode: str = "AUTO_DECLARE"
185
+
186
+ def to_rabbit_config(self) -> RabbitConfig:
187
+ """Convert to a RabbitConfig dataclass."""
188
+ ssl = SSLConfig(
189
+ enabled=self.ssl_enabled,
190
+ certfile=self.ssl_certfile,
191
+ keyfile=self.ssl_keyfile,
192
+ ca_certs=self.ssl_ca_certs,
193
+ server_hostname=self.ssl_server_hostname,
194
+ )
195
+ retry: RetryConfig | None = None
196
+ if self.retry_max_retries > 0:
197
+ delays = tuple(int(d) for d in self.retry_delays.split(",") if d.strip())
198
+ retry = RetryConfig(
199
+ max_retries=self.retry_max_retries,
200
+ delays=delays,
201
+ jitter_factor=self.retry_jitter_factor,
202
+ )
203
+ return RabbitConfig(
204
+ connection=ConnectionConfig(
205
+ host=self.host,
206
+ port=self.port,
207
+ username=self.user,
208
+ password=self.password.get_secret_value(),
209
+ vhost=self.vhost,
210
+ heartbeat=self.heartbeat,
211
+ socket_timeout=self.socket_timeout,
212
+ blocked_connection_timeout=self.blocked_connection_timeout,
213
+ connection_name=self.connection_name,
214
+ reconnect_backoff_base=self.reconnect_backoff_base,
215
+ reconnect_backoff_max=self.reconnect_backoff_max,
216
+ ),
217
+ security=SecurityConfig(ssl=ssl),
218
+ consumer=ConsumerConfig(
219
+ prefetch_count=self.prefetch_count,
220
+ graceful_timeout=self.graceful_timeout,
221
+ ),
222
+ publisher=PublisherConfig(
223
+ confirm_delivery=self.confirm_delivery,
224
+ confirm_timeout=self.confirm_timeout,
225
+ mandatory=self.mandatory,
226
+ persistent=self.persistent,
227
+ exchange=self.default_exchange,
228
+ ),
229
+ pool=PoolConfig(
230
+ channel_pool_size=self.channel_pool_size,
231
+ publisher_connections=self.publisher_connections,
232
+ consumer_connections=self.consumer_connections,
233
+ channel_acquire_timeout=self.channel_acquire_timeout,
234
+ ),
235
+ topology_mode=TopologyMode[self.topology_mode.upper()],
236
+ retry=retry,
237
+ )
238
+
239
+ else: # pragma: no cover
240
+
241
+ class RabbitSettings: # type: ignore[no-redef] # pragma: no cover
242
+ """Placeholder — pydantic-settings not installed."""
243
+
244
+ def __init__(self, **_: Any) -> None:
245
+ raise ImportError(
246
+ "RabbitSettings requires pydantic-settings. Install with: pip install rabbitkit[settings]"
247
+ )
248
+
249
+ def to_rabbit_config(self) -> Any:
250
+ """Not available without pydantic-settings."""
251
+ raise ImportError("pydantic-settings not installed")
@@ -0,0 +1,199 @@
1
+ """Error classification — transport-agnostic.
2
+
3
+ Transport-specific exception tuples (pika.exceptions.StreamLostError, etc.)
4
+ live in sync/connection.py and async_/connection.py, NOT here.
5
+
6
+ This module provides:
7
+ - Generic stdlib exception tuples for classification
8
+ - Pluggable predicate-based classification
9
+ - Configurable unknown_policy (default=PERMANENT)
10
+
11
+ See Contract 7 in the plan for evaluation order and rationale.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from collections.abc import Callable, Sequence
18
+
19
+ from rabbitkit.core.types import ClassifiedError, ErrorSeverity
20
+
21
+ # ── Missing DI dependency (H10) ──────────────────────────────────────────
22
+ # Defined before PERMANENT_ERRORS below so it can be listed there directly.
23
+
24
+
25
+ class MissingDependencyError(Exception):
26
+ """Raised at message-processing time when a required ``Header()`` /
27
+ ``Path()`` / ``Context()`` marker's value is absent from the incoming
28
+ message and no default is available — neither on the marker itself
29
+ (``Header("x-tenant", default=...)``) nor the handler's own parameter
30
+ default (``tenant: Annotated[str | None, Header("x-tenant")] = None``).
31
+
32
+ Names the specific parameter and marker so the failure is immediately
33
+ actionable — unlike the bare ``KeyError`` this replaces, which looked
34
+ identical to a handler bug (e.g. indexing a dict) and gave no indication
35
+ which DI marker was the culprit. Classified PERMANENT by
36
+ :func:`classify_error` (see ``PERMANENT_ERRORS`` below), matching the
37
+ ``KeyError`` classification it replaces: a missing required value means
38
+ the message itself is malformed for this handler — retrying will not fix
39
+ it, so it settles straight to the DLQ rather than looping.
40
+ """
41
+
42
+ def __init__(self, marker_repr: str, param_name: str) -> None:
43
+ super().__init__(
44
+ f"{marker_repr} for parameter {param_name!r} is required but missing from the "
45
+ "message, and no default is available (neither on the marker itself nor as a "
46
+ "Python parameter default)."
47
+ )
48
+ self.param_name = param_name
49
+
50
+
51
+ # Generic (stdlib) error categories — transport layers extend these
52
+ TRANSIENT_ERRORS: tuple[type[BaseException], ...] = (
53
+ TimeoutError,
54
+ EOFError, # NOT an OSError subclass — must be listed explicitly
55
+ OSError, # covers ConnectionResetError, BrokenPipeError, ConnectionAbortedError
56
+ )
57
+
58
+ PERMANENT_ERRORS: tuple[type[BaseException], ...] = (
59
+ json.JSONDecodeError,
60
+ KeyError,
61
+ ValueError,
62
+ TypeError,
63
+ UnicodeDecodeError,
64
+ AttributeError,
65
+ MissingDependencyError,
66
+ )
67
+
68
+ # Type alias for error predicates
69
+ ErrorPredicate = Callable[[BaseException], bool | None]
70
+
71
+
72
+ def classify_error(
73
+ exc: BaseException,
74
+ *,
75
+ predicates: Sequence[ErrorPredicate] = (),
76
+ transient: tuple[type[BaseException], ...] = TRANSIENT_ERRORS,
77
+ permanent: tuple[type[BaseException], ...] = PERMANENT_ERRORS,
78
+ unknown_policy: ErrorSeverity = ErrorSeverity.PERMANENT,
79
+ ) -> ClassifiedError:
80
+ """Classify exception severity.
81
+
82
+ Evaluation order:
83
+ 1. User predicates (first non-None wins)
84
+ 2. transient tuple (isinstance check)
85
+ 3. permanent tuple (isinstance check)
86
+ 4. unknown_policy (configurable, default=PERMANENT)
87
+
88
+ DEFAULT IS PERMANENT, NOT TRANSIENT.
89
+ Reason: unknown errors include malformed payloads, business validation
90
+ failures, handler bugs, and schema mismatches. Treating these as
91
+ transient creates retry storms. Network/connection errors are already
92
+ in the transient tuple.
93
+
94
+ Override per-route: RetryMiddleware accepts unknown_policy to change
95
+ per route.
96
+
97
+ Args:
98
+ exc: The exception to classify.
99
+ predicates: User-provided classification functions.
100
+ Return True=transient, False=permanent, None=no opinion.
101
+ transient: Exception types considered transient.
102
+ permanent: Exception types considered permanent.
103
+ unknown_policy: Severity for unclassified exceptions.
104
+
105
+ Returns:
106
+ ClassifiedError with severity, original exception, and reason.
107
+ """
108
+ # 1. User predicates (first non-None wins)
109
+ for predicate in predicates:
110
+ result = predicate(exc)
111
+ if result is True:
112
+ return ClassifiedError(
113
+ severity=ErrorSeverity.TRANSIENT,
114
+ original=exc,
115
+ reason=f"predicate classified as transient: {type(exc).__name__}",
116
+ )
117
+ if result is False:
118
+ return ClassifiedError(
119
+ severity=ErrorSeverity.PERMANENT,
120
+ original=exc,
121
+ reason=f"predicate classified as permanent: {type(exc).__name__}",
122
+ )
123
+
124
+ # 2. Transient tuple (isinstance check)
125
+ if isinstance(exc, transient):
126
+ return ClassifiedError(
127
+ severity=ErrorSeverity.TRANSIENT,
128
+ original=exc,
129
+ reason=f"transient error: {type(exc).__name__}",
130
+ )
131
+
132
+ # 3. Permanent tuple (isinstance check)
133
+ if isinstance(exc, permanent):
134
+ return ClassifiedError(
135
+ severity=ErrorSeverity.PERMANENT,
136
+ original=exc,
137
+ reason=f"permanent error: {type(exc).__name__}",
138
+ )
139
+
140
+ # 4. Unknown policy
141
+ return ClassifiedError(
142
+ severity=unknown_policy,
143
+ original=exc,
144
+ reason=f"unknown error classified as {unknown_policy.value}: {type(exc).__name__}",
145
+ )
146
+
147
+
148
+ # ── Configuration error (single canonical location) ─────────────────────
149
+
150
+
151
+ class ConfigurationError(Exception):
152
+ """Raised for invalid configuration detected at registration time.
153
+
154
+ Single canonical class for all registration-time misconfigurations (route
155
+ conflicts, invalid handler signatures, bad retry/ack combinations). Both
156
+ ``core/route.py`` and ``di/resolver.py`` raise this; tests/users can catch
157
+ one type regardless of import source.
158
+ """
159
+
160
+
161
+ # ── Unsafe topology error ────────────────────────────────────────────────
162
+
163
+
164
+ class UnsafeTopologyError(ConfigurationError):
165
+ """Raised at startup when ``RejectWithoutDLXPolicy.ERROR`` is active and a
166
+ route can ``reject(requeue=False)`` but its queue has no dead-letter
167
+ exchange — RabbitMQ would silently discard rejected messages.
168
+
169
+ Subclasses :class:`ConfigurationError` so existing catch-alls for
170
+ registration/startup misconfiguration keep working.
171
+ """
172
+
173
+
174
+ # ── Publish error (opt-in) ───────────────────────────────────────────────
175
+
176
+
177
+ class PublishError(Exception):
178
+ """Raised by ``PublishOutcome.raise_for_status()`` on a failed publish.
179
+
180
+ ``broker.publish()`` never raises on its own — it returns a
181
+ ``PublishOutcome`` so callers can decide how to handle NACKED / TIMEOUT /
182
+ RETURNED / ERROR. Code that prefers exceptions can opt in with
183
+ ``broker.publish(...).raise_for_status()``. Carries the ``outcome`` for
184
+ inspection (status, routing_key, underlying error).
185
+ """
186
+
187
+ def __init__(self, outcome: object) -> None:
188
+ self.outcome = outcome
189
+ super().__init__(str(outcome))
190
+
191
+
192
+ # ── Backpressure error ───────────────────────────────────────────────────
193
+
194
+
195
+ class BackpressureError(Exception):
196
+ """Raised when publish-side flow control blocks a publish attempt.
197
+
198
+ Only raised when ``BackpressureConfig.on_blocked == "raise"``.
199
+ """