anydi 0.58.0__py3-none-any.whl → 0.60.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.
anydi/_container.py CHANGED
@@ -257,6 +257,32 @@ class Container:
257
257
  # Register the scope
258
258
  self._scopes[scope] = tuple({scope, "singleton"} | set(parents))
259
259
 
260
+ def get_ordered_scopes(self, scopes: set[Scope]) -> list[str]:
261
+ """Get ordered list of scopes to enter."""
262
+ # Expand scopes to include all parent scopes
263
+ expanded_scopes: set[str] = set()
264
+ for scope in scopes:
265
+ if scope == "transient":
266
+ continue
267
+ elif scope == "singleton":
268
+ expanded_scopes.add("singleton")
269
+ else:
270
+ # Add the scope and all its parents from container._scopes
271
+ expanded_scopes.update(self._scopes[scope])
272
+
273
+ # Separate singleton from other scopes
274
+ has_singleton = "singleton" in expanded_scopes
275
+ other_scopes = expanded_scopes - {"singleton"}
276
+
277
+ # Sort other scopes by dependency depth (parents before children)
278
+ # Scopes with fewer parents come first
279
+ ordered_scopes = sorted(
280
+ other_scopes, key=lambda scope: len(self._scopes[scope])
281
+ )
282
+
283
+ # Return with singleton first if needed
284
+ return ["singleton", *ordered_scopes] if has_singleton else ordered_scopes
285
+
260
286
  # == Provider Registry ==
261
287
 
