fastapi-boot 0.0.1__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_boot/DI.py +268 -0
- fastapi_boot/__init__.py +18 -0
- fastapi_boot/const.py +120 -0
- fastapi_boot/helper.py +301 -0
- fastapi_boot/model.py +253 -0
- fastapi_boot/routing.py +353 -0
- fastapi_boot/util.py +7 -0
- fastapi_boot-0.0.1.dist-info/LICENSE +21 -0
- fastapi_boot-0.0.1.dist-info/METADATA +131 -0
- fastapi_boot-0.0.1.dist-info/RECORD +12 -0
- fastapi_boot-0.0.1.dist-info/WHEEL +5 -0
- fastapi_boot-0.0.1.dist-info/top_level.txt +1 -0
fastapi_boot/DI.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from inspect import Parameter, _empty, signature,isclass
|
|
4
|
+
from typing import Annotated, Generic, TypeVar, get_args, get_origin, no_type_check, overload
|
|
5
|
+
|
|
6
|
+
from fastapi_boot.const import NameDepRecord, TypeDepRecord, app_store, dep_store
|
|
7
|
+
from fastapi_boot.model import AppRecord, DependencyNotFoundException, InjectFailException
|
|
8
|
+
from fastapi_boot.util import get_call_filename
|
|
9
|
+
|
|
10
|
+
T = TypeVar('T')
|
|
11
|
+
|
|
12
|
+
# ------------------------------------------------------- public ------------------------------------------------------ #
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _inject(app_record: AppRecord, tp: type[T], name: str | None = None) -> T:
|
|
16
|
+
"""inject dependency by type or name
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
app_record (AppRecord)
|
|
20
|
+
tp (type[T])
|
|
21
|
+
name (str | None)
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
T: instance
|
|
25
|
+
"""
|
|
26
|
+
start = time.time()
|
|
27
|
+
while True:
|
|
28
|
+
if res := dep_store.inject_by_type(tp) if name is None else dep_store.inject_by_name(name, tp):
|
|
29
|
+
return res
|
|
30
|
+
time.sleep(app_record.inject_retry_step)
|
|
31
|
+
if time.time() - start > app_record.inject_timeout:
|
|
32
|
+
name_info=f"with name '{name}'" if name is not None else ''
|
|
33
|
+
raise DependencyNotFoundException(f"Dependency '{tp}' {name_info} not found")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def inject_params_deps(app_record: AppRecord, params: list[Parameter]):
|
|
37
|
+
"""find dependencies of params
|
|
38
|
+
Args:
|
|
39
|
+
app_record (AppRecord)
|
|
40
|
+
params (list[Parameter]): param list without self
|
|
41
|
+
"""
|
|
42
|
+
params_dict = {}
|
|
43
|
+
for param in params:
|
|
44
|
+
# 1. with default
|
|
45
|
+
if param.default != _empty:
|
|
46
|
+
params_dict.update({param.name: param.default})
|
|
47
|
+
else:
|
|
48
|
+
# 2. no default
|
|
49
|
+
if param.annotation == _empty:
|
|
50
|
+
# 2.1 not annotation
|
|
51
|
+
raise InjectFailException(
|
|
52
|
+
f'The annotation of param {param.name} is missing, add an annotation or give it a default value'
|
|
53
|
+
)
|
|
54
|
+
# 2.2. with annotation
|
|
55
|
+
if get_origin(param.annotation) == Annotated:
|
|
56
|
+
# 2.2.1 Annotated
|
|
57
|
+
tp, name, *_ = get_args(param.annotation)
|
|
58
|
+
if isinstance(name, str):
|
|
59
|
+
# 2.2.1.1 name is str, as dependency's name to _inject
|
|
60
|
+
params_dict.update({param.name: _inject(app_record, tp, name)})
|
|
61
|
+
else:
|
|
62
|
+
# 2.2.1.2 name is not str
|
|
63
|
+
params_dict.update({param.name: _inject(app_record, tp)})
|
|
64
|
+
else:
|
|
65
|
+
# 2.2.2 other
|
|
66
|
+
params_dict.update({param.name: _inject(app_record, param.annotation)})
|
|
67
|
+
return params_dict
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------- Bean ------------------------------------------------------- #
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def collect_bean(app_record: AppRecord, func: Callable, name: str | None = None):
|
|
74
|
+
"""
|
|
75
|
+
1. run function decorated by Bean decorator
|
|
76
|
+
2. add the result to deps_store
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
app_record (AppRecord)
|
|
80
|
+
func (Callable): func
|
|
81
|
+
name (str | None, optional): name of dep
|
|
82
|
+
"""
|
|
83
|
+
params: list[Parameter] = list(signature(func).parameters.values())
|
|
84
|
+
return_annotations = signature(func).return_annotation
|
|
85
|
+
instance = func(**inject_params_deps(app_record, params))
|
|
86
|
+
tp = return_annotations if return_annotations != _empty else type(instance)
|
|
87
|
+
if name is None:
|
|
88
|
+
dep_store.add_dep_by_type(TypeDepRecord(tp, instance))
|
|
89
|
+
else:
|
|
90
|
+
dep_store.add_dep_by_name(NameDepRecord(tp, instance, name))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@overload
|
|
94
|
+
def Bean(value: str): ...
|
|
95
|
+
@overload
|
|
96
|
+
def Bean(value: Callable[..., T]): ...
|
|
97
|
+
@no_type_check
|
|
98
|
+
def Bean(value: str | Callable[..., T]) -> Callable[..., T]:
|
|
99
|
+
"""A decorator, will collect the return value of the func decorated by Bean
|
|
100
|
+
# Example
|
|
101
|
+
1. collect by `type`
|
|
102
|
+
```python
|
|
103
|
+
@dataclass
|
|
104
|
+
class Foo:
|
|
105
|
+
bar: str
|
|
106
|
+
|
|
107
|
+
@Bean
|
|
108
|
+
def _():
|
|
109
|
+
return Foo('baz')
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
2. collect by `name`
|
|
113
|
+
```python
|
|
114
|
+
class User(BaseModel):
|
|
115
|
+
name: str = Field(max_length=20)
|
|
116
|
+
age: int = Field(gt=0)
|
|
117
|
+
|
|
118
|
+
@Bean('user1')
|
|
119
|
+
def _():
|
|
120
|
+
return User(name='zs', age=20)
|
|
121
|
+
|
|
122
|
+
@Bean('user2)
|
|
123
|
+
def _():
|
|
124
|
+
return User(name='zs', age=21)
|
|
125
|
+
```
|
|
126
|
+
"""
|
|
127
|
+
app_record = app_store.get(get_call_filename())
|
|
128
|
+
|
|
129
|
+
if callable(value):
|
|
130
|
+
collect_bean(app_record, value)
|
|
131
|
+
return value
|
|
132
|
+
else:
|
|
133
|
+
|
|
134
|
+
def wrapper(func: Callable[..., T]) -> Callable[..., T]:
|
|
135
|
+
collect_bean(app_record, func, value)
|
|
136
|
+
return func
|
|
137
|
+
|
|
138
|
+
return wrapper
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------- Injectable ---------------------------------------------------- #
|
|
142
|
+
def inject_init_deps_and_get_instance(app_record: AppRecord, cls: type[T]) -> T:
|
|
143
|
+
"""_inject cls's __init__ params and get params deps"""
|
|
144
|
+
old_params = list(signature(cls.__init__).parameters.values())[1:] # self
|
|
145
|
+
new_params = [
|
|
146
|
+
i for i in old_params if i.kind not in (Parameter.VAR_KEYWORD, Parameter.VAR_POSITIONAL)
|
|
147
|
+
] # *args、**kwargs
|
|
148
|
+
return cls(**inject_params_deps(app_record, new_params))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def collect_dep(app_record: AppRecord, cls: type, name: str | None = None):
|
|
152
|
+
"""init class decorated by Inject decorator and collect it's instance as dependency"""
|
|
153
|
+
if hasattr(cls.__init__, '__globals__'):
|
|
154
|
+
cls.__init__.__globals__[cls.__name__] = cls # avoid error when getting cls in __init__ method
|
|
155
|
+
instance = inject_init_deps_and_get_instance(app_record, cls)
|
|
156
|
+
if name is None:
|
|
157
|
+
dep_store.add_dep_by_type(TypeDepRecord(cls, instance))
|
|
158
|
+
else:
|
|
159
|
+
dep_store.add_dep_by_name(NameDepRecord(cls, instance, name))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@overload
|
|
163
|
+
def Injectable(value: str): ...
|
|
164
|
+
@overload
|
|
165
|
+
def Injectable(value: type[T]): ...
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@no_type_check
|
|
169
|
+
def Injectable(value: str | type[T]) -> type[T]:
|
|
170
|
+
"""decorate a class and collect it's instance as a dependency
|
|
171
|
+
# Example
|
|
172
|
+
```python
|
|
173
|
+
@Injectable
|
|
174
|
+
class Foo:...
|
|
175
|
+
|
|
176
|
+
@Injectable('bar1')
|
|
177
|
+
class Bar:...
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
"""
|
|
181
|
+
app_record = app_store.get((get_call_filename()))
|
|
182
|
+
if isclass(value):
|
|
183
|
+
collect_dep(app_record, value)
|
|
184
|
+
return value
|
|
185
|
+
else:
|
|
186
|
+
|
|
187
|
+
def wrapper(cls: type[T]):
|
|
188
|
+
collect_dep(app_record, cls, value)
|
|
189
|
+
return cls
|
|
190
|
+
|
|
191
|
+
return wrapper
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ------------------------------------------------------ Inject ------------------------------------------------------ #
|
|
195
|
+
class AtUsable(type):
|
|
196
|
+
"""support @"""
|
|
197
|
+
|
|
198
|
+
def __matmul__(self: type['Inject'], other: type[T]) -> T:
|
|
199
|
+
filename = get_call_filename()
|
|
200
|
+
app_record = app_store.get(filename)
|
|
201
|
+
return _inject(app_record, other, self.latest_named_deps_record.get(filename))
|
|
202
|
+
|
|
203
|
+
def __rmatmul__(self: type['Inject'], other: type[T]) -> T:
|
|
204
|
+
filename = get_call_filename()
|
|
205
|
+
app_record = app_store.get(filename)
|
|
206
|
+
return _inject(app_record, other, self.latest_named_deps_record.get(filename))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class Inject(Generic[T], metaclass=AtUsable):
|
|
210
|
+
"""inject dependency anywhere
|
|
211
|
+
# Example
|
|
212
|
+
- inject by **type**
|
|
213
|
+
```python
|
|
214
|
+
a = Inject(Foo)
|
|
215
|
+
b = Inject @ Foo
|
|
216
|
+
c = Foo @ Inject
|
|
217
|
+
|
|
218
|
+
@Injectable
|
|
219
|
+
class Bar:
|
|
220
|
+
a = Inject(Foo)
|
|
221
|
+
b = Inject @ Foo
|
|
222
|
+
c = Foo @ Inject
|
|
223
|
+
|
|
224
|
+
def __init__(self,ia: Foo, ic: Foo):
|
|
225
|
+
self.ia = ia
|
|
226
|
+
self.ib = Inject @ Foo
|
|
227
|
+
self.ic = ic
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- inject by **name**
|
|
231
|
+
```python
|
|
232
|
+
a = Inject(Foo, 'foo1')
|
|
233
|
+
b = Inject.Qualifier('foo2') @ Foo
|
|
234
|
+
c = Foo @ Inject.Qualifier('foo3')
|
|
235
|
+
|
|
236
|
+
@Injectable
|
|
237
|
+
class Bar:
|
|
238
|
+
a = Inject(Foo, 'foo1')
|
|
239
|
+
b = Inject.Qualifier('foo2') @ Foo
|
|
240
|
+
c = Foo @ Inject.Qualifier('foo3')
|
|
241
|
+
|
|
242
|
+
def __init__(self,ia: Annotated[Foo, 'foo1'], ic: Annotated[Foo, 'foo3']):
|
|
243
|
+
self.ia = ia
|
|
244
|
+
self.ib = Inject.Qualifier('foo2') @ Foo
|
|
245
|
+
self.ic = ic
|
|
246
|
+
```
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
latest_named_deps_record: dict[str, str | None] = {}
|
|
250
|
+
|
|
251
|
+
def __new__(cls, tp: type[T], name: str | None = None) -> T:
|
|
252
|
+
"""Inject(Type, name = None)"""
|
|
253
|
+
filename = get_call_filename()
|
|
254
|
+
cls.latest_named_deps_record.update({filename: name})
|
|
255
|
+
app_record = app_store.get(filename)
|
|
256
|
+
res = _inject(app_record, tp, name)
|
|
257
|
+
cls.latest_named_deps_record.update({filename: None}) # set name None
|
|
258
|
+
return res
|
|
259
|
+
|
|
260
|
+
@classmethod
|
|
261
|
+
def Qualifier(cls, name: str) -> type['Inject']:
|
|
262
|
+
"""Inject.Qualifier(name)"""
|
|
263
|
+
filename = get_call_filename()
|
|
264
|
+
|
|
265
|
+
class Cls(cls):
|
|
266
|
+
latest_named_deps_record: dict[str, str] = {filename: name}
|
|
267
|
+
|
|
268
|
+
return Cls
|
fastapi_boot/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .DI import Bean
|
|
2
|
+
from .DI import Inject
|
|
3
|
+
from .DI import Inject as Autowired
|
|
4
|
+
from .DI import Injectable
|
|
5
|
+
from .DI import Injectable as Component
|
|
6
|
+
from .DI import Injectable as Repository
|
|
7
|
+
from .DI import Injectable as Service
|
|
8
|
+
from .helper import (
|
|
9
|
+
ExceptionHandler,
|
|
10
|
+
OnAppProvided,
|
|
11
|
+
provide_app,
|
|
12
|
+
use_dep,
|
|
13
|
+
use_http_middleware,
|
|
14
|
+
use_ws_middleware,
|
|
15
|
+
HTTPMiddleware,
|
|
16
|
+
)
|
|
17
|
+
from .routing import Controller, Delete, Get, Head, Options, Patch, Post, Prefix, Put, Req, Trace
|
|
18
|
+
from .routing import WebSocket as WS
|
fastapi_boot/const.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Generic, TypeVar
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
|
|
7
|
+
from fastapi_boot.model import AppNotFoundException, AppRecord, DependencyDuplicatedException
|
|
8
|
+
|
|
9
|
+
T = TypeVar('T')
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------- constant ---------------------------------------------------- #
|
|
12
|
+
# use_dep placeholder
|
|
13
|
+
REQ_DEP_PLACEHOLDER = "fastapi_boot___dependency_placeholder"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# route record's key in controller
|
|
17
|
+
CONTROLLER_ROUTE_RECORD = "fastapi_boot___controller_route_record"
|
|
18
|
+
|
|
19
|
+
# prefix of use_dep params in endpoint
|
|
20
|
+
USE_DEP_PREFIX_IN_ENDPOINT = 'fastapi_boot__use_dep_prefix'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# use_middleware placeholder
|
|
24
|
+
USE_MIDDLEWARE_FIELD_PLACEHOLDER = 'fastapi_boot__use_middleware_field_placeholder'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BlankPlaceholder: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# PRIORITY OF EXCEPTION_HANDLER
|
|
31
|
+
EXCEPTION_HANDLER_PRIORITY = 1
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ------------------------------------------------------- store ------------------------------------------------------ #
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class TypeDepRecord(Generic[T]):
|
|
39
|
+
tp: type[T]
|
|
40
|
+
value: T
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class NameDepRecord(Generic[T], TypeDepRecord[T]):
|
|
45
|
+
name: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DependencyStore(Generic[T]):
|
|
49
|
+
def __init__(self):
|
|
50
|
+
self.type_deps: dict[type[T], TypeDepRecord[T]] = {}
|
|
51
|
+
self.name_deps: dict[str, NameDepRecord[T]] = {}
|
|
52
|
+
|
|
53
|
+
def add_dep_by_type(self, dep: TypeDepRecord[T]):
|
|
54
|
+
if dep.tp in self.type_deps:
|
|
55
|
+
raise DependencyDuplicatedException(f'Dependency {dep.tp} duplicated')
|
|
56
|
+
self.type_deps.update({dep.tp: dep})
|
|
57
|
+
|
|
58
|
+
def add_dep_by_name(self, dep: NameDepRecord[T]):
|
|
59
|
+
if self.name_deps.get(dep.name):
|
|
60
|
+
raise DependencyDuplicatedException(f'Dependency name {dep.name} duplicated')
|
|
61
|
+
self.name_deps.update({dep.name: dep})
|
|
62
|
+
|
|
63
|
+
def inject_by_type(self, tp: type[T]) -> T | None:
|
|
64
|
+
if res := self.type_deps.get(tp):
|
|
65
|
+
return res.value
|
|
66
|
+
|
|
67
|
+
def inject_by_name(self, name: str, tp: type[T]) -> T | None:
|
|
68
|
+
if find := self.name_deps.get(name):
|
|
69
|
+
if find.tp == tp:
|
|
70
|
+
return find.value
|
|
71
|
+
|
|
72
|
+
def clear(self):
|
|
73
|
+
self.type_deps.clear()
|
|
74
|
+
self.name_deps.clear()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AppStore(Generic[T]):
|
|
78
|
+
def __init__(self):
|
|
79
|
+
self.app_dic: dict[str, AppRecord] = {}
|
|
80
|
+
|
|
81
|
+
def add(self, path: str, app_record: AppRecord):
|
|
82
|
+
self.app_dic.update({path: app_record})
|
|
83
|
+
|
|
84
|
+
def get(self, path: str) -> AppRecord:
|
|
85
|
+
path = path[0].upper() + path[1:]
|
|
86
|
+
for k, v in self.app_dic.items():
|
|
87
|
+
if path.startswith(k):
|
|
88
|
+
return v
|
|
89
|
+
raise AppNotFoundException(f'Can"t find app of "{path}"')
|
|
90
|
+
|
|
91
|
+
def clear(self):
|
|
92
|
+
self.app_dic.clear()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TaskStore:
|
|
96
|
+
def __init__(self):
|
|
97
|
+
# will be called after the app becomes available
|
|
98
|
+
self.late_tasks: dict[str, list[tuple[Callable[[FastAPI], None], int]]] = {}
|
|
99
|
+
|
|
100
|
+
def add_late_task(self, path: str, task: Callable[[FastAPI], None], priority: int):
|
|
101
|
+
if curr_tasks := self.late_tasks.get(path):
|
|
102
|
+
self.late_tasks.update({path: [*curr_tasks, (task, priority)]})
|
|
103
|
+
else:
|
|
104
|
+
self.late_tasks.update({path: [(task, priority)]})
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run_late_tasks(self):
|
|
108
|
+
for path, late_tasks in self.late_tasks.items():
|
|
109
|
+
app = app_store.get(path).app
|
|
110
|
+
late_tasks.sort(key=lambda x: x[1], reverse=True)
|
|
111
|
+
for record in late_tasks:
|
|
112
|
+
record[0](app)
|
|
113
|
+
|
|
114
|
+
def clear(self):
|
|
115
|
+
self.late_tasks.clear()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
dep_store = DependencyStore()
|
|
119
|
+
app_store = AppStore()
|
|
120
|
+
task_store = TaskStore()
|
fastapi_boot/helper.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import concurrent
|
|
2
|
+
import concurrent.futures
|
|
3
|
+
from dataclasses import asdict, is_dataclass
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import Callable, Coroutine
|
|
6
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Any, TypeVar
|
|
10
|
+
from inspect import iscoroutinefunction
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from fastapi import Depends, FastAPI, Request, Response, WebSocket
|
|
14
|
+
|
|
15
|
+
from fastapi.responses import JSONResponse
|
|
16
|
+
from fastapi_boot.const import REQ_DEP_PLACEHOLDER, USE_MIDDLEWARE_FIELD_PLACEHOLDER,EXCEPTION_HANDLER_PRIORITY, BlankPlaceholder, app_store, task_store,dep_store
|
|
17
|
+
from fastapi_boot.model import AppRecord,UseMiddlewareRecord
|
|
18
|
+
from fastapi_boot.util import get_call_filename
|
|
19
|
+
T = TypeVar('T')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def use_dep(dependency: Callable[..., T] | None, use_cache: bool = True) -> T:
|
|
23
|
+
"""Depends of FastAPI with type hint
|
|
24
|
+
- use it as value of a controller's classvar
|
|
25
|
+
|
|
26
|
+
# Example
|
|
27
|
+
```python
|
|
28
|
+
def get_ua(request: Request):
|
|
29
|
+
return request.headers.get('user-agent','')
|
|
30
|
+
|
|
31
|
+
@Controller('/foo')
|
|
32
|
+
class Foo:
|
|
33
|
+
ua = use_dep(get_ua)
|
|
34
|
+
|
|
35
|
+
@Get('/ua')
|
|
36
|
+
def foo(self):
|
|
37
|
+
return self.ua
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
"""
|
|
41
|
+
value: T = Depends(dependency=dependency, use_cache=use_cache)
|
|
42
|
+
setattr(value, REQ_DEP_PLACEHOLDER, True)
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _create_bp_from_record(record:UseMiddlewareRecord):
|
|
48
|
+
bp=BlankPlaceholder()
|
|
49
|
+
setattr(bp, USE_MIDDLEWARE_FIELD_PLACEHOLDER, record)
|
|
50
|
+
return bp
|
|
51
|
+
|
|
52
|
+
def use_http_middleware(*dispatches: Callable[[Request, Callable[[Request], Coroutine[Any, Any, Response]]], Any]):
|
|
53
|
+
"""add http middlewares for current Controller or Prefix with http endpoint, exclude inner Prefix
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
|
|
57
|
+
from collections.abc import Callable
|
|
58
|
+
from typing import Any
|
|
59
|
+
from fastapi import Request
|
|
60
|
+
from fastapi_boot import Controller, use_http_middleware
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def middleware_foo(request: Request, call_next: Callable[[Request], Any]):
|
|
64
|
+
print('middleware_foo before')
|
|
65
|
+
resp = await call_next(request)
|
|
66
|
+
print('middleware_foo after')
|
|
67
|
+
return resp
|
|
68
|
+
|
|
69
|
+
async def middleware_bar(request: Request, call_next: Callable[[Request], Any]):
|
|
70
|
+
print('middleware_bar before')
|
|
71
|
+
resp = await call_next(request)
|
|
72
|
+
print('middleware_bar after')
|
|
73
|
+
return resp
|
|
74
|
+
|
|
75
|
+
@Controller('/foo')
|
|
76
|
+
class FooController:
|
|
77
|
+
_ = use_http_middleware(middleware_foo, middleware_bar)
|
|
78
|
+
|
|
79
|
+
# 1. middleware_bar before
|
|
80
|
+
# 2. middleware_foo before
|
|
81
|
+
# 3. call endpoint
|
|
82
|
+
# 4. middleware_foo after
|
|
83
|
+
# 5. middleware_bar after
|
|
84
|
+
|
|
85
|
+
# ...
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
record=UseMiddlewareRecord(http_dispatches=list(dispatches))
|
|
90
|
+
return _create_bp_from_record(record)
|
|
91
|
+
|
|
92
|
+
def use_ws_middleware(*dispatches: Callable[[WebSocket,Callable[[WebSocket],Coroutine[Any,Any,None]]],Any],only_message:bool=False):
|
|
93
|
+
"""add websocket middlewares for current Controller or Prefix with websocket endpoint, exclude inner Prefix
|
|
94
|
+
- if `only_message` and message's type != 'websocket.senf': will ignore dispatches
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
|
|
98
|
+
from collections.abc import Callable
|
|
99
|
+
from typing import Any
|
|
100
|
+
from fastapi import Request, WebSocket
|
|
101
|
+
from fastapi_boot import Controller, use_http_middleware, middleware_ws_foo
|
|
102
|
+
|
|
103
|
+
async def middleware_ws_foo(websocket: WebSocket, call_next: Callable):
|
|
104
|
+
print('before ws send data foo') # as pos a
|
|
105
|
+
res = await call_next(websocket)
|
|
106
|
+
print('after ws send data foo') # as pos b
|
|
107
|
+
return res
|
|
108
|
+
|
|
109
|
+
async def middleware_ws_bar(websocket: WebSocket, call_next: Callable):
|
|
110
|
+
print('before ws send data bar') # as pso c
|
|
111
|
+
res = await call_next()
|
|
112
|
+
print('after ws send data bar') # as pso d
|
|
113
|
+
return res
|
|
114
|
+
|
|
115
|
+
async def middleware_bar(request: Request, call_next: Callable[[Request], Any]):
|
|
116
|
+
print('middleware_bar before') # as pos e
|
|
117
|
+
resp = await call_next(request)
|
|
118
|
+
print('middleware_bar after') # as pos f
|
|
119
|
+
return resp
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@Controller('/chat')
|
|
123
|
+
class WsController:
|
|
124
|
+
_ = use_http_middleware(middleware_bar)
|
|
125
|
+
___ = use_ws_middleware(middleware_ws_bar, middleware_ws_foo, only_message=True)
|
|
126
|
+
|
|
127
|
+
@Socket('/chat')
|
|
128
|
+
async def chat(self, websocket: WebSocket):
|
|
129
|
+
try:
|
|
130
|
+
await websocket.accept()
|
|
131
|
+
while True:
|
|
132
|
+
message = await websocket.receive_text()
|
|
133
|
+
# a c
|
|
134
|
+
await self.send_text(message)
|
|
135
|
+
# d b
|
|
136
|
+
except:
|
|
137
|
+
...
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# e a c d b f
|
|
141
|
+
@Post('/broadcast')
|
|
142
|
+
async def send_broadcast_msg(self, msg: str = Query()):
|
|
143
|
+
await self.broadcast(msg)
|
|
144
|
+
return 'ok'
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
"""
|
|
148
|
+
record=UseMiddlewareRecord(ws_dispatches=list(dispatches),ws_only_message=only_message)
|
|
149
|
+
return _create_bp_from_record(record)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def HTTPMiddleware(dispatch:Callable[[Request, Callable[[Request], Coroutine[Any, Any, Response]]], Any]):
|
|
153
|
+
"""Add global http middleware
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
dispatch (Callable[[Request, Callable[[Request], Coroutine[Any, Any, Response]]], Any]): middleware handler
|
|
157
|
+
Example:
|
|
158
|
+
```python
|
|
159
|
+
from collections.abc import Callable
|
|
160
|
+
from fastapi import Request
|
|
161
|
+
from fastapi_boot import HTTPMiddleware
|
|
162
|
+
|
|
163
|
+
@HTTPMiddleware
|
|
164
|
+
async def barMiddleware(request: Request, call_next: Callable):
|
|
165
|
+
print("before")
|
|
166
|
+
res = await call_next(request)
|
|
167
|
+
print("after")
|
|
168
|
+
return res
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
"""
|
|
172
|
+
app_store.get(get_call_filename()).app.middleware('http')(dispatch)
|
|
173
|
+
return dispatch
|
|
174
|
+
|
|
175
|
+
def provide_app(app: FastAPI, max_workers: int = 20, inject_timeout: float = 20, inject_retry_step: float = 0.05):
|
|
176
|
+
"""enable scan project to collect dependencies which can't been collected automatically
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
app (FastAPI): FastAPI instance
|
|
180
|
+
max_workers (int, optional): workers' num to scan project. Defaults to 20.
|
|
181
|
+
inject_timeout (float, optional): will raise DependencyNotFoundException if time > inject_timeout. Defaults to 20.
|
|
182
|
+
inject_pause_step (float, optional): Retry interval after failing to find a dependency . Defaults to 0.05.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
_type_: original app
|
|
186
|
+
"""
|
|
187
|
+
# clear store before init
|
|
188
|
+
app_store.clear()
|
|
189
|
+
dep_store.clear()
|
|
190
|
+
task_store.clear()
|
|
191
|
+
|
|
192
|
+
provide_filepath = get_call_filename()
|
|
193
|
+
# the file which provides app
|
|
194
|
+
app_root_dir = os.path.dirname(provide_filepath)
|
|
195
|
+
app_record = AppRecord(app, inject_timeout, inject_retry_step)
|
|
196
|
+
app_store.add(os.path.dirname(provide_filepath), app_record)
|
|
197
|
+
# app's prefix in project
|
|
198
|
+
proj_root_dir = os.getcwd()
|
|
199
|
+
app_parts = Path(app_root_dir).parts
|
|
200
|
+
proj_parts = Path(proj_root_dir).parts
|
|
201
|
+
prefix_parts = app_parts[len(proj_parts) :]
|
|
202
|
+
# scan
|
|
203
|
+
dot_paths = []
|
|
204
|
+
for root, _, files in os.walk(app_root_dir):
|
|
205
|
+
for file in files:
|
|
206
|
+
if file.endswith('.py'):
|
|
207
|
+
fullpath = os.path.join(root, file)
|
|
208
|
+
if fullpath == provide_filepath:
|
|
209
|
+
continue
|
|
210
|
+
dot_path = '.'.join(
|
|
211
|
+
prefix_parts + Path(fullpath.replace('.py', '').replace(app_root_dir, '')).parts[1:]
|
|
212
|
+
)
|
|
213
|
+
dot_paths.append(dot_path)
|
|
214
|
+
# clear module cache if exists
|
|
215
|
+
if dot_path in sys.modules:
|
|
216
|
+
sys.modules.pop(dot_path)
|
|
217
|
+
|
|
218
|
+
futures: list[Future] = []
|
|
219
|
+
with ThreadPoolExecutor(max_workers) as executor:
|
|
220
|
+
for dot_path in dot_paths:
|
|
221
|
+
future = executor.submit(__import__,dot_path)
|
|
222
|
+
futures.append(future)
|
|
223
|
+
concurrent.futures.wait(futures)
|
|
224
|
+
# wait all future finished
|
|
225
|
+
for future in futures:
|
|
226
|
+
future.result()
|
|
227
|
+
# before return , run tasks
|
|
228
|
+
task_store.run_late_tasks()
|
|
229
|
+
return app
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def OnAppProvided(priority: int = 1):
|
|
233
|
+
"""Methods to be executed after the app is provided
|
|
234
|
+
- decorated function should be sync.
|
|
235
|
+
```python
|
|
236
|
+
@OnAppProvided()
|
|
237
|
+
def _(app:FastAPI):
|
|
238
|
+
print('foo')
|
|
239
|
+
|
|
240
|
+
@OnAppProvided(priority=10):
|
|
241
|
+
def func():
|
|
242
|
+
print('bar')
|
|
243
|
+
|
|
244
|
+
# bar >> foo
|
|
245
|
+
```
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
def wrapper(func: Callable[[FastAPI], None]):
|
|
249
|
+
task_store.add_late_task(get_call_filename(), func, priority)
|
|
250
|
+
return func
|
|
251
|
+
|
|
252
|
+
return wrapper
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# -------------------------------------------------------------------------------------------------------------------- #
|
|
256
|
+
E = TypeVar('E', bound=Exception)
|
|
257
|
+
|
|
258
|
+
HttpHandler = Callable[[Request, E], Any]
|
|
259
|
+
WsHandler = Callable[[WebSocket, E],Any]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def ExceptionHandler(exp: int | type[E]):
|
|
263
|
+
"""The return value can be BaseModel instance、dataclass、dict or JSONResponse.
|
|
264
|
+
```python
|
|
265
|
+
@ExceptionHandler(MyException)
|
|
266
|
+
async def _(req: Request, exp: AException):
|
|
267
|
+
...
|
|
268
|
+
```
|
|
269
|
+
Declarative style of the following code:
|
|
270
|
+
```python
|
|
271
|
+
@app.exception_handler(AException)
|
|
272
|
+
async def _(req: Request, exp: AException):
|
|
273
|
+
...
|
|
274
|
+
@app.exception_handler(BException)
|
|
275
|
+
def _(req: Request, exp: BException):
|
|
276
|
+
...
|
|
277
|
+
|
|
278
|
+
@app.exception_handler(CException)
|
|
279
|
+
async def _(req: WebSocket, exp: CException):
|
|
280
|
+
...
|
|
281
|
+
@app.exception_handler(DException)
|
|
282
|
+
def _(req: WebSocket, exp: DException):
|
|
283
|
+
...
|
|
284
|
+
```
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
def decorator(handler: HttpHandler | WsHandler):
|
|
288
|
+
# wrap handler
|
|
289
|
+
async def wrapper(*args,**kwds):
|
|
290
|
+
resp=await handler(*args,**kwds) if iscoroutinefunction(handler) else handler(*args,**kwds)
|
|
291
|
+
if isinstance(resp,BaseModel):
|
|
292
|
+
resp=resp.model_dump()
|
|
293
|
+
elif is_dataclass(resp):
|
|
294
|
+
resp=asdict(resp)
|
|
295
|
+
if isinstance(resp,dict):
|
|
296
|
+
resp=JSONResponse(resp)
|
|
297
|
+
return resp
|
|
298
|
+
task_store.add_late_task(get_call_filename(), lambda app: app.add_exception_handler(exp, wrapper), EXCEPTION_HANDLER_PRIORITY)
|
|
299
|
+
return handler
|
|
300
|
+
|
|
301
|
+
return decorator
|