DefChord 0.1.13__63-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.
- DefChord/Adapter/Implements/Python/Server.py +273 -0
- DefChord/Adapter/Implements/Python/__init__.py +5 -0
- DefChord/Adapter/Implements/__init__.py +3 -0
- DefChord/Adapter/RequestHandler.py +109 -0
- DefChord/Adapter/Server.py +54 -0
- DefChord/Adapter/WSGI.py +219 -0
- DefChord/Adapter/__init__.py +5 -0
- DefChord/Application.py +696 -0
- DefChord/Body.py +50 -0
- DefChord/Content.py +18 -0
- DefChord/Credentials.py +245 -0
- DefChord/CrossOriginResourceSharing.py +107 -0
- DefChord/Descriptor/DataObject.py +409 -0
- DefChord/Descriptor/Types.py +248 -0
- DefChord/Descriptor/Value.py +188 -0
- DefChord/Descriptor/__init__.py +172 -0
- DefChord/Error.py +66 -0
- DefChord/Extension.py +191 -0
- DefChord/Extensions/File.py +95 -0
- DefChord/Extensions/FileSystem.py +195 -0
- DefChord/Extensions/Metrics.py +511 -0
- DefChord/Extensions/Ping.py +93 -0
- DefChord/Extensions/Specifications.py +342 -0
- DefChord/Extensions/Status.html +207 -0
- DefChord/Extensions/Status.py +110 -0
- DefChord/Extensions/Swagger.html +77 -0
- DefChord/Extensions/Swagger.py +117 -0
- DefChord/Extensions/Utils/Routes.py +42 -0
- DefChord/Extensions/Utils/__init__.py +43 -0
- DefChord/Extensions/__init__.py +21 -0
- DefChord/IO/Readers/ChunkedStream.py +106 -0
- DefChord/IO/Readers/ContentReader.py +36 -0
- DefChord/IO/Readers/ContentReaders.py +241 -0
- DefChord/IO/Readers/__init__.py +25 -0
- DefChord/IO/RequestReader.py +125 -0
- DefChord/IO/ResponseWriter.py +242 -0
- DefChord/IO/WebSocket/WebSocket.py +288 -0
- DefChord/IO/WebSocket/WebSocketError.py +17 -0
- DefChord/IO/WebSocket/WebSocketFrame.py +68 -0
- DefChord/IO/WebSocket/WebSocketFrameReader.py +98 -0
- DefChord/IO/WebSocket/WebSocketHandshake.py +94 -0
- DefChord/IO/WebSocket/__init__.py +13 -0
- DefChord/IO/Writers/ChunkedStreamWriter.py +19 -0
- DefChord/IO/Writers/ServerSentEvents.py +41 -0
- DefChord/IO/Writers/ServerSentEventsWriter.py +80 -0
- DefChord/IO/Writers/StreamWriter.py +57 -0
- DefChord/IO/Writers/__init__.py +11 -0
- DefChord/IO/__init__.py +7 -0
- DefChord/Request.py +110 -0
- DefChord/RequestRunner.py +261 -0
- DefChord/Response.py +89 -0
- DefChord/ResponseSpecification.py +23 -0
- DefChord/Responses/ResponseFile.py +108 -0
- DefChord/Responses/__init__.py +63 -0
- DefChord/Route.py +55 -0
- DefChord/Router.py +48 -0
- DefChord/Runners/RunExtension.py +129 -0
- DefChord/Runners/RunFactory.py +26 -0
- DefChord/Runners/RunOptions.py +77 -0
- DefChord/Runners/RunRequest.py +208 -0
- DefChord/Runners/__init__.py +5 -0
- DefChord/Startup.py +106 -0
- DefChord/Utils/FileSystemObject/Configuration.py +15 -0
- DefChord/Utils/FileSystemObject/Connection.py +57 -0
- DefChord/Utils/FileSystemObject/File.py +41 -0
- DefChord/Utils/FileSystemObject/Implements/FileSystem/Configuration.py +15 -0
- DefChord/Utils/FileSystemObject/Implements/FileSystem/Connection.py +115 -0
- DefChord/Utils/FileSystemObject/Implements/FileSystem/File.py +77 -0
- DefChord/Utils/FileSystemObject/Implements/FileSystem/__init__.py +11 -0
- DefChord/Utils/FileSystemObject/Implements/__init__.py +3 -0
- DefChord/Utils/FileSystemObject/__init__.py +11 -0
- DefChord/Utils/Loader.py +81 -0
- DefChord/Utils/__init__.py +3 -0
- DefChord/Validator/Pattern.py +36 -0
- DefChord/Validator/Patterns/Array.py +30 -0
- DefChord/Validator/Patterns/Common.py +92 -0
- DefChord/Validator/Patterns/Compare.py +180 -0
- DefChord/Validator/Patterns/Condition.py +60 -0
- DefChord/Validator/Patterns/DataObject.py +85 -0
- DefChord/Validator/Patterns/DateTime.py +111 -0
- DefChord/Validator/Patterns/Number.py +43 -0
- DefChord/Validator/Patterns/Object.py +150 -0
- DefChord/Validator/Patterns/Size.py +110 -0
- DefChord/Validator/Patterns/String.py +85 -0
- DefChord/Validator/Patterns/Type.py +350 -0
- DefChord/Validator/Patterns/__init__.py +137 -0
- DefChord/Validator/Validate.py +29 -0
- DefChord/Validator/Validator.py +30 -0
- DefChord/Validator/__init__.py +11 -0
- DefChord/Validators.py +255 -0
- DefChord/__init__.py +83 -0
- defchord-0.1.13.dist-info/METADATA +80 -0
- defchord-0.1.13.dist-info/RECORD +96 -0
- defchord-0.1.13.dist-info/WHEEL +6 -0
- defchord-0.1.13.dist-info/licenses/LICENSE +674 -0
- defchord-0.1.13.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
from http.server import (
|
|
2
|
+
BaseHTTPRequestHandler,
|
|
3
|
+
ThreadingHTTPServer,
|
|
4
|
+
)
|
|
5
|
+
from logging import getLogger
|
|
6
|
+
from typing import (
|
|
7
|
+
Optional,
|
|
8
|
+
Union,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from ....Application import Application
|
|
12
|
+
from ....Error import Error
|
|
13
|
+
from ....Request import Request
|
|
14
|
+
from ....IO.RequestReader import RequestReader
|
|
15
|
+
from ....Response import Response
|
|
16
|
+
from ....IO.ResponseWriter import ResponseWriter
|
|
17
|
+
from ....Startup import startup
|
|
18
|
+
from ...Server import Server as ServerAdapter
|
|
19
|
+
from ...RequestHandler import handle_request
|
|
20
|
+
|
|
21
|
+
__all__ = (
|
|
22
|
+
'Server',
|
|
23
|
+
'ServerRequestHandler',
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Server(
|
|
28
|
+
ThreadingHTTPServer,
|
|
29
|
+
ServerAdapter,
|
|
30
|
+
):
|
|
31
|
+
"""Run the built-in HTTP transport without external dependencies."""
|
|
32
|
+
|
|
33
|
+
daemon_threads = True
|
|
34
|
+
app: Application
|
|
35
|
+
logger = getLogger('Server.Python')
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
host: str,
|
|
40
|
+
port: int,
|
|
41
|
+
application: Application,
|
|
42
|
+
*,
|
|
43
|
+
poll_interval: float = 0.5,
|
|
44
|
+
):
|
|
45
|
+
ServerAdapter.__init__(
|
|
46
|
+
self,
|
|
47
|
+
host,
|
|
48
|
+
port,
|
|
49
|
+
application,
|
|
50
|
+
)
|
|
51
|
+
self.app = application
|
|
52
|
+
self.poll_interval = poll_interval
|
|
53
|
+
try:
|
|
54
|
+
ThreadingHTTPServer.__init__(
|
|
55
|
+
self,
|
|
56
|
+
(
|
|
57
|
+
host,
|
|
58
|
+
port,
|
|
59
|
+
),
|
|
60
|
+
ServerRequestHandler,
|
|
61
|
+
)
|
|
62
|
+
except Exception:
|
|
63
|
+
self.logger.exception('Server bind failed: adapter=Python host=%s port=%s', host, port)
|
|
64
|
+
raise
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
def run(self) -> None:
|
|
68
|
+
"""Close the listening socket and ensure extensions shut down when serving stops."""
|
|
69
|
+
with self:
|
|
70
|
+
self.serve_forever(self.poll_interval)
|
|
71
|
+
self.logger.info('Server stopped: adapter=Python')
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
def serve_forever(
|
|
75
|
+
self,
|
|
76
|
+
poll_interval: float = 0.5,
|
|
77
|
+
) -> None:
|
|
78
|
+
host = self.server_address[0]
|
|
79
|
+
if not isinstance(host, str):
|
|
80
|
+
raise TypeError('HTTP listen address must be a string')
|
|
81
|
+
self.logger.info('Server starting: adapter=Python host=%s port=%s', host, self.server_port)
|
|
82
|
+
startup(
|
|
83
|
+
self.application,
|
|
84
|
+
'Python',
|
|
85
|
+
host,
|
|
86
|
+
self.server_port,
|
|
87
|
+
)
|
|
88
|
+
self.app.start()
|
|
89
|
+
self.logger.info('Server ready: adapter=Python host=%s port=%s', host, self.server_port)
|
|
90
|
+
try:
|
|
91
|
+
super().serve_forever(poll_interval=poll_interval)
|
|
92
|
+
except KeyboardInterrupt:
|
|
93
|
+
self.logger.info('Shutdown requested by keyboard interrupt: adapter=Python')
|
|
94
|
+
except Exception:
|
|
95
|
+
self.logger.exception('Server execution failed: adapter=Python')
|
|
96
|
+
raise
|
|
97
|
+
finally:
|
|
98
|
+
self.logger.info('Server stopping: adapter=Python')
|
|
99
|
+
self.app.stop()
|
|
100
|
+
self.logger.info('Server serving stopped: adapter=Python')
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ServerRequestHandler(BaseHTTPRequestHandler):
|
|
105
|
+
server: Server
|
|
106
|
+
protocol_version = 'HTTP/1.1'
|
|
107
|
+
server_version = 'DefChord'
|
|
108
|
+
logger = getLogger('Server.Python')
|
|
109
|
+
|
|
110
|
+
def request_reader(self) -> RequestReader:
|
|
111
|
+
lengths = self.headers.get_all('Content-Length', [])
|
|
112
|
+
transfers = self.headers.get_all('Transfer-Encoding', [])
|
|
113
|
+
if len(self.headers.get_all('Content-Type', [])) > 1:
|
|
114
|
+
raise Error(
|
|
115
|
+
400,
|
|
116
|
+
'Duplicate Content-Type',
|
|
117
|
+
)
|
|
118
|
+
if len(lengths) > 1 or len(transfers) > 1 or (lengths and transfers):
|
|
119
|
+
raise Error(
|
|
120
|
+
400,
|
|
121
|
+
'Ambiguous request body framing',
|
|
122
|
+
)
|
|
123
|
+
size: Optional[int] = None
|
|
124
|
+
if lengths:
|
|
125
|
+
if not lengths[0].isascii() or not lengths[0].isdigit():
|
|
126
|
+
raise Error(
|
|
127
|
+
400,
|
|
128
|
+
'Invalid Content-Length',
|
|
129
|
+
)
|
|
130
|
+
size = int(lengths[0])
|
|
131
|
+
if transfers and transfers[0].lower() != 'chunked':
|
|
132
|
+
raise Error(
|
|
133
|
+
400,
|
|
134
|
+
'Unsupported Transfer-Encoding',
|
|
135
|
+
)
|
|
136
|
+
return RequestReader(
|
|
137
|
+
self.rfile,
|
|
138
|
+
size=size,
|
|
139
|
+
chunked=bool(transfers),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def handle_one_request(self) -> None:
|
|
143
|
+
self.close_connection = True
|
|
144
|
+
try:
|
|
145
|
+
self.raw_requestline = self.rfile.readline(65537)
|
|
146
|
+
if len(self.raw_requestline) > 65536:
|
|
147
|
+
self.requestline = ''
|
|
148
|
+
self.request_version = ''
|
|
149
|
+
self.command = ''
|
|
150
|
+
self.send_error(414)
|
|
151
|
+
return
|
|
152
|
+
if not self.raw_requestline:
|
|
153
|
+
return
|
|
154
|
+
if not self.parse_request():
|
|
155
|
+
return
|
|
156
|
+
except ConnectionError:
|
|
157
|
+
self.logger.warning('Client disconnected before request parsing: adapter=Python')
|
|
158
|
+
return
|
|
159
|
+
self.close_connection = True
|
|
160
|
+
writer = ResponseWriter(
|
|
161
|
+
self.begin_response,
|
|
162
|
+
self.wfile,
|
|
163
|
+
method=self.command,
|
|
164
|
+
version=self.request_version,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def create_request() -> Request:
|
|
168
|
+
return Request(
|
|
169
|
+
address=self.client_address[0],
|
|
170
|
+
port=self.client_address[1],
|
|
171
|
+
method=self.command,
|
|
172
|
+
uri=self.path,
|
|
173
|
+
headers=dict(self.headers.items()),
|
|
174
|
+
reader=self.request_reader(),
|
|
175
|
+
writer=writer,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
handle_request(
|
|
180
|
+
self.server.app,
|
|
181
|
+
create_request,
|
|
182
|
+
writer,
|
|
183
|
+
)
|
|
184
|
+
except Exception:
|
|
185
|
+
# The shared handler logs transport errors; finish by closing the connection.
|
|
186
|
+
self.close_connection = True
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
def begin_response(
|
|
190
|
+
self,
|
|
191
|
+
response: Response,
|
|
192
|
+
chunked: bool,
|
|
193
|
+
) -> None:
|
|
194
|
+
length = response.header('Content-Length')
|
|
195
|
+
self.response_status = response.status
|
|
196
|
+
self.send_response(
|
|
197
|
+
response.status,
|
|
198
|
+
response.message,
|
|
199
|
+
)
|
|
200
|
+
no_body = (
|
|
201
|
+
self.command == 'HEAD'
|
|
202
|
+
or response.status in (
|
|
203
|
+
204,
|
|
204
|
+
205,
|
|
205
|
+
304,
|
|
206
|
+
)
|
|
207
|
+
or response.status < 200
|
|
208
|
+
)
|
|
209
|
+
upgrade = response.status == 101
|
|
210
|
+
excluded = (
|
|
211
|
+
'content-length',
|
|
212
|
+
'transfer-encoding',
|
|
213
|
+
) if upgrade else (
|
|
214
|
+
'connection',
|
|
215
|
+
'content-length',
|
|
216
|
+
'transfer-encoding',
|
|
217
|
+
)
|
|
218
|
+
for key, value in response.headers():
|
|
219
|
+
if key.lower() not in excluded:
|
|
220
|
+
self.send_header(
|
|
221
|
+
key,
|
|
222
|
+
value,
|
|
223
|
+
)
|
|
224
|
+
if chunked and not no_body:
|
|
225
|
+
self.send_header(
|
|
226
|
+
'Transfer-Encoding',
|
|
227
|
+
'chunked',
|
|
228
|
+
)
|
|
229
|
+
elif length is not None and response.status not in (
|
|
230
|
+
204,
|
|
231
|
+
205,
|
|
232
|
+
304,
|
|
233
|
+
):
|
|
234
|
+
self.send_header(
|
|
235
|
+
'Content-Length',
|
|
236
|
+
length,
|
|
237
|
+
)
|
|
238
|
+
if response.status == 205:
|
|
239
|
+
self.send_header(
|
|
240
|
+
'Content-Length',
|
|
241
|
+
'0',
|
|
242
|
+
)
|
|
243
|
+
if not upgrade:
|
|
244
|
+
self.send_header(
|
|
245
|
+
'Connection',
|
|
246
|
+
'close',
|
|
247
|
+
)
|
|
248
|
+
self.end_headers()
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
def log_message(
|
|
252
|
+
self,
|
|
253
|
+
format: str,
|
|
254
|
+
*args: object,
|
|
255
|
+
) -> None:
|
|
256
|
+
self.logger.debug(
|
|
257
|
+
'HTTP parser diagnostic: method=%s',
|
|
258
|
+
ascii(self.command[:32]) if self.command is not None else None,
|
|
259
|
+
)
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
def log_request(
|
|
263
|
+
self,
|
|
264
|
+
code: Union[int, str] = '-',
|
|
265
|
+
size: Union[int, str] = '-',
|
|
266
|
+
) -> None:
|
|
267
|
+
"""Keep shared result logging separate because selecting headers does not complete transmission."""
|
|
268
|
+
self.logger.debug(
|
|
269
|
+
'HTTP response headers selected: status=%s method=%s',
|
|
270
|
+
code,
|
|
271
|
+
ascii(self.command[:32]) if self.command is not None else None,
|
|
272
|
+
)
|
|
273
|
+
return
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
from time import monotonic
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
from ..Application import Application
|
|
6
|
+
from ..Error import Error
|
|
7
|
+
from ..IO.RequestReader import ClientDisconnected
|
|
8
|
+
from ..IO.ResponseWriter import ResponseWriter
|
|
9
|
+
from ..Request import Request
|
|
10
|
+
from ..Response import Response
|
|
11
|
+
|
|
12
|
+
__all__ = (
|
|
13
|
+
'handle_request',
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
LOGGER = getLogger('Server')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def handle_request(
|
|
20
|
+
application: Application,
|
|
21
|
+
create_request: Callable[[], Request],
|
|
22
|
+
writer: ResponseWriter,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Handle error responses and resource cleanup independently of the transport layer."""
|
|
25
|
+
logger = LOGGER
|
|
26
|
+
started = monotonic()
|
|
27
|
+
request = None
|
|
28
|
+
response = None
|
|
29
|
+
outcome = 'failed'
|
|
30
|
+
try:
|
|
31
|
+
try:
|
|
32
|
+
request = create_request()
|
|
33
|
+
logger.debug(
|
|
34
|
+
'HTTP request received: request_id=%s method=%s',
|
|
35
|
+
request.id,
|
|
36
|
+
ascii(request.method[:32]),
|
|
37
|
+
)
|
|
38
|
+
response = application(request)
|
|
39
|
+
except ClientDisconnected:
|
|
40
|
+
raise
|
|
41
|
+
except Error as error:
|
|
42
|
+
if writer.started:
|
|
43
|
+
raise
|
|
44
|
+
logger.debug(
|
|
45
|
+
'HTTP error response selected: request_id=%s status=%s',
|
|
46
|
+
request.id if request is not None else None,
|
|
47
|
+
error.status,
|
|
48
|
+
)
|
|
49
|
+
response = Response(
|
|
50
|
+
status=error.status,
|
|
51
|
+
headers=error.headers,
|
|
52
|
+
body=error.reason.encode('utf-8'),
|
|
53
|
+
format='text/plain',
|
|
54
|
+
charset='utf-8',
|
|
55
|
+
)
|
|
56
|
+
except Exception:
|
|
57
|
+
if writer.started:
|
|
58
|
+
raise
|
|
59
|
+
logger.exception(
|
|
60
|
+
'Unexpected error while handling request: request_id=%s',
|
|
61
|
+
request.id if request is not None else None,
|
|
62
|
+
)
|
|
63
|
+
response = Response(
|
|
64
|
+
status=500,
|
|
65
|
+
body=b'Internal Server Error',
|
|
66
|
+
)
|
|
67
|
+
if response is not None:
|
|
68
|
+
logger.debug('Response transmission starting: request_id=%s status=%s', request.id if request is not None else None, response.status)
|
|
69
|
+
writer.response(
|
|
70
|
+
response,
|
|
71
|
+
filtered=True,
|
|
72
|
+
)
|
|
73
|
+
elif not writer.ended:
|
|
74
|
+
raise RuntimeError('Response was neither returned nor completed successfully')
|
|
75
|
+
outcome = 'completed'
|
|
76
|
+
except ConnectionError:
|
|
77
|
+
outcome = 'disconnected'
|
|
78
|
+
logger.warning(
|
|
79
|
+
'Client disconnected: request_id=%s',
|
|
80
|
+
request.id if request is not None else None,
|
|
81
|
+
)
|
|
82
|
+
raise
|
|
83
|
+
except Exception:
|
|
84
|
+
logger.exception(
|
|
85
|
+
'Response transmission failed: closing connection without another error response request_id=%s',
|
|
86
|
+
request.id if request is not None else None,
|
|
87
|
+
)
|
|
88
|
+
raise
|
|
89
|
+
finally:
|
|
90
|
+
duration = monotonic() - started
|
|
91
|
+
logger.info(
|
|
92
|
+
'HTTP transmission result: request_id=%s method=%s status=%s outcome=%s duration=%.6f',
|
|
93
|
+
request.id if request is not None else None,
|
|
94
|
+
ascii(request.method[:32]) if request is not None else None,
|
|
95
|
+
writer.status if writer.started else None,
|
|
96
|
+
outcome,
|
|
97
|
+
duration,
|
|
98
|
+
)
|
|
99
|
+
if response is not None:
|
|
100
|
+
try:
|
|
101
|
+
logger.debug('Response body cleaning up: request_id=%s', request.id if request is not None else None)
|
|
102
|
+
response.close()
|
|
103
|
+
logger.debug('Response body cleaned up: request_id=%s', request.id if request is not None else None)
|
|
104
|
+
except Exception:
|
|
105
|
+
logger.exception(
|
|
106
|
+
'Response body cleanup failed: request_id=%s',
|
|
107
|
+
request.id if request is not None else None,
|
|
108
|
+
)
|
|
109
|
+
return
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from abc import (
|
|
2
|
+
ABC,
|
|
3
|
+
abstractmethod,
|
|
4
|
+
)
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import (
|
|
7
|
+
Optional,
|
|
8
|
+
TypeVar,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from ..Application import Application
|
|
12
|
+
|
|
13
|
+
__all__ = (
|
|
14
|
+
'Server',
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ServerType = TypeVar(
|
|
19
|
+
'ServerType',
|
|
20
|
+
bound='Server',
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Server(ABC):
|
|
25
|
+
"""Define the server adapter contract for running an application at an address."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
host: str,
|
|
30
|
+
port: int,
|
|
31
|
+
application: Application,
|
|
32
|
+
):
|
|
33
|
+
self.host = host
|
|
34
|
+
self.port = port
|
|
35
|
+
self.application = application
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
def __enter__(self: ServerType) -> ServerType:
|
|
39
|
+
"""Return this instance without starting the server."""
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def __exit__(
|
|
43
|
+
self,
|
|
44
|
+
exception_type: Optional[type[BaseException]],
|
|
45
|
+
exception: Optional[BaseException],
|
|
46
|
+
traceback: Optional[TracebackType],
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Propagate exceptions; run() is responsible for cleaning up active resources."""
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def run(self) -> None:
|
|
53
|
+
"""Serve until shutdown and manage the application lifecycle."""
|
|
54
|
+
raise NotImplementedError
|
DefChord/Adapter/WSGI.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Any,
|
|
3
|
+
Callable,
|
|
4
|
+
Iterable,
|
|
5
|
+
Mapping,
|
|
6
|
+
Optional,
|
|
7
|
+
Protocol,
|
|
8
|
+
)
|
|
9
|
+
from urllib.parse import quote_from_bytes
|
|
10
|
+
from wsgiref.util import is_hop_by_hop
|
|
11
|
+
|
|
12
|
+
from ..Application import Application
|
|
13
|
+
from ..Error import Error
|
|
14
|
+
from ..IO.RequestReader import RequestReader
|
|
15
|
+
from ..IO.ResponseWriter import ResponseWriter
|
|
16
|
+
from ..Request import Request
|
|
17
|
+
from ..Response import Response
|
|
18
|
+
from .RequestHandler import handle_request
|
|
19
|
+
|
|
20
|
+
__all__ = (
|
|
21
|
+
'StartResponse',
|
|
22
|
+
'WSGITransport',
|
|
23
|
+
'WSGIResponseWriter',
|
|
24
|
+
'WSGIApplication',
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StartResponse(Protocol):
|
|
29
|
+
def __call__(
|
|
30
|
+
self,
|
|
31
|
+
status: str,
|
|
32
|
+
headers: list[tuple[str, str]],
|
|
33
|
+
/,
|
|
34
|
+
) -> Callable[[bytes], object]:
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class WSGITransport:
|
|
39
|
+
"""Delegate HTTP framing to the WSGI server and forward bytes immediately."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
start_response: StartResponse,
|
|
44
|
+
):
|
|
45
|
+
self.start_response = start_response
|
|
46
|
+
self.output: Optional[Callable[[bytes], object]] = None
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
def begin(
|
|
50
|
+
self,
|
|
51
|
+
response: Response,
|
|
52
|
+
chunked: bool,
|
|
53
|
+
) -> None:
|
|
54
|
+
headers = [
|
|
55
|
+
(
|
|
56
|
+
key,
|
|
57
|
+
value,
|
|
58
|
+
) for key, value in response.headers()
|
|
59
|
+
if not is_hop_by_hop(key)
|
|
60
|
+
and not (key.lower() == 'content-length' and response.status in (
|
|
61
|
+
204,
|
|
62
|
+
205,
|
|
63
|
+
304,
|
|
64
|
+
))
|
|
65
|
+
]
|
|
66
|
+
if response.status == 205:
|
|
67
|
+
headers.append((
|
|
68
|
+
'Content-Length',
|
|
69
|
+
'0',
|
|
70
|
+
))
|
|
71
|
+
self.output = self.start_response(
|
|
72
|
+
f'{response.status} {response.message}',
|
|
73
|
+
headers,
|
|
74
|
+
)
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
def write(
|
|
78
|
+
self,
|
|
79
|
+
data: bytes,
|
|
80
|
+
/,
|
|
81
|
+
) -> int:
|
|
82
|
+
if self.output is None:
|
|
83
|
+
raise RuntimeError('WSGI response must be started before writing')
|
|
84
|
+
self.output(data)
|
|
85
|
+
return len(data)
|
|
86
|
+
|
|
87
|
+
def flush(self) -> None:
|
|
88
|
+
# Calling WSGI write() forwards the bytes to the server.
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class WSGIResponseWriter(ResponseWriter):
|
|
93
|
+
"""Preserve the direct-output API without applying chunk framing twice."""
|
|
94
|
+
|
|
95
|
+
def start(
|
|
96
|
+
self,
|
|
97
|
+
response: Response,
|
|
98
|
+
chunked: bool,
|
|
99
|
+
) -> None:
|
|
100
|
+
super().start(
|
|
101
|
+
response,
|
|
102
|
+
False,
|
|
103
|
+
)
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
def upgrade(
|
|
107
|
+
self,
|
|
108
|
+
response: Response,
|
|
109
|
+
) -> None:
|
|
110
|
+
raise Error(
|
|
111
|
+
501,
|
|
112
|
+
'WSGI adapters do not support protocol upgrades',
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class WSGIApplication:
|
|
117
|
+
"""Adapt DefChord requests to the WSGI calls used by Gunicorn and a2wsgi."""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
application: Application,
|
|
122
|
+
):
|
|
123
|
+
self.application = application
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
def request_reader(
|
|
127
|
+
self,
|
|
128
|
+
environ: Mapping[str, Any],
|
|
129
|
+
) -> RequestReader:
|
|
130
|
+
length = environ.get('CONTENT_LENGTH', '')
|
|
131
|
+
transfer = environ.get('HTTP_TRANSFER_ENCODING', '')
|
|
132
|
+
if length and (not length.isascii() or not length.isdigit()):
|
|
133
|
+
raise Error(
|
|
134
|
+
400,
|
|
135
|
+
'Invalid Content-Length',
|
|
136
|
+
)
|
|
137
|
+
if transfer and (transfer.lower() != 'chunked' or length):
|
|
138
|
+
raise Error(
|
|
139
|
+
400,
|
|
140
|
+
'Ambiguous or unsupported request body framing',
|
|
141
|
+
)
|
|
142
|
+
return RequestReader(
|
|
143
|
+
environ['wsgi.input'],
|
|
144
|
+
size=int(length) if length else None,
|
|
145
|
+
chunked=transfer.lower() == 'chunked',
|
|
146
|
+
decoded=True,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def response_writer(
|
|
150
|
+
self,
|
|
151
|
+
environ: Mapping[str, Any],
|
|
152
|
+
start_response: StartResponse,
|
|
153
|
+
) -> ResponseWriter:
|
|
154
|
+
transport = WSGITransport(start_response)
|
|
155
|
+
return WSGIResponseWriter(
|
|
156
|
+
transport.begin,
|
|
157
|
+
transport,
|
|
158
|
+
method=environ['REQUEST_METHOD'],
|
|
159
|
+
version=environ['SERVER_PROTOCOL'],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def __call__(
|
|
163
|
+
self,
|
|
164
|
+
environ: Mapping[str, Any],
|
|
165
|
+
start_response: StartResponse,
|
|
166
|
+
) -> Iterable[bytes]:
|
|
167
|
+
writer = self.response_writer(
|
|
168
|
+
environ,
|
|
169
|
+
start_response,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def create_request() -> Request:
|
|
173
|
+
reader = self.request_reader(environ)
|
|
174
|
+
headers = {
|
|
175
|
+
key[5:].replace('_', '-'): value
|
|
176
|
+
for key, value in environ.items()
|
|
177
|
+
if key.startswith('HTTP_')
|
|
178
|
+
}
|
|
179
|
+
for source, target in (
|
|
180
|
+
(
|
|
181
|
+
'CONTENT_LENGTH',
|
|
182
|
+
'Content-Length',
|
|
183
|
+
),
|
|
184
|
+
(
|
|
185
|
+
'CONTENT_TYPE',
|
|
186
|
+
'Content-Type',
|
|
187
|
+
),
|
|
188
|
+
):
|
|
189
|
+
if environ.get(source):
|
|
190
|
+
headers[target] = environ[source]
|
|
191
|
+
uri = environ.get('RAW_URI') or environ.get('REQUEST_URI')
|
|
192
|
+
if uri is None:
|
|
193
|
+
raw_path = environ.get('asgi.scope', {}).get('raw_path')
|
|
194
|
+
if raw_path is not None:
|
|
195
|
+
uri = raw_path.decode('latin-1')
|
|
196
|
+
else:
|
|
197
|
+
path = environ.get('SCRIPT_NAME', '') + environ['PATH_INFO']
|
|
198
|
+
uri = quote_from_bytes(
|
|
199
|
+
path.encode('latin-1'),
|
|
200
|
+
safe="/:@!$&'()*+,;=-._~",
|
|
201
|
+
)
|
|
202
|
+
if environ.get('QUERY_STRING'):
|
|
203
|
+
uri += '?' + environ['QUERY_STRING']
|
|
204
|
+
return Request(
|
|
205
|
+
address=environ.get('REMOTE_ADDR', ''),
|
|
206
|
+
port=int(environ.get('REMOTE_PORT', 0)),
|
|
207
|
+
method=environ['REQUEST_METHOD'],
|
|
208
|
+
uri=uri,
|
|
209
|
+
headers=headers,
|
|
210
|
+
reader=reader,
|
|
211
|
+
writer=writer,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
handle_request(
|
|
215
|
+
self.application,
|
|
216
|
+
create_request,
|
|
217
|
+
writer,
|
|
218
|
+
)
|
|
219
|
+
return ()
|