fastapi-boot 3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2024] [hfdy0935]
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 all
13
+ 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 THE
21
+ SOFTWARE.
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.1
2
+ Name: fastapi_boot
3
+ Version: 3.0
4
+ Summary: FastAPI development toolkit
5
+ Home-page: https://github.com/pythonml/douyin_image
6
+ Author: hfdy0935
7
+ Author-email:
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: starlette<0.42.0,>=0.40.0
14
+ Requires-Dist: pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4
15
+ Requires-Dist: typing-extensions>=4.8.0
16
+
17
+ # FastAPI Boot
18
+
19
+ &emsp;&emsp;FastAPI Boot is a FastAPI development toolkit that aims to provide a **more convenient** and **declarative** approach to `routing`, `dependency` `injection`, `error handling`, and `middleware`.
20
+
21
+ ## Features
22
+
23
+ - **Declarative Routing**: Simplify the way you define routes with a clear and intuitive syntax.
24
+ - **Dependency Injection**: Manage dependencies effortlessly with a robust DI system that simplifies your application's architecture.
25
+ - **Error Handling**: Built-in support for error handling that makes it easy to define and handle exceptions across your API.
26
+ - **Middleware Support**: Integrate middleware with ease to add pre-processing and post-processing steps to your requests and responses.
27
+
28
+ ## Getting Started
29
+
30
+ ```bash
31
+ pip install fatsapI_boot
32
+ ```
33
+
34
+ ## USAGE
35
+
36
+ 1. FBV(function based view)
37
+
38
+ ```py
39
+ # controller
40
+ from fastapi_boot import Controller
41
+
42
+
43
+ @Controller('/fbv', tags=['hello_world']).post('/foo')
44
+ def _():
45
+ return 'Hello World'
46
+ ```
47
+
48
+ ```py
49
+ # app.py
50
+ import uvicorn
51
+ from fastapi import FastAPI
52
+ from fastapi_boot import provide_app
53
+
54
+ app = FastAPI()
55
+ provide_app(app) # scans dir automatically
56
+
57
+ if __name__ == '__main__':
58
+ uvicorn.run('main:app', reload=True)
59
+ ```
60
+
61
+ 2. CBV(class based view)
62
+
63
+ ```py
64
+ from dataclasses import asdict, dataclass
65
+ from pydantic import BaseModel
66
+ from fastapi_boot import Controller, Delete, Get, Post, Put, Req
67
+
68
+
69
+ class Baz(BaseModel):
70
+ baz: str
71
+
72
+ @dataclass
73
+ class Baz1:
74
+ baz1: str
75
+
76
+
77
+ @Controller('/base-cbv', tags=['2. base cbv'])
78
+ class FirstController:
79
+
80
+ @Req('/f', methods=['GET'])
81
+ def f():
82
+ return True
83
+
84
+ @Get('/foo')
85
+ def get_foo(self):
86
+ return BaseResp(code=200, msg='success', data='foo')
87
+
88
+ @Post('/bar')
89
+ def post_bar(self, p: str = Query()):
90
+ return p
91
+
92
+ @Put('/baz')
93
+ def put_baz(self, baz: Baz, baz1: Baz1):
94
+ return dict(**baz.model_dump(), **asdict(baz1))
95
+ ```
96
+
97
+ click <a href='https://github.com/hfdy0935/fastapi_boot/tree/main/exmaples' target='_blank'>here</a> to get more examples.
98
+
99
+ ## APIS
100
+
101
+ ```py
102
+ from fastapi_boot.DI import Bean
103
+ from fastapi_boot.DI import Inject
104
+ from fastapi_boot.DI import Injectable
105
+ from fastapi_boot.DI import Component
106
+ from fastapi_boot.DI import Repository
107
+ from fastapi_boot.DI import Service
108
+ from fastapi_boot.helper import ExceptionHandler, OnAppProvided, provide_app, use_dep, use_http_middleware, use_ws_middleware
109
+ from fastapi_boot.routing import Controller, Delete, Get, Head, Options, Patch, Post, Prefix, Put, Req, Trace
110
+ from fastapi_boot.routing import Socket
111
+ ```
@@ -0,0 +1,95 @@
1
+ # FastAPI Boot
2
+
3
+ &emsp;&emsp;FastAPI Boot is a FastAPI development toolkit that aims to provide a **more convenient** and **declarative** approach to `routing`, `dependency` `injection`, `error handling`, and `middleware`.
4
+
5
+ ## Features
6
+
7
+ - **Declarative Routing**: Simplify the way you define routes with a clear and intuitive syntax.
8
+ - **Dependency Injection**: Manage dependencies effortlessly with a robust DI system that simplifies your application's architecture.
9
+ - **Error Handling**: Built-in support for error handling that makes it easy to define and handle exceptions across your API.
10
+ - **Middleware Support**: Integrate middleware with ease to add pre-processing and post-processing steps to your requests and responses.
11
+
12
+ ## Getting Started
13
+
14
+ ```bash
15
+ pip install fatsapI_boot
16
+ ```
17
+
18
+ ## USAGE
19
+
20
+ 1. FBV(function based view)
21
+
22
+ ```py
23
+ # controller
24
+ from fastapi_boot import Controller
25
+
26
+
27
+ @Controller('/fbv', tags=['hello_world']).post('/foo')
28
+ def _():
29
+ return 'Hello World'
30
+ ```
31
+
32
+ ```py
33
+ # app.py
34
+ import uvicorn
35
+ from fastapi import FastAPI
36
+ from fastapi_boot import provide_app
37
+
38
+ app = FastAPI()
39
+ provide_app(app) # scans dir automatically
40
+
41
+ if __name__ == '__main__':
42
+ uvicorn.run('main:app', reload=True)
43
+ ```
44
+
45
+ 2. CBV(class based view)
46
+
47
+ ```py
48
+ from dataclasses import asdict, dataclass
49
+ from pydantic import BaseModel
50
+ from fastapi_boot import Controller, Delete, Get, Post, Put, Req
51
+
52
+
53
+ class Baz(BaseModel):
54
+ baz: str
55
+
56
+ @dataclass
57
+ class Baz1:
58
+ baz1: str
59
+
60
+
61
+ @Controller('/base-cbv', tags=['2. base cbv'])
62
+ class FirstController:
63
+
64
+ @Req('/f', methods=['GET'])
65
+ def f():
66
+ return True
67
+
68
+ @Get('/foo')
69
+ def get_foo(self):
70
+ return BaseResp(code=200, msg='success', data='foo')
71
+
72
+ @Post('/bar')
73
+ def post_bar(self, p: str = Query()):
74
+ return p
75
+
76
+ @Put('/baz')
77
+ def put_baz(self, baz: Baz, baz1: Baz1):
78
+ return dict(**baz.model_dump(), **asdict(baz1))
79
+ ```
80
+
81
+ click <a href='https://github.com/hfdy0935/fastapi_boot/tree/main/exmaples' target='_blank'>here</a> to get more examples.
82
+
83
+ ## APIS
84
+
85
+ ```py
86
+ from fastapi_boot.DI import Bean
87
+ from fastapi_boot.DI import Inject
88
+ from fastapi_boot.DI import Injectable
89
+ from fastapi_boot.DI import Component
90
+ from fastapi_boot.DI import Repository
91
+ from fastapi_boot.DI import Service
92
+ from fastapi_boot.helper import ExceptionHandler, OnAppProvided, provide_app, use_dep, use_http_middleware, use_ws_middleware
93
+ from fastapi_boot.routing import Controller, Delete, Get, Head, Options, Patch, Post, Prefix, Put, Req, Trace
94
+ from fastapi_boot.routing import Socket
95
+ ```
@@ -0,0 +1,267 @@
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
+ raise DependencyNotFoundException(f'Dependency "{tp}" not found')
33
+
34
+
35
+ def inject_params_deps(app_record: AppRecord, params: list[Parameter]):
36
+ """find dependencies of params
37
+ Args:
38
+ app_record (AppRecord)
39
+ params (list[Parameter]): param list without self
40
+ """
41
+ params_dict = {}
42
+ for param in params:
43
+ # 1. with default
44
+ if param.default != _empty:
45
+ params_dict.update({param.name: param.default})
46
+ else:
47
+ # 2. no default
48
+ if param.annotation == _empty:
49
+ # 2.1 not annotation
50
+ raise InjectFailException(
51
+ f'The annotation of param {param.name} is missing, add an annotation or give it a default value'
52
+ )
53
+ # 2.2. with annotation
54
+ if get_origin(param.annotation) == Annotated:
55
+ # 2.2.1 Annotated
56
+ tp, name, *_ = get_args(param.annotation)
57
+ if not isinstance(name, str):
58
+ # 2.2.1.1 name is not str
59
+ params_dict.update({param.name: _inject(app_record, tp)})
60
+ else:
61
+ # 2.2.1.2 name is str, as dependency's name to _inject
62
+ params_dict.update({param.name: _inject(app_record, tp, name)})
63
+ else:
64
+ # 2.2.2 other
65
+ params_dict.update({param.name: _inject(app_record, param.annotation)})
66
+ return params_dict
67
+
68
+
69
+ # ------------------------------------------------------- Bean ------------------------------------------------------- #
70
+
71
+
72
+ def collect_bean(app_record: AppRecord, func: Callable, name: str | None = None):
73
+ """
74
+ 1. run function decorated by Bean decorator
75
+ 2. add the result to deps_store
76
+
77
+ Args:
78
+ app_record (AppRecord)
79
+ func (Callable): func
80
+ name (str | None, optional): name of dep
81
+ """
82
+ params: list[Parameter] = list(signature(func).parameters.values())
83
+ return_annotations = signature(func).return_annotation
84
+ instance = func(**inject_params_deps(app_record, params))
85
+ tp = return_annotations if return_annotations != _empty else type(instance)
86
+ if name is None:
87
+ dep_store.add_dep_by_type(TypeDepRecord(tp, instance))
88
+ else:
89
+ dep_store.add_dep_by_name(NameDepRecord(tp, instance, name))
90
+
91
+
92
+ @overload
93
+ def Bean(value: str): ...
94
+ @overload
95
+ def Bean(value: Callable[..., T]): ...
96
+ @no_type_check
97
+ def Bean(value: str | Callable[..., T]) -> Callable[..., T]:
98
+ """A decorator, will collect the return value of the func decorated by Bean
99
+ # Example
100
+ 1. collect by `type`
101
+ ```python
102
+ @dataclass
103
+ class Foo:
104
+ bar: str
105
+
106
+ @Bean
107
+ def _():
108
+ return Foo('baz')
109
+ ```
110
+
111
+ 2. collect by `name`
112
+ ```python
113
+ class User(BaseModel):
114
+ name: str = Field(max_length=20)
115
+ age: int = Field(gt=0)
116
+
117
+ @Bean('user1')
118
+ def _():
119
+ return User(name='zs', age=20)
120
+
121
+ @Bean('user2)
122
+ def _():
123
+ return User(name='zs', age=21)
124
+ ```
125
+ """
126
+ app_record = app_store.get(get_call_filename())
127
+
128
+ if callable(value):
129
+ collect_bean(app_record, value)
130
+ return value
131
+ else:
132
+
133
+ def wrapper(func: Callable[..., T]) -> Callable[..., T]:
134
+ collect_bean(app_record, func, value)
135
+ return func
136
+
137
+ return wrapper
138
+
139
+
140
+ # ---------------------------------------------------- Injectable ---------------------------------------------------- #
141
+ def inject_init_deps_and_get_instance(app_record: AppRecord, cls: type[T]) -> T:
142
+ """_inject cls's __init__ params and get params deps"""
143
+ old_params = list(signature(cls.__init__).parameters.values())[1:] # self
144
+ new_params = [
145
+ i for i in old_params if i.kind not in (Parameter.VAR_KEYWORD, Parameter.VAR_POSITIONAL)
146
+ ] # *args、**kwargs
147
+ return cls(**inject_params_deps(app_record, new_params))
148
+
149
+
150
+ def collect_dep(app_record: AppRecord, cls: type, name: str | None = None):
151
+ """init class decorated by Inject decorator and collect it's instance as dependency"""
152
+ if hasattr(cls.__init__, '__globals__'):
153
+ cls.__init__.__globals__[cls.__name__] = cls # avoid error when getting cls in __init__ method
154
+ instance = inject_init_deps_and_get_instance(app_record, cls)
155
+ if name is None:
156
+ dep_store.add_dep_by_type(TypeDepRecord(cls, instance))
157
+ else:
158
+ dep_store.add_dep_by_name(NameDepRecord(cls, instance, name))
159
+
160
+
161
+ @overload
162
+ def Injectable(value: str): ...
163
+ @overload
164
+ def Injectable(value: type[T]): ...
165
+
166
+
167
+ @no_type_check
168
+ def Injectable(value: str | type[T]) -> type[T]:
169
+ """decorate a class and collect it's instance as a dependency
170
+ # Example
171
+ ```python
172
+ @Injectable
173
+ class Foo:...
174
+
175
+ @Injectable('bar1')
176
+ class Bar:...
177
+ ```
178
+
179
+ """
180
+ app_record = app_store.get((get_call_filename()))
181
+ if isclass(value):
182
+ collect_dep(app_record, value)
183
+ return value
184
+ else:
185
+
186
+ def wrapper(cls: type[T]):
187
+ collect_dep(app_record, cls, value)
188
+ return cls
189
+
190
+ return wrapper
191
+
192
+
193
+ # ------------------------------------------------------ Inject ------------------------------------------------------ #
194
+ class AtUsable(type):
195
+ """support @"""
196
+
197
+ def __matmul__(self: type['Inject'], other: type[T]) -> T:
198
+ filename = get_call_filename()
199
+ app_record = app_store.get(filename)
200
+ return _inject(app_record, other, self.latest_named_deps_record.get(filename))
201
+
202
+ def __rmatmul__(self: type['Inject'], other: type[T]) -> T:
203
+ filename = get_call_filename()
204
+ app_record = app_store.get(filename)
205
+ return _inject(app_record, other, self.latest_named_deps_record.get(filename))
206
+
207
+
208
+ class Inject(Generic[T], metaclass=AtUsable):
209
+ """inject dependency anywhere
210
+ # Example
211
+ - inject by **type**
212
+ ```python
213
+ a = Inject(Foo)
214
+ b = Inject @ Foo
215
+ c = Foo @ Inject
216
+
217
+ @Injectable
218
+ class Bar:
219
+ a = Inject(Foo)
220
+ b = Inject @ Foo
221
+ c = Foo @ Inject
222
+
223
+ def __init__(self,ia: Foo, ic: Foo):
224
+ self.ia = ia
225
+ self.ib = Inject @ Foo
226
+ self.ic = ic
227
+ ```
228
+
229
+ - inject by **name**
230
+ ```python
231
+ a = Inject(Foo, 'foo1')
232
+ b = Inject.Qualifier('foo2') @ Foo
233
+ c = Foo @ Inject.Qualifier('foo3')
234
+
235
+ @Injectable
236
+ class Bar:
237
+ a = Inject(Foo, 'foo1')
238
+ b = Inject.Qualifier('foo2') @ Foo
239
+ c = Foo @ Inject.Qualifier('foo3')
240
+
241
+ def __init__(self,ia: Annotated[Foo, 'foo1'], ic: Annotated[Foo, 'foo3']):
242
+ self.ia = ia
243
+ self.ib = Inject.Qualifier('foo2') @ Foo
244
+ self.ic = ic
245
+ ```
246
+ """
247
+
248
+ latest_named_deps_record: dict[str, str | None] = {}
249
+
250
+ def __new__(cls, tp: type[T], name: str | None = None) -> T:
251
+ """Inject(Type, name = None)"""
252
+ filename = get_call_filename()
253
+ cls.latest_named_deps_record.update({filename: name})
254
+ app_record = app_store.get(filename)
255
+ res = _inject(app_record, tp, name)
256
+ cls.latest_named_deps_record.update({filename: None}) # set name None
257
+ return res
258
+
259
+ @classmethod
260
+ def Qualifier(cls, name: str) -> type['Inject']:
261
+ """Inject.Qualifier(name)"""
262
+ filename = get_call_filename()
263
+
264
+ class Cls(cls):
265
+ latest_named_deps_record: dict[str, str] = {filename: name}
266
+
267
+ return Cls
@@ -0,0 +1,10 @@
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 ExceptionHandler, OnAppProvided, provide_app, use_dep, use_http_middleware,use_ws_middleware
9
+ from .routing import Controller, Delete, Get, Head, Options, Patch, Post, Prefix, Put, Req, Trace
10
+ from .routing import WebSocket as Socket
@@ -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
+ # app_record.app.add_event_handler()
84
+
85
+ def get(self, path: str) -> AppRecord:
86
+ path = path[0].upper() + path[1:]
87
+ for k, v in self.app_dic.items():
88
+ if path.startswith(k):
89
+ return v
90
+ raise AppNotFoundException(f'Can"t find app of "{path}"')
91
+
92
+ def clear(self):
93
+ self.app_dic.clear()
94
+
95
+
96
+ class TasStore:
97
+ def __init__(self):
98
+ # will be called after the app becomes available
99
+ self.late_tasks: dict[str, list[tuple[Callable[[FastAPI], None], int]]] = {}
100
+
101
+ def add_late_task(self, path: str, task: Callable[[FastAPI], None], priority: int):
102
+ if curr_tasks := self.late_tasks.get(path):
103
+ self.late_tasks.update({path: [*curr_tasks, (task, priority)]})
104
+ else:
105
+ self.late_tasks.update({path: [(task, priority)]})
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 = TasStore()