pyboot-web 0.1.0__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.
- pyboot_web/ExceptionResolver.py +33 -0
- pyboot_web/WebAttr.py +18 -0
- pyboot_web/__init__.py +0 -0
- pyboot_web/exception_handler.py +25 -0
- pyboot_web/middlewares.py +44 -0
- pyboot_web/plugin.py +305 -0
- pyboot_web/py.typed +0 -0
- pyboot_web/rest_mapping.py +57 -0
- pyboot_web/service.py +11 -0
- pyboot_web/types.py +16 -0
- pyboot_web/value.py +63 -0
- pyboot_web-0.1.0.dist-info/METADATA +234 -0
- pyboot_web-0.1.0.dist-info/RECORD +17 -0
- pyboot_web-0.1.0.dist-info/WHEEL +5 -0
- pyboot_web-0.1.0.dist-info/entry_points.txt +3 -0
- pyboot_web-0.1.0.dist-info/licenses/LICENSE +21 -0
- pyboot_web-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
from starlette.requests import Request
|
|
4
|
+
from starlette.responses import JSONResponse
|
|
5
|
+
|
|
6
|
+
#Exception Resolver
|
|
7
|
+
class ExceptionResolver:
|
|
8
|
+
def __init__(self) -> None:
|
|
9
|
+
self.handlers: dict[
|
|
10
|
+
type[Exception], Callable[[Request, Exception], JSONResponse]
|
|
11
|
+
] = dict()
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
def register(
|
|
15
|
+
self,
|
|
16
|
+
exception_type: type[Exception],
|
|
17
|
+
handler: Callable[[Request, Exception], JSONResponse],
|
|
18
|
+
):
|
|
19
|
+
if exception_type in self.handlers:
|
|
20
|
+
raise TypeError(f"Handler already registered for {exception_type.__name__}")
|
|
21
|
+
|
|
22
|
+
self.handlers[exception_type] = handler
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
def resolve(self, request: Request, exception: Exception) -> JSONResponse:
|
|
26
|
+
for exception_class in type(exception).__mro__:
|
|
27
|
+
handler = self.handlers.get(exception_class)
|
|
28
|
+
|
|
29
|
+
if handler is not None:
|
|
30
|
+
return handler(request, exception)
|
|
31
|
+
|
|
32
|
+
raise exception
|
|
33
|
+
|
pyboot_web/WebAttr.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
class WebAttr(Enum):
|
|
4
|
+
IS_REST_CONTROLLER="__is_rest_controller__"
|
|
5
|
+
REST_CONTROLLER_PATH="__rest_controller_path__"
|
|
6
|
+
|
|
7
|
+
IS_ROUTE="__is_route__"
|
|
8
|
+
ROUTE_PATH="__route_path__"
|
|
9
|
+
ROUTE_METHOD="__route_method__"
|
|
10
|
+
|
|
11
|
+
IS_MIDDLEWARE_COMPONENT="__is_middleware_component__"
|
|
12
|
+
MIDDLEWARE_LIST="__middleware_list__"
|
|
13
|
+
|
|
14
|
+
IS_WEB_SERVICE="__is_web_service__"
|
|
15
|
+
|
|
16
|
+
IS_REST_CONTROLLER_ADVICE="__is_rest_controller_advice__"
|
|
17
|
+
IS_EXCEPTION_HANDLER_METHOD="__is_exception_handler__"
|
|
18
|
+
EXCEPTION_HANDLER_FOR="__exception_handler_for__"
|
pyboot_web/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from pyboot.core.types import ClassT
|
|
2
|
+
|
|
3
|
+
from .WebAttr import WebAttr
|
|
4
|
+
|
|
5
|
+
from .types import *
|
|
6
|
+
|
|
7
|
+
class RestControllerAdvice:
|
|
8
|
+
def __init__(self) -> None:
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
def __call__(self,clazz:ClassT) -> ClassT:
|
|
12
|
+
setattr(clazz,WebAttr.IS_REST_CONTROLLER_ADVICE.value,True)
|
|
13
|
+
return clazz
|
|
14
|
+
|
|
15
|
+
class ExceptionHandler:
|
|
16
|
+
def __init__(self,exception_type:type[Exception]) -> None:
|
|
17
|
+
self.exception_type:type[Exception]=exception_type
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
def __call__(self,func:Callable[[Any,Request,E],JSONResponse]) -> Callable[[Any,Request,E],JSONResponse]:
|
|
21
|
+
setattr(func,WebAttr.IS_EXCEPTION_HANDLER_METHOD.value,True)
|
|
22
|
+
setattr(func,WebAttr.EXCEPTION_HANDLER_FOR.value,self.exception_type)
|
|
23
|
+
|
|
24
|
+
return func
|
|
25
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from starlette.requests import Request
|
|
2
|
+
from starlette.responses import JSONResponse
|
|
3
|
+
|
|
4
|
+
from typing import Callable,Awaitable,overload
|
|
5
|
+
from abc import ABC,abstractmethod
|
|
6
|
+
|
|
7
|
+
from .WebAttr import WebAttr
|
|
8
|
+
|
|
9
|
+
from .rest_mapping import ControllerMethodT
|
|
10
|
+
|
|
11
|
+
from pyboot.core.types import ClassTBase,ClassT
|
|
12
|
+
|
|
13
|
+
CallNext=Callable[[Request],JSONResponse | Awaitable[JSONResponse]]
|
|
14
|
+
|
|
15
|
+
class MiddlewareBase(ABC):
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def __call__(self,request:Request,call_next:CallNext) -> JSONResponse|Awaitable[JSONResponse]:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
class UseMiddleware:
|
|
21
|
+
def __init__(self,middlewares:list[type[MiddlewareBase]]) -> None:
|
|
22
|
+
self.middlewares:list[type[MiddlewareBase]]=middlewares
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@overload
|
|
26
|
+
def __call__(self,target:ClassTBase)->ClassTBase:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
@overload
|
|
30
|
+
def __call__(self,target:ControllerMethodT)->ControllerMethodT:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def __call__(self, target:ClassTBase|ControllerMethodT) -> ClassTBase|ControllerMethodT:
|
|
34
|
+
setattr(target,WebAttr.MIDDLEWARE_LIST.value,self.middlewares)
|
|
35
|
+
return target
|
|
36
|
+
|
|
37
|
+
class MiddlewareComponent:
|
|
38
|
+
def __init__(self) -> None:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def __call__(self,clazz:ClassT) -> ClassT:
|
|
42
|
+
setattr(clazz,WebAttr.IS_MIDDLEWARE_COMPONENT.value,True)
|
|
43
|
+
return clazz
|
|
44
|
+
|
pyboot_web/plugin.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
from pyboot.core.pyboot_plugin import PyBootPlugin
|
|
2
|
+
from pyboot.core.pyboot_context import PyBootContext
|
|
3
|
+
|
|
4
|
+
from .rest_mapping import *
|
|
5
|
+
from .ExceptionResolver import ExceptionResolver
|
|
6
|
+
from .middlewares import MiddlewareBase
|
|
7
|
+
|
|
8
|
+
import inspect
|
|
9
|
+
from typing import Callable, cast
|
|
10
|
+
|
|
11
|
+
from starlette.applications import Starlette
|
|
12
|
+
from starlette.requests import Request
|
|
13
|
+
from starlette.responses import JSONResponse
|
|
14
|
+
|
|
15
|
+
#WebPugin
|
|
16
|
+
class WebPlugin(PyBootPlugin):
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self.exception_resolver: ExceptionResolver = ExceptionResolver()
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
def _register_rest_controller_advice(self, context: PyBootContext):
|
|
23
|
+
|
|
24
|
+
for clazz in context.discovered_classes:
|
|
25
|
+
|
|
26
|
+
# skip other classes
|
|
27
|
+
if not getattr(clazz, WebAttr.IS_REST_CONTROLLER_ADVICE.value, False):
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
context.ioc.register_class(clazz=clazz)
|
|
31
|
+
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
def _register_rest_controller_class(self, context: PyBootContext):
|
|
35
|
+
|
|
36
|
+
for clazz in context.discovered_classes:
|
|
37
|
+
|
|
38
|
+
# skip other classes
|
|
39
|
+
if not getattr(clazz, WebAttr.IS_REST_CONTROLLER.value, False):
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
context.ioc.register_class(clazz=clazz)
|
|
43
|
+
|
|
44
|
+
def _register_service_class(self, context: PyBootContext):
|
|
45
|
+
|
|
46
|
+
for clazz in context.discovered_classes:
|
|
47
|
+
|
|
48
|
+
# skip other classes
|
|
49
|
+
if not getattr(clazz, WebAttr.IS_WEB_SERVICE.value, False):
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
context.ioc.register_class(clazz=clazz)
|
|
53
|
+
|
|
54
|
+
def _register_middleware_component_class(self,context:PyBootContext):
|
|
55
|
+
|
|
56
|
+
for clazz in context.discovered_classes:
|
|
57
|
+
|
|
58
|
+
#skip other classes
|
|
59
|
+
if not getattr(clazz,WebAttr.IS_MIDDLEWARE_COMPONENT.value,False):
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
context.ioc.register_class(clazz=clazz)
|
|
63
|
+
|
|
64
|
+
def _load_service_class(self, context: PyBootContext):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
def _load_component_class(self, context: PyBootContext):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
def _load_rest_controller_advice(self, context: PyBootContext):
|
|
71
|
+
|
|
72
|
+
for clazz in context.discovered_classes:
|
|
73
|
+
|
|
74
|
+
# skip other classes
|
|
75
|
+
if not getattr(clazz, WebAttr.IS_REST_CONTROLLER_ADVICE.value, False):
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
exception_handler_bean = context.ioc.get_bean(clazz=clazz)
|
|
79
|
+
|
|
80
|
+
# load the method beans for the exception handling
|
|
81
|
+
for method_name, method in inspect.getmembers(
|
|
82
|
+
exception_handler_bean, predicate=inspect.ismethod
|
|
83
|
+
):
|
|
84
|
+
|
|
85
|
+
# skip other methods
|
|
86
|
+
if not getattr(
|
|
87
|
+
method, WebAttr.IS_EXCEPTION_HANDLER_METHOD.value, False
|
|
88
|
+
):
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
#get the type for which this method of exception is required to resolve
|
|
92
|
+
exception_type = getattr(
|
|
93
|
+
method, WebAttr.EXCEPTION_HANDLER_FOR.value, None
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
#exception resolver cannot resolve None type exceptions
|
|
97
|
+
if exception_type is None:
|
|
98
|
+
raise TypeError(
|
|
99
|
+
f"Exception type cannot have {None} in exception handler method:{method_name} from class:{clazz.__name__}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
#make it sure to exception_type is from the Exception Base class
|
|
103
|
+
if not isinstance(exception_type, type) or not issubclass(
|
|
104
|
+
exception_type, Exception
|
|
105
|
+
):
|
|
106
|
+
raise TypeError(
|
|
107
|
+
f"Invalid exception type in handler method: {method_name} "
|
|
108
|
+
f"from class: {clazz.__name__}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
#register the handler in the exception resolver
|
|
112
|
+
self.exception_resolver.register(
|
|
113
|
+
exception_type=exception_type, handler=method
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def _wrap_exception_handler(self, handler: EndPointT) -> EndPointT:
|
|
117
|
+
|
|
118
|
+
if inspect.iscoroutinefunction(handler):
|
|
119
|
+
# if async function
|
|
120
|
+
async_handler = cast(Callable[[Request], Awaitable[JSONResponse]], handler)
|
|
121
|
+
|
|
122
|
+
async def async_wrapper(request: Request) -> JSONResponse:
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
return await async_handler(request)
|
|
126
|
+
except Exception as exception:
|
|
127
|
+
return self.exception_resolver.resolve(
|
|
128
|
+
request=request, exception=exception
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return async_wrapper
|
|
132
|
+
else:
|
|
133
|
+
sync_handler = cast(Callable[[Request], JSONResponse], handler)
|
|
134
|
+
|
|
135
|
+
def sync_wrapper(request: Request) -> JSONResponse:
|
|
136
|
+
try:
|
|
137
|
+
return sync_handler(request)
|
|
138
|
+
except Exception as exception:
|
|
139
|
+
return self.exception_resolver.resolve(
|
|
140
|
+
exception=exception, request=request
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return sync_wrapper
|
|
144
|
+
|
|
145
|
+
def _wrap_middlewares(self,handler:EndPointT,middlewares:list[MiddlewareBase])->EndPointT:
|
|
146
|
+
|
|
147
|
+
for middleware in reversed(middlewares):
|
|
148
|
+
next_handler=handler
|
|
149
|
+
|
|
150
|
+
async def wrapper(
|
|
151
|
+
request:Request,
|
|
152
|
+
middleware:MiddlewareBase=middleware,
|
|
153
|
+
next_handler:EndPointT=next_handler
|
|
154
|
+
)->JSONResponse:
|
|
155
|
+
async def call_next(request:Request)->JSONResponse:
|
|
156
|
+
result=next_handler(request)
|
|
157
|
+
|
|
158
|
+
if inspect.isawaitable(result):
|
|
159
|
+
return await result
|
|
160
|
+
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
result=middleware(request=request,call_next=call_next)
|
|
164
|
+
|
|
165
|
+
if inspect.isawaitable(result):
|
|
166
|
+
return await result
|
|
167
|
+
|
|
168
|
+
return result
|
|
169
|
+
handler=wrapper
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
return handler
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _load_rest_controllers(self, context: PyBootContext):
|
|
176
|
+
|
|
177
|
+
app: Starlette = context.app
|
|
178
|
+
|
|
179
|
+
for clazz in context.discovered_classes:
|
|
180
|
+
|
|
181
|
+
# skip other classes
|
|
182
|
+
if not getattr(clazz, WebAttr.IS_REST_CONTROLLER.value, False):
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
#base path of the rest controller class
|
|
186
|
+
base_path: str = getattr(clazz, WebAttr.REST_CONTROLLER_PATH.value, "")
|
|
187
|
+
|
|
188
|
+
#the base middleware to be used
|
|
189
|
+
base_middleware_list:list[type[MiddlewareBase]]=getattr(clazz,WebAttr.MIDDLEWARE_LIST.value,[])
|
|
190
|
+
|
|
191
|
+
controller_bean = context.ioc.get_bean(clazz=clazz)
|
|
192
|
+
|
|
193
|
+
# scan all the methods
|
|
194
|
+
for method_name, method in inspect.getmembers(
|
|
195
|
+
controller_bean, inspect.ismethod
|
|
196
|
+
):
|
|
197
|
+
|
|
198
|
+
# skip other methods
|
|
199
|
+
if not getattr(method, WebAttr.IS_ROUTE.value, False):
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
#path annotated on this method
|
|
203
|
+
route_path: str = getattr(method, WebAttr.ROUTE_PATH.value, "")
|
|
204
|
+
|
|
205
|
+
#total path of the endpoint
|
|
206
|
+
full_path = f"{base_path.rstrip("/")}/{route_path.lstrip("/")}"
|
|
207
|
+
|
|
208
|
+
#method of this endpoint
|
|
209
|
+
route_http_method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"] = (
|
|
210
|
+
getattr(method, WebAttr.ROUTE_METHOD.value, "GET")
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
#middlewares annotated to be used on this route
|
|
214
|
+
route_middlewares:list[type[MiddlewareBase]]=getattr(method,WebAttr.MIDDLEWARE_LIST.value,[])
|
|
215
|
+
|
|
216
|
+
#full list of the middlewares to be applied on this route
|
|
217
|
+
full_middleware_list:list[type[MiddlewareBase]]=base_middleware_list+route_middlewares
|
|
218
|
+
|
|
219
|
+
#getting the beans of middleware from the ioc containers
|
|
220
|
+
middleware_beans:list[MiddlewareBase]=[
|
|
221
|
+
cast(
|
|
222
|
+
MiddlewareBase,
|
|
223
|
+
context.ioc.get_bean(clazz=middleware_class)
|
|
224
|
+
)
|
|
225
|
+
for middleware_class in full_middleware_list
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
#the handler method itself
|
|
229
|
+
handler: EndPointT = cast(EndPointT, method)
|
|
230
|
+
|
|
231
|
+
#wrap the endpoint in the middlewares
|
|
232
|
+
wrapped_handler=self._wrap_middlewares(
|
|
233
|
+
handler=handler,
|
|
234
|
+
middlewares=middleware_beans
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
#wrap the handler in the exception resolver
|
|
238
|
+
wrapped_handler = self._wrap_exception_handler(handler=wrapped_handler)
|
|
239
|
+
|
|
240
|
+
#register the route in the app instance
|
|
241
|
+
app.add_route(
|
|
242
|
+
path=full_path,
|
|
243
|
+
methods=[route_http_method],
|
|
244
|
+
route=wrapped_handler
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
print(f"Registered route: {route_http_method} {full_path} for method: {method_name} in controller: {clazz.__name__}")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _load_middleware_component_class(self,context:PyBootContext):
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
def after_plugins_discovery(self, context: PyBootContext) -> None:
|
|
254
|
+
return super().after_plugins_discovery(context)
|
|
255
|
+
|
|
256
|
+
def before_classes_discovery(self, context: PyBootContext) -> None:
|
|
257
|
+
return super().before_classes_discovery(context)
|
|
258
|
+
|
|
259
|
+
def after_classes_discovery(self, context: PyBootContext) -> None:
|
|
260
|
+
return super().after_classes_discovery(context)
|
|
261
|
+
|
|
262
|
+
def before_register(self, context: PyBootContext) -> None:
|
|
263
|
+
return super().before_register(context)
|
|
264
|
+
|
|
265
|
+
def on_register(self, context: PyBootContext) -> None:
|
|
266
|
+
self._register_service_class(context=context)
|
|
267
|
+
|
|
268
|
+
#register rest controller advice because its bean will be used for the exception resolver of rest controller
|
|
269
|
+
self._register_rest_controller_advice(context=context)
|
|
270
|
+
|
|
271
|
+
#register the middleware component class
|
|
272
|
+
self._register_middleware_component_class(context=context)
|
|
273
|
+
|
|
274
|
+
#register the rest controller at last
|
|
275
|
+
self._register_rest_controller_class(context=context)
|
|
276
|
+
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
def after_register(self, context: PyBootContext) -> None:
|
|
280
|
+
return super().after_register(context)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def before_load(self, context: PyBootContext) -> None:
|
|
284
|
+
return super().before_load(context)
|
|
285
|
+
|
|
286
|
+
def on_load(self, context: PyBootContext) -> None:
|
|
287
|
+
self._load_service_class(context=context)
|
|
288
|
+
self._load_component_class(context=context)
|
|
289
|
+
|
|
290
|
+
#make sure to initialize the rest controller advice class so exception resolver can register it
|
|
291
|
+
self._load_rest_controller_advice(context=context)
|
|
292
|
+
|
|
293
|
+
#load the middleware component class
|
|
294
|
+
self._load_middleware_component_class(context=context)
|
|
295
|
+
|
|
296
|
+
#configure and load the rest controllers
|
|
297
|
+
self._load_rest_controllers(context=context)
|
|
298
|
+
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
def after_load(self, context: PyBootContext) -> None:
|
|
302
|
+
return super().after_load(context)
|
|
303
|
+
|
|
304
|
+
def on_ready(self, context: PyBootContext) -> None:
|
|
305
|
+
print(f"WebPlugin Ready for context:{context}")
|
pyboot_web/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from .types import *
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from .WebAttr import *
|
|
6
|
+
from pyboot.core.types import ClassT
|
|
7
|
+
|
|
8
|
+
class RestController:
|
|
9
|
+
def __init__(self,path:str) -> None:
|
|
10
|
+
self.path:str=path
|
|
11
|
+
return
|
|
12
|
+
|
|
13
|
+
def __call__(self, clazz:ClassT) -> ClassT:
|
|
14
|
+
setattr(clazz,WebAttr.IS_REST_CONTROLLER.value,True)
|
|
15
|
+
setattr(clazz,WebAttr.REST_CONTROLLER_PATH.value,self.path)
|
|
16
|
+
|
|
17
|
+
return clazz
|
|
18
|
+
|
|
19
|
+
class RequestMapping:
|
|
20
|
+
def __init__(self,path:str,method:Literal["GET","POST","PUT","PATCH","DELETE"]) -> None:
|
|
21
|
+
self.path:str=path
|
|
22
|
+
self.method:str=method
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
def __call__(self,func:ControllerMethodT) ->ControllerMethodT:
|
|
26
|
+
setattr(func,WebAttr.IS_ROUTE.value,True)
|
|
27
|
+
setattr(func,WebAttr.ROUTE_PATH.value,self.path)
|
|
28
|
+
setattr(func,WebAttr.ROUTE_METHOD.value,self.method)
|
|
29
|
+
|
|
30
|
+
return func
|
|
31
|
+
|
|
32
|
+
class GetMapping(RequestMapping):
|
|
33
|
+
def __init__(self, path: str) -> None:
|
|
34
|
+
super().__init__(path=path,method="GET")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PostMapping(RequestMapping):
|
|
38
|
+
def __init__(self,path:str)->None:
|
|
39
|
+
super().__init__(path=path,method="POST")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PutMapping(RequestMapping):
|
|
43
|
+
def __init__(self,path:str)->None:
|
|
44
|
+
super().__init__(path=path,method="PUT")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PatchMapping(RequestMapping):
|
|
48
|
+
def __init__(self,path:str)->None:
|
|
49
|
+
super().__init__(path=path,method="PATCH")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DeleteMapping(RequestMapping):
|
|
53
|
+
def __init__(self,path:str)->None:
|
|
54
|
+
super().__init__(path=path,method="DELETE")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
pyboot_web/service.py
ADDED
pyboot_web/types.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import TypeVar,TypeAlias,Callable,Any,Awaitable
|
|
2
|
+
|
|
3
|
+
from starlette.requests import Request
|
|
4
|
+
from starlette.responses import JSONResponse
|
|
5
|
+
|
|
6
|
+
E=TypeVar("E",bound=Exception)
|
|
7
|
+
|
|
8
|
+
ControllerMethodT: TypeAlias = (
|
|
9
|
+
Callable[[Any, Request], JSONResponse]
|
|
10
|
+
| Callable[[Any, Request], Awaitable[JSONResponse]]
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
EndPointT: TypeAlias = (
|
|
14
|
+
Callable[[Request], JSONResponse]
|
|
15
|
+
| Callable[[Request], Awaitable[JSONResponse]]
|
|
16
|
+
)
|
pyboot_web/value.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any
|
|
3
|
+
from dotenv import load_dotenv,find_dotenv
|
|
4
|
+
|
|
5
|
+
from pyboot import PyBootContext
|
|
6
|
+
from pyboot.core.pyboot_plugin import PyBootPlugin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def Value(key:str,default:Any):
|
|
10
|
+
"""
|
|
11
|
+
This function retrieves the value of an environment variable specified by the key. It first checks if the environment variable is set; if not, it falls back to the provided default value. If neither is available, it raises a MissingValueException. This function is useful for accessing configuration values that may be defined in a .env file or as system environment variables.
|
|
12
|
+
"""
|
|
13
|
+
from_env = os.getenv(key=key) if os.getenv(key=key) is not None else default
|
|
14
|
+
|
|
15
|
+
if from_env is None:
|
|
16
|
+
raise MissingValueException(f"Missing value for key:{key}.Got Value {from_env}")
|
|
17
|
+
|
|
18
|
+
return from_env
|
|
19
|
+
|
|
20
|
+
class MissingValueException(Exception):
|
|
21
|
+
"""
|
|
22
|
+
This exception is raised when a required value is missing. It is used in the Value function to indicate that an expected environment variable or default value was not provided, signaling a configuration issue that needs to be addressed.
|
|
23
|
+
"""
|
|
24
|
+
def __init__(self,message:str) -> None:
|
|
25
|
+
self.message:str=message
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
#ValuePlugin class implements the PyBootPlugin interface and provides lifecycle hooks for managing environment variable loading. It uses the dotenv library to load environment variables from a .env file before class discovery, ensuring that configuration values are available throughout the application lifecycle.
|
|
29
|
+
class ValuePlugin(PyBootPlugin):
|
|
30
|
+
"""
|
|
31
|
+
This is the plugin class for managing environment variable loading in a PyBoot application. It implements the PyBootPlugin interface and provides lifecycle hooks that are called at various stages of the application startup process. The primary purpose of this plugin is to load environment variables from a .env file before class discovery, ensuring that configuration values are available for use throughout the application.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def after_plugins_discovery(self, context: PyBootContext) -> None:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def before_classes_discovery(self, context: PyBootContext) -> None:
|
|
38
|
+
load_dotenv(find_dotenv(usecwd=True),override=True)
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
def after_classes_discovery(self, context: PyBootContext) -> None:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def before_register(self, context: PyBootContext) -> None:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
def on_register(self, context: PyBootContext) -> None:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def after_register(self, context: PyBootContext) -> None:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def before_load(self, context: PyBootContext) -> None:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
def on_load(self, context: PyBootContext) -> None:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
def after_load(self, context: PyBootContext) -> None:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
def on_ready(self, context: PyBootContext) -> None:
|
|
63
|
+
pass
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyboot-web
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Starlette web plugin for PyBoot
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: pyboot-framework>=0.1.0
|
|
10
|
+
Requires-Dist: starlette>=0.40.0
|
|
11
|
+
Requires-Dist: pydantic>=2.0
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# pyboot-web
|
|
15
|
+
|
|
16
|
+
`pyboot-web` is the Starlette web plugin for [PyBoot](../../pyboot/README.md).
|
|
17
|
+
It adds decorator-based HTTP controllers, route mappings, services, request
|
|
18
|
+
middleware, centralized exception handlers, and `.env` configuration loading
|
|
19
|
+
to a PyBoot application.
|
|
20
|
+
|
|
21
|
+
The plugin is intentionally small: PyBoot remains responsible for class
|
|
22
|
+
discovery, dependency injection, bean creation, and lifecycle orchestration;
|
|
23
|
+
this package translates annotated classes and methods into Starlette routes.
|
|
24
|
+
|
|
25
|
+
## Documentation
|
|
26
|
+
|
|
27
|
+
- [Examples](./EXAMPLES.md) - runnable snippets for routing, services,
|
|
28
|
+
middleware, exception advice, and configuration.
|
|
29
|
+
- [Production starter](./STARTER.md) - a recommended project layout,
|
|
30
|
+
bootstrap implementation, deployment checklist, and testing approach.
|
|
31
|
+
- [License](./LICENSE) - MIT license terms.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
| Capability | API |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| HTTP controller | `RestController` |
|
|
38
|
+
| HTTP routes | `GetMapping`, `PostMapping`, `PutMapping`, `PatchMapping`, `DeleteMapping` |
|
|
39
|
+
| Application services | `Service` |
|
|
40
|
+
| PyBoot-managed components | `Component` |
|
|
41
|
+
| Bean factories | `Configuration`, `Bean` |
|
|
42
|
+
| Middleware | `MiddlewareBase`, `MiddlewareComponent`, `UseMiddleware` |
|
|
43
|
+
| Exception advice | `RestControllerAdvice`, `ExceptionHandler` |
|
|
44
|
+
| Environment configuration | `Value`, `ValuePlugin` |
|
|
45
|
+
| PyBoot integration | `WebPlugin`, registered as the `web` PyBoot plugin |
|
|
46
|
+
|
|
47
|
+
The package supports Python 3.11 and newer and uses Starlette for routing and
|
|
48
|
+
request/response types.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
Install PyBoot and this plugin in the same environment:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python -m pip install pyboot-framework pyboot-web
|
|
56
|
+
python -m pip install python-dotenv uvicorn
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For a source checkout, from the repository root:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
python -m pip install -e pyboot
|
|
63
|
+
python -m pip install -e plugins/pyboot_web
|
|
64
|
+
python -m pip install python-dotenv uvicorn
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The package metadata declares `starlette`. `python-dotenv` is required by the
|
|
68
|
+
`ValuePlugin` implementation.
|
|
69
|
+
|
|
70
|
+
Verify that PyBoot can discover the plugin entry points:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python -c "from importlib.metadata import entry_points; print([e.name for e in entry_points(group='pyboot.plugins')])"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The output should include `web`. It may also include `value` and other
|
|
77
|
+
installed plugins.
|
|
78
|
+
|
|
79
|
+
## Minimal bootstrap contract
|
|
80
|
+
|
|
81
|
+
Create a Starlette application, pass it to PyBoot, bootstrap the package that
|
|
82
|
+
contains the application classes, and expose the original Starlette object:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from starlette.applications import Starlette
|
|
86
|
+
from pyboot.core.application import PyBoot
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
app = Starlette(debug=False)
|
|
90
|
+
PyBoot(app_instance=app, debug=False).bootstrap(package="app")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Run it with an ASGI server:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
python -m uvicorn main:app --host 127.0.0.1 --port 8000
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`PyBoot.bootstrap()` returns `None`; it mutates the supplied Starlette
|
|
100
|
+
application by registering routes. Do not assign the return value to `app`.
|
|
101
|
+
|
|
102
|
+
## API reference
|
|
103
|
+
|
|
104
|
+
### `pyboot_web.rest_mapping`
|
|
105
|
+
|
|
106
|
+
- `RestController(path: str)` marks a class as an HTTP controller.
|
|
107
|
+
- `GetMapping(path)`, `PostMapping(path)`, `PutMapping(path)`,
|
|
108
|
+
`PatchMapping(path)`, and `DeleteMapping(path)` provide method-specific
|
|
109
|
+
route decorators for methods declared inside a `RestController` class.
|
|
110
|
+
|
|
111
|
+
Route decorators are intended for controller methods, following the
|
|
112
|
+
class-oriented style used by Spring Boot:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
@RestController("/api")
|
|
116
|
+
class HealthController:
|
|
117
|
+
@GetMapping("/health")
|
|
118
|
+
async def health(self, request: Request) -> JSONResponse:
|
|
119
|
+
...
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Supported methods are `GET`, `POST`, `PUT`, `PATCH`, and `DELETE`. A standalone
|
|
123
|
+
function decorated as a route is not discovered by `WebPlugin`.
|
|
124
|
+
|
|
125
|
+
### `pyboot_web.service`
|
|
126
|
+
|
|
127
|
+
- `Service()` marks a class for web-plugin registration and IoC construction.
|
|
128
|
+
|
|
129
|
+
Use `Service` for web-layer services. Use PyBoot's built-in `Component` for
|
|
130
|
+
general application dependencies that should be managed by the core IoC
|
|
131
|
+
container:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from pyboot.core.builtins.component import Component
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@Component()
|
|
138
|
+
class AuditService:
|
|
139
|
+
...
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`Component` classes can be injected into `Service` classes and controllers
|
|
143
|
+
when their constructor parameters have type annotations.
|
|
144
|
+
|
|
145
|
+
### PyBoot built-in bean management
|
|
146
|
+
|
|
147
|
+
`Component` is the general-purpose PyBoot IoC decorator. For factory-created
|
|
148
|
+
objects, use a configuration class with methods annotated by `Bean`:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from pyboot.core.builtins.configuration import Bean, Configuration
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class Clock:
|
|
155
|
+
...
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@Configuration()
|
|
159
|
+
class ApplicationConfiguration:
|
|
160
|
+
@Bean()
|
|
161
|
+
def clock(self) -> Clock:
|
|
162
|
+
return Clock()
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The `@Bean` method must have a return annotation. PyBoot registers that return
|
|
166
|
+
type as a bean factory, resolves any annotated method dependencies, and creates
|
|
167
|
+
the bean through the IoC container. `Component`, `Configuration`, and `Bean`
|
|
168
|
+
are core PyBoot features, not web-specific decorators.
|
|
169
|
+
|
|
170
|
+
### `pyboot_web.middlewares`
|
|
171
|
+
|
|
172
|
+
- `MiddlewareBase` defines the callable middleware contract.
|
|
173
|
+
- `MiddlewareComponent()` marks a middleware class for IoC registration.
|
|
174
|
+
- `UseMiddleware(middlewares)` attaches middleware classes to a controller or
|
|
175
|
+
route method.
|
|
176
|
+
|
|
177
|
+
Pass middleware classes, not instances. The plugin obtains instances from
|
|
178
|
+
PyBoot's IoC container.
|
|
179
|
+
|
|
180
|
+
### `pyboot_web.exception_handler`
|
|
181
|
+
|
|
182
|
+
- `RestControllerAdvice()` marks an exception advice class.
|
|
183
|
+
- `ExceptionHandler(exception_type)` maps an exception type to a handler method.
|
|
184
|
+
|
|
185
|
+
Handlers receive `(request, exception)` after `self` and should return a
|
|
186
|
+
Starlette response. A matching base-class handler can handle subclasses.
|
|
187
|
+
Duplicate registrations for the same exception type are rejected. Unhandled
|
|
188
|
+
exceptions are re-raised.
|
|
189
|
+
|
|
190
|
+
### `pyboot_web.value`
|
|
191
|
+
|
|
192
|
+
- `Value(key, default)` returns an environment value or the supplied default.
|
|
193
|
+
- `MissingValueException` is raised when both are `None`.
|
|
194
|
+
- `ValuePlugin` loads a discovered `.env` file before class discovery.
|
|
195
|
+
|
|
196
|
+
Environment values are strings and should be converted explicitly at the
|
|
197
|
+
configuration boundary. `ValuePlugin` uses `override=True`, so `.env` values
|
|
198
|
+
can replace values already present in the process environment.
|
|
199
|
+
|
|
200
|
+
### `pyboot_web.plugin`
|
|
201
|
+
|
|
202
|
+
- `WebPlugin` registers web beans, resolves exception advice, wraps routes with
|
|
203
|
+
middleware and exception handling, and adds routes to the supplied Starlette
|
|
204
|
+
application.
|
|
205
|
+
|
|
206
|
+
## Discovery and lifecycle
|
|
207
|
+
|
|
208
|
+
PyBoot loads installed `pyboot.plugins` entry points, imports the package
|
|
209
|
+
passed to `bootstrap()`, and scans its submodules for classes defined there.
|
|
210
|
+
The web plugin then:
|
|
211
|
+
|
|
212
|
+
1. Registers `Service`, `Component`, `MiddlewareComponent`,
|
|
213
|
+
`RestControllerAdvice`, and `RestController` classes with the IoC container.
|
|
214
|
+
2. Lets PyBoot create registered beans.
|
|
215
|
+
3. Registers exception advice handlers.
|
|
216
|
+
4. Combines controller and route middleware.
|
|
217
|
+
5. Adds annotated controller methods as Starlette routes.
|
|
218
|
+
|
|
219
|
+
Keep application classes inside the scanned package and keep module-level
|
|
220
|
+
side effects small and deterministic. Directories intended as packages should
|
|
221
|
+
contain `__init__.py`.
|
|
222
|
+
|
|
223
|
+
## Compatibility and scope
|
|
224
|
+
|
|
225
|
+
- This package targets Starlette and does not provide Flask or Django adapters.
|
|
226
|
+
- It adds routes to a supplied Starlette application; it does not create one.
|
|
227
|
+
- Services and middleware depend on PyBoot IoC registration and constructor
|
|
228
|
+
resolution.
|
|
229
|
+
- The package root does not re-export the decorators; import them from their
|
|
230
|
+
defining modules.
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
This project is distributed under the MIT License. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
pyboot_web/ExceptionResolver.py,sha256=Oy88bUSAO7ngDPtsiAW9o7lQyQF7dX5RBen2YymUBMo,1009
|
|
2
|
+
pyboot_web/WebAttr.py,sha256=AFDjiNVj8ik0ggk-B3YySy00f5OQ6mqkRakwsWJyWFE,575
|
|
3
|
+
pyboot_web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
pyboot_web/exception_handler.py,sha256=N3ykGEXa8z8iasti9VbETprpCmqZgjtse50a1sFyNU8,766
|
|
5
|
+
pyboot_web/middlewares.py,sha256=g2fclsMJ_N0CucUsasBT-bbgIh9zp-i19UjfOXcxeVI,1314
|
|
6
|
+
pyboot_web/plugin.py,sha256=cwupIrhhZld5UZMv1mSfYjWThM6IbR_MqTFZ2Sx1FSM,11191
|
|
7
|
+
pyboot_web/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
pyboot_web/rest_mapping.py,sha256=wgLooK3w0YlQFtYngAqTTgWqF3ion4nttrACVPicjEg,1557
|
|
9
|
+
pyboot_web/service.py,sha256=CAtxlHjiprQH5-42vThpfCHwp-Gv7D6AOsaeRtdjw34,263
|
|
10
|
+
pyboot_web/types.py,sha256=tjCyId9IxPFkqkUV6xZNkMgv8gkhXvUIjWCsvzyF59g,443
|
|
11
|
+
pyboot_web/value.py,sha256=NFUZBLwrkTkKB4a3MX2KZQT2ofWtc4bqCEU_YjWLDTo,2883
|
|
12
|
+
pyboot_web-0.1.0.dist-info/licenses/LICENSE,sha256=MaPMFkS6sDLwCqmK7QNFZRB8EJng3h8TLkrFQcByH7s,1097
|
|
13
|
+
pyboot_web-0.1.0.dist-info/METADATA,sha256=-YX-brKti0ub9J6EvcaFg7_cA14UczO_XpQDsA5dPOM,8000
|
|
14
|
+
pyboot_web-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
15
|
+
pyboot_web-0.1.0.dist-info/entry_points.txt,sha256=NRI3Ju3uBCLJIUaxMHHoRh5ont2dHkY4gtCzf0DCIBw,88
|
|
16
|
+
pyboot_web-0.1.0.dist-info/top_level.txt,sha256=hbEdvPKJgVkiL2-eWC3IJBd2ESzyBiZHmsDIxqNxvd4,11
|
|
17
|
+
pyboot_web-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pyboot contributors
|
|
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 @@
|
|
|
1
|
+
pyboot_web
|