DefEssential 0.1.8__23-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 +82 -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,63 @@
1
+ from dataclasses import (
2
+ asdict,
3
+ is_dataclass,
4
+ )
5
+ from json import dumps
6
+
7
+ from ..Response import Response
8
+ from .ResponseFile import ResponseFile
9
+
10
+ __all__ = (
11
+ 'ResponseFile',
12
+ 'ResponseJSON',
13
+ 'ResponseText',
14
+ )
15
+
16
+
17
+ class ResponseJSON(Response):
18
+ """Serialize JSON or dataclass values as UTF-8 responses."""
19
+
20
+ def __init__(
21
+ self,
22
+ value,
23
+ status=200,
24
+ headers=None,
25
+ ):
26
+ def encode(value):
27
+ if is_dataclass(value) and not isinstance(value, type):
28
+ return asdict(value)
29
+ raise TypeError('Value is not JSON serializable: ' + type(value).__name__)
30
+
31
+ super().__init__(
32
+ status=status,
33
+ headers=headers,
34
+ body=dumps(
35
+ value,
36
+ default=encode,
37
+ ensure_ascii=False,
38
+ allow_nan=False,
39
+ ).encode('utf-8'),
40
+ format='application/json',
41
+ charset='utf-8',
42
+ )
43
+ return
44
+
45
+
46
+ class ResponseText(Response):
47
+ """Encode a string using an explicit charset."""
48
+
49
+ def __init__(
50
+ self,
51
+ text,
52
+ status=200,
53
+ headers=None,
54
+ charset='utf-8',
55
+ ):
56
+ super().__init__(
57
+ status=status,
58
+ headers=headers,
59
+ body=text.encode(charset),
60
+ format='text/plain',
61
+ charset=charset,
62
+ )
63
+ return
DefEssential/Route.py ADDED
@@ -0,0 +1,55 @@
1
+ from re import (
2
+ compile,
3
+ escape,
4
+ )
5
+ from typing import (
6
+ Optional,
7
+ Type,
8
+ )
9
+
10
+ from .RequestRunner import RequestRunner
11
+
12
+ __all__ = (
13
+ 'Route',
14
+ )
15
+
16
+
17
+ class Route:
18
+ """Represent a route using the existing :name path parameter syntax."""
19
+
20
+ variable = compile(r':([A-Za-z_][A-Za-z_0-9]*)')
21
+
22
+ def __init__(
23
+ self,
24
+ method: str,
25
+ url: str,
26
+ obj: Type[RequestRunner],
27
+ ):
28
+ self.method = method
29
+ self.url = url
30
+ self.object = obj
31
+ parts = []
32
+ position = 0
33
+ for match in self.variable.finditer(url):
34
+ parts.append(escape(url[position:match.start()]))
35
+ parts.append('(?P<{}>[^/]+)'.format(match.group(1)))
36
+ position = match.end()
37
+ parts.append(escape(url[position:]))
38
+ self.pattern = compile(''.join(parts))
39
+ self.parameter_count = self.pattern.groups
40
+ return
41
+
42
+ @property
43
+ def priority(self) -> tuple[int, int, int]:
44
+ return (
45
+ 0,
46
+ self.parameter_count,
47
+ 0,
48
+ )
49
+
50
+ def match(
51
+ self,
52
+ url: str,
53
+ ) -> Optional[dict[str, str]]:
54
+ match = self.pattern.fullmatch(url)
55
+ return match.groupdict() if match is not None else None
DefEssential/Router.py ADDED
@@ -0,0 +1,48 @@
1
+ from typing import (
2
+ Dict,
3
+ Tuple,
4
+ )
5
+
6
+ from .Route import Route
7
+
8
+ __all__ = (
9
+ 'Router',
10
+ )
11
+
12
+
13
+ class Router:
14
+ """Maintain per-Application registrations and prioritize static routes."""
15
+
16
+ def __init__(self):
17
+ self.routes: list[Route] = []
18
+ return
19
+
20
+ def add(
21
+ self,
22
+ route: Route,
23
+ ) -> None:
24
+ for registered in self.routes:
25
+ if registered.method == route.method and registered.url == route.url:
26
+ if registered.object is route.object:
27
+ return
28
+ raise ValueError('Route already registered: {} {}'.format(
29
+ route.method,
30
+ route.url,
31
+ ))
32
+ self.routes.append(route)
33
+ self.routes.sort(key=lambda entry: entry.priority)
34
+ return
35
+
36
+ def matches(
37
+ self,
38
+ url: str,
39
+ ) -> Dict[str, Tuple[Route, dict[str, str]]]:
40
+ matches = {}
41
+ for route in self.routes:
42
+ parameters = route.match(url)
43
+ if parameters is not None and route.method not in matches:
44
+ matches[route.method] = (
45
+ route,
46
+ parameters,
47
+ )
48
+ return matches
@@ -0,0 +1,116 @@
1
+ from logging import getLogger
2
+ from typing import (
3
+ Optional,
4
+ Type,
5
+ )
6
+
7
+ from ..Extension import Extension
8
+ from ..Request import Request
9
+ from ..RequestRunner import RequestRunner
10
+ from ..Response import Response
11
+ from .RunFactory import RunFactory
12
+
13
+ __all__ = (
14
+ 'RunExtensionFactory',
15
+ )
16
+
17
+ logger = getLogger('DefEssential.Extension')
18
+
19
+
20
+ def run_extension(
21
+ extension: Extension,
22
+ request: Request,
23
+ ) -> Optional[Response]:
24
+ """Wrap only the extension-processing boundary after internal RequestRunner validation."""
25
+ try:
26
+ for index, hook in enumerate(extension.request_hooks):
27
+ logger.debug('Extension request hook starting: request_id=%s index=%s hook=%s', request.id, index, type(hook).__qualname__)
28
+ result = hook(request)
29
+ if not isinstance(result, Request):
30
+ raise TypeError('ExtensionRequest hooks must return a Request')
31
+ logger.debug('Extension request hook completed: request_id=%s index=%s', request.id, index)
32
+ if request.id != result.id:
33
+ logger.debug('Request replaced: previous_request_id=%s request_id=%s', request.id, result.id)
34
+ request = result
35
+ logger.debug('Extension running: request_id=%s extension=%s', request.id, type(extension).__qualname__)
36
+ response = extension(request)
37
+ if response is not None and not isinstance(response, Response):
38
+ raise TypeError('Extension must return a Response or None')
39
+ writer = request.response_writer
40
+ if response is None and (writer is None or not writer.started):
41
+ raise TypeError('Extension must return a Response or start direct output')
42
+ if response is not None and writer is not None and writer.started:
43
+ response.close()
44
+ raise RuntimeError('Cannot return a Response after starting direct output')
45
+ logger.debug('Extension response provided: request_id=%s extension=%s', request.id, type(extension).__qualname__)
46
+ for index, hook in enumerate(extension.request_response_hooks):
47
+ previous = response
48
+ previous_body = previous.body if previous is not None else None
49
+ try:
50
+ logger.debug('Extension response hook starting: request_id=%s index=%s hook=%s', request.id, index, type(hook).__qualname__)
51
+ response = hook(request, response)
52
+ if response is not None and not isinstance(response, Response):
53
+ raise TypeError('ExtensionRequestResponse hooks must return a Response or None')
54
+ if previous is None and response is not None:
55
+ response.close()
56
+ raise TypeError('ExtensionRequestResponse hooks cannot replace direct output')
57
+ writer = request.response_writer
58
+ if response is None and (writer is None or not writer.started):
59
+ raise TypeError('ExtensionRequestResponse hooks cannot discard a response without direct output')
60
+ logger.debug('Extension response hook completed: request_id=%s index=%s', request.id, index)
61
+ except Exception:
62
+ if previous is not None:
63
+ previous.close()
64
+ raise
65
+ finally:
66
+ if previous is not None and previous.body is not previous_body:
67
+ close = getattr(previous_body, 'close', None)
68
+ if close is not None:
69
+ close()
70
+ if previous is not None and response is not previous:
71
+ if response is None or response.body is not previous_body:
72
+ previous.close()
73
+ return response
74
+ except Exception as error:
75
+ logger.debug('Extension failed: request_id=%s extension=%s error_type=%s', request.id, type(extension).__qualname__, type(error).__qualname__)
76
+ for index, hook in enumerate(extension.request_error_hooks):
77
+ logger.debug('Extension error hook starting: request_id=%s index=%s hook=%s', request.id, index, type(hook).__qualname__)
78
+ if hook(request, error) is not None:
79
+ raise TypeError('ExtensionRequestError hooks must return None')
80
+ logger.debug('Extension error hook completed: request_id=%s index=%s', request.id, index)
81
+ raise
82
+
83
+
84
+ class RunExtensionFactory(RunFactory[Type[RequestRunner]]):
85
+ """Create a request handler class that delegates calls to an extension."""
86
+
87
+ def __init__(
88
+ self,
89
+ extension: Extension,
90
+ method: str,
91
+ path: str,
92
+ ):
93
+ self.extension = extension
94
+ self.method = method
95
+ self.path = path
96
+ return
97
+
98
+ def __call__(self) -> Type[RequestRunner]:
99
+ extension = self.extension
100
+
101
+ class RunExtension(
102
+ RequestRunner,
103
+ method=self.method,
104
+ url=self.path,
105
+ name=type(extension).__name__,
106
+ ):
107
+ def run(
108
+ self,
109
+ request: Request,
110
+ ) -> Optional[Response]:
111
+ return run_extension(
112
+ extension,
113
+ request,
114
+ )
115
+
116
+ return RunExtension
@@ -0,0 +1,26 @@
1
+ from abc import (
2
+ ABC,
3
+ abstractmethod,
4
+ )
5
+ from typing import (
6
+ Generic,
7
+ TypeVar,
8
+ )
9
+
10
+ __all__ = (
11
+ 'RunFactory',
12
+ )
13
+
14
+
15
+ T = TypeVar('T')
16
+
17
+
18
+ class RunFactory(
19
+ ABC,
20
+ Generic[T],
21
+ ):
22
+ """Store configuration and create a request handler to register when called."""
23
+
24
+ @abstractmethod
25
+ def __call__(self) -> T:
26
+ raise NotImplementedError
@@ -0,0 +1,77 @@
1
+ from ..Error import ClientError
2
+ from ..Request import Request
3
+ from ..Response import Response
4
+ from ..Router import Router
5
+ from ..CrossOriginResourceSharing import CrossOriginResourceSharing
6
+
7
+ __all__ = (
8
+ 'RunOptions',
9
+ )
10
+
11
+
12
+ class RunOptions:
13
+ """Compute Allow and CORS preflight responses from Application-owned routes."""
14
+
15
+ def run(
16
+ self,
17
+ request: Request,
18
+ router: Router,
19
+ ) -> Response:
20
+ if request.uri == '*':
21
+ methods = {route.method for route in router.routes}
22
+ methods.add('OPTIONS')
23
+ return Response(
24
+ status=204,
25
+ headers={'Allow': ', '.join(sorted(methods))},
26
+ )
27
+ matches = router.matches(request.path)
28
+ if not matches:
29
+ raise ClientError(
30
+ 'No matching route',
31
+ status=404,
32
+ )
33
+ methods = sorted(set(matches) | {'OPTIONS'})
34
+ response = Response(
35
+ status=204,
36
+ headers={'Allow': ', '.join(methods)},
37
+ )
38
+ method = request.header('Access-Control-Request-Method')
39
+ if method is None:
40
+ return response
41
+ if method not in matches:
42
+ raise ClientError(
43
+ 'Preflight method not allowed',
44
+ status=405,
45
+ )
46
+ properties = matches[method][0].object.properties
47
+ cross_origin_resource_sharing = CrossOriginResourceSharing(properties)
48
+ if request.header('Origin') is None or not cross_origin_resource_sharing.has_origin_policy():
49
+ raise ClientError(
50
+ 'No CORS policy is configured for this preflight request',
51
+ status=403,
52
+ )
53
+ cross_origin_resource_sharing.validate_origin(request)
54
+ headers = cross_origin_resource_sharing.allowed_request_headers()
55
+ requested = request.header('Access-Control-Request-Headers')
56
+ if requested is not None:
57
+ allowed = {name.lower() for name in headers}
58
+ if any(name.strip().lower() not in allowed for name in requested.split(',')):
59
+ raise ClientError('Preflight header not allowed')
60
+ response.header(
61
+ 'Access-Control-Allow-Methods',
62
+ method,
63
+ )
64
+ if headers:
65
+ response.header(
66
+ 'Access-Control-Allow-Headers',
67
+ ', '.join(headers),
68
+ )
69
+ response.header(
70
+ 'Vary',
71
+ 'Access-Control-Request-Method, Access-Control-Request-Headers',
72
+ )
73
+ return cross_origin_resource_sharing.apply_to_response(
74
+ request,
75
+ response,
76
+ preflight=True,
77
+ )
@@ -0,0 +1,198 @@
1
+ from logging import getLogger
2
+ from typing import (
3
+ Optional,
4
+ Type,
5
+ )
6
+
7
+ from ..RequestRunner import RequestRunner
8
+ from ..Request import Request
9
+ from ..Response import (
10
+ Response,
11
+ close_bodies,
12
+ )
13
+ from ..CrossOriginResourceSharing import CrossOriginResourceSharing
14
+
15
+ __all__ = (
16
+ 'RunRequest',
17
+ )
18
+
19
+
20
+ class RunRequest:
21
+ """Manage validation, handler execution, response processing, and error observation."""
22
+
23
+ logger = getLogger('DefEssential.RequestRunner')
24
+
25
+ def __init__(
26
+ self,
27
+ obj: Type[RequestRunner],
28
+ ):
29
+ self.object = obj
30
+ self.cross_origin_resource_sharing = CrossOriginResourceSharing(obj.properties)
31
+ return
32
+
33
+ def run(
34
+ self,
35
+ request: Request,
36
+ ) -> Optional[Response]:
37
+ properties = self.object.properties
38
+ request_id = request.id
39
+ phase = 'runner.cors_origin'
40
+
41
+ def apply_response_hooks(response: Response) -> Response:
42
+ for index, hook in enumerate(properties.on_request_response):
43
+ previous = response
44
+ previous_body = previous.body
45
+ phase = 'runner.response_hook.' + str(index)
46
+ self.logger.debug(
47
+ 'Response hook starting: request_id=%s phase=%s hook=%s',
48
+ request.id,
49
+ phase,
50
+ type(hook).__qualname__,
51
+ )
52
+ try:
53
+ result = hook(request, previous)
54
+ if not isinstance(result, Response):
55
+ raise TypeError('RequestRunnerRequestResponse hooks must return a Response')
56
+ except Exception:
57
+ close_bodies(
58
+ previous_body,
59
+ previous.body,
60
+ preserve_error=True,
61
+ )
62
+ raise
63
+ response = result
64
+ self.logger.debug('Response hook completed: request_id=%s phase=%s', request.id, phase)
65
+ try:
66
+ close_bodies(*(
67
+ body for body in (
68
+ previous_body,
69
+ previous.body,
70
+ )
71
+ if body is not response.body
72
+ ))
73
+ except Exception:
74
+ close_bodies(
75
+ response.body,
76
+ preserve_error=True,
77
+ )
78
+ raise
79
+ try:
80
+ self.logger.debug('CORS response starting: request_id=%s', request.id)
81
+ response = self.cross_origin_resource_sharing.apply_to_response(
82
+ request,
83
+ response,
84
+ )
85
+ self.logger.debug('CORS response completed: request_id=%s', request.id)
86
+ return response
87
+ except Exception:
88
+ close_bodies(
89
+ response.body,
90
+ preserve_error=True,
91
+ )
92
+ raise
93
+
94
+ writer = None
95
+ try:
96
+ self.logger.debug('CORS origin check starting: request_id=%s', request.id)
97
+ self.cross_origin_resource_sharing.validate_origin(request)
98
+ self.logger.debug('CORS origin check completed: request_id=%s', request.id)
99
+ for name, check in (
100
+ (
101
+ 'auth',
102
+ properties.auth,
103
+ ),
104
+ (
105
+ 'parameters',
106
+ properties.parameters,
107
+ ),
108
+ (
109
+ 'query',
110
+ properties.qs,
111
+ ),
112
+ (
113
+ 'headers',
114
+ properties.headers,
115
+ ),
116
+ ):
117
+ if check is not None:
118
+ phase = 'runner.validation.' + name
119
+ self.logger.debug(
120
+ 'Validation starting: request_id=%s phase=%s validator=%s',
121
+ request.id,
122
+ phase,
123
+ type(check).__qualname__,
124
+ )
125
+ check(request)
126
+ self.logger.debug('Validation completed: request_id=%s phase=%s', request.id, phase)
127
+ if properties.body is not None:
128
+ phase = 'runner.body'
129
+ self.logger.debug('Request body processing: request_id=%s processor=%s', request.id, type(properties.body).__qualname__)
130
+ request.body = properties.body(request)
131
+ self.logger.debug('Request body processed: request_id=%s', request.id)
132
+ phase = 'runner.create'
133
+ self.logger.debug('Handler creating: request_id=%s handler=%s.%s', request.id, self.object.__module__, self.object.__qualname__)
134
+ runner = self.object()
135
+ self.logger.debug('Handler created: request_id=%s', request.id)
136
+ for index, hook in enumerate(properties.on_request):
137
+ phase = 'runner.request_hook.' + str(index)
138
+ self.logger.debug('Request hook starting: request_id=%s phase=%s hook=%s', request.id, phase, type(hook).__qualname__)
139
+ result = hook(request)
140
+ if not isinstance(result, Request):
141
+ raise TypeError('RequestRunnerRequest hooks must return a Request')
142
+ self.logger.debug('Request hook completed: request_id=%s phase=%s', request.id, phase)
143
+ if request.id != result.id:
144
+ self.logger.debug('Request replaced: previous_request_id=%s request_id=%s', request.id, result.id)
145
+ request = result
146
+ writer = request.response_writer
147
+ if writer is not None:
148
+ writer.on_request_response = apply_response_hooks
149
+ phase = 'runner.run'
150
+ self.logger.debug('Handler running: request_id=%s handler=%s.%s', request.id, self.object.__module__, self.object.__qualname__)
151
+ response = runner.run(request)
152
+ self.logger.debug('Handler returned: request_id=%s', request.id)
153
+ phase = 'runner.response_validation'
154
+ if writer is not None and writer.started:
155
+ if response is not None:
156
+ if isinstance(response, Response):
157
+ close_bodies(
158
+ response.body,
159
+ preserve_error=True,
160
+ )
161
+ raise RuntimeError('Cannot return a Response after starting direct output')
162
+ phase = 'runner.direct_end'
163
+ self.logger.debug('Direct response ending: request_id=%s', request.id)
164
+ writer.end()
165
+ self.logger.debug('Runner response provided: request_id=%s mode=direct', request.id)
166
+ return None
167
+ if not isinstance(response, Response):
168
+ raise TypeError('run must return a Response or start a response through the writer')
169
+ phase = 'runner.response_hooks'
170
+ response = apply_response_hooks(response)
171
+ self.logger.debug(
172
+ 'Runner response provided: request_id=%s mode=returned status=%s',
173
+ request.id,
174
+ response.status,
175
+ )
176
+ return response
177
+ except Exception as error:
178
+ self.logger.debug(
179
+ 'Runner failed: request_id=%s phase=%s handler=%s error_type=%s',
180
+ request_id,
181
+ phase,
182
+ self.object.__module__ + '.' + self.object.__qualname__,
183
+ type(error).__qualname__,
184
+ )
185
+ for index, hook in enumerate(properties.on_request_error):
186
+ self.logger.debug(
187
+ 'Error hook starting: request_id=%s phase=runner.error_hook.%s hook=%s',
188
+ request.id,
189
+ index,
190
+ type(hook).__qualname__,
191
+ )
192
+ if hook(request, error) is not None:
193
+ raise TypeError('RequestRunnerRequestError hooks must return None')
194
+ self.logger.debug('Error hook completed: request_id=%s index=%s', request.id, index)
195
+ raise
196
+ finally:
197
+ if writer is not None:
198
+ writer.on_request_response = None
@@ -0,0 +1,5 @@
1
+ from .RunRequest import RunRequest
2
+
3
+ __all__ = (
4
+ 'RunRequest',
5
+ )
@@ -0,0 +1,104 @@
1
+ from os import environ
2
+ import sys
3
+ from .Application import Application
4
+
5
+ __all__ = (
6
+ 'LOGO',
7
+ 'supports_color',
8
+ 'colorize_logo',
9
+ 'startup',
10
+ )
11
+
12
+
13
+ LOGO = r"""
14
+ ▄▄▄▄▄▄ ▄▄ ▄▄▄▄▄▄▄ ▄▄ ▄▄
15
+ ███▀▀██▄ ██ ███▀▀▀▀ ██ ▀▀ ██
16
+ ███ ███ ▄█▀█▄ ▀██▀ █████ ▄██▀▀ ▄██▀▀ ▄█▀█▄ ████▄ ▀██▀ ██ ▀▀█▄ ██
17
+ ███ ███ ██▄█▀ ██ ███ ▀▀██▄ ▀▀██▄ ██▄█▀ ██ ██ ██ ██ ▄█▀██ ██
18
+ ██████▀ ▀█▄▄▄ ██ ███████ ▀███▀ ▀███▀ ▀█▄▄▄ ██ ██ ██ ██ ▀█▄██ ██
19
+
20
+ Just def. Let web apps do the work. You don't need to await this.
21
+ """
22
+
23
+
24
+ def supports_color() -> bool:
25
+ """Use colors only for terminal output and honor NO_COLOR."""
26
+ return (
27
+ getattr(sys.stdout, 'isatty', lambda: False)()
28
+ and 'NO_COLOR' not in environ
29
+ )
30
+
31
+
32
+ def colorize_logo(logo: str) -> str:
33
+ """Apply the existing startup color gradient to the five banner lines."""
34
+ colors = (
35
+ '\033[38;5;39m',
36
+ '\033[38;5;45m',
37
+ '\033[38;5;51m',
38
+ '\033[38;5;45m',
39
+ '\033[38;5;39m',
40
+ )
41
+ lines = []
42
+ art_line = 0
43
+ for line in logo.splitlines(keepends=True):
44
+ if line.strip() and art_line < len(colors):
45
+ newline = '\n' if line.endswith('\n') else ''
46
+ lines.append(
47
+ f'{colors[art_line]}{line[:-1] if newline else line}'
48
+ f'\033[0m{newline}',
49
+ )
50
+ art_line += 1
51
+ else:
52
+ lines.append(line)
53
+ return ''.join(lines)
54
+
55
+
56
+ def startup(
57
+ application: Application,
58
+ server: str,
59
+ host: str,
60
+ port: int,
61
+ ) -> None:
62
+ """Log the application's registration details, adapter name, and listening address."""
63
+ if not isinstance(host, str):
64
+ raise TypeError('HTTP listen address must be a string')
65
+ address = f'[{host}]' if ':' in host else host
66
+ logo = LOGO.rstrip()
67
+ starting = 'Starting'
68
+ if supports_color():
69
+ logo = colorize_logo(logo)
70
+ starting = f'\033[1;38;5;51m{starting}\033[0m'
71
+ routes = [
72
+ route for route in application.router.routes
73
+ if route not in application.extension_routes
74
+ ]
75
+ lines = [
76
+ logo,
77
+ '',
78
+ starting,
79
+ f' Server: {server}',
80
+ f' Listen: http://{address}:{port}',
81
+ f' Applications: {len(routes)} applications',
82
+ ]
83
+ for route in routes:
84
+ lines.extend((
85
+ f' - {route.object.properties.name if route.object.properties.name is not None else route.object.__qualname__}',
86
+ f' {route.method} {route.url}',
87
+ ))
88
+ if application.exts:
89
+ lines.append(f' Extensions: {len(application.exts)} extensions')
90
+ for extension in application.exts:
91
+ lines.append(
92
+ f' - {extension}',
93
+ )
94
+ lines.append('\n')
95
+ text = '\n'.join(lines)
96
+ encoding = getattr(sys.stdout, 'encoding', None)
97
+ if encoding is not None:
98
+ # Escape only unprintable characters to preserve the startup details.
99
+ text = text.encode(encoding, errors='backslashreplace').decode(encoding)
100
+ print(
101
+ text,
102
+ flush=True,
103
+ )
104
+ return