fastapi-standalone-di 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.
- fastapi_standalone_di/__init__.py +48 -0
- fastapi_standalone_di/_compat.py +49 -0
- fastapi_standalone_di/app_state.py +128 -0
- fastapi_standalone_di/py.typed +0 -0
- fastapi_standalone_di/registration.py +132 -0
- fastapi_standalone_di/resolve.py +1088 -0
- fastapi_standalone_di-0.1.0.dist-info/METADATA +536 -0
- fastapi_standalone_di-0.1.0.dist-info/RECORD +10 -0
- fastapi_standalone_di-0.1.0.dist-info/WHEEL +4 -0
- fastapi_standalone_di-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Use FastAPI's dependency injection outside of any web/ASGI context."""
|
|
2
|
+
|
|
3
|
+
from fastapi_standalone_di.app_state import (
|
|
4
|
+
AppState,
|
|
5
|
+
get_app_state,
|
|
6
|
+
set_app_state_value,
|
|
7
|
+
)
|
|
8
|
+
from fastapi_standalone_di.registration import (
|
|
9
|
+
RegistrableDependency,
|
|
10
|
+
patch_for_registrable_dependency_support,
|
|
11
|
+
)
|
|
12
|
+
from fastapi_standalone_di.resolve import (
|
|
13
|
+
DependantCache,
|
|
14
|
+
DependencyOverrides,
|
|
15
|
+
DependencyScope,
|
|
16
|
+
FastAPIContainer,
|
|
17
|
+
MissingParameterError,
|
|
18
|
+
ParameterError,
|
|
19
|
+
ParameterValidationError,
|
|
20
|
+
ParamSource,
|
|
21
|
+
ResolutionScope,
|
|
22
|
+
ResolvedDependencies,
|
|
23
|
+
ScopeError,
|
|
24
|
+
get_container,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"AppState",
|
|
31
|
+
"DependantCache",
|
|
32
|
+
"DependencyOverrides",
|
|
33
|
+
"DependencyScope",
|
|
34
|
+
"FastAPIContainer",
|
|
35
|
+
"MissingParameterError",
|
|
36
|
+
"ParamSource",
|
|
37
|
+
"ParameterError",
|
|
38
|
+
"ParameterValidationError",
|
|
39
|
+
"RegistrableDependency",
|
|
40
|
+
"ResolutionScope",
|
|
41
|
+
"ResolvedDependencies",
|
|
42
|
+
"ScopeError",
|
|
43
|
+
"__version__",
|
|
44
|
+
"get_app_state",
|
|
45
|
+
"get_container",
|
|
46
|
+
"patch_for_registrable_dependency_support",
|
|
47
|
+
"set_app_state_value",
|
|
48
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Version-independent introspection of a callable's execution model.
|
|
2
|
+
|
|
3
|
+
FastAPI historically exposed ``is_async_gen_callable`` / ``is_gen_callable`` /
|
|
4
|
+
``is_coroutine_callable`` as functions in ``fastapi.dependencies.utils`` (older
|
|
5
|
+
releases) and later moved the information onto ``Dependant`` attributes,
|
|
6
|
+
removing the module-level helpers. To stay compatible across the whole
|
|
7
|
+
supported FastAPI range we reimplement the (small, stable) detection logic here
|
|
8
|
+
instead of importing it — it depends only on the standard library.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import functools
|
|
12
|
+
import inspect
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _unwrap(call: Callable[..., Any]) -> Callable[..., Any]:
|
|
18
|
+
while isinstance(call, functools.partial):
|
|
19
|
+
call = call.func
|
|
20
|
+
return call
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_async_gen_callable(call: Callable[..., Any]) -> bool:
|
|
24
|
+
"""True if calling *call* returns an async generator."""
|
|
25
|
+
call = _unwrap(call)
|
|
26
|
+
if inspect.isasyncgenfunction(call):
|
|
27
|
+
return True
|
|
28
|
+
dunder_call = getattr(call, "__call__", None) # noqa: B004
|
|
29
|
+
return inspect.isasyncgenfunction(dunder_call)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_gen_callable(call: Callable[..., Any]) -> bool:
|
|
33
|
+
"""True if calling *call* returns a (sync) generator."""
|
|
34
|
+
call = _unwrap(call)
|
|
35
|
+
if inspect.isgeneratorfunction(call):
|
|
36
|
+
return True
|
|
37
|
+
dunder_call = getattr(call, "__call__", None) # noqa: B004
|
|
38
|
+
return inspect.isgeneratorfunction(dunder_call)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
|
|
42
|
+
"""True if *call* is a coroutine function (or a callable with one)."""
|
|
43
|
+
call = _unwrap(call)
|
|
44
|
+
if inspect.iscoroutinefunction(call):
|
|
45
|
+
return True
|
|
46
|
+
if inspect.isclass(call):
|
|
47
|
+
return False
|
|
48
|
+
dunder_call = getattr(call, "__call__", None) # noqa: B004
|
|
49
|
+
return inspect.iscoroutinefunction(dunder_call)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Application state abstraction for FastAPI and standalone contexts.
|
|
2
|
+
|
|
3
|
+
``AppState`` provides a unified interface to access application-level state
|
|
4
|
+
(database clients, caches, managers, …) regardless of whether the code runs
|
|
5
|
+
inside a FastAPI ASGI request or in a standalone context (CLI scripts,
|
|
6
|
+
background tasks, :class:`~fastapi_standalone_di.resolve.FastAPIContainer`).
|
|
7
|
+
|
|
8
|
+
In **FastAPI mode**, ``AppState`` delegates to ``request.app.state`` (Starlette's
|
|
9
|
+
built-in state). In **standalone mode**, it falls back to an internal dict.
|
|
10
|
+
|
|
11
|
+
Usage in a FastAPI dependency::
|
|
12
|
+
|
|
13
|
+
def get_db(app_state: AppState = Depends(get_app_state)) -> Database:
|
|
14
|
+
return app_state.get("db")
|
|
15
|
+
|
|
16
|
+
Usage with ``FastAPIContainer``::
|
|
17
|
+
|
|
18
|
+
set_app_state_value("db", db_client)
|
|
19
|
+
container = FastAPIContainer()
|
|
20
|
+
db = await container.get(get_db)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from typing import Any, ClassVar, Self
|
|
24
|
+
|
|
25
|
+
from fastapi import FastAPI
|
|
26
|
+
from starlette.datastructures import State
|
|
27
|
+
from starlette.requests import HTTPConnection
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AppState:
|
|
31
|
+
"""Abstraction over Starlette's ``State`` that works with and without a request.
|
|
32
|
+
|
|
33
|
+
* **FastAPI mode** — created via :meth:`from_request`; reads/writes go to
|
|
34
|
+
``request.app.state``.
|
|
35
|
+
* **Standalone mode** — created via :meth:`standalone`; reads/writes go to
|
|
36
|
+
a module-level singleton dict.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
_standalone_instance: ClassVar[Self | None] = None
|
|
40
|
+
|
|
41
|
+
def __init__(self, state: State | None = None) -> None:
|
|
42
|
+
self._state = state
|
|
43
|
+
self._store: dict[str, Any] = {}
|
|
44
|
+
|
|
45
|
+
# --- read / write ---------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
def get(self, key: str) -> Any | None:
|
|
48
|
+
if self._state is not None:
|
|
49
|
+
return getattr(self._state, key, None)
|
|
50
|
+
return self._store.get(key)
|
|
51
|
+
|
|
52
|
+
def set(self, key: str, value: Any) -> None:
|
|
53
|
+
if self._state is not None:
|
|
54
|
+
setattr(self._state, key, value)
|
|
55
|
+
else:
|
|
56
|
+
self._store[key] = value
|
|
57
|
+
|
|
58
|
+
def delete(self, key: str) -> None:
|
|
59
|
+
if self._state is not None:
|
|
60
|
+
if hasattr(self._state, key):
|
|
61
|
+
delattr(self._state, key)
|
|
62
|
+
else:
|
|
63
|
+
self._store.pop(key, None)
|
|
64
|
+
|
|
65
|
+
def as_state(self) -> State:
|
|
66
|
+
"""Return a Starlette ``State`` backed by this ``AppState``'s storage.
|
|
67
|
+
|
|
68
|
+
Reads and writes on the returned ``State`` share the same underlying
|
|
69
|
+
store, so a stubbed ``request.app.state`` stays in sync with this
|
|
70
|
+
``AppState`` (and with :func:`set_app_state_value`).
|
|
71
|
+
"""
|
|
72
|
+
if self._state is not None:
|
|
73
|
+
return self._state
|
|
74
|
+
return State(self._store)
|
|
75
|
+
|
|
76
|
+
# --- constructors ---------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_request(cls, request: HTTPConnection) -> Self:
|
|
80
|
+
"""Create an ``AppState`` backed by the ASGI application state."""
|
|
81
|
+
return cls(state=request.app.state)
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_app(cls, app: FastAPI) -> Self:
|
|
85
|
+
"""Create an ``AppState`` backed by a Starlette/FastAPI application."""
|
|
86
|
+
return cls(state=app.state)
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def standalone(cls) -> Self:
|
|
90
|
+
"""Return the module-level singleton (no ASGI context needed)."""
|
|
91
|
+
if cls._standalone_instance is None:
|
|
92
|
+
cls._standalone_instance = cls()
|
|
93
|
+
return cls._standalone_instance
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def reset_standalone(cls) -> None:
|
|
97
|
+
"""Reset the standalone singleton (useful in tests)."""
|
|
98
|
+
cls._standalone_instance = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# --- FastAPI dependency -------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_app_state(
|
|
105
|
+
request: HTTPConnection = None, # type: ignore[assignment]
|
|
106
|
+
) -> AppState:
|
|
107
|
+
"""FastAPI dependency that returns an :class:`AppState`.
|
|
108
|
+
|
|
109
|
+
When injected by FastAPI, *request* is provided automatically and the
|
|
110
|
+
returned ``AppState`` delegates to ``request.app.state``. When resolved
|
|
111
|
+
outside ASGI (e.g. via :class:`FastAPIContainer`), *request* is ``None`` and
|
|
112
|
+
the standalone singleton is used instead.
|
|
113
|
+
"""
|
|
114
|
+
if request is not None:
|
|
115
|
+
return AppState.from_request(request)
|
|
116
|
+
return AppState.standalone()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# --- convenience helpers for startup / scripts --------------------------------
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def set_app_state_value(key: str, value: Any) -> None:
|
|
123
|
+
"""Set a value in the standalone ``AppState`` store.
|
|
124
|
+
|
|
125
|
+
Call this at application startup (alongside ``app.state.xxx = …``) so that
|
|
126
|
+
the value is available for both FastAPI and standalone contexts.
|
|
127
|
+
"""
|
|
128
|
+
AppState.standalone().set(key, value)
|
|
File without changes
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Registrable dependency interfaces for FastAPI.
|
|
2
|
+
|
|
3
|
+
``RegistrableDependency`` lets you declare an *interface* (an abstract base)
|
|
4
|
+
and depend on it via ``Depends(IMyService)`` while binding the concrete
|
|
5
|
+
implementation elsewhere with ``IMyService.register(MyServiceImpl)``. This
|
|
6
|
+
decouples the dependency declaration from its wiring, which is convenient both
|
|
7
|
+
inside FastAPI routes and when resolving dependencies standalone via
|
|
8
|
+
:class:`~fastapi_standalone_di.resolve.FastAPIContainer`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import inspect
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from inspect import isclass
|
|
14
|
+
from typing import Any, Literal
|
|
15
|
+
|
|
16
|
+
import fastapi.params
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class classproperty(property):
|
|
20
|
+
fget: Callable[[Any], Any]
|
|
21
|
+
|
|
22
|
+
def __init__(self, fget: Callable[[Any], Any], *arg: Any, **kw: Any):
|
|
23
|
+
super().__init__(fget, *arg, **kw)
|
|
24
|
+
self.__doc__ = fget.__doc__
|
|
25
|
+
|
|
26
|
+
def __get__(self, obj: Any, cls: type | None = None) -> Any:
|
|
27
|
+
return self.fget(cls)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RegistrableDependency:
|
|
31
|
+
"""Base class for a dependency interface with a swappable implementation."""
|
|
32
|
+
|
|
33
|
+
_impl: Callable[..., Any] | None = None
|
|
34
|
+
|
|
35
|
+
@classproperty
|
|
36
|
+
def impl(cls) -> Callable[..., Any]:
|
|
37
|
+
return cls.dependency()
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def register(cls, impl: Callable[..., Any] | None) -> None:
|
|
41
|
+
"""Register (or clear, with ``None``) the implementation for this interface."""
|
|
42
|
+
cls._impl = impl
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def dependency(cls) -> Callable[..., Any]:
|
|
46
|
+
"""Entry point for ``fastapi.Depends``: return the registered implementation.
|
|
47
|
+
|
|
48
|
+
Raises :class:`RuntimeError` when no implementation is registered — it
|
|
49
|
+
never returns ``None``.
|
|
50
|
+
"""
|
|
51
|
+
if cls._impl is None:
|
|
52
|
+
raise RuntimeError(
|
|
53
|
+
f"No implementation registered for {cls.__module__}.{cls.__name__}"
|
|
54
|
+
)
|
|
55
|
+
return cls._impl
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
FastAPIDepends = fastapi.params.Depends
|
|
59
|
+
|
|
60
|
+
# ``scope`` was added to ``Depends.__init__`` only in recent FastAPI; detect it
|
|
61
|
+
# so ``_Depends`` stays constructible on older releases.
|
|
62
|
+
_DEPENDS_SUPPORTS_SCOPE = (
|
|
63
|
+
"scope" in inspect.signature(FastAPIDepends.__init__).parameters
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class _Depends(FastAPIDepends):
|
|
68
|
+
"""A ``Depends`` that dereferences a ``RegistrableDependency`` to its impl.
|
|
69
|
+
|
|
70
|
+
``scope`` (``"function"``/``"request"``) is forwarded to FastAPI for
|
|
71
|
+
introspection parity but has no intrinsic effect when resolving standalone:
|
|
72
|
+
there is no request lifecycle outside ASGI. :class:`FastAPIContainer`
|
|
73
|
+
interprets it only when its ``default_scope`` is a mapping keyed by these
|
|
74
|
+
literals; otherwise the value is inert.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
# The ``scope`` branch is resolved once, at class-definition time, rather
|
|
78
|
+
# than on every instantiation.
|
|
79
|
+
if _DEPENDS_SUPPORTS_SCOPE:
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
dependency: Callable[..., Any] | None = None,
|
|
84
|
+
*,
|
|
85
|
+
use_cache: bool = True,
|
|
86
|
+
scope: Literal["function", "request"] | None = None,
|
|
87
|
+
):
|
|
88
|
+
FastAPIDepends.__init__(self, dependency, use_cache=use_cache, scope=scope)
|
|
89
|
+
self._dependency = dependency
|
|
90
|
+
else:
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
dependency: Callable[..., Any] | None = None,
|
|
95
|
+
*,
|
|
96
|
+
use_cache: bool = True,
|
|
97
|
+
scope: Literal["function", "request"] | None = None,
|
|
98
|
+
):
|
|
99
|
+
FastAPIDepends.__init__(self, dependency, use_cache=use_cache)
|
|
100
|
+
self._dependency = dependency
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def dependency(self) -> Callable[..., Any] | None:
|
|
104
|
+
return (
|
|
105
|
+
self._dependency.dependency()
|
|
106
|
+
if isclass(self._dependency)
|
|
107
|
+
and issubclass(self._dependency, RegistrableDependency)
|
|
108
|
+
else self._dependency
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@dependency.setter
|
|
112
|
+
def dependency(self, value: Callable[..., Any] | None) -> None:
|
|
113
|
+
self._dependency = value
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def patch_for_registrable_dependency_support() -> bool:
|
|
117
|
+
"""Patch ``fastapi.params.Depends`` to resolve ``RegistrableDependency`` eagerly.
|
|
118
|
+
|
|
119
|
+
Only needed when FastAPI itself must see the concrete implementation at
|
|
120
|
+
introspection time (e.g. for OpenAPI). :class:`FastAPIContainer` resolves the
|
|
121
|
+
indirection on its own and does not require this patch.
|
|
122
|
+
|
|
123
|
+
The patch only affects ``Depends`` objects created **after** it runs: any
|
|
124
|
+
``Depends()`` already instantiated keeps the original class. Apply it at
|
|
125
|
+
import time, before the routes declaring the dependencies are defined.
|
|
126
|
+
|
|
127
|
+
Returns ``True`` if the patch was applied, ``False`` if already patched.
|
|
128
|
+
"""
|
|
129
|
+
if FastAPIDepends is fastapi.params.Depends:
|
|
130
|
+
fastapi.params.Depends = _Depends # type: ignore[assignment,misc]
|
|
131
|
+
return True
|
|
132
|
+
return False
|