DefEssential 0.1.7__21-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 +510 -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 +241 -0
  45. DefEssential/IO/WebSocket/WebSocket.py +286 -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.7.dist-info/METADATA +41 -0
  101. defessential-0.1.7.dist-info/RECORD +104 -0
  102. defessential-0.1.7.dist-info/WHEEL +6 -0
  103. defessential-0.1.7.dist-info/licenses/LICENSE +674 -0
  104. defessential-0.1.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,152 @@
1
+ from os import name
2
+ from logging import getLogger
3
+ from typing import (
4
+ Any,
5
+ Union,
6
+ )
7
+
8
+ from ....Application import Application
9
+ from ....Startup import startup
10
+ from ...Server import Server as ServerAdapter
11
+ from .Transport import (
12
+ Body,
13
+ GunicornApplication as RequestApplication,
14
+ )
15
+
16
+ __all__ = (
17
+ 'Server',
18
+ )
19
+
20
+
21
+ class Server(ServerAdapter):
22
+ """Gunicorn의 워커별로 애플리케이션을 시작하고 종료한다."""
23
+
24
+ logger = getLogger('DefEssential.Server')
25
+
26
+ def __init__(
27
+ self,
28
+ host: str,
29
+ port: int,
30
+ application: Application,
31
+ *,
32
+ workers: int = 1,
33
+ worker_class: Union[str, type] = 'sync',
34
+ **options: Any,
35
+ ):
36
+ super().__init__(
37
+ host,
38
+ port,
39
+ application,
40
+ )
41
+ if workers < 1:
42
+ raise ValueError('workers must be at least 1')
43
+ if 'bind' in options:
44
+ raise ValueError('Use host and port instead of bind')
45
+ self.workers = workers
46
+ self.worker_class = worker_class
47
+ self.options = options
48
+ return
49
+
50
+ def run(self) -> None:
51
+ if name == 'nt':
52
+ raise RuntimeError('Gunicorn requires a POSIX platform; use Linux or WSL')
53
+ # Gunicorn은 선택적 의존성이며 Windows에서는 가져올 수 없다.
54
+ from gunicorn.app.base import BaseApplication
55
+
56
+ application = self.application
57
+ options = dict(self.options)
58
+ when_ready = options.pop('when_ready', None)
59
+ post_worker_init = options.pop('post_worker_init', None)
60
+ worker_exit = options.pop('worker_exit', None)
61
+ pre_request = options.pop('pre_request', None)
62
+ address = f'[{self.host}]' if ':' in self.host else self.host
63
+ options.update(
64
+ bind=f'{address}:{self.port}',
65
+ workers=self.workers,
66
+ worker_class=self.worker_class,
67
+ )
68
+
69
+ def ready(server: Any) -> None:
70
+ if when_ready is not None:
71
+ when_ready(server)
72
+ startup(
73
+ application,
74
+ 'Gunicorn',
75
+ self.host,
76
+ self.port,
77
+ )
78
+ for listener in server.LISTENERS:
79
+ self.logger.info(
80
+ 'Server ready: adapter=Gunicorn address=%s',
81
+ listener.getsockname(),
82
+ )
83
+ return
84
+
85
+ def start(worker: Any) -> None:
86
+ application.start()
87
+ if post_worker_init is not None:
88
+ post_worker_init(worker)
89
+ return
90
+
91
+ def prepare(
92
+ worker: Any,
93
+ request: Any,
94
+ ) -> None:
95
+ if pre_request is not None:
96
+ pre_request(
97
+ worker,
98
+ request,
99
+ )
100
+ request.body = Body(request)
101
+ if any(
102
+ key == 'UPGRADE' and value.lower() == 'websocket'
103
+ for key, value in request.headers
104
+ ):
105
+ request.force_close()
106
+ return
107
+
108
+ def stop(
109
+ server: Any,
110
+ worker: Any,
111
+ ) -> None:
112
+ try:
113
+ application.stop()
114
+ finally:
115
+ if worker_exit is not None:
116
+ worker_exit(
117
+ server,
118
+ worker,
119
+ )
120
+ return
121
+
122
+ options.update(
123
+ when_ready=ready,
124
+ post_worker_init=start,
125
+ worker_exit=stop,
126
+ pre_request=prepare,
127
+ )
128
+
129
+ class GunicornApplication(BaseApplication):
130
+ def load_config(self) -> None:
131
+ for key, value in options.items():
132
+ self.cfg.set(
133
+ key,
134
+ value,
135
+ )
136
+ return
137
+
138
+ def load(self) -> RequestApplication:
139
+ return RequestApplication(application)
140
+
141
+ self.logger.info('Server starting: adapter=Gunicorn host=%s port=%s', self.host, self.port)
142
+ try:
143
+ GunicornApplication().run()
144
+ except SystemExit as error:
145
+ if error.code:
146
+ self.logger.error('Server exited unsuccessfully: adapter=Gunicorn code=%s', error.code)
147
+ raise
148
+ except Exception:
149
+ self.logger.exception('Server execution failed: adapter=Gunicorn')
150
+ raise
151
+ self.logger.info('Server stopped: adapter=Gunicorn')
152
+ return
@@ -0,0 +1,171 @@
1
+ from typing import (
2
+ Any,
3
+ Mapping,
4
+ )
5
+
6
+ from ....IO.Readers.ChunkedStream import ChunkedStream
7
+ from ....IO.RequestReader import RequestReader
8
+ from ....IO.ResponseWriter import ResponseWriter
9
+ from ....Response import Response
10
+ from ...WSGI import (
11
+ StartResponse,
12
+ WSGIApplication,
13
+ WSGIResponseWriter,
14
+ WSGITransport,
15
+ )
16
+
17
+ __all__ = (
18
+ 'Input',
19
+ 'Body',
20
+ 'Transport',
21
+ 'Writer',
22
+ 'GunicornApplication',
23
+ )
24
+
25
+
26
+ class Input:
27
+ """Gunicorn 파서의 선행 입력과 본문 상태를 프레임워크 및 워커가 공유한다."""
28
+
29
+ def __init__(
30
+ self,
31
+ request: Any,
32
+ ):
33
+ self.unreader = request.unreader
34
+ headers = dict(request.headers)
35
+ length = headers.get('CONTENT-LENGTH')
36
+ self.request_reader = RequestReader(
37
+ self,
38
+ size=int(length) if length is not None else None,
39
+ chunked=headers.get('TRANSFER-ENCODING', '').lower() == 'chunked',
40
+ )
41
+ self.body = ChunkedStream(self.request_reader) if self.request_reader.chunked else self.request_reader
42
+ return
43
+
44
+ def read(
45
+ self,
46
+ size: int = -1,
47
+ /,
48
+ ) -> bytes:
49
+ return self.unreader.read(size)
50
+
51
+ def readline(
52
+ self,
53
+ size: int = -1,
54
+ /,
55
+ ) -> bytes:
56
+ parts = []
57
+ while size < 0 or len(parts) < size:
58
+ data = self.unreader.read(1)
59
+ if not data:
60
+ break
61
+ parts.append(data)
62
+ if data == b'\n':
63
+ break
64
+ return b''.join(parts)
65
+
66
+
67
+ class Body:
68
+ """워커가 남은 본문을 버릴 때도 애플리케이션이 읽은 위치를 유지한다."""
69
+
70
+ def __init__(
71
+ self,
72
+ request: Any,
73
+ ):
74
+ self.input = Input(request)
75
+ return
76
+
77
+ def read(
78
+ self,
79
+ size: int = -1,
80
+ ) -> bytes:
81
+ return self.input.body.read(size)
82
+
83
+ def readline(
84
+ self,
85
+ size: int = -1,
86
+ ) -> bytes:
87
+ return self.input.body.readline(size)
88
+
89
+
90
+ class Transport(WSGITransport):
91
+ """HTTP는 Gunicorn에 맡기고 101 전환 뒤에는 원시 소켓을 사용한다."""
92
+
93
+ def __init__(
94
+ self,
95
+ start_response: StartResponse,
96
+ socket: Any,
97
+ ):
98
+ super().__init__(start_response)
99
+ self.socket = socket
100
+ self.upgraded = False
101
+ return
102
+
103
+ def begin(
104
+ self,
105
+ response: Response,
106
+ chunked: bool,
107
+ ) -> None:
108
+ if response.status != 101:
109
+ super().begin(
110
+ response,
111
+ chunked,
112
+ )
113
+ return
114
+ output = self.start_response(
115
+ f'{response.status} {response.message}',
116
+ response.headers(),
117
+ )
118
+ output(b'')
119
+ self.upgraded = True
120
+ return
121
+
122
+ def write(
123
+ self,
124
+ data: bytes,
125
+ /,
126
+ ) -> int:
127
+ if self.upgraded:
128
+ self.socket.sendall(data)
129
+ return len(data)
130
+ return super().write(data)
131
+
132
+
133
+ class Writer(WSGIResponseWriter):
134
+ def upgrade(
135
+ self,
136
+ response: Response,
137
+ ) -> None:
138
+ ResponseWriter.upgrade(
139
+ self,
140
+ response,
141
+ )
142
+ return
143
+
144
+
145
+ class GunicornApplication(WSGIApplication):
146
+ """sync·gthread·gevent에서 같은 입출력 및 WebSocket API를 제공한다."""
147
+
148
+ def request_reader(
149
+ self,
150
+ environ: Mapping[str, Any],
151
+ ) -> RequestReader:
152
+ body = environ['wsgi.input']
153
+ if not isinstance(body, Body):
154
+ raise TypeError('Gunicorn input hook was not installed')
155
+ return body.input.request_reader
156
+
157
+ def response_writer(
158
+ self,
159
+ environ: Mapping[str, Any],
160
+ start_response: StartResponse,
161
+ ) -> ResponseWriter:
162
+ transport = Transport(
163
+ start_response,
164
+ environ['gunicorn.socket'],
165
+ )
166
+ return Writer(
167
+ transport.begin,
168
+ transport,
169
+ method=environ['REQUEST_METHOD'],
170
+ version=environ['SERVER_PROTOCOL'],
171
+ )
@@ -0,0 +1,5 @@
1
+ from .Server import Server
2
+
3
+ __all__ = (
4
+ 'Server',
5
+ )
@@ -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
+ """외부 의존성 없이 자체 HTTP 전송 계층으로 실행하는 기본 서버."""
32
+
33
+ daemon_threads = True
34
+ app: Application
35
+ logger = getLogger('DefEssential.Server')
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
+ """종료 시 수신 소켓까지 닫고 확장 기능의 종료를 보장한다."""
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 = 'DefEssential'
108
+ logger = getLogger('DefEssential.Server')
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
+ # 공통 처리기가 기록한 전송 오류는 연결 종료로 마무리한다.
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
+ """헤더 생성은 전송 완료가 아니므로 공통 결과 기록과 분리한다."""
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
+ )