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,503 @@
1
+ """DI Resolver — resolves handler parameters at processing time.
2
+
3
+ See Contract 4 (Parameter Resolution Precedence) for the full rules.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import inspect
9
+ import logging
10
+ import re
11
+ import typing
12
+ from collections.abc import AsyncGenerator, Callable, Generator
13
+ from typing import Annotated, Any, get_args, get_origin
14
+
15
+ from rabbitkit.core.errors import ConfigurationError, MissingDependencyError
16
+ from rabbitkit.core.message import RabbitMessage, is_rabbit_message_annotation
17
+ from rabbitkit.di.context import Context, ContextRepo, Header, Path
18
+ from rabbitkit.di.depends import Depends
19
+
20
+ _logger = logging.getLogger(__name__)
21
+
22
+ # L11: textual match on a DI marker CALL, used only once real hint resolution
23
+ # (get_type_hints_with_fallback's three attempts) has already failed and we
24
+ # are left with a raw, unresolved string annotation (PEP 563). See
25
+ # get_type_hints_with_fallback and DIResolver.validate_handler.
26
+ _DI_MARKER_CALL_RE = re.compile(r"\b(?:Depends|Header|Path|Context)\(")
27
+
28
+
29
+ def get_type_hints_with_fallback(handler: Callable[..., Any]) -> dict[str, Any]:
30
+ """Resolve ``handler``'s type hints, tolerating ``from __future__ import
31
+ annotations`` forward references that plain ``typing.get_type_hints()``
32
+ can't reach on its own.
33
+
34
+ When annotations are strings (PEP 563), they need evaluating. For
35
+ locally-defined handlers (e.g. built by a factory function), the
36
+ annotated type may live in the enclosing closure rather than the
37
+ handler's module globals -- plain ``get_type_hints()`` can't see it.
38
+
39
+ Strategy, each tried in order, first success wins:
40
+ 1. ``typing.get_type_hints()`` — the common case.
41
+ 2. ``typing.get_type_hints()`` with the handler's closure variables
42
+ supplied as ``localns`` — resolves closure-scoped forward refs.
43
+ 3. ``inspect.get_annotations(eval_str=True)`` — a different evaluation
44
+ path that occasionally succeeds where (1)/(2) don't.
45
+ 4. Final fallback: the raw ``inspect.signature()`` annotations, which
46
+ are still PEP 563 strings if every real attempt above failed (e.g. a
47
+ forward ref to a name that is genuinely unreachable, such as one only
48
+ imported under ``if TYPE_CHECKING:``).
49
+
50
+ This single implementation backs BOTH :class:`DIResolver` (the actual
51
+ per-message resolver) and the pipeline's own DI-need detector
52
+ (``HandlerPipeline._handler_needs_di``) — the two must use identical
53
+ resolution strength. They used to diverge (the detector had a weaker,
54
+ 2-attempt version without the closure-``localns`` retry), which meant a
55
+ closure-scoped ``Depends(...)`` annotation could resolve fine here but
56
+ still be mis-detected as "no DI needed" by the weaker detector, silently
57
+ binding the marked parameter to the message body instead (L11).
58
+ """
59
+ try:
60
+ return typing.get_type_hints(handler, include_extras=True)
61
+ except Exception:
62
+ pass
63
+
64
+ localns: dict[str, Any] = {}
65
+ if hasattr(handler, "__code__") and hasattr(handler, "__closure__"):
66
+ code = handler.__code__
67
+ closure = handler.__closure__
68
+ if closure is not None:
69
+ for name, cell in zip(code.co_freevars, closure, strict=False):
70
+ try:
71
+ localns[name] = cell.cell_contents
72
+ except ValueError: # pragma: no cover
73
+ pass # pragma: no cover
74
+ try:
75
+ return typing.get_type_hints(handler, localns=localns, include_extras=True)
76
+ except Exception:
77
+ pass
78
+
79
+ try:
80
+ return inspect.get_annotations(handler, eval_str=True)
81
+ except Exception:
82
+ pass
83
+
84
+ # Final fallback: raw (possibly still-a-string) annotations from signature.
85
+ sig = inspect.signature(handler)
86
+ return {
87
+ name: param.annotation
88
+ for name, param in sig.parameters.items()
89
+ if param.annotation is not inspect.Parameter.empty
90
+ }
91
+
92
+
93
+ def _looks_like_unresolved_di_marker(ann: Any) -> bool:
94
+ """L11: best-effort detection of a DI marker in an annotation that
95
+ :func:`get_type_hints_with_fallback` could not resolve to a real type
96
+ (still a raw PEP 563 string after all three real resolution attempts).
97
+
98
+ A plain string has no ``__metadata__``, so the structural
99
+ ``Annotated[...]`` check used everywhere else can't see a marker here —
100
+ this falls back to a textual match on the marker call syntax
101
+ (``Depends(``/``Header(``/``Path(``/``Context(``). That's imperfect (an
102
+ aliased import, e.g. ``from ... import Depends as D``, slips through),
103
+ but a silent, wrong body-binding of a DI-marked parameter is worse than
104
+ an occasional false positive turned into a clear registration-time error.
105
+ """
106
+ return isinstance(ann, str) and bool(_DI_MARKER_CALL_RE.search(ann))
107
+
108
+
109
+ def _is_rabbit_message(ann: Any) -> bool:
110
+ """True if ``ann`` is/mentions :class:`RabbitMessage`.
111
+
112
+ Handles both the resolved class and the string form (``"RabbitMessage"``)
113
+ produced by ``from __future__ import annotations`` when the hint can't be
114
+ resolved by ``typing.get_type_hints``. Recognizing the string form prevents
115
+ valid ``(body: bytes, msg: RabbitMessage)`` handlers from being mis-classified
116
+ as having two body-like parameters.
117
+ """
118
+ return is_rabbit_message_annotation(ann)
119
+
120
+
121
+ class DependencyScope:
122
+ """Tracks generator dependencies for cleanup after handler completes."""
123
+
124
+ def __init__(self) -> None:
125
+ self._sync_generators: list[Generator[Any, None, None]] = []
126
+ self._async_generators: list[AsyncGenerator[Any, None]] = []
127
+
128
+ def add_sync_generator(self, gen: Generator[Any, None, None]) -> None:
129
+ self._sync_generators.append(gen)
130
+
131
+ def add_async_generator(self, gen: AsyncGenerator[Any, None]) -> None:
132
+ self._async_generators.append(gen)
133
+
134
+ def cleanup(self) -> None:
135
+ """Close all sync generators (in reverse order).
136
+
137
+ Each generator's teardown is isolated: a raising teardown is logged
138
+ and skipped so one misbehaving generator does not leak the rest or
139
+ prevent ``clear()`` from running.
140
+ """
141
+ for gen in reversed(self._sync_generators):
142
+ try:
143
+ next(gen, None)
144
+ except StopIteration: # pragma: no cover
145
+ pass # pragma: no cover
146
+ except Exception:
147
+ _logger.warning("sync generator teardown raised", exc_info=True)
148
+ finally:
149
+ try:
150
+ gen.close()
151
+ except Exception:
152
+ _logger.warning("sync generator close() raised", exc_info=True)
153
+ self._sync_generators.clear()
154
+
155
+ async def cleanup_async(self) -> None:
156
+ """Close all generators (async generators + sync generators, in reverse order).
157
+
158
+ Each generator's teardown is isolated: a raising async teardown is
159
+ logged and skipped, and the sync-generator pass always runs even if an
160
+ async generator raised. ``clear()`` always runs.
161
+ """
162
+ for gen in reversed(self._async_generators):
163
+ try:
164
+ await gen.__anext__()
165
+ except StopAsyncIteration:
166
+ pass
167
+ except Exception:
168
+ _logger.warning("async generator teardown raised", exc_info=True)
169
+ finally:
170
+ try:
171
+ await gen.aclose()
172
+ except Exception:
173
+ _logger.warning("async generator aclose() raised", exc_info=True)
174
+ self._async_generators.clear()
175
+
176
+ for sync_gen in reversed(self._sync_generators):
177
+ try:
178
+ next(sync_gen, None)
179
+ except StopIteration: # pragma: no cover
180
+ pass # pragma: no cover
181
+ except Exception:
182
+ _logger.warning("sync generator teardown raised", exc_info=True)
183
+ finally:
184
+ try:
185
+ sync_gen.close()
186
+ except Exception:
187
+ _logger.warning("sync generator close() raised", exc_info=True)
188
+ self._sync_generators.clear()
189
+
190
+
191
+ class DIResolver:
192
+ """Resolves handler parameters.
193
+
194
+ Resolution rules (Contract 4):
195
+ 1. Annotated with DI marker (Depends/Header/Path/Context) → resolve via marker
196
+ 2. Type is RabbitMessage → inject the message object
197
+ 3. Remaining unannotated parameters → ONE body-bound parameter allowed
198
+ 4. Multiple body-like parameters → ConfigurationError at registration time
199
+ 5. Parameters with defaults → use default if no other resolution applies
200
+
201
+ Constraint: At most one body-bound parameter per handler.
202
+ """
203
+
204
+ def __init__(self) -> None:
205
+ # Per-handler cache of (signature, type-hints). Reflection — especially
206
+ # typing.get_type_hints() — is expensive and STATIC per handler, so it is
207
+ # computed once and reused on the per-message hot path.
208
+ self._sig_hints_cache: dict[Any, tuple[inspect.Signature, dict[str, Any]]] = {}
209
+
210
+ def _sig_and_hints(self, handler: Callable[..., Any]) -> tuple[inspect.Signature, dict[str, Any]]:
211
+ cached = self._sig_hints_cache.get(handler)
212
+ if cached is None:
213
+ cached = (inspect.signature(handler), self._get_type_hints(handler))
214
+ self._sig_hints_cache[handler] = cached
215
+ return cached
216
+
217
+ def _get_type_hints(self, handler: Callable[..., Any]) -> dict[str, Any]:
218
+ """Get resolved type hints for handler. See :func:`get_type_hints_with_fallback`."""
219
+ return get_type_hints_with_fallback(handler)
220
+
221
+ def validate_handler(self, handler: Callable[..., Any]) -> None:
222
+ """Validate handler signature at registration time.
223
+
224
+ Raises ConfigurationError for:
225
+ - *args or **kwargs
226
+ - Multiple body-like parameters
227
+ - An annotation that looks like an unresolved DI marker (L11) — see
228
+ ``_looks_like_unresolved_di_marker``
229
+ """
230
+ sig, hints = self._sig_and_hints(handler)
231
+ body_params: list[str] = []
232
+
233
+ for param_name, param in sig.parameters.items():
234
+ # Reject *args and **kwargs
235
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
236
+ raise ConfigurationError(
237
+ f"Handler '{handler.__qualname__}': *args/**kwargs not supported. "
238
+ "Use explicit parameters with type annotations."
239
+ )
240
+
241
+ ann = hints.get(param_name, inspect.Parameter.empty)
242
+
243
+ # L11: get_type_hints_with_fallback() could not resolve this
244
+ # annotation to a real type (it's still a raw PEP 563 string),
245
+ # and it textually looks like a DI marker call. Silently
246
+ # continuing would bind this parameter to the message body
247
+ # instead of resolving it via the marker — fail fast instead.
248
+ if _looks_like_unresolved_di_marker(ann):
249
+ raise ConfigurationError(
250
+ f"Handler '{handler.__qualname__}': parameter '{param_name}' has an "
251
+ f"annotation ({ann!r}) that looks like it uses a DI marker "
252
+ "(Depends/Header/Path/Context) but rabbitkit could not resolve it to "
253
+ "a real type. This usually means the annotated type is not reachable "
254
+ "from the handler's module globals or enclosing closure -- e.g. it is "
255
+ "only imported under `if TYPE_CHECKING:`, or defined somewhere "
256
+ "typing.get_type_hints() can't see. Left unresolved, this parameter "
257
+ "would silently bind to the message body instead of the marker. Make "
258
+ "the annotated type resolvable (e.g. import it unconditionally) to fix."
259
+ )
260
+
261
+ # Check if it's a DI-annotated parameter
262
+ if self._has_di_marker(ann):
263
+ continue
264
+
265
+ # Check if it's RabbitMessage type (class or string form)
266
+ if _is_rabbit_message(ann):
267
+ continue
268
+
269
+ # Check if it has a default (non-body)
270
+ if param.default is not inspect.Parameter.empty:
271
+ continue
272
+
273
+ # Unannotated parameters are NOT body-like candidates: the fallback
274
+ # resolver binds the first unannotated param to the body and subsequent
275
+ # unannotated params to the message (a documented pattern, e.g.
276
+ # ``handle(body, msg)``). Only flag ANNOTATED params that look like body
277
+ # types (a clear intent signal that multiple body bindings are expected,
278
+ # which the resolver does not support) — e.g. ``handle(a: Order, b: Customer)``.
279
+ if ann is inspect.Parameter.empty:
280
+ continue
281
+
282
+ # This is an annotated body-like parameter
283
+ body_params.append(param_name)
284
+
285
+ if len(body_params) > 1:
286
+ raise ConfigurationError(
287
+ f"Handler '{handler.__qualname__}': multiple body-like parameters "
288
+ f"({', '.join(body_params)}). At most one body parameter allowed. "
289
+ "Annotate extra parameters with Depends(), Header(), Path(), or Context()."
290
+ )
291
+
292
+ def resolve(
293
+ self,
294
+ handler: Callable[..., Any],
295
+ message: RabbitMessage,
296
+ context_repo: ContextRepo | None,
297
+ body: Any,
298
+ scope: DependencyScope | None = None,
299
+ ) -> dict[str, Any]:
300
+ """Resolve all handler parameters at message processing time."""
301
+ sig, hints = self._sig_and_hints(handler)
302
+ kwargs: dict[str, Any] = {}
303
+ depends_cache: dict[int, Any] = {}
304
+ body_injected = False
305
+
306
+ for param_name, param in sig.parameters.items():
307
+ ann = hints.get(param_name, inspect.Parameter.empty)
308
+
309
+ # Rule 1: DI-annotated parameters
310
+ marker = self._extract_di_marker(ann)
311
+ if marker is not None:
312
+ if isinstance(marker, Depends):
313
+ kwargs[param_name] = self._resolve_depends(marker, depends_cache, scope)
314
+ else:
315
+ kwargs[param_name] = self._resolve_marker_with_fallback(
316
+ marker, param, param_name, message, context_repo
317
+ )
318
+ continue
319
+
320
+ # Rule 2: RabbitMessage type (class or string form)
321
+ if _is_rabbit_message(ann):
322
+ kwargs[param_name] = message
323
+ continue
324
+
325
+ # Rule 3: Body-bound parameter (first one)
326
+ if not body_injected and param.default is inspect.Parameter.empty:
327
+ kwargs[param_name] = body
328
+ body_injected = True
329
+ continue
330
+
331
+ # Rule 5: Parameters with defaults — omit (use default)
332
+
333
+ return kwargs
334
+
335
+ async def resolve_async(
336
+ self,
337
+ handler: Callable[..., Any],
338
+ message: RabbitMessage,
339
+ context_repo: ContextRepo | None,
340
+ body: Any,
341
+ scope: DependencyScope | None = None,
342
+ ) -> dict[str, Any]:
343
+ """Resolve all handler parameters, supporting async generator dependencies."""
344
+ sig, hints = self._sig_and_hints(handler)
345
+ kwargs: dict[str, Any] = {}
346
+ depends_cache: dict[int, Any] = {}
347
+ body_injected = False
348
+
349
+ for param_name, param in sig.parameters.items():
350
+ ann = hints.get(param_name, inspect.Parameter.empty)
351
+
352
+ # Rule 1: DI-annotated parameters
353
+ marker = self._extract_di_marker(ann)
354
+ if marker is not None:
355
+ if isinstance(marker, Depends):
356
+ kwargs[param_name] = await self._resolve_depends_async(marker, depends_cache, scope)
357
+ else:
358
+ kwargs[param_name] = self._resolve_marker_with_fallback(
359
+ marker, param, param_name, message, context_repo
360
+ )
361
+ continue
362
+
363
+ # Rule 2: RabbitMessage type (class or string form)
364
+ if _is_rabbit_message(ann):
365
+ kwargs[param_name] = message
366
+ continue
367
+
368
+ # Rule 3: Body-bound parameter (first one)
369
+ if not body_injected and param.default is inspect.Parameter.empty:
370
+ kwargs[param_name] = body
371
+ body_injected = True
372
+ continue
373
+
374
+ # Rule 5: Parameters with defaults — omit (use default)
375
+
376
+ return kwargs
377
+
378
+ # ── Internal helpers ─────────────────────────────────────────────────
379
+
380
+ def _has_di_marker(self, ann: Any) -> bool:
381
+ """Check if annotation has a DI marker."""
382
+ return self._extract_di_marker(ann) is not None
383
+
384
+ def _extract_di_marker(self, ann: Any) -> Depends | Header | Path | Context | None:
385
+ """Extract DI marker from annotation (supports Annotated)."""
386
+ if ann is inspect.Parameter.empty:
387
+ return None
388
+
389
+ # Check Annotated[Type, Marker]
390
+ if get_origin(ann) is Annotated:
391
+ args = get_args(ann)
392
+ for arg in args[1:]:
393
+ if isinstance(arg, (Depends, Header, Path, Context)):
394
+ return arg
395
+ return None
396
+
397
+ def _resolve_marker_with_fallback(
398
+ self,
399
+ marker: Header | Path | Context,
400
+ param: inspect.Parameter,
401
+ param_name: str,
402
+ message: RabbitMessage,
403
+ context_repo: ContextRepo | None,
404
+ ) -> Any:
405
+ """Resolve a Header()/Path()/Context() marker (H10).
406
+
407
+ Fallback order when the value is absent from the message:
408
+ 1. The marker's own ``default=`` (checked first, inside
409
+ ``_resolve_header``/``_resolve_path``/``_resolve_context``).
410
+ 2. The handler parameter's own Python default (checked here).
411
+ 3. Neither present → ``MissingDependencyError`` (PERMANENT).
412
+
413
+ ``Depends()`` markers never reach here — both ``resolve()`` and
414
+ ``resolve_async()`` special-case them before calling this.
415
+ """
416
+ try:
417
+ if isinstance(marker, Header):
418
+ return self._resolve_header(marker, message, param_name)
419
+ if isinstance(marker, Path):
420
+ return self._resolve_path(marker, message, param_name)
421
+ if isinstance(marker, Context):
422
+ return self._resolve_context(marker, context_repo, param_name)
423
+ raise ConfigurationError(f"Unknown DI marker: {marker!r}") # pragma: no cover - defensive
424
+ except MissingDependencyError:
425
+ if param.default is not inspect.Parameter.empty:
426
+ return param.default
427
+ raise
428
+
429
+ def _resolve_depends(self, marker: Depends, cache: dict[int, Any], scope: DependencyScope | None = None) -> Any:
430
+ """Resolve a Depends() marker, with support for sync generator dependencies."""
431
+ dep_id = id(marker.dependency)
432
+ if marker.use_cache and dep_id in cache:
433
+ return cache[dep_id]
434
+
435
+ if inspect.isgeneratorfunction(marker.dependency):
436
+ gen = marker.dependency()
437
+ result = next(gen)
438
+ if scope is not None:
439
+ scope.add_sync_generator(gen)
440
+ elif inspect.isasyncgenfunction(marker.dependency):
441
+ raise ConfigurationError(
442
+ f"Async generator dependency '{marker.dependency.__qualname__}' "
443
+ "requires async pipeline. Use resolve_async() or ensure handler is async."
444
+ )
445
+ else:
446
+ result = marker.dependency()
447
+
448
+ if marker.use_cache:
449
+ cache[dep_id] = result
450
+ return result
451
+
452
+ async def _resolve_depends_async(
453
+ self, marker: Depends, cache: dict[int, Any], scope: DependencyScope | None = None
454
+ ) -> Any:
455
+ """Resolve a Depends() marker (async), with support for async/sync generator dependencies."""
456
+ dep_id = id(marker.dependency)
457
+ if marker.use_cache and dep_id in cache:
458
+ return cache[dep_id]
459
+
460
+ if inspect.isasyncgenfunction(marker.dependency):
461
+ gen = marker.dependency()
462
+ result = await gen.__anext__()
463
+ if scope is not None:
464
+ scope.add_async_generator(gen)
465
+ elif inspect.isgeneratorfunction(marker.dependency):
466
+ gen = marker.dependency()
467
+ result = next(gen)
468
+ if scope is not None:
469
+ scope.add_sync_generator(gen)
470
+ else:
471
+ result = marker.dependency()
472
+ if hasattr(result, "__await__"):
473
+ result = await result
474
+
475
+ if marker.use_cache:
476
+ cache[dep_id] = result
477
+ return result
478
+
479
+ def _resolve_header(self, marker: Header, message: RabbitMessage, param_name: str) -> Any:
480
+ """Resolve a Header() marker (H10: marker default checked first)."""
481
+ if marker.name in message.headers:
482
+ return message.headers[marker.name]
483
+ if marker.has_default:
484
+ return marker.default
485
+ raise MissingDependencyError(repr(marker), param_name)
486
+
487
+ def _resolve_path(self, marker: Path, message: RabbitMessage, param_name: str) -> Any:
488
+ """Resolve a Path() marker (H10: marker default checked first)."""
489
+ if marker.segment in message.path:
490
+ return message.path[marker.segment]
491
+ if marker.has_default:
492
+ return marker.default
493
+ raise MissingDependencyError(repr(marker), param_name)
494
+
495
+ def _resolve_context(self, marker: Context, context_repo: ContextRepo | None, param_name: str) -> Any:
496
+ """Resolve a Context() marker (H10: marker default checked first)."""
497
+ if context_repo is None:
498
+ raise ConfigurationError("ContextRepo not available for Context() resolution")
499
+ if context_repo.has(marker.key):
500
+ return context_repo.get(marker.key)
501
+ if marker.has_default:
502
+ return marker.default
503
+ raise MissingDependencyError(repr(marker), param_name)