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