262
288
  def register(
anydi/ext/faststream.py CHANGED
@@ -3,16 +3,27 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import inspect
6
- from typing import Any, cast
6
+ from functools import cached_property
7
+ from typing import TYPE_CHECKING, Any, cast
7
8
 
8
- from fast_depends.dependencies import Depends
9
- from faststream import ContextRepo
10
- from faststream.broker.core.usecase import BrokerUsecase
9
+ from fast_depends.dependencies import Dependant
10
+ from faststream import BaseMiddleware, ContextRepo, StreamMessage
11
11
 
12
12
  from anydi import Container
13
13
  from anydi._types import Inject, ProvideMarker, set_provide_factory
14
14
 
15
- __all__ = ["install", "get_container", "Inject"]
15
+ if TYPE_CHECKING:
16
+ from faststream._internal.basic_types import AsyncFuncAny
17
+ from faststream._internal.broker import BrokerUsecase
18
+ from faststream._internal.types import AnyMsg
19
+
20
+ __all__ = [
21
+ "install",
22
+ "get_container",
23
+ "get_container_from_context",
24
+ "Inject",
25
+ "RequestScopedMiddleware",
26
+ ]
16
27
 
17
28
 
18
29
  def get_container(broker: BrokerUsecase[Any, Any]) -> Container:
@@ -20,13 +31,29 @@ def get_container(broker: BrokerUsecase[Any, Any]) -> Container:
20
31
  return cast(Container, getattr(broker, "_container")) # noqa
21
32
 
22
33
 
23
- class _ProvideMarker(Depends, ProvideMarker):
34
+ def get_container_from_context(context: ContextRepo) -> Container:
35
+ return get_container(context.broker)
36
+
37
+
38
+ class RequestScopedMiddleware(BaseMiddleware):
39
+ @cached_property
40
+ def container(self) -> Container:
41
+ return get_container_from_context(self.context)
42
+
43
+ async def consume_scope(
44
+ self, call_next: AsyncFuncAny, msg: StreamMessage[AnyMsg]
45
+ ) -> Any:
46
+ async with self.container.arequest_context():
47
+ return await call_next(msg)
48
+
49
+
50
+ class _ProvideMarker(Dependant, ProvideMarker):
24
51
  def __init__(self) -> None:
25
- super().__init__(dependency=self._dependency, use_cache=True, cast=True)
52
+ super().__init__(self._dependency, use_cache=True, cast=True, cast_result=True)
26
53
  ProvideMarker.__init__(self)
27
54
 
28
55
  async def _dependency(self, context: ContextRepo) -> Any:
29
- container = get_container(context.get("broker"))
56
+ container = get_container_from_context(context)
30
57
  return await container.aresolve(self.interface)
31
58
 
32
59
 
@@ -35,12 +62,7 @@ set_provide_factory(_ProvideMarker)
35
62
 
36
63
 
37
64
  def _get_broker_handlers(broker: BrokerUsecase[Any, Any]) -> list[Any]:
38
- if (handlers := getattr(broker, "handlers", None)) is not None:
39
- return [handler.calls[0][0] for handler in handlers.values()]
40
- return [
41
- subscriber.calls[0].handler
42
- for subscriber in broker._subscribers.values() # noqa
43
- ]
65
+ return [subscriber.calls[0].handler for subscriber in broker.subscribers]
44
66
 
45
67
 
46
68
  def install(broker: BrokerUsecase[Any, Any], container: Container) -> None:
anydi/ext/typer.py ADDED
@@ -0,0 +1,133 @@
1
+ """AnyDI Typer extension."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import functools
7
+ import inspect
8
+ from collections.abc import Awaitable, Callable
9
+ from typing import Any
10
+
11
+ import anyio
12
+ from typer import Typer
13
+
14
+ from anydi import Container, Scope
15
+
16
+ __all__ = ["install"]
17
+
18
+
19
+ def _wrap_async_callback_no_injection(callback: Callable[..., Any]) -> Any:
20
+ """Wrap async callback without injection in anyio.run()."""
21
+
22
+ @functools.wraps(callback)
23
+ def async_no_injection_wrapper(*args: Any, **kwargs: Any) -> Any:
24
+ return anyio.run(callback, *args, **kwargs)
25
+
26
+ return async_no_injection_wrapper
27
+
28
+
29
+ def _wrap_async_callback_with_injection(
30
+ callback: Callable[..., Awaitable[Any]],
31
+ container: Container,
32
+ sig: inspect.Signature,
33
+ non_injected_params: set[inspect.Parameter],
34
+ scopes: set[Scope],
35
+ ) -> Any:
36
+ """Wrap async callback with injection in anyio.run()."""
37
+
38
+ @functools.wraps(callback)
39
+ def async_wrapper(*args: Any, **kwargs: Any) -> Any:
40
+ async def _run() -> Any:
41
+ ordered_scopes = container.get_ordered_scopes(scopes)
42
+
43
+ async with contextlib.AsyncExitStack() as stack:
44
+ # Start scoped contexts in dependency order
45
+ for scope in ordered_scopes:
46
+ if scope == "singleton":
47
+ await stack.enter_async_context(container)
48
+ else:
49
+ await stack.enter_async_context(
50
+ container.ascoped_context(scope)
51
+ )
52
+
53
+ return await container.run(callback, *args, **kwargs)
54
+
55
+ return anyio.run(_run)
56
+
57
+ # Update the wrapper's signature to only show non-injected parameters to Typer
58
+ async_wrapper.__signature__ = sig.replace(parameters=non_injected_params) # type: ignore
59
+
60
+ return async_wrapper
61
+
62
+
63
+ def _process_callback(callback: Callable[..., Any], container: Container) -> Any:
64
+ """Validate and wrap a callback for dependency injection."""
65
+ sig = inspect.signature(callback, eval_str=True)
66
+ injected_param_names: set[str] = set()
67
+ non_injected_params: set[inspect.Parameter] = set()
68
+ scopes: set[Scope] = set()
69
+
70
+ # Validate parameters and collect which ones need injection
71
+ for parameter in sig.parameters.values():
72
+ interface, should_inject = container.validate_injected_parameter(
73
+ parameter, call=callback
74
+ )
75
+ if should_inject:
76
+ injected_param_names.add(parameter.name)
77
+ scopes.add(container.providers[interface].scope)
78
+ else:
79
+ non_injected_params.add(parameter)
80
+
81
+ # If no parameters need injection and callback is not async, return original
82
+ if not injected_param_names and not inspect.iscoroutinefunction(callback):
83
+ return callback
84
+
85
+ # If async callback with no injection, just wrap in anyio.run()
86
+ if not injected_param_names and inspect.iscoroutinefunction(callback):
87
+ return _wrap_async_callback_no_injection(callback)
88
+
89
+ # Handle async callbacks - wrap them in anyio.run() for Typer
90
+ if inspect.iscoroutinefunction(callback):
91
+ return _wrap_async_callback_with_injection(
92
+ callback, container, sig, non_injected_params, scopes
93
+ )
94
+
95
+ @functools.wraps(callback)
96
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
97
+ ordered_scopes = container.get_ordered_scopes(scopes)
98
+
99
+ with contextlib.ExitStack() as stack:
100
+ # Start scoped contexts in dependency order
101
+ for scope in ordered_scopes:
102
+ if scope == "singleton":
103
+ stack.enter_context(container)
104
+ else:
105
+ stack.enter_context(container.scoped_context(scope))
106
+
107
+ return container.run(callback, *args, **kwargs)
108
+
109
+ # Update the wrapper's signature to only show non-injected parameters to Typer
110
+ wrapper.__signature__ = sig.replace(parameters=non_injected_params) # type: ignore
111
+
112
+ return wrapper
113
+
114
+
115
+ def install(app: Typer, container: Container) -> None:
116
+ """Install AnyDI into a Typer application."""
117
+ # Process main callback if exists
118
+ if app.registered_callback:
119
+ callback = app.registered_callback.callback
120
+ if callback:
121
+ app.registered_callback.callback = _process_callback(callback, container)
122
+
123
+ # Process all registered commands
124
+ for command_info in app.registered_commands:
125
+ callback = command_info.callback
126
+ if callback:
127
+ command_info.callback = _process_callback(callback, container)
128
+
129
+ # Process nested Typer groups
130
+ for group_info in app.registered_groups:
131
+ # Recursively install for nested Typer apps
132
+ if group_info.typer_instance:
133
+ install(group_info.typer_instance, container)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anydi
3
- Version: 0.58.0
3
+ Version: 0.60.0
4
4
  Summary: Dependency Injection library
5
5
  Keywords: dependency injection,dependencies,di,async,asyncio,application
6
6
  Author: Anton Ruhlov
@@ -280,6 +280,7 @@ Ready to learn more? Check out these resources:
280
280
  - [FastAPI](https://anydi.readthedocs.io/en/latest/extensions/fastapi/) - Build modern APIs with automatic dependency injection
281
281
  - [Django](https://anydi.readthedocs.io/en/latest/extensions/django/) - Integrate with Django and Django Ninja
282
282
  - [FastStream](https://anydi.readthedocs.io/en/latest/extensions/faststream/) - Message broker applications
283
+ - [Typer](https://anydi.readthedocs.io/en/latest/extensions/typer/) - CLI applications with async support
283
284
  - [Pydantic Settings](https://anydi.readthedocs.io/en/latest/extensions/pydantic_settings/) - Configuration management
284
285
 
285
286
  **Full Documentation:**
@@ -1,6 +1,6 @@
1
1
  anydi/__init__.py,sha256=bQKzn9qfNnIMi1m3J-DdSknSDwNg8j08fdQg_-Edkto,613
2
2
  anydi/_async_lock.py,sha256=3dwZr0KthXFYha0XKMyXf8jMmGb1lYoNC0O5w29V9ic,1104
3
- anydi/_container.py,sha256=j8XM5UYw3PVwPq92TMtEwLwqcMObs-9ZTGAEsTj4caE,26023
3
+ anydi/_container.py,sha256=r7qcUCu4KO0YLPaUO5SEgUwnNOcZsP7aVDZ1oDTi0kA,27093
4
4
  anydi/_context.py,sha256=-9QqeMWo9OpZVXZxZCQgIsswggl3Ch7lgx1KiFX_ezc,3752
5
5
  anydi/_decorators.py,sha256=J3W261ZAG7q4XKm4tbAv1wsWr9ysx9_5MUbUvSJB_MQ,2809
6
6
  anydi/_injector.py,sha256=IxKTh2rzMHrsW554tbiJl33Hb5sRGKYY_NU1rC4UvxE,4378
@@ -12,14 +12,15 @@ anydi/_types.py,sha256=tLJS27j0lWJFd4fIIGlGbbfKBVTPTnWgPYqZlenktis,2939
12
12
  anydi/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  anydi/ext/django/__init__.py,sha256=Ve8lncLU9dPY_Vjt4zihPgsSxwAtFHACn0XvBM5JG8k,367
14
14
  anydi/ext/fastapi.py,sha256=TQoS1Vh9chV0Tj0qyxjgqoGTYEMC3jp0il9CDZCZLcc,2326
15
- anydi/ext/faststream.py,sha256=XT80r1FGL-xlU7r8urm9sNpUfl4OPMJseW4dade_fR4,1836
15
+ anydi/ext/faststream.py,sha256=i6uXE21UawmTYDBqRv7yqDMc-lRiYSYf45sD87O2wX4,2442
16
16
  anydi/ext/pydantic_settings.py,sha256=jVJZ1wPaPpsxdNPlJj9yq282ebqLZ9tckWpZ0eIwWLg,1533
17
17
  anydi/ext/pytest_plugin.py,sha256=M54DkA-KxD9GqLnXdoCyn-Qur2c44MB6d0AgJuYCZ5w,16171
18
18
  anydi/ext/starlette/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  anydi/ext/starlette/middleware.py,sha256=9CQtGg5ZzUz2gFSzJr8U4BWzwNjK8XMctm3n52M77Z0,792
20
+ anydi/ext/typer.py,sha256=3_3Q1fhxx7qElB6AQ_8dZUe5gIrrBHw7PbznAEzW5wA,4761
20
21
  anydi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
22
  anydi/testing.py,sha256=cHg3mMScZbEep9smRqSNQ81BZMQOkyugHe8TvKdPnEg,1347
22
- anydi-0.58.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
23
- anydi-0.58.0.dist-info/entry_points.txt,sha256=AgOcQYM5KyS4D37QcYb00tiid0QA-pD1VrjHHq4QAps,44
24
- anydi-0.58.0.dist-info/METADATA,sha256=KE1cG2fc8ZyuoUBMhGP0pUqcUkr2z8ZOGjPVy3-ILV0,7901
25
- anydi-0.58.0.dist-info/RECORD,,
23
+ anydi-0.60.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
24
+ anydi-0.60.0.dist-info/entry_points.txt,sha256=AgOcQYM5KyS4D37QcYb00tiid0QA-pD1VrjHHq4QAps,44
25
+ anydi-0.60.0.dist-info/METADATA,sha256=_KyXL81AfiP_HtBSOQmdVVxPaV7jqt_KPgWOLPmDK-I,8007
26
+ anydi-0.60.0.dist-info/RECORD,,
File without changes