anydi 0.58.0__tar.gz → 0.60.0__tar.gz

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 (26) hide show
  1. {anydi-0.58.0 → anydi-0.60.0}/PKG-INFO +2 -1
  2. {anydi-0.58.0 → anydi-0.60.0}/README.md +1 -0
  3. {anydi-0.58.0 → anydi-0.60.0}/anydi/_container.py +26 -0
  4. anydi-0.60.0/anydi/ext/faststream.py +74 -0
  5. anydi-0.60.0/anydi/ext/typer.py +133 -0
  6. {anydi-0.58.0 → anydi-0.60.0}/pyproject.toml +3 -2
  7. anydi-0.58.0/anydi/ext/faststream.py +0 -52
  8. {anydi-0.58.0 → anydi-0.60.0}/anydi/__init__.py +0 -0
  9. {anydi-0.58.0 → anydi-0.60.0}/anydi/_async_lock.py +0 -0
  10. {anydi-0.58.0 → anydi-0.60.0}/anydi/_context.py +0 -0
  11. {anydi-0.58.0 → anydi-0.60.0}/anydi/_decorators.py +0 -0
  12. {anydi-0.58.0 → anydi-0.60.0}/anydi/_injector.py +0 -0
  13. {anydi-0.58.0 → anydi-0.60.0}/anydi/_module.py +0 -0
  14. {anydi-0.58.0 → anydi-0.60.0}/anydi/_provider.py +0 -0
  15. {anydi-0.58.0 → anydi-0.60.0}/anydi/_resolver.py +0 -0
  16. {anydi-0.58.0 → anydi-0.60.0}/anydi/_scanner.py +0 -0
  17. {anydi-0.58.0 → anydi-0.60.0}/anydi/_types.py +0 -0
  18. {anydi-0.58.0 → anydi-0.60.0}/anydi/ext/__init__.py +0 -0
  19. {anydi-0.58.0 → anydi-0.60.0}/anydi/ext/django/__init__.py +0 -0
  20. {anydi-0.58.0 → anydi-0.60.0}/anydi/ext/fastapi.py +0 -0
  21. {anydi-0.58.0 → anydi-0.60.0}/anydi/ext/pydantic_settings.py +0 -0
  22. {anydi-0.58.0 → anydi-0.60.0}/anydi/ext/pytest_plugin.py +0 -0
  23. {anydi-0.58.0 → anydi-0.60.0}/anydi/ext/starlette/__init__.py +0 -0
  24. {anydi-0.58.0 → anydi-0.60.0}/anydi/ext/starlette/middleware.py +0 -0
  25. {anydi-0.58.0 → anydi-0.60.0}/anydi/py.typed +0 -0
  26. {anydi-0.58.0 → anydi-0.60.0}/anydi/testing.py +0 -0
