mvcx 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.
- mvcx/__init__.py +0 -0
- mvcx/application.py +337 -0
- mvcx/asgi.py +394 -0
- mvcx/collections.py +107 -0
- mvcx/controller.py +131 -0
- mvcx/crypto.py +9 -0
- mvcx/di.py +148 -0
- mvcx/http.py +348 -0
- mvcx/middleware.py +30 -0
- mvcx/result.py +21 -0
- mvcx/routing.py +226 -0
- mvcx/rsgi.py +180 -0
- mvcx/typing.py +75 -0
- mvcx/url.py +42 -0
- mvcx/view.py +62 -0
- mvcx/websocket.py +103 -0
- mvcx-0.1.0.dist-info/METADATA +20 -0
- mvcx-0.1.0.dist-info/RECORD +20 -0
- mvcx-0.1.0.dist-info/WHEEL +4 -0
- mvcx-0.1.0.dist-info/entry_points.txt +3 -0
mvcx/__init__.py
ADDED
|
File without changes
|
mvcx/application.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from asyncio import AbstractEventLoop
|
|
3
|
+
from typing import Literal, Self, cast
|
|
4
|
+
|
|
5
|
+
from granian import Granian
|
|
6
|
+
from granian.constants import Interfaces
|
|
7
|
+
from granian.log import LogLevels
|
|
8
|
+
|
|
9
|
+
from mvcx.asgi import (
|
|
10
|
+
AsgiBodyFactory,
|
|
11
|
+
AsgiHttpReceive,
|
|
12
|
+
AsgiHttpSend,
|
|
13
|
+
AsgiLifespanReceive,
|
|
14
|
+
AsgiLifespanSend,
|
|
15
|
+
AsgiReceive,
|
|
16
|
+
AsgiScope,
|
|
17
|
+
AsgiSend,
|
|
18
|
+
AsgiWsAdapter,
|
|
19
|
+
AsgiWsReceive,
|
|
20
|
+
AsgiWsSend,
|
|
21
|
+
close_ws_asgi,
|
|
22
|
+
create_http_context_asgi,
|
|
23
|
+
send_http_exc_asgi,
|
|
24
|
+
send_response_asgi,
|
|
25
|
+
)
|
|
26
|
+
from mvcx.controller import ControllerFactory
|
|
27
|
+
from mvcx.di import Container, DefaultContainer, Invokable, Lifetime, Resolvable
|
|
28
|
+
from mvcx.http import (
|
|
29
|
+
HttpContext,
|
|
30
|
+
HttpHandler,
|
|
31
|
+
HttpHandlerFactory,
|
|
32
|
+
HttpResult,
|
|
33
|
+
Request,
|
|
34
|
+
)
|
|
35
|
+
from mvcx.middleware import (
|
|
36
|
+
HttpMiddlewareFactory,
|
|
37
|
+
MiddlewareFactory,
|
|
38
|
+
WsMiddlewareFactory,
|
|
39
|
+
)
|
|
40
|
+
from mvcx.routing import DefaultRouter, RouteMethod, Router
|
|
41
|
+
from mvcx.rsgi import (
|
|
42
|
+
RsgiHttpProto,
|
|
43
|
+
RsgiProto,
|
|
44
|
+
RsgiScope,
|
|
45
|
+
RsgiWsAdapter,
|
|
46
|
+
RsgiWsProto,
|
|
47
|
+
close_ws_rsgi,
|
|
48
|
+
send_http_exc_rsgi,
|
|
49
|
+
send_response_rsgi,
|
|
50
|
+
)
|
|
51
|
+
from mvcx.url import Path
|
|
52
|
+
from mvcx.view import View
|
|
53
|
+
from mvcx.websocket import WebSocket, WsHandler, WsHandlerFactory, WsResult
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def _default_http_handler(request: Request) -> HttpResult:
|
|
57
|
+
return await request.handler(request)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def _default_ws_handler(ws: WebSocket) -> WsResult:
|
|
61
|
+
return await ws.handler(ws)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Application:
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
container: Container | None = None,
|
|
69
|
+
router: Router | None = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
self._container: Container = container or DefaultContainer()
|
|
72
|
+
self._router: Router = router or DefaultRouter()
|
|
73
|
+
|
|
74
|
+
self._http_handler: HttpHandler = _default_http_handler
|
|
75
|
+
self._ws_handler: WsHandler = _default_ws_handler
|
|
76
|
+
|
|
77
|
+
if pathlib.Path("./views").is_dir():
|
|
78
|
+
self.add_dependency(View)
|
|
79
|
+
|
|
80
|
+
def add_route(
|
|
81
|
+
self, path: str, method: RouteMethod, handler: HttpHandlerFactory
|
|
82
|
+
) -> Self:
|
|
83
|
+
self._router.add_route(Path(path), method, handler)
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def add_controller(self, path: str, controller: ControllerFactory) -> Self:
|
|
87
|
+
async def http_handler(request: Request) -> HttpResult:
|
|
88
|
+
return await self._container.invoke(
|
|
89
|
+
controller, request.scoped_cache
|
|
90
|
+
).handle_http(request)
|
|
91
|
+
|
|
92
|
+
async def ws_handler(ws: WebSocket) -> WsResult:
|
|
93
|
+
return await self._container.invoke(controller, ws.scoped_cache).handle_ws(
|
|
94
|
+
ws
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
self._router.add_route(Path(path), "*", lambda: http_handler)
|
|
98
|
+
self._router.add_ws_route(Path(path), lambda: ws_handler)
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
def add_fallback_route(self, status: int, handler: HttpHandlerFactory) -> Self:
|
|
102
|
+
self._router.add_fallback_route(status, handler)
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def add_ws_route(self, path: str, handler: WsHandlerFactory) -> Self:
|
|
106
|
+
self._router.add_ws_route(Path(path), handler)
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
def add_dependency[T](
|
|
110
|
+
self,
|
|
111
|
+
resolvable: Resolvable[T],
|
|
112
|
+
invokable: Invokable[T] | None = None,
|
|
113
|
+
lifetime: Lifetime | None = None,
|
|
114
|
+
) -> Self:
|
|
115
|
+
self._container.register(
|
|
116
|
+
resolvable, invokable or resolvable, lifetime or "singleton"
|
|
117
|
+
)
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
def overwrite_dependency[T](
|
|
121
|
+
self, resolvable: Resolvable[T], invokable: Invokable[T]
|
|
122
|
+
) -> Self:
|
|
123
|
+
self._container.overwrite(resolvable, invokable)
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
def add_middleware(self, middleware_factory: MiddlewareFactory) -> Self:
|
|
127
|
+
async def http_handler(
|
|
128
|
+
request: Request, call_next: HttpHandler = self._http_handler
|
|
129
|
+
) -> HttpResult:
|
|
130
|
+
return await self._container.invoke(
|
|
131
|
+
middleware_factory, request.scoped_cache
|
|
132
|
+
).handle_http(request, call_next)
|
|
133
|
+
|
|
134
|
+
async def ws_handler(
|
|
135
|
+
ws: WebSocket, call_next: WsHandler = self._ws_handler
|
|
136
|
+
) -> WsResult:
|
|
137
|
+
return await self._container.invoke(
|
|
138
|
+
middleware_factory, ws.scoped_cache
|
|
139
|
+
).handle_ws(ws, call_next)
|
|
140
|
+
|
|
141
|
+
self._http_handler = http_handler
|
|
142
|
+
self._ws_handler = ws_handler
|
|
143
|
+
return self
|
|
144
|
+
|
|
145
|
+
def add_http_middleware(self, middleware_factory: HttpMiddlewareFactory) -> Self:
|
|
146
|
+
async def handler(
|
|
147
|
+
request: Request, call_next: HttpHandler = self._http_handler
|
|
148
|
+
) -> HttpResult:
|
|
149
|
+
return await self._container.invoke(
|
|
150
|
+
middleware_factory, request.scoped_cache
|
|
151
|
+
)(request, call_next)
|
|
152
|
+
|
|
153
|
+
self._http_handler = handler
|
|
154
|
+
return self
|
|
155
|
+
|
|
156
|
+
def add_ws_middleware(self, middleware_factory: WsMiddlewareFactory) -> Self:
|
|
157
|
+
async def handler(
|
|
158
|
+
ws: WebSocket, call_next: WsHandler = self._ws_handler
|
|
159
|
+
) -> WsResult:
|
|
160
|
+
return await self._container.invoke(middleware_factory, ws.scoped_cache)(
|
|
161
|
+
ws, call_next
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
self._ws_handler = handler
|
|
165
|
+
return self
|
|
166
|
+
|
|
167
|
+
def run(
|
|
168
|
+
self,
|
|
169
|
+
app: str,
|
|
170
|
+
port: int,
|
|
171
|
+
*,
|
|
172
|
+
host: str = "0.0.0.0",
|
|
173
|
+
reload: bool = False,
|
|
174
|
+
static_url: str = "/static",
|
|
175
|
+
static_root: str = "static",
|
|
176
|
+
access_log: bool = True,
|
|
177
|
+
interface: Literal["asgi", "rsgi"] = "rsgi",
|
|
178
|
+
) -> None:
|
|
179
|
+
static_root_path = pathlib.Path(static_root)
|
|
180
|
+
|
|
181
|
+
Granian(
|
|
182
|
+
app,
|
|
183
|
+
port=port,
|
|
184
|
+
address=host,
|
|
185
|
+
reload=reload,
|
|
186
|
+
static_path_route=[static_url] if static_root_path.is_dir() else None,
|
|
187
|
+
static_path_mount=[static_root_path] if static_root_path.is_dir() else None,
|
|
188
|
+
log_access=access_log,
|
|
189
|
+
interface=Interfaces.ASGI if interface == "asgi" else Interfaces.RSGI,
|
|
190
|
+
log_level=LogLevels.info if reload else LogLevels.warning,
|
|
191
|
+
).serve()
|
|
192
|
+
|
|
193
|
+
async def __call__(
|
|
194
|
+
self, scope: AsgiScope, receive: AsgiReceive, send: AsgiSend
|
|
195
|
+
) -> None:
|
|
196
|
+
if scope["type"] == "lifespan":
|
|
197
|
+
receive = cast(AsgiLifespanReceive, receive)
|
|
198
|
+
send = cast(AsgiLifespanSend, send)
|
|
199
|
+
event = await receive()
|
|
200
|
+
match event["type"]:
|
|
201
|
+
case "lifespan.startup":
|
|
202
|
+
return await send({"type": "lifespan.startup.complete"})
|
|
203
|
+
case "lifespan.shutdown":
|
|
204
|
+
try:
|
|
205
|
+
await self._container.clear()
|
|
206
|
+
await send({"type": "lifespan.shutdown.complete"})
|
|
207
|
+
except Exception as e:
|
|
208
|
+
await send(
|
|
209
|
+
{"type": "lifespan.shutdown.failed", "message": str(e)}
|
|
210
|
+
)
|
|
211
|
+
raise e
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
context = create_http_context_asgi(scope, self._container)
|
|
215
|
+
try:
|
|
216
|
+
match scope["type"]:
|
|
217
|
+
case "http":
|
|
218
|
+
receive = cast(AsgiHttpReceive, receive)
|
|
219
|
+
send = cast(AsgiHttpSend, send)
|
|
220
|
+
body_factory = AsgiBodyFactory(receive)
|
|
221
|
+
request = Request(context, body_factory)
|
|
222
|
+
route = self._router.get_route(request.path, request.method)
|
|
223
|
+
if route.error is not None:
|
|
224
|
+
fallback = self._router.get_fallback_route(route.error)
|
|
225
|
+
if fallback.error is not None:
|
|
226
|
+
return await send_http_exc_asgi(send, fallback.error)
|
|
227
|
+
response = await self._container.invoke(
|
|
228
|
+
fallback.value, request.scoped_cache
|
|
229
|
+
)(request)
|
|
230
|
+
if response.error is not None:
|
|
231
|
+
return await send_http_exc_asgi(send, response.error)
|
|
232
|
+
return await send_response_asgi(send, response.value)
|
|
233
|
+
request.params = route.value.params
|
|
234
|
+
request.handler = self._container.invoke(
|
|
235
|
+
route.value.handler, request.scoped_cache
|
|
236
|
+
)
|
|
237
|
+
response = await self._http_handler(request)
|
|
238
|
+
if response.error is not None:
|
|
239
|
+
fallback = self._router.get_fallback_route(response.error)
|
|
240
|
+
if fallback.error is not None:
|
|
241
|
+
return await send_http_exc_asgi(send, fallback.error)
|
|
242
|
+
handler = self._container.invoke(
|
|
243
|
+
fallback.value, request.scoped_cache
|
|
244
|
+
)
|
|
245
|
+
response = await handler(request)
|
|
246
|
+
if response.error is not None:
|
|
247
|
+
return await send_http_exc_asgi(send, response.error)
|
|
248
|
+
return await send_response_asgi(send, response.value)
|
|
249
|
+
await send_response_asgi(send, response.value)
|
|
250
|
+
|
|
251
|
+
case "websocket":
|
|
252
|
+
receive = cast(AsgiWsReceive, receive)
|
|
253
|
+
send = cast(AsgiWsSend, send)
|
|
254
|
+
adapter = AsgiWsAdapter(receive, send)
|
|
255
|
+
ws = WebSocket(context, adapter)
|
|
256
|
+
route = self._router.get_ws_route(ws.path)
|
|
257
|
+
if route.error is not None:
|
|
258
|
+
return await close_ws_asgi(send, route.error)
|
|
259
|
+
handler = self._container.invoke(
|
|
260
|
+
route.value.handler, ws.scoped_cache
|
|
261
|
+
)
|
|
262
|
+
ws.handler = handler
|
|
263
|
+
ws.params = route.value.params
|
|
264
|
+
response = await self._ws_handler(ws)
|
|
265
|
+
if response.error is not None:
|
|
266
|
+
await close_ws_asgi(send, response.error)
|
|
267
|
+
|
|
268
|
+
finally:
|
|
269
|
+
await context.scoped_cache.clear()
|
|
270
|
+
|
|
271
|
+
def __rsgi_del__(self, loop: AbstractEventLoop) -> None:
|
|
272
|
+
loop.run_until_complete(self._container.clear())
|
|
273
|
+
|
|
274
|
+
async def __rsgi__(self, scope: RsgiScope, proto: RsgiProto) -> None:
|
|
275
|
+
context = HttpContext(
|
|
276
|
+
Path(scope.path),
|
|
277
|
+
scope.method,
|
|
278
|
+
lambda: scope.headers,
|
|
279
|
+
scope.query_string,
|
|
280
|
+
{},
|
|
281
|
+
self._container,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
match scope.proto:
|
|
286
|
+
case "http":
|
|
287
|
+
proto = cast(RsgiHttpProto, proto)
|
|
288
|
+
request = Request(context, proto)
|
|
289
|
+
route = self._router.get_route(request.path, request.method)
|
|
290
|
+
if route.error is not None:
|
|
291
|
+
fallback = self._router.get_fallback_route(route.error)
|
|
292
|
+
if fallback.error is not None:
|
|
293
|
+
return send_http_exc_rsgi(proto, fallback.error)
|
|
294
|
+
handler = self._container.invoke(
|
|
295
|
+
fallback.value, request.scoped_cache
|
|
296
|
+
)
|
|
297
|
+
response = await handler(request)
|
|
298
|
+
if response.error is not None:
|
|
299
|
+
return send_http_exc_rsgi(proto, response.error)
|
|
300
|
+
return await send_response_rsgi(proto, response.value)
|
|
301
|
+
handler = self._container.invoke(
|
|
302
|
+
route.value.handler, request.scoped_cache
|
|
303
|
+
)
|
|
304
|
+
request.handler = handler
|
|
305
|
+
request.params = route.value.params
|
|
306
|
+
response = await self._http_handler(request)
|
|
307
|
+
if response.error is not None:
|
|
308
|
+
fallback = self._router.get_fallback_route(response.error)
|
|
309
|
+
if fallback.error is not None:
|
|
310
|
+
return send_http_exc_rsgi(proto, fallback.error)
|
|
311
|
+
handler = self._container.invoke(
|
|
312
|
+
fallback.value, request.scoped_cache
|
|
313
|
+
)
|
|
314
|
+
response = await handler(request)
|
|
315
|
+
if response.error is not None:
|
|
316
|
+
return send_http_exc_rsgi(proto, response.error)
|
|
317
|
+
return await send_response_rsgi(proto, response.value)
|
|
318
|
+
return await send_response_rsgi(proto, response.value)
|
|
319
|
+
|
|
320
|
+
case "ws":
|
|
321
|
+
proto = cast(RsgiWsProto, proto)
|
|
322
|
+
adapter = RsgiWsAdapter(proto)
|
|
323
|
+
ws = WebSocket(context, adapter)
|
|
324
|
+
route = self._router.get_ws_route(ws.path)
|
|
325
|
+
if route.error is not None:
|
|
326
|
+
return close_ws_rsgi(proto, route.error)
|
|
327
|
+
handler = self._container.invoke(
|
|
328
|
+
route.value.handler, ws.scoped_cache
|
|
329
|
+
)
|
|
330
|
+
ws.handler = handler
|
|
331
|
+
ws.params = route.value.params
|
|
332
|
+
response = await self._ws_handler(ws)
|
|
333
|
+
if response.error is not None:
|
|
334
|
+
close_ws_rsgi(proto, response.error)
|
|
335
|
+
|
|
336
|
+
finally:
|
|
337
|
+
await context.scoped_cache.clear()
|