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.
@@ -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