@@ -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:**
@@ -245,6 +245,7 @@ Ready to learn more? Check out these resources:
245
245
  - [FastAPI](https://anydi.readthedocs.io/en/latest/extensions/fastapi/) - Build modern APIs with automatic dependency injection
246
246
  - [Django](https://anydi.readthedocs.io/en/latest/extensions/django/) - Integrate with Django and Django Ninja
247
247
  - [FastStream](https://anydi.readthedocs.io/en/latest/extensions/faststream/) - Message broker applications
248
+ - [Typer](https://anydi.readthedocs.io/en/latest/extensions/typer/) - CLI applications with async support
248
249
  - [Pydantic Settings](https://anydi.readthedocs.io/en/latest/extensions/pydantic_settings/) - Configuration management
249
250
 
250
251
  **Full Documentation:**
@@ -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(
@@ -0,0 +1,74 @@
1
+ """AnyDI FastStream extension."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from functools import cached_property
7
+ from typing import TYPE_CHECKING, Any, cast
8
+
9
+ from fast_depends.dependencies import Dependant
10
+ from faststream import BaseMiddleware, ContextRepo, StreamMessage
11
+
12
+ from anydi import Container
13
+ from anydi._types import Inject, ProvideMarker, set_provide_factory
14
+
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
+ ]
27
+
28
+
29
+ def get_container(broker: BrokerUsecase[Any, Any]) -> Container:
30
+ """Get the AnyDI container from a FastStream broker."""
31
+ return cast(Container, getattr(broker, "_container")) # noqa
32
+
33
+
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):
51
+ def __init__(self) -> None:
52
+ super().__init__(self._dependency, use_cache=True, cast=True, cast_result=True)
53
+ ProvideMarker.__init__(self)
54
+
55
+ async def _dependency(self, context: ContextRepo) -> Any:
56
+ container = get_container_from_context(context)
57
+ return await container.aresolve(self.interface)
58
+
59
+
60
+ # Configure Inject() and Provide[T] to use FastStream-specific marker
61
+ set_provide_factory(_ProvideMarker)
62
+
63
+
64
+ def _get_broker_handlers(broker: BrokerUsecase[Any, Any]) -> list[Any]:
65
+ return [subscriber.calls[0].handler for subscriber in broker.subscribers]
66
+
67
+
68
+ def install(broker: BrokerUsecase[Any, Any], container: Container) -> None:
69
+ """Install AnyDI into a FastStream broker."""
70
+ broker._container = container # type: ignore
71
+ for handler in _get_broker_handlers(broker):
72
+ call = handler._original_call # noqa
73
+ for parameter in inspect.signature(call, eval_str=True).parameters.values():
74
+ container.validate_injected_parameter(parameter, call=call)
@@ -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
  [project]
2
2
  name = "anydi"
3
- version = "0.58.0"
3
+ version = "0.60.0"
4
4
  description = "Dependency Injection library"
5
5
  authors = [{ name = "Anton Ruhlov", email = "antonruhlov@gmail.com" }]
6
6
  requires-python = ">=3.10.0, <3.15"
@@ -58,9 +58,10 @@ dev = [
58
58
  "starlette>=0.37.2",
59
59
  "fastapi>=0.100.0",
60
60
  "httpx>=0.26.0",
61
- "faststream>=0.5.10,<0.6",
61
+ "faststream>=0.6,<0.7",
62
62
  "redis>=5.0.4,<6",
63
63
  "pydantic-settings>=2.4.0,<3",
64
+ "typer>=0.20.0",
64
65
  ]
65
66
  docs = [
66
67
  "mkdocs>=1.4.2,<2",
@@ -1,52 +0,0 @@
1
- """AnyDI FastStream extension."""
2
-
3
- from __future__ import annotations
4
-
5
- import inspect
6
- from typing import Any, cast
7
-
8
- from fast_depends.dependencies import Depends
9
- from faststream import ContextRepo
10
- from faststream.broker.core.usecase import BrokerUsecase
11
-
12
- from anydi import Container
13
- from anydi._types import Inject, ProvideMarker, set_provide_factory
14
-
15
- __all__ = ["install", "get_container", "Inject"]
16
-
17
-
18
- def get_container(broker: BrokerUsecase[Any, Any]) -> Container:
19
- """Get the AnyDI container from a FastStream broker."""
20
- return cast(Container, getattr(broker, "_container")) # noqa
21
-
22
-
23
- class _ProvideMarker(Depends, ProvideMarker):
24
- def __init__(self) -> None:
25
- super().__init__(dependency=self._dependency, use_cache=True, cast=True)
26
- ProvideMarker.__init__(self)
27
-
28
- async def _dependency(self, context: ContextRepo) -> Any:
29
- container = get_container(context.get("broker"))
30
- return await container.aresolve(self.interface)
31
-
32
-
33
- # Configure Inject() and Provide[T] to use FastStream-specific marker
34
- set_provide_factory(_ProvideMarker)
35
-
36
-
37
- 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
- ]
44
-
45
-
46
- def install(broker: BrokerUsecase[Any, Any], container: Container) -> None:
47
- """Install AnyDI into a FastStream broker."""
48
- broker._container = container # type: ignore
49
- for handler in _get_broker_handlers(broker):
50
- call = handler._original_call # noqa
51
- for parameter in inspect.signature(call, eval_str=True).parameters.values():
52
- container.validate_injected_parameter(parameter, call=call)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes