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.
@@ -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
@@ -0,0 +1,11 @@
1
+ from pyboot.core.types import ClassT
2
+
3
+ from .WebAttr import WebAttr
4
+
5
+ class Service:
6
+ def __init__(self) -> None:
7
+ pass
8
+
9
+ def __call__(self,clazz:ClassT) -> ClassT:
10
+ setattr(clazz,WebAttr.IS_WEB_SERVICE.value,True)
11
+ return clazz
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [pyboot.plugins]
2
+ value = pyboot_web.value:ValuePlugin
3
+ web = pyboot_web.plugin:WebPlugin
@@ -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