fastapi-injected 0.1.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.
@@ -0,0 +1,13 @@
1
+ from .inject import inject
2
+ from .scope import push_inject_scope
3
+ from .solve import solve
4
+ from .types import Dep, DepFactory, Inejected
5
+
6
+ __all__ = [
7
+ "Dep",
8
+ "DepFactory",
9
+ "Inejected",
10
+ "inject",
11
+ "push_inject_scope",
12
+ "solve",
13
+ ]
@@ -0,0 +1,158 @@
1
+ import inspect
2
+ from collections.abc import Callable
3
+ from contextvars import ContextVar
4
+ from copy import copy
5
+ from functools import lru_cache, wraps
6
+ from typing import Annotated, Any, Literal, cast, overload
7
+
8
+ from fastapi import Depends
9
+ from fastapi.dependencies.models import Dependant
10
+ from fastapi.dependencies.utils import get_dependant, get_typed_signature
11
+ from fastapi.dependencies.utils import solve_dependencies as _solve_dependencies
12
+ from starlette.requests import Request
13
+ from starlette.types import Message, Scope
14
+
15
+ from .scope import InjectScope
16
+ from .sign import prepare_sign, update_func_sign
17
+ from .types import Coro, HasSignature
18
+
19
+
20
+ def _dummy_scope() -> Scope:
21
+ return {
22
+ "type": "http",
23
+ "http_version": "1.1",
24
+ "query_string": b"",
25
+ "headers": [],
26
+ }
27
+
28
+
29
+ def _dummy_request(
30
+ *,
31
+ extra_scope: Scope | None = None,
32
+ ) -> Request:
33
+ async def _dummy_receive() -> Message:
34
+ return {
35
+ "type": "http.request",
36
+ "body": b"",
37
+ }
38
+
39
+ async def _dummy_send(_: Message, /) -> None:
40
+ pass
41
+
42
+ scope = _dummy_scope()
43
+ if extra_scope:
44
+ scope.update(extra_scope)
45
+
46
+ return Request(
47
+ scope,
48
+ receive=_dummy_receive,
49
+ send=_dummy_send,
50
+ )
51
+
52
+
53
+ @lru_cache(maxsize=1024)
54
+ def create_dependant[**P, R](func: Callable[P, Coro[R]], /) -> Dependant:
55
+ @wraps(func)
56
+ async def __call(*args: P.args, **kwargs: P.kwargs) -> R:
57
+ return await func(*args, **kwargs)
58
+
59
+ update_func_sign(
60
+ __call,
61
+ prepare_sign(get_typed_signature(func)),
62
+ )
63
+
64
+ return get_dependant(
65
+ path="",
66
+ call=__call,
67
+ )
68
+
69
+
70
+ @lru_cache(maxsize=1024)
71
+ def create_single_dependant[**P, R](func: Callable[P, R], /) -> Dependant:
72
+ async def _factory(__value__: R) -> R:
73
+ return __value__
74
+
75
+ cast("HasSignature", _factory).__signature__ = inspect.Signature(
76
+ parameters=[
77
+ inspect.Parameter(
78
+ "__value__",
79
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
80
+ annotation=Annotated[Any, Depends(func)],
81
+ ),
82
+ ],
83
+ return_annotation=Any,
84
+ )
85
+
86
+ return get_dependant(
87
+ path="",
88
+ call=_factory,
89
+ )
90
+
91
+
92
+ @overload
93
+ async def solve_dependencies(
94
+ dependant: Dependant,
95
+ scope: InjectScope,
96
+ *,
97
+ single: Literal[False] = False,
98
+ ) -> dict[str, Any]: ...
99
+
100
+
101
+ @overload
102
+ async def solve_dependencies(
103
+ dependant: Dependant,
104
+ scope: InjectScope,
105
+ *,
106
+ single: Literal[True],
107
+ ) -> Any: ...
108
+
109
+
110
+ async def solve_dependencies(
111
+ dependant: Dependant,
112
+ scope: InjectScope,
113
+ *,
114
+ single: bool = False,
115
+ ) -> dict[str, Any]:
116
+ solved = await _solve_dependencies(
117
+ request=_dummy_request(
118
+ extra_scope={
119
+ "fastapi_inner_astack": scope.request_astack,
120
+ "fastapi_function_astack": scope.func_astack,
121
+ },
122
+ ),
123
+ dependant=dependant,
124
+ async_exit_stack=scope.request_astack,
125
+ dependency_cache=copy(scope.dependency_cache),
126
+ dependency_overrides_provider=_dependency_override_provider.get(),
127
+ embed_body_fields=False,
128
+ )
129
+
130
+ scope.dependency_cache.update(solved.dependency_cache)
131
+
132
+ if solved.errors:
133
+ raise ValueError(solved.errors)
134
+
135
+ if single:
136
+ try:
137
+ return solved.values["__value__"]
138
+ except KeyError:
139
+ raise ValueError("No single dependency found") from None
140
+
141
+ return solved.values
142
+
143
+
144
+ _dependency_override_provider: ContextVar[Any] = ContextVar(
145
+ "_dependency_override_provider",
146
+ default=None,
147
+ )
148
+
149
+
150
+ def set_inject_dependency_override_provider(provider: Any, /) -> None:
151
+ _dependency_override_provider.set(provider)
152
+
153
+
154
+ __all__ = [
155
+ "create_dependant",
156
+ "set_inject_dependency_override_provider",
157
+ "solve_dependencies",
158
+ ]
@@ -0,0 +1,55 @@
1
+ from functools import partial, wraps
2
+ from typing import overload
3
+
4
+ from .deps import create_dependant, solve_dependencies
5
+ from .scope import inside_inject_scope
6
+ from .sign import strip_sign
7
+ from .types import AsyncFunc, Decorator
8
+
9
+
10
+ @overload
11
+ def inject[**P, R](
12
+ func: AsyncFunc[P, R],
13
+ /,
14
+ ) -> AsyncFunc[P, R]:
15
+ pass
16
+
17
+
18
+ @overload
19
+ def inject[**P, R](
20
+ *,
21
+ new_scope: bool = False,
22
+ ) -> Decorator[P, R]:
23
+ pass
24
+
25
+
26
+ def inject[**P, R](
27
+ func: AsyncFunc[P, R] | None = None,
28
+ /,
29
+ *,
30
+ new_scope: bool = False,
31
+ ) -> AsyncFunc[P, R] | Decorator[P, R]:
32
+ if func is None:
33
+ return partial(inject, new_scope=new_scope) # type: ignore[ty:invalid-return-type]
34
+
35
+ dependant = create_dependant(func)
36
+
37
+ @wraps(func)
38
+ @strip_sign(dependant)
39
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
40
+ async with inside_inject_scope(
41
+ new_scope=new_scope,
42
+ ) as inject_scope:
43
+ solved = await solve_dependencies(dependant, inject_scope)
44
+
45
+ for key, value in solved.items():
46
+ kwargs.setdefault(key, value)
47
+
48
+ return await func(*args, **kwargs)
49
+
50
+ return wrapper
51
+
52
+
53
+ __all__ = [
54
+ "inject",
55
+ ]
@@ -0,0 +1,75 @@
1
+ from collections.abc import AsyncIterator
2
+ from contextlib import (
3
+ AbstractAsyncContextManager,
4
+ AsyncExitStack,
5
+ asynccontextmanager,
6
+ nullcontext,
7
+ )
8
+ from contextvars import ContextVar
9
+ from dataclasses import dataclass
10
+ from typing import Any
11
+
12
+ from .types import DependencyCache
13
+
14
+
15
+ @dataclass
16
+ class InjectScope:
17
+ dependency_cache: DependencyCache
18
+ func_astack: AsyncExitStack
19
+ request_astack: AsyncExitStack
20
+
21
+
22
+ _inject_scope: ContextVar[InjectScope | None] = ContextVar(
23
+ "_inject_scope",
24
+ default=None,
25
+ )
26
+
27
+
28
+ @asynccontextmanager
29
+ async def push_inject_scope(
30
+ dependency_cache: DependencyCache | None = None,
31
+ ) -> AsyncIterator[InjectScope]:
32
+ async with AsyncExitStack() as stack:
33
+ scope = InjectScope(
34
+ dependency_cache or {},
35
+ stack,
36
+ stack,
37
+ )
38
+ token = _inject_scope.set(scope)
39
+
40
+ try:
41
+ yield scope
42
+ finally:
43
+ _inject_scope.reset(token)
44
+
45
+
46
+ def current_inject_scope() -> InjectScope | None:
47
+ return _inject_scope.get()
48
+
49
+
50
+ @asynccontextmanager
51
+ async def inside_inject_scope(
52
+ *,
53
+ new_scope: bool = False,
54
+ ) -> AsyncIterator[InjectScope]:
55
+ scope = current_inject_scope()
56
+
57
+ _ctx: AbstractAsyncContextManager[Any]
58
+
59
+ if scope is None or new_scope:
60
+ stack = AsyncExitStack()
61
+ scope = await stack.enter_async_context(push_inject_scope())
62
+
63
+ _ctx = stack
64
+ else:
65
+ _ctx = nullcontext()
66
+
67
+ async with _ctx:
68
+ yield scope
69
+
70
+
71
+ __all__ = [
72
+ "InjectScope",
73
+ "current_inject_scope",
74
+ "inside_inject_scope",
75
+ ]
@@ -0,0 +1,61 @@
1
+ import inspect
2
+ from typing import cast
3
+
4
+ from fastapi.dependencies.models import Dependant
5
+ from fastapi.dependencies.utils import analyze_param
6
+
7
+ from .types import Decorator, Func, HasSignature, Inejected
8
+
9
+
10
+ def update_func_sign[**P, R](func: Func[P, R], sign: inspect.Signature) -> Func[P, R]:
11
+ cast("HasSignature", func).__signature__ = sign
12
+ return func
13
+
14
+
15
+ def prepare_sign(sign: inspect.Signature) -> inspect.Signature:
16
+ def _update_param(param: inspect.Parameter) -> inspect.Parameter:
17
+ if param.default is Inejected:
18
+ param = param.replace(default=inspect.Parameter.empty)
19
+
20
+ return param
21
+
22
+ def _is_depends(param: inspect.Parameter) -> bool:
23
+ result = analyze_param(
24
+ param_name=param.name,
25
+ annotation=param.annotation,
26
+ value=param.default,
27
+ is_path_param=False,
28
+ )
29
+
30
+ return result.depends is not None
31
+
32
+ return sign.replace(
33
+ parameters=[_update_param(param) for param in sign.parameters.values() if _is_depends(param)],
34
+ )
35
+
36
+
37
+ def strip_deps_from_sign(
38
+ sign: inspect.Signature,
39
+ dependent: Dependant,
40
+ ) -> inspect.Signature:
41
+ names = {param.name for param in dependent.dependencies}
42
+
43
+ return sign.replace(parameters=[param for param in sign.parameters.values() if param.name not in names])
44
+
45
+
46
+ def strip_sign[**P, R](dependant: Dependant, /) -> Decorator[P, R]:
47
+ def decorator(func: Func[P, R]) -> Func[P, R]:
48
+ return update_func_sign(
49
+ func,
50
+ strip_deps_from_sign(inspect.signature(func), dependant),
51
+ )
52
+
53
+ return decorator
54
+
55
+
56
+ __all__ = [
57
+ "prepare_sign",
58
+ "strip_deps_from_sign",
59
+ "strip_sign",
60
+ "update_func_sign",
61
+ ]
@@ -0,0 +1,26 @@
1
+ from typing_extensions import TypeForm
2
+
3
+ from .deps import create_single_dependant, solve_dependencies
4
+ from .scope import inside_inject_scope
5
+
6
+
7
+ async def solve[R](
8
+ tp: TypeForm[R],
9
+ *,
10
+ new_scope: bool = False,
11
+ ) -> R:
12
+ dependant = create_single_dependant(tp)
13
+
14
+ async with inside_inject_scope(
15
+ new_scope=new_scope,
16
+ ) as inject_scope:
17
+ return await solve_dependencies(
18
+ dependant,
19
+ inject_scope,
20
+ single=True,
21
+ )
22
+
23
+
24
+ __all__ = [
25
+ "solve",
26
+ ]
@@ -0,0 +1,62 @@
1
+ import inspect
2
+ from collections.abc import Callable, Coroutine
3
+ from typing import TYPE_CHECKING, Annotated, Any, Protocol, TypeVar
4
+
5
+ from fastapi import Depends
6
+ from fastapi.types import DependencyCacheKey
7
+ from typing_extensions import sentinel
8
+
9
+ if TYPE_CHECKING:
10
+ Inejected: Any = object()
11
+ else:
12
+ Inejected = sentinel("Injected")
13
+
14
+ if TYPE_CHECKING:
15
+ type Dep[T] = Annotated[T, Depends()]
16
+ else:
17
+ _T = TypeVar("_T")
18
+
19
+ Dep = Annotated[_T, Depends()]
20
+
21
+
22
+ if TYPE_CHECKING:
23
+ from typing import Annotated as DepFactory
24
+ else:
25
+
26
+ class DepFactory:
27
+ def __class_getitem__(cls, item: Any) -> Any:
28
+ match item:
29
+ case (tp, factory):
30
+ return Annotated[tp, Depends(factory)]
31
+ case _:
32
+ raise TypeError(f"Invalid item: {item}")
33
+
34
+
35
+ type Coro[R] = Coroutine[Any, Any, R]
36
+
37
+ type AsyncFunc[**P, R] = Callable[P, Coro[R]]
38
+ type Func[**P, R] = Callable[P, R]
39
+
40
+ type Decorator[**P, R] = Callable[
41
+ [Callable[P, R]],
42
+ Callable[P, R],
43
+ ]
44
+
45
+ type DependencyCache = dict[DependencyCacheKey, Any]
46
+
47
+
48
+ class HasSignature(Protocol):
49
+ __signature__: inspect.Signature
50
+
51
+
52
+ __all__ = [
53
+ "AsyncFunc",
54
+ "Coro",
55
+ "Decorator",
56
+ "Dep",
57
+ "DepFactory",
58
+ "DependencyCache",
59
+ "Func",
60
+ "HasSignature",
61
+ "Inejected",
62
+ ]
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-injected
3
+ Version: 0.1.0
4
+ Summary: Yet another library to reuse fastapi dependency injection
5
+ Project-URL: Repository, https://github.com/uriyyo/fastapi-injected
6
+ Author-email: Yurii Karabas <1998uriyyo@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.12
17
+ Requires-Dist: fastapi>=0.139.2
18
+ Description-Content-Type: text/markdown
19
+
20
+ # fastapi-injected
21
+
22
+ Yet another attempt to reuse FastAPI's dependency injection outside of request handlers.
23
+
24
+ This is an opinionated library: it takes the DI machinery you already know from FastAPI (`Depends`, generator dependencies with teardown, dependency caching) and makes it usable in plain async functions — background jobs, CLI commands, workers, scripts — without a `Request` in sight.
25
+
26
+ ## Installation
27
+
28
+ ```sh
29
+ pip install fastapi-injected
30
+ ```
31
+
32
+ Requires Python 3.12+.
33
+
34
+ ## Usage
35
+
36
+ Declare dependencies as regular classes and annotate fields with `Dep[...]`:
37
+
38
+ ```python
39
+ from dataclasses import dataclass
40
+ from typing import AsyncIterator
41
+
42
+ from fastapi_injected import Dep, DepFactory, Inejected, inject
43
+
44
+
45
+ @dataclass
46
+ class Session:
47
+ closed: bool = False
48
+
49
+
50
+ async def session_dep() -> AsyncIterator[Session]:
51
+ session = Session()
52
+ try:
53
+ yield session
54
+ finally:
55
+ session.closed = True
56
+
57
+
58
+ @dataclass
59
+ class Repository:
60
+ session: DepFactory[Session, session_dep]
61
+
62
+
63
+ @dataclass
64
+ class Service:
65
+ repo: Dep[Repository]
66
+
67
+
68
+ @inject
69
+ async def handler(*, service: Dep[Service] = Inejected) -> None:
70
+ ... # service is built and injected, session is closed on exit
71
+
72
+
73
+ await handler()
74
+ ```
75
+
76
+ - `Dep[T]` — resolve `T` by calling it, same as FastAPI's `Annotated[T, Depends()]`.
77
+ - `DepFactory[T, factory]` — resolve `T` via a factory, same as `Annotated[T, Depends(factory)]`. Generator factories get proper teardown.
78
+ - `Inejected` — a sentinel default that exists purely to make type checkers happy: without it they would complain about a missing argument at call sites. At runtime the parameter is always filled in by `@inject`.
79
+
80
+ Injected parameters mix freely with regular ones — pass your own arguments as usual and the rest is injected:
81
+
82
+ ```python
83
+ @inject
84
+ async def add(a: int, b: int, *, service: Dep[Service] = Inejected) -> int:
85
+ ...
86
+
87
+
88
+ result = await add(1, 2)
89
+ ```
90
+
91
+ ### Solving a type directly
92
+
93
+ No decorator needed — resolve a dependency graph on demand:
94
+
95
+ ```python
96
+ from fastapi_injected import solve
97
+
98
+ service = await solve(Service)
99
+ ```
100
+
101
+ ### Scopes and caching
102
+
103
+ By default every call to an injected function gets its own scope: dependencies are built, cached within the call, and torn down when it returns. Wrap several calls in `push_inject_scope()` to share one cache (and defer teardown to the end of the scope):
104
+
105
+ ```python
106
+ from fastapi_injected import push_inject_scope
107
+
108
+ async with push_inject_scope():
109
+ a = await handler() # dependencies built here
110
+ b = await handler() # same instances reused
111
+ # generator dependencies are torn down here
112
+ ```
113
+
114
+ Use `@inject(new_scope=True)` to opt a function out of the surrounding scope and always get fresh dependencies.
115
+
116
+ ## License
117
+
118
+ MIT
@@ -0,0 +1,11 @@
1
+ fastapi_injected/__init__.py,sha256=FCIqhlzoaXEFvCgRpp4Gh_TPE9y_XJJ9YnDs2HY9C1o,248
2
+ fastapi_injected/deps.py,sha256=2YaeVI_JifdCDaqPrNzKK5MC7UC0-Iljo7pD73qphUY,3819
3
+ fastapi_injected/inject.py,sha256=oGymgj6pKi5k3FEjzsUZ9AB7c-PSMB9yH5slRCLgo-o,1194
4
+ fastapi_injected/scope.py,sha256=8gBQ2xoGJYGg02fsNlURzF0Oqhlklj1XTPTXl62PtEs,1576
5
+ fastapi_injected/sign.py,sha256=EYDUDGFAgiSj7WCe7zUZJMiKxNAy4bagj4e3yrdSF0c,1701
6
+ fastapi_injected/solve.py,sha256=2ChVdxKq8j5qo1JvuElpHAEa4YlZoWK6jUMDbCTb1Lk,515
7
+ fastapi_injected/types.py,sha256=xulauhqiC5nwJqOVfh8AzgkyArVtyTutgDRKl2HJKtI,1304
8
+ fastapi_injected-0.1.0.dist-info/METADATA,sha256=vDLM2ZXQ-KCSEJGkzOB7dI_sbEkCD1MdPXdA4b31GYc,3460
9
+ fastapi_injected-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
10
+ fastapi_injected-0.1.0.dist-info/licenses/LICENSE,sha256=HCD36n-kWe513ObB4_rz7wgkBrc5BXuuPcqCh5QEiOk,1069
11
+ fastapi_injected-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yurii Karabas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.