fast-depends 1.0.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.
- fast_depends-1.0.0/.gitignore +20 -0
- fast_depends-1.0.0/LICENSE +21 -0
- fast_depends-1.0.0/PKG-INFO +135 -0
- fast_depends-1.0.0/README.md +81 -0
- fast_depends-1.0.0/fast_depends/__about__.py +3 -0
- fast_depends-1.0.0/fast_depends/__init__.py +8 -0
- fast_depends-1.0.0/fast_depends/construct.py +250 -0
- fast_depends-1.0.0/fast_depends/injector.py +308 -0
- fast_depends-1.0.0/fast_depends/model.py +51 -0
- fast_depends-1.0.0/fast_depends/provider.py +17 -0
- fast_depends-1.0.0/fast_depends/types.py +8 -0
- fast_depends-1.0.0/fast_depends/usage.py +106 -0
- fast_depends-1.0.0/fast_depends/utils.py +14 -0
- fast_depends-1.0.0/pyproject.toml +190 -0
- fast_depends-1.0.0/scripts/lint.sh +4 -0
- fast_depends-1.0.0/scripts/publish.sh +3 -0
- fast_depends-1.0.0/scripts/test-cov.sh +5 -0
- fast_depends-1.0.0/scripts/test.sh +1 -0
- fast_depends-1.0.0/wtf.py +13 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Pastukhov Nikita
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: fast-depends
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: FastDepends - extracted and cleared from HTTP domain Fastapi Dependency Injection System
|
|
5
|
+
Project-URL: Homepage, https://lancetnik.github.io/FastDI/
|
|
6
|
+
Project-URL: Documentation, https://lancetnik.github.io/FastDI/
|
|
7
|
+
Project-URL: Tracker, https://github.com/Lancetnik/FastDI/issues
|
|
8
|
+
Project-URL: Source, https://github.com/Lancetnik/FastDI
|
|
9
|
+
Author-email: Pastukhov Nikita <diementros@yandex.ru>
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: dependency injection,fastapi
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: Pydantic
|
|
14
|
+
Classifier: Framework :: Pydantic :: 1
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Information Technology
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
27
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
28
|
+
Classifier: Topic :: Software Development
|
|
29
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
30
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
31
|
+
Classifier: Typing :: Typed
|
|
32
|
+
Requires-Python: >=3.7
|
|
33
|
+
Requires-Dist: anyio
|
|
34
|
+
Requires-Dist: pydantic>=1.8
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: black>=23.3.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: fastdepends[doc]; extra == 'dev'
|
|
38
|
+
Requires-Dist: fastdepends[test]; extra == 'dev'
|
|
39
|
+
Requires-Dist: isort>=5; extra == 'dev'
|
|
40
|
+
Requires-Dist: mypy>=1.1; extra == 'dev'
|
|
41
|
+
Requires-Dist: ruff>=0.0.260; extra == 'dev'
|
|
42
|
+
Provides-Extra: doc
|
|
43
|
+
Requires-Dist: mdx-include<2.0.0,>=1.4.1; extra == 'doc'
|
|
44
|
+
Requires-Dist: mkdocs-markdownextradata-plugin<0.3.0,>=0.1.7; extra == 'doc'
|
|
45
|
+
Requires-Dist: mkdocs-material<9.0.0,>=8.1.4; extra == 'doc'
|
|
46
|
+
Requires-Dist: mkdocstrings[python]>=0.18; extra == 'doc'
|
|
47
|
+
Provides-Extra: test
|
|
48
|
+
Requires-Dist: asyncmock; python_version < '3.8' and extra == 'test'
|
|
49
|
+
Requires-Dist: coverage[toml]>=7.2; extra == 'test'
|
|
50
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'test'
|
|
51
|
+
Requires-Dist: pytest-xdist[psutil]; extra == 'test'
|
|
52
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
53
|
+
Description-Content-Type: text/markdown
|
|
54
|
+
|
|
55
|
+
# FastDI
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
Documentation: https://lancetnik.github.io/FastDI/
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
FastDI - extracted and cleared from HTTP domain logic Fastapi Dependency Injection System.
|
|
64
|
+
This is a little library, providing you ability to use lovely Fastapi interfaces at your own
|
|
65
|
+
projects or tools.
|
|
66
|
+
|
|
67
|
+
Thanks to [*fastapi*](https://fastapi.tiangolo.com/) and [*pytest*](https://docs.pytest.org/en/7.3.x/) projects for this
|
|
68
|
+
greate functional. This package just a little change Fasapi sources to provide DI functionality pyre-python way.
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
There is no way to make Dependency Injection easier
|
|
73
|
+
|
|
74
|
+
You can use this library without any frameworks at **sync** and **async** code both
|
|
75
|
+
|
|
76
|
+
### Async code
|
|
77
|
+
```python
|
|
78
|
+
import asyncio
|
|
79
|
+
|
|
80
|
+
from fastdi import inject, Depends
|
|
81
|
+
|
|
82
|
+
async def dependency(a: int) -> int:
|
|
83
|
+
return a
|
|
84
|
+
|
|
85
|
+
@inject
|
|
86
|
+
async def main(
|
|
87
|
+
a: int,
|
|
88
|
+
b: int,
|
|
89
|
+
c: int = Depends(dependency)
|
|
90
|
+
) -> float:
|
|
91
|
+
return a + b + c
|
|
92
|
+
|
|
93
|
+
assert asyncio.run(main("1", 2)) == 4.0
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Sync code
|
|
97
|
+
```python
|
|
98
|
+
from fastdi import inject, Depends
|
|
99
|
+
|
|
100
|
+
def dependency(a: int) -> int:
|
|
101
|
+
return a
|
|
102
|
+
|
|
103
|
+
@inject
|
|
104
|
+
def main(
|
|
105
|
+
a: int,
|
|
106
|
+
b: int,
|
|
107
|
+
c: int = Depends(dependency)
|
|
108
|
+
) -> float:
|
|
109
|
+
return a + b + c
|
|
110
|
+
|
|
111
|
+
assert main("1", 2) == 4.0
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`@inject` decorator playing multiple roles at the same time:
|
|
115
|
+
|
|
116
|
+
* resolve *Depends* classes
|
|
117
|
+
* cast types according python annotation
|
|
118
|
+
* validate incoming parameters using *pydantic*
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### Features
|
|
123
|
+
Syncronous code is fully supported at this package: without any `async_to_sync`, `run_sync`, `syncify` or any another tricks.
|
|
124
|
+
|
|
125
|
+
Also, *FastDI* casts function return the same way, it can be very felpfull to build your own tools.
|
|
126
|
+
|
|
127
|
+
There is two main defferences from native Fastapi DI System.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### Note
|
|
132
|
+
Library was build by actual **0.95.0 FastAPI** version.
|
|
133
|
+
|
|
134
|
+
If we'll be too far behind, please, contact [me](mailto:diementros@yandex.ru)
|
|
135
|
+
or contubute yourself. Really appreciate your help.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# FastDI
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Documentation: https://lancetnik.github.io/FastDI/
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
FastDI - extracted and cleared from HTTP domain logic Fastapi Dependency Injection System.
|
|
10
|
+
This is a little library, providing you ability to use lovely Fastapi interfaces at your own
|
|
11
|
+
projects or tools.
|
|
12
|
+
|
|
13
|
+
Thanks to [*fastapi*](https://fastapi.tiangolo.com/) and [*pytest*](https://docs.pytest.org/en/7.3.x/) projects for this
|
|
14
|
+
greate functional. This package just a little change Fasapi sources to provide DI functionality pyre-python way.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
There is no way to make Dependency Injection easier
|
|
19
|
+
|
|
20
|
+
You can use this library without any frameworks at **sync** and **async** code both
|
|
21
|
+
|
|
22
|
+
### Async code
|
|
23
|
+
```python
|
|
24
|
+
import asyncio
|
|
25
|
+
|
|
26
|
+
from fastdi import inject, Depends
|
|
27
|
+
|
|
28
|
+
async def dependency(a: int) -> int:
|
|
29
|
+
return a
|
|
30
|
+
|
|
31
|
+
@inject
|
|
32
|
+
async def main(
|
|
33
|
+
a: int,
|
|
34
|
+
b: int,
|
|
35
|
+
c: int = Depends(dependency)
|
|
36
|
+
) -> float:
|
|
37
|
+
return a + b + c
|
|
38
|
+
|
|
39
|
+
assert asyncio.run(main("1", 2)) == 4.0
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Sync code
|
|
43
|
+
```python
|
|
44
|
+
from fastdi import inject, Depends
|
|
45
|
+
|
|
46
|
+
def dependency(a: int) -> int:
|
|
47
|
+
return a
|
|
48
|
+
|
|
49
|
+
@inject
|
|
50
|
+
def main(
|
|
51
|
+
a: int,
|
|
52
|
+
b: int,
|
|
53
|
+
c: int = Depends(dependency)
|
|
54
|
+
) -> float:
|
|
55
|
+
return a + b + c
|
|
56
|
+
|
|
57
|
+
assert main("1", 2) == 4.0
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`@inject` decorator playing multiple roles at the same time:
|
|
61
|
+
|
|
62
|
+
* resolve *Depends* classes
|
|
63
|
+
* cast types according python annotation
|
|
64
|
+
* validate incoming parameters using *pydantic*
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### Features
|
|
69
|
+
Syncronous code is fully supported at this package: without any `async_to_sync`, `run_sync`, `syncify` or any another tricks.
|
|
70
|
+
|
|
71
|
+
Also, *FastDI* casts function return the same way, it can be very felpfull to build your own tools.
|
|
72
|
+
|
|
73
|
+
There is two main defferences from native Fastapi DI System.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### Note
|
|
78
|
+
Library was build by actual **0.95.0 FastAPI** version.
|
|
79
|
+
|
|
80
|
+
If we'll be too far behind, please, contact [me](mailto:diementros@yandex.ru)
|
|
81
|
+
or contubute yourself. Really appreciate your help.
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import Any, Dict, Optional, Tuple, Type, Union
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseConfig
|
|
5
|
+
from pydantic.class_validators import Validator
|
|
6
|
+
from pydantic.fields import (
|
|
7
|
+
SHAPE_FROZENSET,
|
|
8
|
+
SHAPE_LIST,
|
|
9
|
+
SHAPE_SEQUENCE,
|
|
10
|
+
SHAPE_SET,
|
|
11
|
+
SHAPE_TUPLE,
|
|
12
|
+
SHAPE_TUPLE_ELLIPSIS,
|
|
13
|
+
FieldInfo,
|
|
14
|
+
ModelField,
|
|
15
|
+
Required,
|
|
16
|
+
Undefined,
|
|
17
|
+
UndefinedType,
|
|
18
|
+
)
|
|
19
|
+
from pydantic.schema import get_annotation_from_field_info
|
|
20
|
+
from pydantic.typing import ForwardRef, evaluate_forwardref, get_args, get_origin
|
|
21
|
+
from typing_extensions import Annotated
|
|
22
|
+
|
|
23
|
+
from fast_depends import model
|
|
24
|
+
from fast_depends.types import AnyCallable, AnyDict
|
|
25
|
+
|
|
26
|
+
sequence_shapes = {
|
|
27
|
+
SHAPE_LIST,
|
|
28
|
+
SHAPE_SET,
|
|
29
|
+
SHAPE_FROZENSET,
|
|
30
|
+
SHAPE_TUPLE,
|
|
31
|
+
SHAPE_SEQUENCE,
|
|
32
|
+
SHAPE_TUPLE_ELLIPSIS,
|
|
33
|
+
}
|
|
34
|
+
sequence_types = (list, set, tuple)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_dependant(
|
|
38
|
+
*,
|
|
39
|
+
path: str,
|
|
40
|
+
call: AnyCallable,
|
|
41
|
+
name: Optional[str] = None,
|
|
42
|
+
use_cache: bool = True,
|
|
43
|
+
) -> model.Dependant:
|
|
44
|
+
dependant = model.Dependant(
|
|
45
|
+
call=call,
|
|
46
|
+
path=path,
|
|
47
|
+
name=name,
|
|
48
|
+
use_cache=use_cache,
|
|
49
|
+
return_field=None,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
endpoint_signature = get_typed_signature(call)
|
|
53
|
+
signature_params = endpoint_signature.parameters
|
|
54
|
+
|
|
55
|
+
for param in signature_params.values():
|
|
56
|
+
depends, param_field = analyze_param(
|
|
57
|
+
param_name=param.name,
|
|
58
|
+
annotation=param.annotation,
|
|
59
|
+
default=param.default,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if param.name == model.RETURN_FIELD:
|
|
63
|
+
dependant.return_field = param_field
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
if depends is not None:
|
|
67
|
+
sub_dependant = get_param_sub_dependant(
|
|
68
|
+
param_name=param.name,
|
|
69
|
+
depends=depends,
|
|
70
|
+
path=path,
|
|
71
|
+
)
|
|
72
|
+
dependant.dependencies.append(sub_dependant)
|
|
73
|
+
|
|
74
|
+
dependant.params.append(param_field)
|
|
75
|
+
|
|
76
|
+
return dependant
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def analyze_param(
|
|
80
|
+
*,
|
|
81
|
+
param_name: str,
|
|
82
|
+
annotation: Any,
|
|
83
|
+
default: Any,
|
|
84
|
+
) -> Tuple[Any, Optional[model.Depends], Optional[ModelField]]:
|
|
85
|
+
depends = None
|
|
86
|
+
field_info = None
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
annotation is not inspect.Signature.empty
|
|
90
|
+
and get_origin(annotation) is Annotated # type: ignore[comparison-overlap]
|
|
91
|
+
):
|
|
92
|
+
annotated_args = get_args(annotation)
|
|
93
|
+
custom_annotations = [
|
|
94
|
+
arg
|
|
95
|
+
for arg in annotated_args[1:]
|
|
96
|
+
if isinstance(arg, (FieldInfo, model.Depends))
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
custom_annotations = next(iter(custom_annotations), None)
|
|
100
|
+
if isinstance(custom_annotations, FieldInfo):
|
|
101
|
+
field_info = custom_annotations
|
|
102
|
+
assert field_info.default is Undefined or field_info.default is Required, (
|
|
103
|
+
f"`{field_info.__class__.__name__}` default value cannot be set in"
|
|
104
|
+
f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
|
|
105
|
+
)
|
|
106
|
+
field_info.default = Required
|
|
107
|
+
|
|
108
|
+
elif isinstance(custom_annotations, model.Depends): # pragma: no branch
|
|
109
|
+
depends = custom_annotations
|
|
110
|
+
|
|
111
|
+
if isinstance(default, model.Depends):
|
|
112
|
+
assert depends is None, (
|
|
113
|
+
"Cannot specify `Depends` in `Annotated` and default value"
|
|
114
|
+
f" together for {param_name!r}"
|
|
115
|
+
)
|
|
116
|
+
assert field_info is None, (
|
|
117
|
+
"Cannot specify a annotation in `Annotated` and `Depends` as a"
|
|
118
|
+
f" default value together for {param_name!r}"
|
|
119
|
+
)
|
|
120
|
+
depends = default
|
|
121
|
+
|
|
122
|
+
elif isinstance(default, FieldInfo):
|
|
123
|
+
assert field_info is None, (
|
|
124
|
+
"Cannot specify annotations in `Annotated` and default value"
|
|
125
|
+
f" together for {param_name!r}"
|
|
126
|
+
)
|
|
127
|
+
field_info = default
|
|
128
|
+
|
|
129
|
+
if depends is not None:
|
|
130
|
+
field = None
|
|
131
|
+
|
|
132
|
+
if field_info is not None:
|
|
133
|
+
annotation = get_annotation_from_field_info(
|
|
134
|
+
annotation if annotation is not inspect.Signature.empty else Any,
|
|
135
|
+
field_info,
|
|
136
|
+
param_name,
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
field_info = FieldInfo(default=default)
|
|
140
|
+
|
|
141
|
+
alias = field_info.alias or param_name
|
|
142
|
+
|
|
143
|
+
field = create_response_field(
|
|
144
|
+
name=param_name,
|
|
145
|
+
type_=annotation,
|
|
146
|
+
default=field_info.default if depends is None else None,
|
|
147
|
+
alias=alias,
|
|
148
|
+
required=field_info.default in (Required, Undefined, inspect._empty),
|
|
149
|
+
field_info=field_info,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return depends, field
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_typed_signature(call: AnyCallable) -> inspect.Signature:
|
|
156
|
+
signature = inspect.signature(call)
|
|
157
|
+
globalns = getattr(call, "__globals__", {})
|
|
158
|
+
typed_params = [
|
|
159
|
+
inspect.Parameter(
|
|
160
|
+
name=param.name,
|
|
161
|
+
kind=param.kind,
|
|
162
|
+
default=param.default,
|
|
163
|
+
annotation=get_typed_annotation(param.annotation, globalns),
|
|
164
|
+
)
|
|
165
|
+
for param in signature.parameters.values()
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
if signature.return_annotation is not signature.empty:
|
|
169
|
+
typed_params.append(
|
|
170
|
+
inspect.Parameter(
|
|
171
|
+
name=model.RETURN_FIELD,
|
|
172
|
+
kind=inspect._KEYWORD_ONLY,
|
|
173
|
+
annotation=get_typed_annotation(signature.return_annotation, globalns),
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
typed_signature = inspect.Signature(typed_params)
|
|
177
|
+
return typed_signature
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def get_typed_annotation(annotation: Any, globalns: AnyDict) -> Any:
|
|
181
|
+
if isinstance(annotation, str):
|
|
182
|
+
try:
|
|
183
|
+
annotation = ForwardRef(annotation)
|
|
184
|
+
annotation = evaluate_forwardref(annotation, globalns, globalns)
|
|
185
|
+
except Exception:
|
|
186
|
+
raise ValueError( # noqa: B904
|
|
187
|
+
f"Invalid filed annotation! Hint: check that {annotation} is a valid pydantic field type"
|
|
188
|
+
)
|
|
189
|
+
return annotation
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def get_param_sub_dependant(
|
|
193
|
+
*,
|
|
194
|
+
param_name: str,
|
|
195
|
+
depends: model.Depends,
|
|
196
|
+
path: str,
|
|
197
|
+
) -> model.Dependant:
|
|
198
|
+
assert depends.dependency
|
|
199
|
+
return get_sub_dependant(
|
|
200
|
+
depends=depends,
|
|
201
|
+
dependency=depends.dependency,
|
|
202
|
+
path=path,
|
|
203
|
+
name=param_name,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def get_sub_dependant(
|
|
208
|
+
*,
|
|
209
|
+
depends: model.Depends,
|
|
210
|
+
dependency: AnyCallable,
|
|
211
|
+
path: str,
|
|
212
|
+
name: Optional[str] = None,
|
|
213
|
+
) -> model.Dependant:
|
|
214
|
+
sub_dependant = get_dependant(
|
|
215
|
+
path=path,
|
|
216
|
+
call=dependency,
|
|
217
|
+
name=name,
|
|
218
|
+
use_cache=depends.use_cache,
|
|
219
|
+
)
|
|
220
|
+
return sub_dependant
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def create_response_field(
|
|
224
|
+
name: str,
|
|
225
|
+
type_: Type[Any],
|
|
226
|
+
class_validators: Optional[Dict[str, Validator]] = None,
|
|
227
|
+
default: Optional[Any] = None,
|
|
228
|
+
required: Union[bool, UndefinedType] = True,
|
|
229
|
+
model_config: Type[BaseConfig] = BaseConfig,
|
|
230
|
+
field_info: Optional[FieldInfo] = None,
|
|
231
|
+
alias: Optional[str] = None,
|
|
232
|
+
) -> ModelField:
|
|
233
|
+
"""
|
|
234
|
+
Create a new response field. Raises if type_ is invalid.
|
|
235
|
+
"""
|
|
236
|
+
try:
|
|
237
|
+
return ModelField(
|
|
238
|
+
name=name,
|
|
239
|
+
type_=type_ if type_ is not inspect._empty else Any,
|
|
240
|
+
class_validators=class_validators or {},
|
|
241
|
+
default=default,
|
|
242
|
+
required=required,
|
|
243
|
+
model_config=model_config,
|
|
244
|
+
alias=alias,
|
|
245
|
+
field_info=field_info or FieldInfo(),
|
|
246
|
+
)
|
|
247
|
+
except RuntimeError: # pragma: no cover
|
|
248
|
+
raise ValueError( # noqa: B904
|
|
249
|
+
f"Invalid args for response field! Hint: check that {type_} is a valid pydantic field type"
|
|
250
|
+
)
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
import inspect
|
|
4
|
+
from contextlib import AsyncExitStack, ExitStack, asynccontextmanager, contextmanager
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
AsyncGenerator,
|
|
9
|
+
Callable,
|
|
10
|
+
ContextManager,
|
|
11
|
+
Dict,
|
|
12
|
+
List,
|
|
13
|
+
Mapping,
|
|
14
|
+
Optional,
|
|
15
|
+
Sequence,
|
|
16
|
+
Tuple,
|
|
17
|
+
TypeVar,
|
|
18
|
+
cast,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
import anyio
|
|
22
|
+
from pydantic.error_wrappers import ErrorWrapper
|
|
23
|
+
from pydantic.errors import MissingError
|
|
24
|
+
from pydantic.fields import ModelField
|
|
25
|
+
|
|
26
|
+
from fast_depends.construct import get_dependant
|
|
27
|
+
from fast_depends.model import Dependant
|
|
28
|
+
from fast_depends.types import AnyCallable, AnyDict, P
|
|
29
|
+
|
|
30
|
+
T = TypeVar("T")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def solve_dependencies_async(
|
|
34
|
+
*,
|
|
35
|
+
dependant: Dependant,
|
|
36
|
+
stack: AsyncExitStack,
|
|
37
|
+
body: Optional[AnyDict] = None,
|
|
38
|
+
dependency_overrides_provider: Optional[Any] = None,
|
|
39
|
+
dependency_cache: Optional[Dict[Tuple[AnyCallable, Tuple[str]], Any]] = None,
|
|
40
|
+
) -> Tuple[
|
|
41
|
+
Dict[str, Any],
|
|
42
|
+
List[ErrorWrapper],
|
|
43
|
+
Dict[Tuple[AnyCallable, Tuple[str]], Any],
|
|
44
|
+
]:
|
|
45
|
+
errors: List[ErrorWrapper] = []
|
|
46
|
+
|
|
47
|
+
dependency_cache = dependency_cache or {}
|
|
48
|
+
|
|
49
|
+
sub_dependant: Dependant
|
|
50
|
+
for sub_dependant in dependant.dependencies:
|
|
51
|
+
sub_dependant.call = cast(AnyCallable, sub_dependant.call)
|
|
52
|
+
sub_dependant.cache_key = cast(
|
|
53
|
+
Tuple[AnyCallable, Tuple[str]], sub_dependant.cache_key
|
|
54
|
+
)
|
|
55
|
+
call = sub_dependant.call
|
|
56
|
+
use_sub_dependant = sub_dependant
|
|
57
|
+
if (
|
|
58
|
+
dependency_overrides_provider
|
|
59
|
+
and dependency_overrides_provider.dependency_overrides
|
|
60
|
+
):
|
|
61
|
+
call = getattr(
|
|
62
|
+
dependency_overrides_provider, "dependency_overrides", {}
|
|
63
|
+
).get(sub_dependant.call)
|
|
64
|
+
if call is not None: # pragma: no branch
|
|
65
|
+
use_sub_dependant = get_dependant(
|
|
66
|
+
path=sub_dependant.path,
|
|
67
|
+
call=call,
|
|
68
|
+
name=sub_dependant.name,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
solved_result = await solve_dependencies_async(
|
|
72
|
+
dependant=use_sub_dependant,
|
|
73
|
+
body=body,
|
|
74
|
+
dependency_overrides_provider=dependency_overrides_provider,
|
|
75
|
+
dependency_cache=dependency_cache,
|
|
76
|
+
stack=stack,
|
|
77
|
+
)
|
|
78
|
+
(
|
|
79
|
+
sub_values,
|
|
80
|
+
sub_errors,
|
|
81
|
+
sub_dependency_cache,
|
|
82
|
+
) = solved_result
|
|
83
|
+
|
|
84
|
+
dependency_cache.update(sub_dependency_cache)
|
|
85
|
+
|
|
86
|
+
if sub_errors:
|
|
87
|
+
errors.extend(sub_errors)
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
use_sub_dependant.use_cache
|
|
92
|
+
and use_sub_dependant.cache_key in dependency_cache
|
|
93
|
+
):
|
|
94
|
+
solved = dependency_cache[use_sub_dependant.cache_key]
|
|
95
|
+
elif is_gen_callable(call) or is_async_gen_callable(call):
|
|
96
|
+
solved, sub_errors = use_sub_dependant.cast_response(
|
|
97
|
+
await solve_generator_async(
|
|
98
|
+
call=call, stack=stack, sub_values=sub_values
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
solved, sub_errors = use_sub_dependant.cast_response(
|
|
103
|
+
await run_async(dependant=use_sub_dependant, values=sub_values)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if sub_errors:
|
|
107
|
+
errors.append(sub_errors)
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
if use_sub_dependant.name is not None: # pragma: no branch
|
|
111
|
+
body[use_sub_dependant.name] = solved
|
|
112
|
+
|
|
113
|
+
if use_sub_dependant.cache_key not in dependency_cache:
|
|
114
|
+
dependency_cache[use_sub_dependant.cache_key] = solved
|
|
115
|
+
|
|
116
|
+
params, main_errors = params_to_args(dependant.params, body or {})
|
|
117
|
+
errors.extend(main_errors)
|
|
118
|
+
return params, errors, dependency_cache
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def solve_dependencies_sync(
|
|
122
|
+
*,
|
|
123
|
+
dependant: Dependant,
|
|
124
|
+
stack: ExitStack,
|
|
125
|
+
body: Optional[AnyDict] = None,
|
|
126
|
+
dependency_overrides_provider: Optional[Any] = None,
|
|
127
|
+
dependency_cache: Optional[Dict[Tuple[AnyCallable, Tuple[str]], Any]] = None,
|
|
128
|
+
) -> Tuple[
|
|
129
|
+
Dict[str, Any],
|
|
130
|
+
List[ErrorWrapper],
|
|
131
|
+
Dict[Tuple[AnyCallable, Tuple[str]], Any],
|
|
132
|
+
]:
|
|
133
|
+
assert not is_coroutine_callable(dependant.call) and not is_async_gen_callable(
|
|
134
|
+
dependant.call
|
|
135
|
+
), f"You can't call async `{dependant.call.__name__}` at sync code"
|
|
136
|
+
|
|
137
|
+
errors: List[ErrorWrapper] = []
|
|
138
|
+
|
|
139
|
+
dependency_cache = dependency_cache or {}
|
|
140
|
+
|
|
141
|
+
sub_dependant: Dependant
|
|
142
|
+
for sub_dependant in dependant.dependencies:
|
|
143
|
+
sub_dependant.call = cast(AnyCallable, sub_dependant.call)
|
|
144
|
+
sub_dependant.cache_key = cast(
|
|
145
|
+
Tuple[AnyCallable, Tuple[str]], sub_dependant.cache_key
|
|
146
|
+
)
|
|
147
|
+
call = sub_dependant.call
|
|
148
|
+
use_sub_dependant = sub_dependant
|
|
149
|
+
if (
|
|
150
|
+
dependency_overrides_provider
|
|
151
|
+
and dependency_overrides_provider.dependency_overrides
|
|
152
|
+
):
|
|
153
|
+
call = getattr(
|
|
154
|
+
dependency_overrides_provider, "dependency_overrides", {}
|
|
155
|
+
).get(sub_dependant.call)
|
|
156
|
+
if call is not None: # pragma: no branch
|
|
157
|
+
use_sub_dependant = get_dependant(
|
|
158
|
+
path=sub_dependant.path,
|
|
159
|
+
call=call,
|
|
160
|
+
name=sub_dependant.name,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
solved_result = solve_dependencies_sync(
|
|
164
|
+
dependant=use_sub_dependant,
|
|
165
|
+
body=body,
|
|
166
|
+
stack=stack,
|
|
167
|
+
dependency_overrides_provider=dependency_overrides_provider,
|
|
168
|
+
dependency_cache=dependency_cache,
|
|
169
|
+
)
|
|
170
|
+
(
|
|
171
|
+
sub_values,
|
|
172
|
+
sub_errors,
|
|
173
|
+
sub_dependency_cache,
|
|
174
|
+
) = solved_result
|
|
175
|
+
|
|
176
|
+
dependency_cache.update(sub_dependency_cache)
|
|
177
|
+
|
|
178
|
+
if sub_errors:
|
|
179
|
+
errors.extend(sub_errors)
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
use_sub_dependant.use_cache
|
|
184
|
+
and use_sub_dependant.cache_key in dependency_cache
|
|
185
|
+
):
|
|
186
|
+
solved = dependency_cache[sub_dependant.cache_key]
|
|
187
|
+
elif is_gen_callable(call):
|
|
188
|
+
solved, sub_errors = use_sub_dependant.cast_response(
|
|
189
|
+
solve_generator_sync(call=call, stack=stack, sub_values=sub_values)
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
solved, sub_errors = use_sub_dependant.cast_response(call(**sub_values))
|
|
193
|
+
|
|
194
|
+
if sub_errors:
|
|
195
|
+
errors.append(sub_errors)
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
if use_sub_dependant.name is not None: # pragma: no branch
|
|
199
|
+
body[sub_dependant.name] = solved
|
|
200
|
+
|
|
201
|
+
if use_sub_dependant.cache_key not in dependency_cache:
|
|
202
|
+
dependency_cache[use_sub_dependant.cache_key] = solved
|
|
203
|
+
|
|
204
|
+
params, main_errors = params_to_args(dependant.params, body or {})
|
|
205
|
+
errors.extend(main_errors)
|
|
206
|
+
return params, errors, dependency_cache
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def is_async_gen_callable(call: Callable[..., Any]) -> bool:
|
|
210
|
+
if inspect.isasyncgenfunction(call):
|
|
211
|
+
return True
|
|
212
|
+
dunder_call = getattr(call, "__call__", None) # noqa: B004
|
|
213
|
+
return inspect.isasyncgenfunction(dunder_call)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def is_gen_callable(call: Callable[..., Any]) -> bool:
|
|
217
|
+
if inspect.isgeneratorfunction(call):
|
|
218
|
+
return True
|
|
219
|
+
dunder_call = getattr(call, "__call__", None) # noqa: B004
|
|
220
|
+
return inspect.isgeneratorfunction(dunder_call)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def is_coroutine_callable(call: AnyCallable) -> bool:
|
|
224
|
+
if inspect.isroutine(call):
|
|
225
|
+
return inspect.iscoroutinefunction(call)
|
|
226
|
+
if inspect.isclass(call):
|
|
227
|
+
return False
|
|
228
|
+
call = getattr(call, "__call__", None) # noqa: B004
|
|
229
|
+
return inspect.iscoroutinefunction(call)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
async def solve_generator_async(
|
|
233
|
+
*, call: Callable[..., Any], stack: AsyncExitStack, sub_values: Dict[str, Any]
|
|
234
|
+
) -> Any:
|
|
235
|
+
if is_gen_callable(call):
|
|
236
|
+
cm = contextmanager_in_threadpool(contextmanager(call)(**sub_values))
|
|
237
|
+
elif is_async_gen_callable(call): # pragma: no branch
|
|
238
|
+
cm = asynccontextmanager(call)(**sub_values)
|
|
239
|
+
return await stack.enter_async_context(cm)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def solve_generator_sync(
|
|
243
|
+
*, call: Callable[..., Any], stack: ExitStack, sub_values: Dict[str, Any]
|
|
244
|
+
) -> Any:
|
|
245
|
+
cm = contextmanager(call)(**sub_values)
|
|
246
|
+
return stack.enter_context(cm)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
async def run_async(*, dependant: Dependant, values: AnyDict) -> Any:
|
|
250
|
+
assert dependant.call is not None, "dependant.call must be a function"
|
|
251
|
+
if asyncio.iscoroutinefunction(dependant.call):
|
|
252
|
+
return await dependant.call(**values)
|
|
253
|
+
else:
|
|
254
|
+
return await run_in_threadpool(dependant.call, **values)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
async def run_in_threadpool(
|
|
258
|
+
func: Callable[P, T], *args: P.args, **kwargs: P.kwargs
|
|
259
|
+
) -> T:
|
|
260
|
+
if kwargs: # pragma: no cover
|
|
261
|
+
func = functools.partial(func, **kwargs)
|
|
262
|
+
return await anyio.to_thread.run_sync(func, *args)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@asynccontextmanager
|
|
266
|
+
async def contextmanager_in_threadpool(
|
|
267
|
+
cm: ContextManager[T],
|
|
268
|
+
) -> AsyncGenerator[T, None]:
|
|
269
|
+
exit_limiter = anyio.CapacityLimiter(1)
|
|
270
|
+
try:
|
|
271
|
+
yield await run_in_threadpool(cm.__enter__)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
ok = bool(
|
|
274
|
+
await anyio.to_thread.run_sync(
|
|
275
|
+
cm.__exit__, type(e), e, None, limiter=exit_limiter
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
if not ok: # pragma: no branch
|
|
279
|
+
raise e
|
|
280
|
+
else:
|
|
281
|
+
await anyio.to_thread.run_sync(
|
|
282
|
+
cm.__exit__, None, None, None, limiter=exit_limiter
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def params_to_args(
|
|
287
|
+
required_params: Sequence[ModelField],
|
|
288
|
+
received_params: Mapping[str, Any],
|
|
289
|
+
) -> Tuple[AnyDict, List[ErrorWrapper]]:
|
|
290
|
+
values: AnyDict = {}
|
|
291
|
+
errors: List[ErrorWrapper] = []
|
|
292
|
+
for field in required_params:
|
|
293
|
+
value = received_params.get(field.alias)
|
|
294
|
+
if value is None:
|
|
295
|
+
if field.required:
|
|
296
|
+
errors.append(ErrorWrapper(MissingError(), loc=(field.alias,)))
|
|
297
|
+
else:
|
|
298
|
+
values[field.name] = deepcopy(field.default)
|
|
299
|
+
continue
|
|
300
|
+
|
|
301
|
+
v_, errors_ = field.validate(value, values, loc=(field.alias,))
|
|
302
|
+
if isinstance(errors_, ErrorWrapper):
|
|
303
|
+
errors.append(errors_)
|
|
304
|
+
elif isinstance(errors_, list): # pragma: no cover
|
|
305
|
+
errors.extend(errors_)
|
|
306
|
+
else:
|
|
307
|
+
values[field.name] = v_
|
|
308
|
+
return values, errors
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from typing import Any, List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from pydantic.error_wrappers import ErrorList
|
|
4
|
+
from pydantic.fields import ModelField
|
|
5
|
+
|
|
6
|
+
from fast_depends.types import AnyCallable
|
|
7
|
+
|
|
8
|
+
RETURN_FIELD = "custom_return"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Dependant:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
*,
|
|
15
|
+
call: Optional[AnyCallable] = None,
|
|
16
|
+
params: Optional[List[ModelField]] = None,
|
|
17
|
+
return_field: Optional[ModelField] = None,
|
|
18
|
+
dependencies: Optional[List["Dependant"]] = None,
|
|
19
|
+
use_cache: bool = True,
|
|
20
|
+
path: Optional[str] = None,
|
|
21
|
+
name: Optional[str] = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
self.params = params or []
|
|
24
|
+
self.return_field = return_field
|
|
25
|
+
self.dependencies = dependencies or []
|
|
26
|
+
self.call = call
|
|
27
|
+
self.use_cache = use_cache
|
|
28
|
+
# Parent argument name at subdependency
|
|
29
|
+
self.name = name
|
|
30
|
+
# Store the path to be able to re-generate a dependable from it in overrides
|
|
31
|
+
self.path = path
|
|
32
|
+
# Save the cache key at creation to optimize performance
|
|
33
|
+
self.cache_key = (self.call,)
|
|
34
|
+
|
|
35
|
+
def cast_response(self, response: Any) -> Tuple[Optional[Any], Optional[ErrorList]]:
|
|
36
|
+
if self.return_field is None:
|
|
37
|
+
return response, []
|
|
38
|
+
return self.return_field.validate(response, {}, loc=RETURN_FIELD)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Depends:
|
|
42
|
+
def __init__(
|
|
43
|
+
self, dependency: Optional[AnyCallable] = None, *, use_cache: bool = True
|
|
44
|
+
) -> None:
|
|
45
|
+
self.dependency = dependency
|
|
46
|
+
self.use_cache = use_cache
|
|
47
|
+
|
|
48
|
+
def __repr__(self) -> str:
|
|
49
|
+
attr = getattr(self.dependency, "__name__", type(self.dependency).__name__)
|
|
50
|
+
cache = "" if self.use_cache else ", use_cache=False"
|
|
51
|
+
return f"{self.__class__.__name__}({attr}{cache})"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from fast_depends.types import AnyCallable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Provider:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.dependency_overrides: Dict[AnyCallable, AnyCallable] = {}
|
|
9
|
+
|
|
10
|
+
def override(self, original: AnyCallable, override: AnyCallable) -> None:
|
|
11
|
+
self.dependency_overrides[original] = override
|
|
12
|
+
|
|
13
|
+
def clear(self) -> None:
|
|
14
|
+
self.dependency_overrides = {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
dependency_provider = Provider()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from typing import Any, Callable, Dict, TypeVar
|
|
2
|
+
from typing_extensions import ParamSpec, TypeAlias
|
|
3
|
+
|
|
4
|
+
P = ParamSpec("P")
|
|
5
|
+
|
|
6
|
+
AnyCallable = TypeVar("AnyCallable", bound=Callable[..., Any])
|
|
7
|
+
DecoratedCallable: TypeAlias = AnyCallable
|
|
8
|
+
AnyDict = TypeVar("AnyDict", bound=Dict[str, Any])
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from contextlib import AsyncExitStack, ExitStack
|
|
2
|
+
from functools import partial, wraps
|
|
3
|
+
from typing import Any, Callable, Optional, TypeVar
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ValidationError, create_model
|
|
6
|
+
|
|
7
|
+
from fast_depends import model
|
|
8
|
+
from fast_depends.construct import get_dependant
|
|
9
|
+
from fast_depends.injector import (
|
|
10
|
+
is_coroutine_callable,
|
|
11
|
+
run_async,
|
|
12
|
+
solve_dependencies_async,
|
|
13
|
+
solve_dependencies_sync,
|
|
14
|
+
)
|
|
15
|
+
from fast_depends.provider import dependency_provider
|
|
16
|
+
from fast_depends.types import AnyCallable, P
|
|
17
|
+
from fast_depends.utils import args_to_kwargs
|
|
18
|
+
|
|
19
|
+
T = TypeVar("T")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def Depends( # noqa: N802
|
|
23
|
+
dependency: Optional[AnyCallable] = None, *, use_cache: bool = True
|
|
24
|
+
) -> Any:
|
|
25
|
+
return model.Depends(dependency=dependency, use_cache=use_cache)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def inject(
|
|
29
|
+
func: Callable[P, T],
|
|
30
|
+
*,
|
|
31
|
+
dependency_overrides_provider: Optional[Any] = dependency_provider,
|
|
32
|
+
) -> Callable[P, T]:
|
|
33
|
+
dependant = get_dependant(call=func, path=func.__name__)
|
|
34
|
+
error_model = create_model(func.__name__)
|
|
35
|
+
|
|
36
|
+
if is_coroutine_callable(func) is True:
|
|
37
|
+
f = async_typed_wrapper
|
|
38
|
+
else:
|
|
39
|
+
f = sync_typed_wrapper
|
|
40
|
+
|
|
41
|
+
return wraps(func)(
|
|
42
|
+
partial(
|
|
43
|
+
f,
|
|
44
|
+
dependant=dependant,
|
|
45
|
+
error_model=error_model,
|
|
46
|
+
dependency_overrides_provider=dependency_overrides_provider,
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def async_typed_wrapper(
|
|
52
|
+
*args: P.args,
|
|
53
|
+
dependant: model.Dependant,
|
|
54
|
+
error_model: BaseModel,
|
|
55
|
+
dependency_overrides_provider: Optional[Any],
|
|
56
|
+
**kwargs: P.kwargs,
|
|
57
|
+
) -> Any:
|
|
58
|
+
kwargs = args_to_kwargs((x.name for x in dependant.params), *args, **kwargs)
|
|
59
|
+
|
|
60
|
+
async with AsyncExitStack() as stack:
|
|
61
|
+
solved_result, errors, _ = await solve_dependencies_async(
|
|
62
|
+
body=kwargs,
|
|
63
|
+
dependant=dependant,
|
|
64
|
+
stack=stack,
|
|
65
|
+
dependency_overrides_provider=dependency_overrides_provider,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if errors:
|
|
69
|
+
raise ValidationError(errors, error_model)
|
|
70
|
+
|
|
71
|
+
v, errors = dependant.cast_response(
|
|
72
|
+
await run_async(dependant=dependant, values=solved_result)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if errors:
|
|
76
|
+
raise ValidationError(errors, error_model)
|
|
77
|
+
|
|
78
|
+
return v
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def sync_typed_wrapper(
|
|
82
|
+
*args: P.args,
|
|
83
|
+
dependant: model.Dependant,
|
|
84
|
+
error_model: BaseModel,
|
|
85
|
+
dependency_overrides_provider: Optional[Any],
|
|
86
|
+
**kwargs: P.kwargs,
|
|
87
|
+
) -> Any:
|
|
88
|
+
kwargs = args_to_kwargs((x.name for x in dependant.params), *args, **kwargs)
|
|
89
|
+
|
|
90
|
+
with ExitStack() as stack:
|
|
91
|
+
solved_result, errors, _ = solve_dependencies_sync(
|
|
92
|
+
body=kwargs,
|
|
93
|
+
dependant=dependant,
|
|
94
|
+
stack=stack,
|
|
95
|
+
dependency_overrides_provider=dependency_overrides_provider,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if errors:
|
|
99
|
+
raise ValidationError(errors, error_model)
|
|
100
|
+
|
|
101
|
+
v, errors = dependant.cast_response(dependant.call(**solved_result))
|
|
102
|
+
|
|
103
|
+
if errors:
|
|
104
|
+
raise ValidationError(errors, error_model)
|
|
105
|
+
|
|
106
|
+
return v
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Sequence
|
|
2
|
+
|
|
3
|
+
from fast_depends.types import AnyDict, P
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def args_to_kwargs(
|
|
7
|
+
arguments: Sequence[str], *args: P.args, **kwargs: P.kwargs
|
|
8
|
+
) -> AnyDict:
|
|
9
|
+
if not args:
|
|
10
|
+
return kwargs
|
|
11
|
+
|
|
12
|
+
unused = filter(lambda x: x not in kwargs, arguments)
|
|
13
|
+
|
|
14
|
+
return dict((*zip(unused, args), *kwargs.items()))
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fast-depends"
|
|
7
|
+
description = "FastDepends - extracted and cleared from HTTP domain Fastapi Dependency Injection System"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Pastukhov Nikita", email = "diementros@yandex.ru" },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
keywords = ["fastapi", "dependency injection"]
|
|
14
|
+
|
|
15
|
+
requires-python = ">=3.7"
|
|
16
|
+
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python",
|
|
21
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
24
|
+
"Programming Language :: Python :: 3.7",
|
|
25
|
+
"Programming Language :: Python :: 3.8",
|
|
26
|
+
"Programming Language :: Python :: 3.9",
|
|
27
|
+
"Programming Language :: Python :: 3.10",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
31
|
+
"Topic :: Software Development :: Libraries",
|
|
32
|
+
"Topic :: Software Development",
|
|
33
|
+
"Typing :: Typed",
|
|
34
|
+
"Intended Audience :: Developers",
|
|
35
|
+
"Intended Audience :: Information Technology",
|
|
36
|
+
"Framework :: Pydantic",
|
|
37
|
+
"Framework :: Pydantic :: 1",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
dependencies = [
|
|
41
|
+
"pydantic>=1.8",
|
|
42
|
+
"anyio",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
dynamic = ["version"]
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Homepage = "https://lancetnik.github.io/FastDI/"
|
|
49
|
+
Documentation = "https://lancetnik.github.io/FastDI/"
|
|
50
|
+
Tracker = "https://github.com/Lancetnik/FastDI/issues"
|
|
51
|
+
Source = "https://github.com/Lancetnik/FastDI"
|
|
52
|
+
|
|
53
|
+
[project.optional-dependencies]
|
|
54
|
+
test = [
|
|
55
|
+
"coverage[toml]>=7.2",
|
|
56
|
+
"pytest>=7",
|
|
57
|
+
"pytest-asyncio>=0.21",
|
|
58
|
+
"pytest-xdist[psutil]",
|
|
59
|
+
|
|
60
|
+
"asyncmock; python_version < '3.8'",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
doc = [
|
|
64
|
+
"mkdocs-material >=8.1.4,<9.0.0",
|
|
65
|
+
"mkdocstrings[python]>=0.18",
|
|
66
|
+
"mdx-include >=1.4.1,<2.0.0",
|
|
67
|
+
"mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
dev = [
|
|
71
|
+
"FastDepends[test]",
|
|
72
|
+
"FastDepends[doc]",
|
|
73
|
+
|
|
74
|
+
"mypy>=1.1",
|
|
75
|
+
"black>=23.3.0",
|
|
76
|
+
"isort>=5",
|
|
77
|
+
"ruff>=0.0.260",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
[tool.hatch.metadata]
|
|
81
|
+
allow-direct-references = true
|
|
82
|
+
allow-ambiguous-features = true
|
|
83
|
+
|
|
84
|
+
[tool.hatch.version]
|
|
85
|
+
path = "fast_depends/__about__.py"
|
|
86
|
+
|
|
87
|
+
[tool.hatch.build]
|
|
88
|
+
skip-excluded-dirs = true
|
|
89
|
+
exclude = [
|
|
90
|
+
"/tests",
|
|
91
|
+
"/docs",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
[tool.hatch.envs.default]
|
|
95
|
+
python = "3.11"
|
|
96
|
+
skip-install = false
|
|
97
|
+
features = [
|
|
98
|
+
"dev",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
[tool.hatch.envs.test]
|
|
102
|
+
features = [
|
|
103
|
+
"test",
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
[tool.hatch.envs.test.scripts]
|
|
107
|
+
run = "pytest -q"
|
|
108
|
+
|
|
109
|
+
[[tool.hatch.envs.test.matrix]]
|
|
110
|
+
python = ["37", "38", "39", "310", "311"]
|
|
111
|
+
|
|
112
|
+
[tool.hatch.envs.test-last]
|
|
113
|
+
python = "3.11"
|
|
114
|
+
features = [
|
|
115
|
+
"test",
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
[tool.hatch.envs.test-last.scripts]
|
|
119
|
+
run = "pytest -q"
|
|
120
|
+
cov = "bash ./scripts/test-cov.sh -v"
|
|
121
|
+
|
|
122
|
+
[tool.mypy]
|
|
123
|
+
strict = true
|
|
124
|
+
ignore_missing_imports = true
|
|
125
|
+
|
|
126
|
+
[tool.isort]
|
|
127
|
+
profile = "black"
|
|
128
|
+
known_third_party = ["pydantic", "anyio"]
|
|
129
|
+
|
|
130
|
+
[tool.ruff]
|
|
131
|
+
select = [
|
|
132
|
+
"E", # pycodestyle errors
|
|
133
|
+
"W", # pycodestyle warnings
|
|
134
|
+
"F", # pyflakes
|
|
135
|
+
"I", # isort
|
|
136
|
+
"C", # flake8-comprehensions
|
|
137
|
+
"B", # flake8-bugbear
|
|
138
|
+
]
|
|
139
|
+
ignore = [
|
|
140
|
+
"E501", # line too long, handled by black
|
|
141
|
+
"C901", # too complex
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
[tool.pytest.ini_options]
|
|
145
|
+
minversion = "7.0"
|
|
146
|
+
addopts = "-q"
|
|
147
|
+
testpaths = [
|
|
148
|
+
"tests",
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
[tool.coverage.run]
|
|
152
|
+
parallel = true
|
|
153
|
+
branch = true
|
|
154
|
+
concurrency = [
|
|
155
|
+
"multiprocessing",
|
|
156
|
+
"thread"
|
|
157
|
+
]
|
|
158
|
+
source = [
|
|
159
|
+
"fastdi",
|
|
160
|
+
"tests"
|
|
161
|
+
]
|
|
162
|
+
context = '${CONTEXT}'
|
|
163
|
+
omit = [
|
|
164
|
+
"**/__init__.py",
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
[tool.coverage.report]
|
|
168
|
+
show_missing = true
|
|
169
|
+
skip_empty = true
|
|
170
|
+
exclude_lines = [
|
|
171
|
+
".*# pragma: no cover",
|
|
172
|
+
"if __name__ == .__main__.:",
|
|
173
|
+
"self.logger",
|
|
174
|
+
"def __repr__",
|
|
175
|
+
"lambda: None",
|
|
176
|
+
"from .*",
|
|
177
|
+
"import .*",
|
|
178
|
+
'@(abc\.)?abstractmethod',
|
|
179
|
+
"raise NotImplementedError",
|
|
180
|
+
'raise AssertionError',
|
|
181
|
+
'raise ValueError',
|
|
182
|
+
'logger\..*',
|
|
183
|
+
"pass",
|
|
184
|
+
'\.\.\.',
|
|
185
|
+
]
|
|
186
|
+
omit = [
|
|
187
|
+
'*/__about__.py',
|
|
188
|
+
'*/__main__.py',
|
|
189
|
+
'*/__init__.py',
|
|
190
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
coverage run -m pytest "$@"
|