DefEssential 0.1.8__25-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.
- DefEssential/Adapter/Implements/Gunicorn/Server.py +152 -0
- DefEssential/Adapter/Implements/Gunicorn/Transport.py +171 -0
- DefEssential/Adapter/Implements/Gunicorn/__init__.py +5 -0
- DefEssential/Adapter/Implements/Python/Server.py +273 -0
- DefEssential/Adapter/Implements/Python/__init__.py +5 -0
- DefEssential/Adapter/Implements/Uvicorn/HTTP.py +256 -0
- DefEssential/Adapter/Implements/Uvicorn/Portal.py +65 -0
- DefEssential/Adapter/Implements/Uvicorn/Server.py +137 -0
- DefEssential/Adapter/Implements/Uvicorn/WebSocket.py +222 -0
- DefEssential/Adapter/Implements/Uvicorn/__init__.py +5 -0
- DefEssential/Adapter/Implements/__init__.py +3 -0
- DefEssential/Adapter/RequestHandler.py +109 -0
- DefEssential/Adapter/Server.py +54 -0
- DefEssential/Adapter/WSGI.py +219 -0
- DefEssential/Adapter/__init__.py +5 -0
- DefEssential/Application.py +657 -0
- DefEssential/Body.py +50 -0
- DefEssential/Content.py +18 -0
- DefEssential/Credentials.py +245 -0
- DefEssential/CrossOriginResourceSharing.py +107 -0
- DefEssential/Descriptor/DataObject.py +409 -0
- DefEssential/Descriptor/Types.py +248 -0
- DefEssential/Descriptor/Value.py +188 -0
- DefEssential/Descriptor/__init__.py +172 -0
- DefEssential/Error.py +66 -0
- DefEssential/Extension.py +191 -0
- DefEssential/Extensions/File.py +95 -0
- DefEssential/Extensions/FileSystem.py +195 -0
- DefEssential/Extensions/Metrics.py +511 -0
- DefEssential/Extensions/Ping.py +93 -0
- DefEssential/Extensions/Specifications.py +342 -0
- DefEssential/Extensions/Status.html +207 -0
- DefEssential/Extensions/Status.py +110 -0
- DefEssential/Extensions/Swagger.html +77 -0
- DefEssential/Extensions/Swagger.py +117 -0
- DefEssential/Extensions/Utils/Routes.py +42 -0
- DefEssential/Extensions/Utils/__init__.py +43 -0
- DefEssential/Extensions/__init__.py +21 -0
- DefEssential/IO/Readers/ChunkedStream.py +106 -0
- DefEssential/IO/Readers/ContentReader.py +36 -0
- DefEssential/IO/Readers/ContentReaders.py +241 -0
- DefEssential/IO/Readers/__init__.py +25 -0
- DefEssential/IO/RequestReader.py +125 -0
- DefEssential/IO/ResponseWriter.py +242 -0
- DefEssential/IO/WebSocket/WebSocket.py +288 -0
- DefEssential/IO/WebSocket/WebSocketError.py +17 -0
- DefEssential/IO/WebSocket/WebSocketFrame.py +68 -0
- DefEssential/IO/WebSocket/WebSocketFrameReader.py +98 -0
- DefEssential/IO/WebSocket/WebSocketHandshake.py +94 -0
- DefEssential/IO/WebSocket/__init__.py +13 -0
- DefEssential/IO/Writers/ChunkedStreamWriter.py +19 -0
- DefEssential/IO/Writers/ServerSentEvents.py +41 -0
- DefEssential/IO/Writers/ServerSentEventsWriter.py +67 -0
- DefEssential/IO/Writers/StreamWriter.py +57 -0
- DefEssential/IO/Writers/__init__.py +11 -0
- DefEssential/IO/__init__.py +7 -0
- DefEssential/Request.py +110 -0
- DefEssential/RequestRunner.py +261 -0
- DefEssential/Response.py +89 -0
- DefEssential/ResponseSpecification.py +23 -0
- DefEssential/Responses/ResponseFile.py +108 -0
- DefEssential/Responses/__init__.py +63 -0
- DefEssential/Route.py +55 -0
- DefEssential/Router.py +48 -0
- DefEssential/Runners/RunExtension.py +116 -0
- DefEssential/Runners/RunFactory.py +26 -0
- DefEssential/Runners/RunOptions.py +77 -0
- DefEssential/Runners/RunRequest.py +198 -0
- DefEssential/Runners/__init__.py +5 -0
- DefEssential/Startup.py +104 -0
- DefEssential/Utils/FileSystemObject/Configuration.py +15 -0
- DefEssential/Utils/FileSystemObject/Connection.py +57 -0
- DefEssential/Utils/FileSystemObject/File.py +41 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/Configuration.py +15 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/Connection.py +115 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/File.py +77 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/__init__.py +11 -0
- DefEssential/Utils/FileSystemObject/Implements/__init__.py +3 -0
- DefEssential/Utils/FileSystemObject/__init__.py +11 -0
- DefEssential/Utils/Loader.py +81 -0
- DefEssential/Utils/__init__.py +3 -0
- DefEssential/Validator/Pattern.py +36 -0
- DefEssential/Validator/Patterns/Array.py +30 -0
- DefEssential/Validator/Patterns/Common.py +92 -0
- DefEssential/Validator/Patterns/Compare.py +180 -0
- DefEssential/Validator/Patterns/Condition.py +60 -0
- DefEssential/Validator/Patterns/DataObject.py +85 -0
- DefEssential/Validator/Patterns/DateTime.py +111 -0
- DefEssential/Validator/Patterns/Number.py +43 -0
- DefEssential/Validator/Patterns/Object.py +150 -0
- DefEssential/Validator/Patterns/Size.py +110 -0
- DefEssential/Validator/Patterns/String.py +85 -0
- DefEssential/Validator/Patterns/Type.py +350 -0
- DefEssential/Validator/Patterns/__init__.py +137 -0
- DefEssential/Validator/Validate.py +29 -0
- DefEssential/Validator/Validator.py +30 -0
- DefEssential/Validator/__init__.py +11 -0
- DefEssential/Validators.py +255 -0
- DefEssential/__init__.py +83 -0
- defessential-0.1.8.dist-info/METADATA +86 -0
- defessential-0.1.8.dist-info/RECORD +104 -0
- defessential-0.1.8.dist-info/WHEEL +6 -0
- defessential-0.1.8.dist-info/licenses/LICENSE +674 -0
- defessential-0.1.8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
from abc import (
|
|
2
|
+
ABC,
|
|
3
|
+
abstractmethod,
|
|
4
|
+
)
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from inspect import isabstract
|
|
7
|
+
from logging import getLogger
|
|
8
|
+
from threading import (
|
|
9
|
+
RLock,
|
|
10
|
+
local,
|
|
11
|
+
)
|
|
12
|
+
from typing import (
|
|
13
|
+
Callable,
|
|
14
|
+
Optional,
|
|
15
|
+
Sequence,
|
|
16
|
+
Type,
|
|
17
|
+
TypeVar,
|
|
18
|
+
Union,
|
|
19
|
+
)
|
|
20
|
+
from weakref import ref
|
|
21
|
+
|
|
22
|
+
from .Error import ClientError
|
|
23
|
+
from .RequestRunner import RequestRunner
|
|
24
|
+
from .Request import Request
|
|
25
|
+
from .Response import Response
|
|
26
|
+
from .Route import Route
|
|
27
|
+
from .Router import Router
|
|
28
|
+
from .Runners import RunRequest
|
|
29
|
+
from .Utils.Loader import Loader
|
|
30
|
+
from .Runners.RunOptions import RunOptions
|
|
31
|
+
from .Extension import Extension
|
|
32
|
+
|
|
33
|
+
__all__ = (
|
|
34
|
+
'Application',
|
|
35
|
+
'ApplicationStartup',
|
|
36
|
+
'ApplicationCleanup',
|
|
37
|
+
'ApplicationRequest',
|
|
38
|
+
'ApplicationRequestResponse',
|
|
39
|
+
'ApplicationRequestError',
|
|
40
|
+
'RequestHookRegistration',
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ApplicationStartup(ABC):
|
|
45
|
+
"""Define the call contract for application startup hooks."""
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def __call__(self) -> None:
|
|
49
|
+
raise NotImplementedError
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ApplicationCleanup(ABC):
|
|
53
|
+
"""Define the call contract for application cleanup hooks."""
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def __call__(self) -> None:
|
|
57
|
+
raise NotImplementedError
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ApplicationRequest(ABC):
|
|
61
|
+
"""Define the call contract for request-entry hooks."""
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def __call__(
|
|
65
|
+
self,
|
|
66
|
+
request: Request,
|
|
67
|
+
) -> Request:
|
|
68
|
+
raise NotImplementedError
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ApplicationRequestResponse(ABC):
|
|
72
|
+
"""Define the call contract for response-return hooks."""
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def __call__(
|
|
76
|
+
self,
|
|
77
|
+
request: Request,
|
|
78
|
+
response: Optional[Response],
|
|
79
|
+
) -> Optional[Response]:
|
|
80
|
+
raise NotImplementedError
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ApplicationRequestError(ABC):
|
|
84
|
+
"""Define the call contract for request-error hooks."""
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def __call__(
|
|
88
|
+
self,
|
|
89
|
+
request: Request,
|
|
90
|
+
error: Exception,
|
|
91
|
+
) -> None:
|
|
92
|
+
raise NotImplementedError
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True, eq=False)
|
|
96
|
+
class RequestHookRegistration:
|
|
97
|
+
"""Provide a token for unregistering request hooks registered during startup."""
|
|
98
|
+
|
|
99
|
+
owner: Extension
|
|
100
|
+
request: tuple[ApplicationRequest, ...]
|
|
101
|
+
response: tuple[ApplicationRequestResponse, ...]
|
|
102
|
+
error: tuple[ApplicationRequestError, ...]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
Hook = TypeVar(
|
|
106
|
+
'Hook',
|
|
107
|
+
)
|
|
108
|
+
HookResult = TypeVar(
|
|
109
|
+
'HookResult',
|
|
110
|
+
)
|
|
111
|
+
EXTENSION_OWNERS: dict[int, tuple[ref[Extension], ref['Application']]] = {}
|
|
112
|
+
EXTENSION_OWNERS_LOCK = RLock()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def forget_extension(
|
|
116
|
+
identity: int,
|
|
117
|
+
reference: ref[Extension],
|
|
118
|
+
) -> None:
|
|
119
|
+
with EXTENSION_OWNERS_LOCK:
|
|
120
|
+
owner = EXTENSION_OWNERS.get(identity)
|
|
121
|
+
if owner is not None and owner[0] is reference:
|
|
122
|
+
del EXTENSION_OWNERS[identity]
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def application_hooks(
|
|
127
|
+
value: Union[Hook, Sequence[Hook]],
|
|
128
|
+
contract: Type[Hook],
|
|
129
|
+
name: str,
|
|
130
|
+
) -> tuple[Hook, ...]:
|
|
131
|
+
if isinstance(value, contract):
|
|
132
|
+
return (value,)
|
|
133
|
+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
|
134
|
+
result = tuple(value)
|
|
135
|
+
if all(isinstance(hook, contract) for hook in result):
|
|
136
|
+
return result
|
|
137
|
+
raise TypeError(name + ' must be a ' + contract.__name__ + ' instance or a sequence of them')
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Application:
|
|
141
|
+
"""Own registration state and execute a new handler instance per request."""
|
|
142
|
+
|
|
143
|
+
logger = getLogger('DefEssential.Application')
|
|
144
|
+
|
|
145
|
+
def __init__(
|
|
146
|
+
self,
|
|
147
|
+
exts: Sequence[Extension] = (),
|
|
148
|
+
*,
|
|
149
|
+
on_startup: Union[ApplicationStartup, Sequence[ApplicationStartup]] = (),
|
|
150
|
+
on_cleanup: Union[ApplicationCleanup, Sequence[ApplicationCleanup]] = (),
|
|
151
|
+
on_request: Union[ApplicationRequest, Sequence[ApplicationRequest]] = (),
|
|
152
|
+
on_request_response: Union[
|
|
153
|
+
ApplicationRequestResponse,
|
|
154
|
+
Sequence[ApplicationRequestResponse],
|
|
155
|
+
] = (),
|
|
156
|
+
on_request_error: Union[
|
|
157
|
+
ApplicationRequestError,
|
|
158
|
+
Sequence[ApplicationRequestError],
|
|
159
|
+
] = (),
|
|
160
|
+
):
|
|
161
|
+
self.startup_hooks = application_hooks(
|
|
162
|
+
on_startup,
|
|
163
|
+
ApplicationStartup,
|
|
164
|
+
'on_startup',
|
|
165
|
+
)
|
|
166
|
+
self.cleanup_hooks = application_hooks(
|
|
167
|
+
on_cleanup,
|
|
168
|
+
ApplicationCleanup,
|
|
169
|
+
'on_cleanup',
|
|
170
|
+
)
|
|
171
|
+
self.request_hooks = application_hooks(
|
|
172
|
+
on_request,
|
|
173
|
+
ApplicationRequest,
|
|
174
|
+
'on_request',
|
|
175
|
+
)
|
|
176
|
+
self.request_response_hooks = application_hooks(
|
|
177
|
+
on_request_response,
|
|
178
|
+
ApplicationRequestResponse,
|
|
179
|
+
'on_request_response',
|
|
180
|
+
)
|
|
181
|
+
self.request_error_hooks = application_hooks(
|
|
182
|
+
on_request_error,
|
|
183
|
+
ApplicationRequestError,
|
|
184
|
+
'on_request_error',
|
|
185
|
+
)
|
|
186
|
+
self.hook_context = local()
|
|
187
|
+
self.router = Router()
|
|
188
|
+
self.extension_routes: list[Route] = []
|
|
189
|
+
if not isinstance(exts, Sequence) or isinstance(exts, (str, bytes)):
|
|
190
|
+
raise TypeError('exts must be a sequence of Extension instances')
|
|
191
|
+
self.exts = tuple(exts)
|
|
192
|
+
if not all(isinstance(extension, Extension) for extension in self.exts):
|
|
193
|
+
raise TypeError('exts must contain Extension instances')
|
|
194
|
+
if len({id(extension) for extension in self.exts}) != len(self.exts):
|
|
195
|
+
raise ValueError('Extension instances cannot be registered more than once')
|
|
196
|
+
with EXTENSION_OWNERS_LOCK:
|
|
197
|
+
for extension in self.exts:
|
|
198
|
+
owner = EXTENSION_OWNERS.get(id(extension))
|
|
199
|
+
if owner is not None and owner[0]() is extension and owner[1]() is not None:
|
|
200
|
+
raise ValueError('Extension instances cannot be shared between Applications')
|
|
201
|
+
for extension in self.exts:
|
|
202
|
+
identity = id(extension)
|
|
203
|
+
EXTENSION_OWNERS[identity] = (
|
|
204
|
+
ref(
|
|
205
|
+
extension,
|
|
206
|
+
lambda reference, key=identity: forget_extension(
|
|
207
|
+
key,
|
|
208
|
+
reference,
|
|
209
|
+
),
|
|
210
|
+
),
|
|
211
|
+
ref(self),
|
|
212
|
+
)
|
|
213
|
+
self.running = False
|
|
214
|
+
self.lifecycle_lock = RLock()
|
|
215
|
+
self.lifecycle_transition = False
|
|
216
|
+
self.current_extension: Optional[Extension] = None
|
|
217
|
+
self.started_extensions: list[Extension] = []
|
|
218
|
+
self.request_hook_registrations: list[RequestHookRegistration] = []
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
def start(self) -> None:
|
|
222
|
+
"""Bind and start registered extensions in order after application startup hooks."""
|
|
223
|
+
self.ensure_not_in_hook()
|
|
224
|
+
with self.lifecycle_lock:
|
|
225
|
+
if self.running or self.lifecycle_transition:
|
|
226
|
+
raise RuntimeError('Application is already running')
|
|
227
|
+
self.lifecycle_transition = True
|
|
228
|
+
self.started_extensions.clear()
|
|
229
|
+
previous_routes = tuple(
|
|
230
|
+
route for route in self.router.routes
|
|
231
|
+
if route not in self.extension_routes
|
|
232
|
+
)
|
|
233
|
+
self.router.routes[:] = previous_routes
|
|
234
|
+
self.extension_routes.clear()
|
|
235
|
+
previous_hooks = tuple(self.request_hook_registrations)
|
|
236
|
+
self.logger.info('Application starting: extensions=%s', len(self.exts))
|
|
237
|
+
try:
|
|
238
|
+
self.run_hooks(self.startup_hooks)
|
|
239
|
+
for extension in self.exts:
|
|
240
|
+
self.current_extension = extension
|
|
241
|
+
self.logger.debug('Extension starting: extension=%s', type(extension).__qualname__)
|
|
242
|
+
if extension.startup(self) is not None:
|
|
243
|
+
raise TypeError('Extension.startup must return None')
|
|
244
|
+
self.logger.debug('Extension started: extension=%s', type(extension).__qualname__)
|
|
245
|
+
self.started_extensions.append(extension)
|
|
246
|
+
except Exception:
|
|
247
|
+
self.logger.exception('Application startup failed')
|
|
248
|
+
for extension in reversed(self.started_extensions):
|
|
249
|
+
self.current_extension = extension
|
|
250
|
+
try:
|
|
251
|
+
self.logger.debug('Extension rolling back: extension=%s', type(extension).__qualname__)
|
|
252
|
+
if extension.cleanup() is not None:
|
|
253
|
+
raise TypeError('Extension.cleanup must return None')
|
|
254
|
+
self.logger.debug('Extension rolled back: extension=%s', type(extension).__qualname__)
|
|
255
|
+
except Exception:
|
|
256
|
+
self.logger.exception(
|
|
257
|
+
'Cleanup failed after extension startup failure: extension=%s',
|
|
258
|
+
type(extension).__qualname__,
|
|
259
|
+
)
|
|
260
|
+
self.router.routes[:] = previous_routes
|
|
261
|
+
self.extension_routes.clear()
|
|
262
|
+
self.started_extensions.clear()
|
|
263
|
+
self.request_hook_registrations[:] = previous_hooks
|
|
264
|
+
raise
|
|
265
|
+
else:
|
|
266
|
+
self.running = True
|
|
267
|
+
self.logger.info('Application started: extensions=%s', len(self.started_extensions))
|
|
268
|
+
finally:
|
|
269
|
+
self.current_extension = None
|
|
270
|
+
self.lifecycle_transition = False
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
def stop(self) -> None:
|
|
274
|
+
"""Clean up started extensions in reverse order and raise the first error after all attempts."""
|
|
275
|
+
self.ensure_not_in_hook()
|
|
276
|
+
with self.lifecycle_lock:
|
|
277
|
+
if self.lifecycle_transition:
|
|
278
|
+
raise RuntimeError('Application lifecycle transition is already in progress')
|
|
279
|
+
if not self.running:
|
|
280
|
+
return
|
|
281
|
+
self.running = False
|
|
282
|
+
self.lifecycle_transition = True
|
|
283
|
+
self.logger.info('Application stopping: extensions=%s', len(self.started_extensions))
|
|
284
|
+
first_error = None
|
|
285
|
+
try:
|
|
286
|
+
for extension in reversed(self.started_extensions):
|
|
287
|
+
self.current_extension = extension
|
|
288
|
+
try:
|
|
289
|
+
self.logger.debug('Extension cleaning up: extension=%s', type(extension).__qualname__)
|
|
290
|
+
if extension.cleanup() is not None:
|
|
291
|
+
raise TypeError('Extension.cleanup must return None')
|
|
292
|
+
self.logger.debug('Extension cleaned up: extension=%s', type(extension).__qualname__)
|
|
293
|
+
except Exception as error:
|
|
294
|
+
self.logger.exception('Extension shutdown failed: extension=%s', type(extension).__qualname__)
|
|
295
|
+
if first_error is None:
|
|
296
|
+
first_error = error
|
|
297
|
+
finally:
|
|
298
|
+
self.request_hook_registrations[:] = [
|
|
299
|
+
record for record in self.request_hook_registrations
|
|
300
|
+
if record.owner is not extension
|
|
301
|
+
]
|
|
302
|
+
self.current_extension = None
|
|
303
|
+
try:
|
|
304
|
+
self.run_hooks(self.cleanup_hooks)
|
|
305
|
+
except Exception as error:
|
|
306
|
+
self.logger.exception('Application cleanup failed')
|
|
307
|
+
if first_error is None:
|
|
308
|
+
first_error = error
|
|
309
|
+
finally:
|
|
310
|
+
self.current_extension = None
|
|
311
|
+
self.started_extensions.clear()
|
|
312
|
+
self.lifecycle_transition = False
|
|
313
|
+
if first_error is not None:
|
|
314
|
+
raise first_error
|
|
315
|
+
self.logger.info('Application stopped')
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
def add_extension_route(
|
|
319
|
+
self,
|
|
320
|
+
extension: Extension,
|
|
321
|
+
route: Route,
|
|
322
|
+
) -> None:
|
|
323
|
+
"""Validate and register a route owned by the extension currently starting."""
|
|
324
|
+
self.ensure_not_in_hook()
|
|
325
|
+
with self.lifecycle_lock:
|
|
326
|
+
if (
|
|
327
|
+
self.running
|
|
328
|
+
or not self.lifecycle_transition
|
|
329
|
+
or self.current_extension is not extension
|
|
330
|
+
or any(started is extension for started in self.started_extensions)
|
|
331
|
+
):
|
|
332
|
+
raise RuntimeError('Extension routes can only be registered during their startup')
|
|
333
|
+
if not isinstance(route, Route):
|
|
334
|
+
raise TypeError('A Route instance is required')
|
|
335
|
+
if (
|
|
336
|
+
not isinstance(route.object, type)
|
|
337
|
+
or not issubclass(route.object, RequestRunner)
|
|
338
|
+
or isabstract(route.object)
|
|
339
|
+
):
|
|
340
|
+
raise TypeError('Extension routes require concrete RequestRunner classes')
|
|
341
|
+
if any(
|
|
342
|
+
entry.method == route.method and entry.url == route.url
|
|
343
|
+
for entry in self.router.routes
|
|
344
|
+
):
|
|
345
|
+
raise ValueError('Extension route already registered: ' + route.method + ' ' + route.url)
|
|
346
|
+
self.router.add(route)
|
|
347
|
+
self.extension_routes.append(route)
|
|
348
|
+
self.logger.debug(
|
|
349
|
+
'Extension route registered: extension=%s method=%s route=%s',
|
|
350
|
+
type(extension).__qualname__,
|
|
351
|
+
route.method,
|
|
352
|
+
ascii(route.url[:256]),
|
|
353
|
+
)
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
def add_request_hooks(
|
|
357
|
+
self,
|
|
358
|
+
*,
|
|
359
|
+
on_request: Union[ApplicationRequest, Sequence[ApplicationRequest]] = (),
|
|
360
|
+
on_request_response: Union[
|
|
361
|
+
ApplicationRequestResponse,
|
|
362
|
+
Sequence[ApplicationRequestResponse],
|
|
363
|
+
] = (),
|
|
364
|
+
on_request_error: Union[
|
|
365
|
+
ApplicationRequestError,
|
|
366
|
+
Sequence[ApplicationRequestError],
|
|
367
|
+
] = (),
|
|
368
|
+
) -> RequestHookRegistration:
|
|
369
|
+
"""Register request hooks during extension startup, before constructor hooks."""
|
|
370
|
+
self.ensure_not_in_hook()
|
|
371
|
+
with self.lifecycle_lock:
|
|
372
|
+
owner = self.current_extension
|
|
373
|
+
if (
|
|
374
|
+
self.running
|
|
375
|
+
or not self.lifecycle_transition
|
|
376
|
+
or owner is None
|
|
377
|
+
or any(started is owner for started in self.started_extensions)
|
|
378
|
+
):
|
|
379
|
+
raise RuntimeError('Request hooks can only be registered during extension startup')
|
|
380
|
+
registration = RequestHookRegistration(
|
|
381
|
+
owner,
|
|
382
|
+
application_hooks(on_request, ApplicationRequest, 'on_request'),
|
|
383
|
+
application_hooks(on_request_response, ApplicationRequestResponse, 'on_request_response'),
|
|
384
|
+
application_hooks(on_request_error, ApplicationRequestError, 'on_request_error'),
|
|
385
|
+
)
|
|
386
|
+
if not (registration.request or registration.response or registration.error):
|
|
387
|
+
raise ValueError('At least one request hook is required')
|
|
388
|
+
for incoming, configured in (
|
|
389
|
+
(
|
|
390
|
+
registration.request,
|
|
391
|
+
self.request_hooks,
|
|
392
|
+
),
|
|
393
|
+
(
|
|
394
|
+
registration.response,
|
|
395
|
+
self.request_response_hooks,
|
|
396
|
+
),
|
|
397
|
+
(
|
|
398
|
+
registration.error,
|
|
399
|
+
self.request_error_hooks,
|
|
400
|
+
),
|
|
401
|
+
):
|
|
402
|
+
if len({id(hook) for hook in incoming}) != len(incoming) or any(
|
|
403
|
+
first is second for first in configured for second in incoming
|
|
404
|
+
):
|
|
405
|
+
raise ValueError('Request hook instance is already registered')
|
|
406
|
+
for existing in self.request_hook_registrations:
|
|
407
|
+
for previous, incoming in (
|
|
408
|
+
(
|
|
409
|
+
existing.request,
|
|
410
|
+
registration.request,
|
|
411
|
+
),
|
|
412
|
+
(
|
|
413
|
+
existing.response,
|
|
414
|
+
registration.response,
|
|
415
|
+
),
|
|
416
|
+
(
|
|
417
|
+
existing.error,
|
|
418
|
+
registration.error,
|
|
419
|
+
),
|
|
420
|
+
):
|
|
421
|
+
if any(first is second for first in previous for second in incoming):
|
|
422
|
+
raise ValueError('Request hook instance is already registered')
|
|
423
|
+
self.request_hook_registrations.append(registration)
|
|
424
|
+
self.logger.debug(
|
|
425
|
+
'Request hooks registered: extension=%s request=%s response=%s error=%s',
|
|
426
|
+
type(registration.owner).__qualname__,
|
|
427
|
+
len(registration.request),
|
|
428
|
+
len(registration.response),
|
|
429
|
+
len(registration.error),
|
|
430
|
+
)
|
|
431
|
+
return registration
|
|
432
|
+
|
|
433
|
+
def remove_request_hooks(
|
|
434
|
+
self,
|
|
435
|
+
registration: RequestHookRegistration,
|
|
436
|
+
) -> None:
|
|
437
|
+
"""Unregister request hooks using a token during the owning extension's startup or cleanup."""
|
|
438
|
+
self.ensure_not_in_hook()
|
|
439
|
+
with self.lifecycle_lock:
|
|
440
|
+
if self.running or not self.lifecycle_transition or self.current_extension is None:
|
|
441
|
+
raise RuntimeError('Request hooks can only be removed during extension startup or cleanup')
|
|
442
|
+
if not isinstance(registration, RequestHookRegistration):
|
|
443
|
+
raise TypeError('A RequestHookRegistration token is required')
|
|
444
|
+
if registration.owner is not self.current_extension:
|
|
445
|
+
raise ValueError('Request hooks belong to another Extension')
|
|
446
|
+
if not any(record is registration for record in self.request_hook_registrations):
|
|
447
|
+
raise ValueError('Request hook registration is not active')
|
|
448
|
+
self.request_hook_registrations.remove(registration)
|
|
449
|
+
self.logger.debug('Request hooks removed: extension=%s', type(registration.owner).__qualname__)
|
|
450
|
+
return
|
|
451
|
+
|
|
452
|
+
def ensure_not_in_hook(self) -> None:
|
|
453
|
+
if getattr(self.hook_context, 'active', False):
|
|
454
|
+
raise RuntimeError('Application methods cannot be called from an Application hook')
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
def run_hooks(
|
|
458
|
+
self,
|
|
459
|
+
hooks: Sequence[Callable[..., object]],
|
|
460
|
+
*args: object,
|
|
461
|
+
) -> None:
|
|
462
|
+
for index, hook in enumerate(hooks):
|
|
463
|
+
self.logger.debug('Application lifecycle hook starting: index=%s hook=%s', index, type(hook).__qualname__)
|
|
464
|
+
result = self.invoke_hook(
|
|
465
|
+
hook,
|
|
466
|
+
*args,
|
|
467
|
+
)
|
|
468
|
+
if result is not None:
|
|
469
|
+
raise TypeError('Lifecycle hooks must return None')
|
|
470
|
+
self.logger.debug('Application lifecycle hook completed: index=%s', index)
|
|
471
|
+
return
|
|
472
|
+
|
|
473
|
+
def invoke_hook(
|
|
474
|
+
self,
|
|
475
|
+
hook: Callable[..., HookResult],
|
|
476
|
+
*args: object,
|
|
477
|
+
) -> HookResult:
|
|
478
|
+
active = getattr(self.hook_context, 'active', False)
|
|
479
|
+
self.hook_context.active = True
|
|
480
|
+
try:
|
|
481
|
+
return hook(*args)
|
|
482
|
+
finally:
|
|
483
|
+
self.hook_context.active = active
|
|
484
|
+
|
|
485
|
+
def add(
|
|
486
|
+
self,
|
|
487
|
+
handler: Type[RequestRunner],
|
|
488
|
+
) -> None:
|
|
489
|
+
if self.running or self.lifecycle_transition:
|
|
490
|
+
raise RuntimeError('Cannot add handlers while Application is running')
|
|
491
|
+
if not issubclass(
|
|
492
|
+
handler,
|
|
493
|
+
RequestRunner,
|
|
494
|
+
) or isabstract(handler):
|
|
495
|
+
raise TypeError('A concrete RequestRunner class is required')
|
|
496
|
+
properties = handler.properties
|
|
497
|
+
if properties.method is None or properties.url is None:
|
|
498
|
+
raise ValueError('Handlers must define method and url before registration')
|
|
499
|
+
self.router.add(Route(
|
|
500
|
+
properties.method,
|
|
501
|
+
properties.url,
|
|
502
|
+
handler,
|
|
503
|
+
))
|
|
504
|
+
self.logger.debug(
|
|
505
|
+
'Handler registered: method=%s route=%s handler=%s',
|
|
506
|
+
properties.method,
|
|
507
|
+
ascii(properties.url[:256]),
|
|
508
|
+
handler.__module__ + '.' + handler.__qualname__,
|
|
509
|
+
)
|
|
510
|
+
return
|
|
511
|
+
|
|
512
|
+
def load(
|
|
513
|
+
self,
|
|
514
|
+
mod: Optional[str] = None,
|
|
515
|
+
path: Optional[str] = None,
|
|
516
|
+
) -> None:
|
|
517
|
+
"""Register modules independently for each Application, even if already imported."""
|
|
518
|
+
for module in Loader()(
|
|
519
|
+
mod=mod,
|
|
520
|
+
path=path,
|
|
521
|
+
):
|
|
522
|
+
self.logger.debug('Module loaded: module=%s', module.__name__)
|
|
523
|
+
for value in vars(module).values():
|
|
524
|
+
if (
|
|
525
|
+
isinstance(value, type)
|
|
526
|
+
and issubclass(value, RequestRunner)
|
|
527
|
+
and value is not RequestRunner
|
|
528
|
+
and value.__module__ == module.__name__
|
|
529
|
+
and not isabstract(value)
|
|
530
|
+
):
|
|
531
|
+
self.add(value)
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
def __call__(
|
|
535
|
+
self,
|
|
536
|
+
request: Request,
|
|
537
|
+
) -> Optional[Response]:
|
|
538
|
+
self.ensure_not_in_hook()
|
|
539
|
+
self.logger.debug('Application request received: request_id=%s method=%s', request.id, ascii(request.method[:32]))
|
|
540
|
+
request_id = request.id
|
|
541
|
+
|
|
542
|
+
def dispatch() -> Optional[Response]:
|
|
543
|
+
if request.method == 'OPTIONS' and request.uri == '*':
|
|
544
|
+
self.logger.debug('OPTIONS wildcard selected: request_id=%s', request.id)
|
|
545
|
+
return RunOptions().run(
|
|
546
|
+
request,
|
|
547
|
+
self.router,
|
|
548
|
+
)
|
|
549
|
+
matches = self.router.matches(request.path)
|
|
550
|
+
if not matches:
|
|
551
|
+
self.logger.debug('Route not found: request_id=%s', request.id)
|
|
552
|
+
raise ClientError(
|
|
553
|
+
'No matching route',
|
|
554
|
+
status=404,
|
|
555
|
+
)
|
|
556
|
+
if request.method == 'OPTIONS' and 'OPTIONS' not in matches:
|
|
557
|
+
self.logger.debug('Automatic OPTIONS selected: request_id=%s', request.id)
|
|
558
|
+
return RunOptions().run(
|
|
559
|
+
request,
|
|
560
|
+
self.router,
|
|
561
|
+
)
|
|
562
|
+
if request.method not in matches:
|
|
563
|
+
self.logger.debug('Method not allowed: request_id=%s', request.id)
|
|
564
|
+
raise ClientError(
|
|
565
|
+
'Method not allowed',
|
|
566
|
+
status=405,
|
|
567
|
+
)
|
|
568
|
+
route, parameters = matches[request.method]
|
|
569
|
+
self.logger.debug(
|
|
570
|
+
'Route selected: request_id=%s method=%s route=%s handler=%s',
|
|
571
|
+
request.id,
|
|
572
|
+
ascii(request.method[:32]),
|
|
573
|
+
ascii(route.url[:256]),
|
|
574
|
+
route.object.__module__ + '.' + route.object.__qualname__,
|
|
575
|
+
)
|
|
576
|
+
if (request.reader.chunked or request.reader.remaining) and request.format is None:
|
|
577
|
+
raise ClientError('Content-Type is required for requests with a body')
|
|
578
|
+
if (request.reader.chunked or request.reader.remaining) and route.object.properties.body is None:
|
|
579
|
+
raise ClientError(
|
|
580
|
+
'No body reader is configured for this handler',
|
|
581
|
+
status=415,
|
|
582
|
+
)
|
|
583
|
+
request.params = parameters
|
|
584
|
+
return RunRequest(route.object).run(request)
|
|
585
|
+
|
|
586
|
+
with self.lifecycle_lock:
|
|
587
|
+
registrations = tuple(self.request_hook_registrations)
|
|
588
|
+
request_hooks = tuple(
|
|
589
|
+
hook for registration in registrations for hook in registration.request
|
|
590
|
+
) + self.request_hooks
|
|
591
|
+
response_hooks = tuple(
|
|
592
|
+
hook for registration in registrations for hook in registration.response
|
|
593
|
+
) + self.request_response_hooks
|
|
594
|
+
error_hooks = tuple(
|
|
595
|
+
hook for registration in registrations for hook in registration.error
|
|
596
|
+
) + self.request_error_hooks
|
|
597
|
+
previous_request = getattr(self.hook_context, 'request', None)
|
|
598
|
+
self.hook_context.request = request
|
|
599
|
+
try:
|
|
600
|
+
for index, hook in enumerate(request_hooks):
|
|
601
|
+
previous_id = request.id
|
|
602
|
+
self.logger.debug('Application request hook starting: request_id=%s index=%s hook=%s', request.id, index, type(hook).__qualname__)
|
|
603
|
+
request = self.invoke_hook(
|
|
604
|
+
hook,
|
|
605
|
+
request,
|
|
606
|
+
)
|
|
607
|
+
if not isinstance(request, Request):
|
|
608
|
+
raise TypeError('ApplicationRequest hooks must return a Request')
|
|
609
|
+
self.logger.debug('Application request hook completed: request_id=%s index=%s', request.id, index)
|
|
610
|
+
if previous_id != request.id:
|
|
611
|
+
self.logger.debug('Request replaced: previous_request_id=%s request_id=%s', previous_id, request.id)
|
|
612
|
+
self.logger.debug('Application dispatch starting: request_id=%s', request.id)
|
|
613
|
+
response = dispatch()
|
|
614
|
+
self.logger.debug('Application dispatch completed: request_id=%s', request.id)
|
|
615
|
+
for index, hook in enumerate(response_hooks):
|
|
616
|
+
previous = response
|
|
617
|
+
self.logger.debug('Application response hook starting: request_id=%s index=%s hook=%s', request.id, index, type(hook).__qualname__)
|
|
618
|
+
response = self.invoke_hook(
|
|
619
|
+
hook,
|
|
620
|
+
request,
|
|
621
|
+
response,
|
|
622
|
+
)
|
|
623
|
+
if response is not None and not isinstance(response, Response):
|
|
624
|
+
raise TypeError('ApplicationRequestResponse hooks must return a Response or None')
|
|
625
|
+
if previous is None and response is not None:
|
|
626
|
+
raise TypeError('ApplicationRequestResponse hooks cannot replace direct output')
|
|
627
|
+
self.logger.debug('Application response hook completed: request_id=%s index=%s', request.id, index)
|
|
628
|
+
self.logger.debug(
|
|
629
|
+
'Application response provided: request_id=%s mode=%s',
|
|
630
|
+
request.id,
|
|
631
|
+
'returned' if response is not None else 'direct',
|
|
632
|
+
)
|
|
633
|
+
return response
|
|
634
|
+
except Exception as error:
|
|
635
|
+
self.logger.debug(
|
|
636
|
+
'Application request failed: request_id=%s error_type=%s',
|
|
637
|
+
request_id,
|
|
638
|
+
type(error).__qualname__,
|
|
639
|
+
)
|
|
640
|
+
for index, hook in enumerate(error_hooks):
|
|
641
|
+
self.logger.debug(
|
|
642
|
+
'Application error hook starting: request_id=%s index=%s hook=%s',
|
|
643
|
+
request.id if isinstance(request, Request) else request_id,
|
|
644
|
+
index,
|
|
645
|
+
type(hook).__qualname__,
|
|
646
|
+
)
|
|
647
|
+
result = self.invoke_hook(
|
|
648
|
+
hook,
|
|
649
|
+
request,
|
|
650
|
+
error,
|
|
651
|
+
)
|
|
652
|
+
if result is not None:
|
|
653
|
+
raise TypeError('ApplicationRequestError hooks must return None')
|
|
654
|
+
self.logger.debug('Application error hook completed: request_id=%s index=%s', request_id, index)
|
|
655
|
+
raise
|
|
656
|
+
finally:
|
|
657
|
+
self.hook_context.request = previous_request
|
DefEssential/Body.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from .IO.Readers import ContentReader
|
|
2
|
+
from .Error import ClientError
|
|
3
|
+
from .Request import Request
|
|
4
|
+
from .Content import Content
|
|
5
|
+
from typing import (
|
|
6
|
+
Sequence,
|
|
7
|
+
Union,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = (
|
|
11
|
+
'Body',
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Body:
|
|
16
|
+
"""Apply body requirements and the explicitly selected reader."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
reader: ContentReader,
|
|
21
|
+
content: Union[Content, Sequence[Content], None] = None,
|
|
22
|
+
required: bool = True,
|
|
23
|
+
):
|
|
24
|
+
self.reader = reader
|
|
25
|
+
self.required = required
|
|
26
|
+
self.content = (content,) if isinstance(content, Content) else tuple(content or ())
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
def __call__(
|
|
30
|
+
self,
|
|
31
|
+
request: Request,
|
|
32
|
+
) -> object:
|
|
33
|
+
if request.reader.finished:
|
|
34
|
+
if self.required:
|
|
35
|
+
raise ClientError('Required request body is missing')
|
|
36
|
+
return None
|
|
37
|
+
format = request.format
|
|
38
|
+
if format is None:
|
|
39
|
+
raise ClientError('Content-Type is required for requests with a body')
|
|
40
|
+
if self.content and format not in (
|
|
41
|
+
content.format.lower() for content in self.content
|
|
42
|
+
):
|
|
43
|
+
raise ClientError(
|
|
44
|
+
'Unsupported body format',
|
|
45
|
+
status=415,
|
|
46
|
+
)
|
|
47
|
+
return self.reader(
|
|
48
|
+
request,
|
|
49
|
+
request.reader,
|
|
50
|
+
)
|