fastapi-injected 0.2.2__tar.gz → 0.3.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.
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/.pre-commit-config.yaml +1 -1
- fastapi_injected-0.3.0/PKG-INFO +371 -0
- fastapi_injected-0.3.0/README.md +350 -0
- fastapi_injected-0.3.0/fastapi_injected/__init__.py +57 -0
- fastapi_injected-0.3.0/fastapi_injected/_bind.py +180 -0
- fastapi_injected-0.3.0/fastapi_injected/_cache.py +83 -0
- fastapi_injected-0.3.0/fastapi_injected/_dataclass.py +58 -0
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/fastapi_injected/_deps_tp.py +9 -0
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/fastapi_injected/_fastapi_lifecycle.py +16 -12
- fastapi_injected-0.3.0/fastapi_injected/_given.py +24 -0
- fastapi_injected-0.3.0/fastapi_injected/_injected.py +32 -0
- fastapi_injected-0.3.0/fastapi_injected/_overrides.py +79 -0
- fastapi_injected-0.3.0/fastapi_injected/_rlock.py +63 -0
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/fastapi_injected/deps.py +49 -50
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/fastapi_injected/inject.py +3 -3
- fastapi_injected-0.3.0/fastapi_injected/overrides.py +36 -0
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/fastapi_injected/resolve.py +14 -3
- fastapi_injected-0.3.0/fastapi_injected/scope.py +275 -0
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/fastapi_injected/sign.py +30 -13
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/fastapi_injected/types.py +29 -2
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/pyproject.toml +1 -1
- fastapi_injected-0.3.0/tests/_typing/annotations.py +24 -0
- fastapi_injected-0.3.0/tests/_typing/arg.py +60 -0
- fastapi_injected-0.3.0/tests/_typing/bind.py +105 -0
- fastapi_injected-0.3.0/tests/_typing/dataclass.py +37 -0
- fastapi_injected-0.3.0/tests/_typing/deps.py +76 -0
- fastapi_injected-0.3.0/tests/_typing/errors.py +28 -0
- fastapi_injected-0.3.0/tests/_typing/given.py +33 -0
- fastapi_injected-0.3.0/tests/_typing/inject.py +70 -0
- fastapi_injected-0.3.0/tests/_typing/injected.py +51 -0
- fastapi_injected-0.3.0/tests/_typing/integration.py +37 -0
- fastapi_injected-0.3.0/tests/_typing/markers.py +42 -0
- fastapi_injected-0.3.0/tests/_typing/overrides.py +62 -0
- fastapi_injected-0.3.0/tests/_typing/resolve.py +90 -0
- fastapi_injected-0.3.0/tests/_typing/scope.py +54 -0
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/tests/deps.py +6 -0
- fastapi_injected-0.3.0/tests/ext/__init__.py +0 -0
- fastapi_injected-0.3.0/tests/test_bind.py +121 -0
- fastapi_injected-0.3.0/tests/test_cache.py +66 -0
- fastapi_injected-0.3.0/tests/test_concurrency.py +79 -0
- fastapi_injected-0.3.0/tests/test_dataclass.py +71 -0
- fastapi_injected-0.3.0/tests/test_dependant_cache.py +92 -0
- fastapi_injected-0.3.0/tests/test_errors.py +58 -0
- fastapi_injected-0.3.0/tests/test_fastapi.py +213 -0
- fastapi_injected-0.3.0/tests/test_given.py +66 -0
- fastapi_injected-0.3.0/tests/test_inject.py +195 -0
- fastapi_injected-0.3.0/tests/test_injected.py +96 -0
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/tests/test_overrides.py +30 -17
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/tests/test_resolve.py +57 -0
- fastapi_injected-0.3.0/tests/test_rlock.py +107 -0
- fastapi_injected-0.3.0/tests/test_scope.py +50 -0
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/uv.lock +1 -1
- fastapi_injected-0.2.2/PKG-INFO +0 -196
- fastapi_injected-0.2.2/README.md +0 -175
- fastapi_injected-0.2.2/fastapi_injected/__init__.py +0 -24
- fastapi_injected-0.2.2/fastapi_injected/overrides.py +0 -122
- fastapi_injected-0.2.2/fastapi_injected/scope.py +0 -117
- fastapi_injected-0.2.2/tests/test_fastapi.py +0 -121
- fastapi_injected-0.2.2/tests/test_inject.py +0 -101
- fastapi_injected-0.2.2/tests/test_typing.py +0 -33
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/.github/dependabot.yml +0 -0
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/.github/workflows/automerge.yml +0 -0
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/.github/workflows/lint.yml +0 -0
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/.github/workflows/publish.yml +0 -0
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/.github/workflows/test.yml +0 -0
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/.gitignore +0 -0
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/LICENSE +0 -0
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/tests/__init__.py +0 -0
- {fastapi_injected-0.2.2/tests/ext → fastapi_injected-0.3.0/tests/_typing}/__init__.py +0 -0
- {fastapi_injected-0.2.2 → fastapi_injected-0.3.0}/tests/ext/test_pydantic_ai.py +0 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fastapi-injected
|
|
3
|
+
Version: 0.3.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
|
+
Requires-Dist: typing-extensions>=4.16.0
|
|
19
|
+
Requires-Dist: typing-inspection>=0.4.4
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# fastapi-injected
|
|
23
|
+
|
|
24
|
+
Yet another attempt to reuse FastAPI's dependency injection outside of request handlers.
|
|
25
|
+
|
|
26
|
+
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.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pip install fastapi-injected
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires Python 3.12+.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
Declare dependencies as regular classes and annotate fields with `Dep[...]`:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from dataclasses import dataclass
|
|
42
|
+
from typing import AsyncIterator
|
|
43
|
+
|
|
44
|
+
from fastapi_injected import Dep, DepFactory, Injected, inject
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Session:
|
|
49
|
+
closed: bool = False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def session_dep() -> AsyncIterator[Session]:
|
|
53
|
+
session = Session()
|
|
54
|
+
try:
|
|
55
|
+
yield session
|
|
56
|
+
finally:
|
|
57
|
+
session.closed = True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class Repository:
|
|
62
|
+
session: DepFactory[Session, session_dep]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class Service:
|
|
67
|
+
repo: Dep[Repository]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@inject
|
|
71
|
+
async def handler(*, service: Dep[Service] = Injected) -> None:
|
|
72
|
+
... # service is built and injected, session is closed on exit
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
await handler()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- `Dep[T]` — resolve `T` by calling it, same as FastAPI's `Annotated[T, Depends()]`.
|
|
79
|
+
- `DepFactory[T, factory]` — resolve `T` via a factory, same as `Annotated[T, Depends(factory)]`. Generator factories get proper teardown.
|
|
80
|
+
- `Arg[T]` — keep a parameter out of the dependency graph even though its annotation is a dependency, so the caller supplies it. See [Caller-supplied arguments](#caller-supplied-arguments).
|
|
81
|
+
- `Injected` — 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`.
|
|
82
|
+
|
|
83
|
+
Injected parameters mix freely with regular ones — pass your own arguments as usual and the rest is injected:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
@inject
|
|
87
|
+
async def add(a: int, b: int, *, service: Dep[Service] = Injected) -> int:
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
result = await add(1, 2)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Caller-supplied arguments
|
|
95
|
+
|
|
96
|
+
A parameter is injected only if it is written as a dependency — `Dep[...]`, `DepFactory[...]`, an `Annotated[..., Depends(...)]` of your own, or a `Depends(...)` default. Everything else is left for the caller, annotation untouched: it is never handed to pydantic, so any type works with no marker at all.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
class Connection: # not a pydantic-friendly type, and it does not have to be
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@inject
|
|
104
|
+
async def handler(conn: Connection, *, service: Dep[Service] = Injected) -> None:
|
|
105
|
+
...
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
await handler(connection)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`Arg[...]` is for the opposite case — a parameter that *is* spelled as a dependency, in a function that wants it passed in instead:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from fastapi_injected import Arg
|
|
115
|
+
|
|
116
|
+
type ServiceDep = Annotated[Service, Depends(get_service)]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@inject
|
|
120
|
+
async def handler(service: Arg[ServiceDep], *, session: Dep[Session] = Injected) -> None:
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
await handler(service)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`Arg[T]` is a no-op for type checkers — the parameter stays typed as `T` — and at runtime it tells `@inject` to keep this parameter out of the dependency graph.
|
|
128
|
+
|
|
129
|
+
Writing `Injected` as the default of a parameter that is not a dependency raises `NotADependencyError` at decoration time, since nothing would ever fill it in.
|
|
130
|
+
|
|
131
|
+
### Resolving a type directly
|
|
132
|
+
|
|
133
|
+
No decorator needed — resolve a dependency graph on demand:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from fastapi_injected import resolve
|
|
137
|
+
|
|
138
|
+
service = await resolve(Service)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Like `@inject`, `resolve` accepts `new_scope=True` to force a fresh scope instead of reusing the surrounding one.
|
|
142
|
+
|
|
143
|
+
### Scopes and caching
|
|
144
|
+
|
|
145
|
+
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):
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from fastapi_injected import push_inject_scope
|
|
149
|
+
|
|
150
|
+
async with push_inject_scope():
|
|
151
|
+
a = await handler() # dependencies built here
|
|
152
|
+
b = await handler() # same instances reused
|
|
153
|
+
# generator dependencies are torn down here
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Use `@inject(new_scope=True)` to opt a function out of the surrounding scope and always get fresh dependencies. It builds them again, but stays in the request they are being built for, so request-bound dependencies keep resolving.
|
|
157
|
+
|
|
158
|
+
Outside of a request there is no request to resolve against, so the scope makes one up. It answers what a dependency usually reads — `method`, `url`, `headers`, `path_params`, `client`, `state` — with the values of a request nobody sent. The application is the one thing it cannot invent: pass it when a dependency reaches for `request.app`:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
async with push_inject_scope(app=app):
|
|
162
|
+
await handler() # `request.app.state` resolves as it does in a route
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Without it, reading `request.app` raises a `KeyError` naming what is missing rather than a bare `'app'`.
|
|
166
|
+
|
|
167
|
+
Analysing a dependency is the expensive part of resolving one, so the result is cached — keyed by the dependency itself, not by whatever object carried it, so nothing that only passed through is kept alive. `clear_dependant_cache()` drops it, for long-lived processes and test suites that want the memory back.
|
|
168
|
+
|
|
169
|
+
### Overriding dependencies
|
|
170
|
+
|
|
171
|
+
`push_overrides` swaps dependencies out for the duration of a `with` block — handy in tests, or anywhere you need to run the same code against a different implementation:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from fastapi_injected import push_overrides
|
|
175
|
+
|
|
176
|
+
with push_overrides({Session: Session(closed=True)}):
|
|
177
|
+
await handler() # gets the override instead of the real dependency
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
A key can be the dependency itself, or the annotation you wrote in the signature — `Dep[Session]` and `DepFactory[Session, session_dep]` both work and are normalized to the same underlying dependency:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
with push_overrides({DepFactory[Session, session_dep]: my_session}):
|
|
184
|
+
...
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Values are used as-is, but two wrappers make the intent explicit and cover the ambiguous cases:
|
|
188
|
+
|
|
189
|
+
- `ValueOverride(value)` — always inject `value`, even when it is itself callable.
|
|
190
|
+
- `FactoryOverride(factory)` — call `factory` to produce the value. Sync, async, and generator factories are all supported, with the same teardown semantics as regular dependencies.
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from fastapi_injected import FactoryOverride, ValueOverride
|
|
194
|
+
|
|
195
|
+
with push_overrides(
|
|
196
|
+
{
|
|
197
|
+
Session: ValueOverride(fake_session),
|
|
198
|
+
Repository: FactoryOverride(lambda: FakeRepository()),
|
|
199
|
+
},
|
|
200
|
+
):
|
|
201
|
+
...
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Overrides apply to the whole graph, not just top-level parameters — overriding a nested dependency changes what its dependents receive. Nested `push_overrides` blocks merge, with the innermost one winning.
|
|
205
|
+
|
|
206
|
+
An override block is a scope of its own: it caches what it builds, and that cache goes away with the block. The surrounding scope keeps what it had resolved before, and it stays visible — except for the overridden dependencies and anything built from them, which are resolved again under the override:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
async with push_inject_scope():
|
|
210
|
+
real = await handler() # dependencies cached here
|
|
211
|
+
|
|
212
|
+
with push_overrides({Session: fake}):
|
|
213
|
+
await handler() # rebuilt with `fake`, everything else reused from the cache
|
|
214
|
+
|
|
215
|
+
await handler() # back to the cached, real dependencies
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Overrides can also be handed to the scope directly, which is the same thing in one call:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
async with push_inject_scope({Session: fake}) as scope:
|
|
222
|
+
await handler()
|
|
223
|
+
|
|
224
|
+
with scope.override({Session: other}): # nest freely
|
|
225
|
+
await handler()
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Dependencies that are objects
|
|
229
|
+
|
|
230
|
+
Dependants are cached by the callable that resolves them, so a dependency that is an object — a class holding configuration, a parametrized resolver — has to be hashable to get there. A plain dataclass is not, and a frozen one still refuses as soon as it holds a list or a dict.
|
|
231
|
+
|
|
232
|
+
`MakeDataclass` is a base class that makes its subclasses dataclasses with a hash that always answers: by fields when they can be hashed, by identity when they cannot.
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from dataclasses import field
|
|
236
|
+
from fastapi_injected import MakeDataclass, resolve
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class Settings(MakeDataclass):
|
|
240
|
+
hosts: list[str] = field(default_factory=list)
|
|
241
|
+
|
|
242
|
+
def __call__(self) -> list[str]:
|
|
243
|
+
return self.hosts
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
await resolve(Settings(["a", "b"])) # a plain dataclass would raise TypeError here
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Every `dataclasses.dataclass` option is accepted as a class keyword and passed straight through:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
class Config(MakeDataclass, frozen=True, kw_only=True):
|
|
253
|
+
retries: int = 3
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### When resolution fails
|
|
257
|
+
|
|
258
|
+
A dependency can fail to resolve for the same reasons it would in a route — a missing header, a query parameter that does not validate. `DependencyResolutionError` carries those errors in the shape pydantic produced them, and turns into the response FastAPI would have returned:
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from fastapi_injected import DependencyResolutionError
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
await handler()
|
|
265
|
+
except DependencyResolutionError as exc:
|
|
266
|
+
exc.errors # [{'type': 'missing', 'loc': ('header', 'x-trace'), ...}]
|
|
267
|
+
raise exc.as_validation_error() from exc # a RequestValidationError, so a 422
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
It is a `ValueError`, which is what resolution used to raise, so code catching that keeps working.
|
|
271
|
+
|
|
272
|
+
### Dependencies decided at runtime
|
|
273
|
+
|
|
274
|
+
A dependency is usually written as an annotation, but sometimes it is a value a caller already has, or one picked while the program runs. `Given` turns a value into a dependency that resolves to it:
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
from fastapi_injected import Given, resolve
|
|
278
|
+
|
|
279
|
+
doc = Doc(title="readme")
|
|
280
|
+
|
|
281
|
+
await resolve(Given(doc)) # the doc itself
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Constants holding equal values are the same dependency, so they share a cache entry and can be overridden like any other. What `Given` returns is a marker held as a value — `DepOf[R]`, as opposed to `Dep[R]`, which is the same marker in annotation position.
|
|
285
|
+
|
|
286
|
+
`bind_deps` binds such markers to the leading parameters of a function, which is how a dependency graph gets built from markers that were not known when the function was written:
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from fastapi_injected import bind_deps
|
|
290
|
+
|
|
291
|
+
async def describe(doc: Doc, role: Role) -> str:
|
|
292
|
+
return f"{doc.title}:{role.name}"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
bound = bind_deps(describe, Given(doc), RoleDep)
|
|
296
|
+
|
|
297
|
+
await resolve(bound) # "readme:admin", with `role` resolved as usual
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
What a parameter is bound to can be written any way a dependency can be: a marker like `Given(...)`, `Dep[T]` or `DepFactory[T, factory]`, a `Depends(...)`, or the callable itself — the same things `resolve` accepts.
|
|
301
|
+
|
|
302
|
+
Binding takes the parameters away, and a type checker sees what is left: `bind_deps(describe, Given(doc))` is a function of `(role)`, and binding both leaves one of no arguments at all. The bound function is a dependency like any other — resolve it, `@inject` it, override what it depends on. Binding more markers than the function has parameters is a `TypeError` rather than a silent truncation.
|
|
303
|
+
|
|
304
|
+
### Dependencies as objects
|
|
305
|
+
|
|
306
|
+
When the markers belong together, `MakeInjected` makes a dataclass of them: fields annotated with `DepOf` hold the markers, and the call receives what they resolve to.
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
from fastapi_injected import Given, MakeInjected
|
|
310
|
+
from fastapi_injected.types import DepOf
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class IsOwner(MakeInjected):
|
|
314
|
+
doc: DepOf[Doc]
|
|
315
|
+
role: DepOf[Role]
|
|
316
|
+
loud: bool = False # a plain field stays a plain field
|
|
317
|
+
|
|
318
|
+
async def __call__(self, doc: Doc, role: Role) -> bool:
|
|
319
|
+
return doc.owner == role.name
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
await resolve(IsOwner(doc=Given(doc), role=RoleDep))
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
It is a `MakeDataclass`, so instances built from equal markers are equal — and therefore one dependency, resolved once per scope and overridable as a whole. For callables that are not dataclasses, the two halves of the binding are available on their own: `signature_with_deps(func, deps)` builds the signature, and `remap_dep_args` turns the arguments it names back into positional ones.
|
|
326
|
+
|
|
327
|
+
### Inspecting annotations
|
|
328
|
+
|
|
329
|
+
A few helpers are exported for code that needs to reason about `Dep[...]` annotations — building override maps, custom decorators, and the like:
|
|
330
|
+
|
|
331
|
+
- `is_dep(tp)` — whether `tp` is a `Dep`/`DepFactory` annotation.
|
|
332
|
+
- `unwrap_dep_tp(tp)` — the annotated type (`Any` for a bare `Dep`).
|
|
333
|
+
- `unwrap_dep_dependency(tp)` — the callable that resolves it: the factory for `DepFactory[T, factory]`, the type itself for `Dep[T]`.
|
|
334
|
+
|
|
335
|
+
### FastAPI request integration
|
|
336
|
+
|
|
337
|
+
Inside a FastAPI app, `@inject`-ed functions and `resolve` can share the request's own dependency cache — the same instances FastAPI built for the handler. Register `init_inject_scope` as a dependency:
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
from fastapi import Depends, FastAPI
|
|
341
|
+
from fastapi_injected import Dep, init_inject_scope, resolve
|
|
342
|
+
|
|
343
|
+
app = FastAPI(dependencies=[Depends(init_inject_scope)])
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@app.get("/")
|
|
347
|
+
async def route(service: Dep[Service]) -> str:
|
|
348
|
+
same = await resolve(Service) # same instance as `service`
|
|
349
|
+
...
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Anything called from the handler — including `@inject`-ed helpers — resolves against the request's cache, so a per-request dependency like a DB session stays a single instance for the whole request.
|
|
353
|
+
|
|
354
|
+
## What is public
|
|
355
|
+
|
|
356
|
+
Everything the package supports is importable from `fastapi_injected` itself, and that is the surface a release keeps:
|
|
357
|
+
|
|
358
|
+
| | |
|
|
359
|
+
| --- | --- |
|
|
360
|
+
| Markers | `Dep`, `DepFactory`, `DepOf`, `Arg`, `Given`, `Injected` |
|
|
361
|
+
| Resolving | `inject`, `resolve`, `bind_deps`, `signature_with_deps`, `remap_dep_args`, `clear_dependant_cache` |
|
|
362
|
+
| Scopes and overrides | `InjectScope`, `push_inject_scope`, `inside_inject_scope`, `push_overrides`, `Overrides`, `OverridesProvider`, `ValueOverride`, `FactoryOverride` |
|
|
363
|
+
| Building on top | `MakeDataclass`, `MakeInjected`, `HasDependsHook`, `ArgMarker`, `is_arg`, `is_dep`, `unwrap_dep_tp`, `unwrap_dep_dependency` |
|
|
364
|
+
| FastAPI integration | `add_injected_scope`, `init_inject_scope` |
|
|
365
|
+
| Errors | `DependencyResolutionError`, `MissedDependencyError`, `MissingDependencyCacheError`, `NotADependencyError`, `UnboundDepArgsError`, `UnboundScopeError` |
|
|
366
|
+
|
|
367
|
+
`fastapi_injected.types` holds the typing vocabulary the signatures are written in — `DepReturn`, `DepShape`, `DepDecl`, `AsyncFunc`, `Coro` and friends — and is public too. Anything else, including every module whose name starts with an underscore, is machinery that can change in a patch release.
|
|
368
|
+
|
|
369
|
+
## License
|
|
370
|
+
|
|
371
|
+
MIT
|