DefEssential 0.1.6__19-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 +134 -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 +245 -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 +102 -0
  9. DefEssential/Adapter/Implements/Uvicorn/WebSocket.py +220 -0
  10. DefEssential/Adapter/Implements/Uvicorn/__init__.py +5 -0
  11. DefEssential/Adapter/Implements/__init__.py +3 -0
  12. DefEssential/Adapter/RequestHandler.py +70 -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 +579 -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 +184 -0
  27. DefEssential/Extensions/File.py +89 -0
  28. DefEssential/Extensions/FileSystem.py +184 -0
  29. DefEssential/Extensions/Metrics.py +500 -0
  30. DefEssential/Extensions/Ping.py +89 -0
  31. DefEssential/Extensions/Specifications.py +328 -0
  32. DefEssential/Extensions/Status.html +207 -0
  33. DefEssential/Extensions/Status.py +104 -0
  34. DefEssential/Extensions/Swagger.html +77 -0
  35. DefEssential/Extensions/Swagger.py +111 -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 +241 -0
  45. DefEssential/IO/WebSocket/WebSocket.py +282 -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 +87 -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 +101 -0
  66. DefEssential/Runners/RunFactory.py +26 -0
  67. DefEssential/Runners/RunOptions.py +77 -0
  68. DefEssential/Runners/RunRequest.py +121 -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.6.dist-info/METADATA +41 -0
  101. defessential-0.1.6.dist-info/RECORD +104 -0
  102. defessential-0.1.6.dist-info/WHEEL +6 -0
  103. defessential-0.1.6.dist-info/licenses/LICENSE +674 -0
  104. defessential-0.1.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,579 @@
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
+ """애플리케이션 시작 훅의 호출 계약."""
46
+
47
+ @abstractmethod
48
+ def __call__(self) -> None:
49
+ raise NotImplementedError
50
+
51
+
52
+ class ApplicationCleanup(ABC):
53
+ """애플리케이션 정리 훅의 호출 계약."""
54
+
55
+ @abstractmethod
56
+ def __call__(self) -> None:
57
+ raise NotImplementedError
58
+
59
+
60
+ class ApplicationRequest(ABC):
61
+ """요청 처리 진입 훅의 호출 계약."""
62
+
63
+ @abstractmethod
64
+ def __call__(
65
+ self,
66
+ request: Request,
67
+ ) -> Request:
68
+ raise NotImplementedError
69
+
70
+
71
+ class ApplicationRequestResponse(ABC):
72
+ """요청 응답 반환 훅의 호출 계약."""
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
+ """요청 처리 오류 훅의 호출 계약."""
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
+ """시작 단계에서 등록한 요청 훅 묶음의 해제 토큰."""
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
+ def __init__(
144
+ self,
145
+ exts: Sequence[Extension] = (),
146
+ *,
147
+ on_startup: Union[ApplicationStartup, Sequence[ApplicationStartup]] = (),
148
+ on_cleanup: Union[ApplicationCleanup, Sequence[ApplicationCleanup]] = (),
149
+ on_request: Union[ApplicationRequest, Sequence[ApplicationRequest]] = (),
150
+ on_request_response: Union[
151
+ ApplicationRequestResponse,
152
+ Sequence[ApplicationRequestResponse],
153
+ ] = (),
154
+ on_request_error: Union[
155
+ ApplicationRequestError,
156
+ Sequence[ApplicationRequestError],
157
+ ] = (),
158
+ ):
159
+ self.startup_hooks = application_hooks(
160
+ on_startup,
161
+ ApplicationStartup,
162
+ 'on_startup',
163
+ )
164
+ self.cleanup_hooks = application_hooks(
165
+ on_cleanup,
166
+ ApplicationCleanup,
167
+ 'on_cleanup',
168
+ )
169
+ self.request_hooks = application_hooks(
170
+ on_request,
171
+ ApplicationRequest,
172
+ 'on_request',
173
+ )
174
+ self.request_response_hooks = application_hooks(
175
+ on_request_response,
176
+ ApplicationRequestResponse,
177
+ 'on_request_response',
178
+ )
179
+ self.request_error_hooks = application_hooks(
180
+ on_request_error,
181
+ ApplicationRequestError,
182
+ 'on_request_error',
183
+ )
184
+ self.hook_context = local()
185
+ self.router = Router()
186
+ self.extension_routes: list[Route] = []
187
+ if not isinstance(exts, Sequence) or isinstance(exts, (str, bytes)):
188
+ raise TypeError('exts must be a sequence of Extension instances')
189
+ self.exts = tuple(exts)
190
+ if not all(isinstance(extension, Extension) for extension in self.exts):
191
+ raise TypeError('exts must contain Extension instances')
192
+ if len({id(extension) for extension in self.exts}) != len(self.exts):
193
+ raise ValueError('Extension instances cannot be registered more than once')
194
+ with EXTENSION_OWNERS_LOCK:
195
+ for extension in self.exts:
196
+ owner = EXTENSION_OWNERS.get(id(extension))
197
+ if owner is not None and owner[0]() is extension and owner[1]() is not None:
198
+ raise ValueError('Extension instances cannot be shared between Applications')
199
+ for extension in self.exts:
200
+ identity = id(extension)
201
+ EXTENSION_OWNERS[identity] = (
202
+ ref(
203
+ extension,
204
+ lambda reference, key=identity: forget_extension(
205
+ key,
206
+ reference,
207
+ ),
208
+ ),
209
+ ref(self),
210
+ )
211
+ self.running = False
212
+ self.lifecycle_lock = RLock()
213
+ self.lifecycle_transition = False
214
+ self.current_extension: Optional[Extension] = None
215
+ self.started_extensions: list[Extension] = []
216
+ self.request_hook_registrations: list[RequestHookRegistration] = []
217
+ return
218
+
219
+ def start(self) -> None:
220
+ """Application 시작 훅 뒤 등록된 확장을 순서대로 연결·시작한다."""
221
+ self.ensure_not_in_hook()
222
+ with self.lifecycle_lock:
223
+ if self.running or self.lifecycle_transition:
224
+ raise RuntimeError('Application is already running')
225
+ self.lifecycle_transition = True
226
+ self.started_extensions.clear()
227
+ previous_routes = tuple(
228
+ route for route in self.router.routes
229
+ if route not in self.extension_routes
230
+ )
231
+ self.router.routes[:] = previous_routes
232
+ self.extension_routes.clear()
233
+ previous_hooks = tuple(self.request_hook_registrations)
234
+ try:
235
+ self.run_hooks(self.startup_hooks)
236
+ for extension in self.exts:
237
+ self.current_extension = extension
238
+ if extension.startup(self) is not None:
239
+ raise TypeError('Extension.startup must return None')
240
+ self.started_extensions.append(extension)
241
+ except Exception:
242
+ for extension in reversed(self.started_extensions):
243
+ self.current_extension = extension
244
+ try:
245
+ if extension.cleanup() is not None:
246
+ raise TypeError('Extension.cleanup must return None')
247
+ except Exception:
248
+ getLogger('DefEssential.Application').exception('Cleanup failed after extension startup failure')
249
+ self.router.routes[:] = previous_routes
250
+ self.extension_routes.clear()
251
+ self.started_extensions.clear()
252
+ self.request_hook_registrations[:] = previous_hooks
253
+ raise
254
+ else:
255
+ self.running = True
256
+ finally:
257
+ self.current_extension = None
258
+ self.lifecycle_transition = False
259
+ return
260
+
261
+ def stop(self) -> None:
262
+ """시작한 확장을 역순 정리하고 모든 정리 시도 뒤 첫 오류를 전파한다."""
263
+ self.ensure_not_in_hook()
264
+ with self.lifecycle_lock:
265
+ if self.lifecycle_transition:
266
+ raise RuntimeError('Application lifecycle transition is already in progress')
267
+ if not self.running:
268
+ return
269
+ self.running = False
270
+ self.lifecycle_transition = True
271
+ first_error = None
272
+ try:
273
+ for extension in reversed(self.started_extensions):
274
+ self.current_extension = extension
275
+ try:
276
+ if extension.cleanup() is not None:
277
+ raise TypeError('Extension.cleanup must return None')
278
+ except Exception as error:
279
+ getLogger('DefEssential.Application').exception('Extension shutdown failed')
280
+ if first_error is None:
281
+ first_error = error
282
+ finally:
283
+ self.request_hook_registrations[:] = [
284
+ record for record in self.request_hook_registrations
285
+ if record.owner is not extension
286
+ ]
287
+ self.current_extension = None
288
+ try:
289
+ self.run_hooks(self.cleanup_hooks)
290
+ except Exception as error:
291
+ getLogger('DefEssential.Application').exception('Application cleanup failed')
292
+ if first_error is None:
293
+ first_error = error
294
+ finally:
295
+ self.current_extension = None
296
+ self.started_extensions.clear()
297
+ self.lifecycle_transition = False
298
+ if first_error is not None:
299
+ raise first_error
300
+ return
301
+
302
+ def add_extension_route(
303
+ self,
304
+ extension: Extension,
305
+ route: Route,
306
+ ) -> None:
307
+ """현재 시작 중인 확장의 라우트를 검증하고 소유 관계와 함께 등록한다."""
308
+ self.ensure_not_in_hook()
309
+ with self.lifecycle_lock:
310
+ if (
311
+ self.running
312
+ or not self.lifecycle_transition
313
+ or self.current_extension is not extension
314
+ or any(started is extension for started in self.started_extensions)
315
+ ):
316
+ raise RuntimeError('Extension routes can only be registered during their startup')
317
+ if not isinstance(route, Route):
318
+ raise TypeError('A Route instance is required')
319
+ if (
320
+ not isinstance(route.object, type)
321
+ or not issubclass(route.object, RequestRunner)
322
+ or isabstract(route.object)
323
+ ):
324
+ raise TypeError('Extension routes require concrete RequestRunner classes')
325
+ if any(
326
+ entry.method == route.method and entry.url == route.url
327
+ for entry in self.router.routes
328
+ ):
329
+ raise ValueError('Extension route already registered: ' + route.method + ' ' + route.url)
330
+ self.router.add(route)
331
+ self.extension_routes.append(route)
332
+ return
333
+
334
+ def add_request_hooks(
335
+ self,
336
+ *,
337
+ on_request: Union[ApplicationRequest, Sequence[ApplicationRequest]] = (),
338
+ on_request_response: Union[
339
+ ApplicationRequestResponse,
340
+ Sequence[ApplicationRequestResponse],
341
+ ] = (),
342
+ on_request_error: Union[
343
+ ApplicationRequestError,
344
+ Sequence[ApplicationRequestError],
345
+ ] = (),
346
+ ) -> RequestHookRegistration:
347
+ """확장 시작 중 요청 훅을 등록한다. 생성자 훅보다 먼저 실행한다."""
348
+ self.ensure_not_in_hook()
349
+ with self.lifecycle_lock:
350
+ owner = self.current_extension
351
+ if (
352
+ self.running
353
+ or not self.lifecycle_transition
354
+ or owner is None
355
+ or any(started is owner for started in self.started_extensions)
356
+ ):
357
+ raise RuntimeError('Request hooks can only be registered during extension startup')
358
+ registration = RequestHookRegistration(
359
+ owner,
360
+ application_hooks(on_request, ApplicationRequest, 'on_request'),
361
+ application_hooks(on_request_response, ApplicationRequestResponse, 'on_request_response'),
362
+ application_hooks(on_request_error, ApplicationRequestError, 'on_request_error'),
363
+ )
364
+ if not (registration.request or registration.response or registration.error):
365
+ raise ValueError('At least one request hook is required')
366
+ for incoming, configured in (
367
+ (
368
+ registration.request,
369
+ self.request_hooks,
370
+ ),
371
+ (
372
+ registration.response,
373
+ self.request_response_hooks,
374
+ ),
375
+ (
376
+ registration.error,
377
+ self.request_error_hooks,
378
+ ),
379
+ ):
380
+ if len({id(hook) for hook in incoming}) != len(incoming) or any(
381
+ first is second for first in configured for second in incoming
382
+ ):
383
+ raise ValueError('Request hook instance is already registered')
384
+ for existing in self.request_hook_registrations:
385
+ for previous, incoming in (
386
+ (
387
+ existing.request,
388
+ registration.request,
389
+ ),
390
+ (
391
+ existing.response,
392
+ registration.response,
393
+ ),
394
+ (
395
+ existing.error,
396
+ registration.error,
397
+ ),
398
+ ):
399
+ if any(first is second for first in previous for second in incoming):
400
+ raise ValueError('Request hook instance is already registered')
401
+ self.request_hook_registrations.append(registration)
402
+ return registration
403
+
404
+ def remove_request_hooks(
405
+ self,
406
+ registration: RequestHookRegistration,
407
+ ) -> None:
408
+ """소유 확장의 시작·정리 중 등록 토큰으로 요청 훅을 해제한다."""
409
+ self.ensure_not_in_hook()
410
+ with self.lifecycle_lock:
411
+ if self.running or not self.lifecycle_transition or self.current_extension is None:
412
+ raise RuntimeError('Request hooks can only be removed during extension startup or cleanup')
413
+ if not isinstance(registration, RequestHookRegistration):
414
+ raise TypeError('A RequestHookRegistration token is required')
415
+ if registration.owner is not self.current_extension:
416
+ raise ValueError('Request hooks belong to another Extension')
417
+ if not any(record is registration for record in self.request_hook_registrations):
418
+ raise ValueError('Request hook registration is not active')
419
+ self.request_hook_registrations.remove(registration)
420
+ return
421
+
422
+ def ensure_not_in_hook(self) -> None:
423
+ if getattr(self.hook_context, 'active', False):
424
+ raise RuntimeError('Application methods cannot be called from an Application hook')
425
+ return
426
+
427
+ def run_hooks(
428
+ self,
429
+ hooks: Sequence[Callable[..., object]],
430
+ *args: object,
431
+ ) -> None:
432
+ for hook in hooks:
433
+ result = self.invoke_hook(
434
+ hook,
435
+ *args,
436
+ )
437
+ if result is not None:
438
+ raise TypeError('Lifecycle hooks must return None')
439
+ return
440
+
441
+ def invoke_hook(
442
+ self,
443
+ hook: Callable[..., HookResult],
444
+ *args: object,
445
+ ) -> HookResult:
446
+ active = getattr(self.hook_context, 'active', False)
447
+ self.hook_context.active = True
448
+ try:
449
+ return hook(*args)
450
+ finally:
451
+ self.hook_context.active = active
452
+
453
+ def add(
454
+ self,
455
+ handler: Type[RequestRunner],
456
+ ) -> None:
457
+ if self.running or self.lifecycle_transition:
458
+ raise RuntimeError('Cannot add handlers while Application is running')
459
+ if not issubclass(
460
+ handler,
461
+ RequestRunner,
462
+ ) or isabstract(handler):
463
+ raise TypeError('A concrete RequestRunner class is required')
464
+ properties = handler.properties
465
+ if properties.method is None or properties.url is None:
466
+ raise ValueError('Handlers must define method and url before registration')
467
+ self.router.add(Route(
468
+ properties.method,
469
+ properties.url,
470
+ handler,
471
+ ))
472
+ return
473
+
474
+ def load(
475
+ self,
476
+ mod: Optional[str] = None,
477
+ path: Optional[str] = None,
478
+ ) -> None:
479
+ """Register modules independently for each Application, even if already imported."""
480
+ for module in Loader()(
481
+ mod=mod,
482
+ path=path,
483
+ ):
484
+ for value in vars(module).values():
485
+ if (
486
+ isinstance(value, type)
487
+ and issubclass(value, RequestRunner)
488
+ and value is not RequestRunner
489
+ and value.__module__ == module.__name__
490
+ and not isabstract(value)
491
+ ):
492
+ self.add(value)
493
+ return
494
+
495
+ def __call__(
496
+ self,
497
+ request: Request,
498
+ ) -> Optional[Response]:
499
+ self.ensure_not_in_hook()
500
+
501
+ def dispatch() -> Optional[Response]:
502
+ if request.method == 'OPTIONS' and request.uri == '*':
503
+ return RunOptions().run(
504
+ request,
505
+ self.router,
506
+ )
507
+ matches = self.router.matches(request.path)
508
+ if not matches:
509
+ raise ClientError(
510
+ 'No matching route',
511
+ status=404,
512
+ )
513
+ if request.method == 'OPTIONS' and 'OPTIONS' not in matches:
514
+ return RunOptions().run(
515
+ request,
516
+ self.router,
517
+ )
518
+ if request.method not in matches:
519
+ raise ClientError(
520
+ 'Method not allowed',
521
+ status=405,
522
+ )
523
+ if (request.reader.chunked or request.reader.remaining) and request.format is None:
524
+ raise ClientError('Content-Type is required for requests with a body')
525
+ route, parameters = matches[request.method]
526
+ if (request.reader.chunked or request.reader.remaining) and route.object.properties.body is None:
527
+ raise ClientError(
528
+ 'No body reader is configured for this handler',
529
+ status=415,
530
+ )
531
+ request.params = parameters
532
+ return RunRequest(route.object).run(request)
533
+
534
+ with self.lifecycle_lock:
535
+ registrations = tuple(self.request_hook_registrations)
536
+ request_hooks = tuple(
537
+ hook for registration in registrations for hook in registration.request
538
+ ) + self.request_hooks
539
+ response_hooks = tuple(
540
+ hook for registration in registrations for hook in registration.response
541
+ ) + self.request_response_hooks
542
+ error_hooks = tuple(
543
+ hook for registration in registrations for hook in registration.error
544
+ ) + self.request_error_hooks
545
+ previous_request = getattr(self.hook_context, 'request', None)
546
+ self.hook_context.request = request
547
+ try:
548
+ for hook in request_hooks:
549
+ request = self.invoke_hook(
550
+ hook,
551
+ request,
552
+ )
553
+ if not isinstance(request, Request):
554
+ raise TypeError('ApplicationRequest hooks must return a Request')
555
+ response = dispatch()
556
+ for hook in response_hooks:
557
+ previous = response
558
+ response = self.invoke_hook(
559
+ hook,
560
+ request,
561
+ response,
562
+ )
563
+ if response is not None and not isinstance(response, Response):
564
+ raise TypeError('ApplicationRequestResponse hooks must return a Response or None')
565
+ if previous is None and response is not None:
566
+ raise TypeError('ApplicationRequestResponse hooks cannot replace direct output')
567
+ return response
568
+ except Exception as error:
569
+ for hook in error_hooks:
570
+ result = self.invoke_hook(
571
+ hook,
572
+ request,
573
+ error,
574
+ )
575
+ if result is not None:
576
+ raise TypeError('ApplicationRequestError hooks must return None')
577
+ raise
578
+ finally:
579
+ 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
+ )
@@ -0,0 +1,18 @@
1
+ from dataclasses import dataclass
2
+ from typing import (
3
+ Any,
4
+ Optional,
5
+ )
6
+
7
+ __all__ = (
8
+ 'Content',
9
+ )
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class Content:
14
+ """Describe input or output content formats, schemas, and examples."""
15
+
16
+ format: str
17
+ schema: Optional[object] = None
18
+ example: Any = None