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