DefEssential 0.1.8__27-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.
Files changed (104) hide show
  1. DefEssential/Adapter/Implements/Gunicorn/Server.py +152 -0
  2. DefEssential/Adapter/Implements/Gunicorn/Transport.py +171 -0
  3. DefEssential/Adapter/Implements/Gunicorn/__init__.py +5 -0
  4. DefEssential/Adapter/Implements/Python/Server.py +273 -0
  5. DefEssential/Adapter/Implements/Python/__init__.py +5 -0
  6. DefEssential/Adapter/Implements/Uvicorn/HTTP.py +256 -0
  7. DefEssential/Adapter/Implements/Uvicorn/Portal.py +65 -0
  8. DefEssential/Adapter/Implements/Uvicorn/Server.py +137 -0
  9. DefEssential/Adapter/Implements/Uvicorn/WebSocket.py +222 -0
  10. DefEssential/Adapter/Implements/Uvicorn/__init__.py +5 -0
  11. DefEssential/Adapter/Implements/__init__.py +3 -0
  12. DefEssential/Adapter/RequestHandler.py +109 -0
  13. DefEssential/Adapter/Server.py +54 -0
  14. DefEssential/Adapter/WSGI.py +219 -0
  15. DefEssential/Adapter/__init__.py +5 -0
  16. DefEssential/Application.py +657 -0
  17. DefEssential/Body.py +50 -0
  18. DefEssential/Content.py +18 -0
  19. DefEssential/Credentials.py +245 -0
  20. DefEssential/CrossOriginResourceSharing.py +107 -0
  21. DefEssential/Descriptor/DataObject.py +409 -0
  22. DefEssential/Descriptor/Types.py +248 -0
  23. DefEssential/Descriptor/Value.py +188 -0
  24. DefEssential/Descriptor/__init__.py +172 -0
  25. DefEssential/Error.py +66 -0
  26. DefEssential/Extension.py +191 -0
  27. DefEssential/Extensions/File.py +95 -0
  28. DefEssential/Extensions/FileSystem.py +195 -0
  29. DefEssential/Extensions/Metrics.py +511 -0
  30. DefEssential/Extensions/Ping.py +93 -0
  31. DefEssential/Extensions/Specifications.py +342 -0
  32. DefEssential/Extensions/Status.html +207 -0
  33. DefEssential/Extensions/Status.py +110 -0
  34. DefEssential/Extensions/Swagger.html +77 -0
  35. DefEssential/Extensions/Swagger.py +117 -0
  36. DefEssential/Extensions/Utils/Routes.py +42 -0
  37. DefEssential/Extensions/Utils/__init__.py +43 -0
  38. DefEssential/Extensions/__init__.py +21 -0
  39. DefEssential/IO/Readers/ChunkedStream.py +106 -0
  40. DefEssential/IO/Readers/ContentReader.py +36 -0
  41. DefEssential/IO/Readers/ContentReaders.py +241 -0
  42. DefEssential/IO/Readers/__init__.py +25 -0
  43. DefEssential/IO/RequestReader.py +125 -0
  44. DefEssential/IO/ResponseWriter.py +242 -0
  45. DefEssential/IO/WebSocket/WebSocket.py +288 -0
  46. DefEssential/IO/WebSocket/WebSocketError.py +17 -0
  47. DefEssential/IO/WebSocket/WebSocketFrame.py +68 -0
  48. DefEssential/IO/WebSocket/WebSocketFrameReader.py +98 -0
  49. DefEssential/IO/WebSocket/WebSocketHandshake.py +94 -0
  50. DefEssential/IO/WebSocket/__init__.py +13 -0
  51. DefEssential/IO/Writers/ChunkedStreamWriter.py +19 -0
  52. DefEssential/IO/Writers/ServerSentEvents.py +41 -0
  53. DefEssential/IO/Writers/ServerSentEventsWriter.py +67 -0
  54. DefEssential/IO/Writers/StreamWriter.py +57 -0
  55. DefEssential/IO/Writers/__init__.py +11 -0
  56. DefEssential/IO/__init__.py +7 -0
  57. DefEssential/Request.py +110 -0
  58. DefEssential/RequestRunner.py +261 -0
  59. DefEssential/Response.py +89 -0
  60. DefEssential/ResponseSpecification.py +23 -0
  61. DefEssential/Responses/ResponseFile.py +108 -0
  62. DefEssential/Responses/__init__.py +63 -0
  63. DefEssential/Route.py +55 -0
  64. DefEssential/Router.py +48 -0
  65. DefEssential/Runners/RunExtension.py +116 -0
  66. DefEssential/Runners/RunFactory.py +26 -0
  67. DefEssential/Runners/RunOptions.py +77 -0
  68. DefEssential/Runners/RunRequest.py +198 -0
  69. DefEssential/Runners/__init__.py +5 -0
  70. DefEssential/Startup.py +104 -0
  71. DefEssential/Utils/FileSystemObject/Configuration.py +15 -0
  72. DefEssential/Utils/FileSystemObject/Connection.py +57 -0
  73. DefEssential/Utils/FileSystemObject/File.py +41 -0
  74. DefEssential/Utils/FileSystemObject/Implements/FileSystem/Configuration.py +15 -0
  75. DefEssential/Utils/FileSystemObject/Implements/FileSystem/Connection.py +115 -0
  76. DefEssential/Utils/FileSystemObject/Implements/FileSystem/File.py +77 -0
  77. DefEssential/Utils/FileSystemObject/Implements/FileSystem/__init__.py +11 -0
  78. DefEssential/Utils/FileSystemObject/Implements/__init__.py +3 -0
  79. DefEssential/Utils/FileSystemObject/__init__.py +11 -0
  80. DefEssential/Utils/Loader.py +81 -0
  81. DefEssential/Utils/__init__.py +3 -0
  82. DefEssential/Validator/Pattern.py +36 -0
  83. DefEssential/Validator/Patterns/Array.py +30 -0
  84. DefEssential/Validator/Patterns/Common.py +92 -0
  85. DefEssential/Validator/Patterns/Compare.py +180 -0
  86. DefEssential/Validator/Patterns/Condition.py +60 -0
  87. DefEssential/Validator/Patterns/DataObject.py +85 -0
  88. DefEssential/Validator/Patterns/DateTime.py +111 -0
  89. DefEssential/Validator/Patterns/Number.py +43 -0
  90. DefEssential/Validator/Patterns/Object.py +150 -0
  91. DefEssential/Validator/Patterns/Size.py +110 -0
  92. DefEssential/Validator/Patterns/String.py +85 -0
  93. DefEssential/Validator/Patterns/Type.py +350 -0
  94. DefEssential/Validator/Patterns/__init__.py +137 -0
  95. DefEssential/Validator/Validate.py +29 -0
  96. DefEssential/Validator/Validator.py +30 -0
  97. DefEssential/Validator/__init__.py +11 -0
  98. DefEssential/Validators.py +255 -0
  99. DefEssential/__init__.py +83 -0
  100. defessential-0.1.8.dist-info/METADATA +86 -0
  101. defessential-0.1.8.dist-info/RECORD +104 -0
  102. defessential-0.1.8.dist-info/WHEEL +6 -0
  103. defessential-0.1.8.dist-info/licenses/LICENSE +674 -0
  104. 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
+ )