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.
- DefEssential/Adapter/Implements/Gunicorn/Server.py +152 -0
- DefEssential/Adapter/Implements/Gunicorn/Transport.py +171 -0
- DefEssential/Adapter/Implements/Gunicorn/__init__.py +5 -0
- DefEssential/Adapter/Implements/Python/Server.py +273 -0
- DefEssential/Adapter/Implements/Python/__init__.py +5 -0
- DefEssential/Adapter/Implements/Uvicorn/HTTP.py +256 -0
- DefEssential/Adapter/Implements/Uvicorn/Portal.py +65 -0
- DefEssential/Adapter/Implements/Uvicorn/Server.py +137 -0
- DefEssential/Adapter/Implements/Uvicorn/WebSocket.py +222 -0
- DefEssential/Adapter/Implements/Uvicorn/__init__.py +5 -0
- DefEssential/Adapter/Implements/__init__.py +3 -0
- DefEssential/Adapter/RequestHandler.py +109 -0
- DefEssential/Adapter/Server.py +54 -0
- DefEssential/Adapter/WSGI.py +219 -0
- DefEssential/Adapter/__init__.py +5 -0
- DefEssential/Application.py +657 -0
- DefEssential/Body.py +50 -0
- DefEssential/Content.py +18 -0
- DefEssential/Credentials.py +245 -0
- DefEssential/CrossOriginResourceSharing.py +107 -0
- DefEssential/Descriptor/DataObject.py +409 -0
- DefEssential/Descriptor/Types.py +248 -0
- DefEssential/Descriptor/Value.py +188 -0
- DefEssential/Descriptor/__init__.py +172 -0
- DefEssential/Error.py +66 -0
- DefEssential/Extension.py +191 -0
- DefEssential/Extensions/File.py +95 -0
- DefEssential/Extensions/FileSystem.py +195 -0
- DefEssential/Extensions/Metrics.py +510 -0
- DefEssential/Extensions/Ping.py +93 -0
- DefEssential/Extensions/Specifications.py +342 -0
- DefEssential/Extensions/Status.html +207 -0
- DefEssential/Extensions/Status.py +110 -0
- DefEssential/Extensions/Swagger.html +77 -0
- DefEssential/Extensions/Swagger.py +117 -0
- DefEssential/Extensions/Utils/Routes.py +42 -0
- DefEssential/Extensions/Utils/__init__.py +43 -0
- DefEssential/Extensions/__init__.py +21 -0
- DefEssential/IO/Readers/ChunkedStream.py +106 -0
- DefEssential/IO/Readers/ContentReader.py +36 -0
- DefEssential/IO/Readers/ContentReaders.py +241 -0
- DefEssential/IO/Readers/__init__.py +25 -0
- DefEssential/IO/RequestReader.py +125 -0
- DefEssential/IO/ResponseWriter.py +241 -0
- DefEssential/IO/WebSocket/WebSocket.py +286 -0
- DefEssential/IO/WebSocket/WebSocketError.py +17 -0
- DefEssential/IO/WebSocket/WebSocketFrame.py +68 -0
- DefEssential/IO/WebSocket/WebSocketFrameReader.py +98 -0
- DefEssential/IO/WebSocket/WebSocketHandshake.py +94 -0
- DefEssential/IO/WebSocket/__init__.py +13 -0
- DefEssential/IO/Writers/ChunkedStreamWriter.py +19 -0
- DefEssential/IO/Writers/ServerSentEvents.py +41 -0
- DefEssential/IO/Writers/ServerSentEventsWriter.py +67 -0
- DefEssential/IO/Writers/StreamWriter.py +57 -0
- DefEssential/IO/Writers/__init__.py +11 -0
- DefEssential/IO/__init__.py +7 -0
- DefEssential/Request.py +110 -0
- DefEssential/RequestRunner.py +261 -0
- DefEssential/Response.py +89 -0
- DefEssential/ResponseSpecification.py +23 -0
- DefEssential/Responses/ResponseFile.py +108 -0
- DefEssential/Responses/__init__.py +63 -0
- DefEssential/Route.py +55 -0
- DefEssential/Router.py +48 -0
- DefEssential/Runners/RunExtension.py +116 -0
- DefEssential/Runners/RunFactory.py +26 -0
- DefEssential/Runners/RunOptions.py +77 -0
- DefEssential/Runners/RunRequest.py +198 -0
- DefEssential/Runners/__init__.py +5 -0
- DefEssential/Startup.py +104 -0
- DefEssential/Utils/FileSystemObject/Configuration.py +15 -0
- DefEssential/Utils/FileSystemObject/Connection.py +57 -0
- DefEssential/Utils/FileSystemObject/File.py +41 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/Configuration.py +15 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/Connection.py +115 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/File.py +77 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/__init__.py +11 -0
- DefEssential/Utils/FileSystemObject/Implements/__init__.py +3 -0
- DefEssential/Utils/FileSystemObject/__init__.py +11 -0
- DefEssential/Utils/Loader.py +81 -0
- DefEssential/Utils/__init__.py +3 -0
- DefEssential/Validator/Pattern.py +36 -0
- DefEssential/Validator/Patterns/Array.py +30 -0
- DefEssential/Validator/Patterns/Common.py +92 -0
- DefEssential/Validator/Patterns/Compare.py +180 -0
- DefEssential/Validator/Patterns/Condition.py +60 -0
- DefEssential/Validator/Patterns/DataObject.py +85 -0
- DefEssential/Validator/Patterns/DateTime.py +111 -0
- DefEssential/Validator/Patterns/Number.py +43 -0
- DefEssential/Validator/Patterns/Object.py +150 -0
- DefEssential/Validator/Patterns/Size.py +110 -0
- DefEssential/Validator/Patterns/String.py +85 -0
- DefEssential/Validator/Patterns/Type.py +350 -0
- DefEssential/Validator/Patterns/__init__.py +137 -0
- DefEssential/Validator/Validate.py +29 -0
- DefEssential/Validator/Validator.py +30 -0
- DefEssential/Validator/__init__.py +11 -0
- DefEssential/Validators.py +255 -0
- DefEssential/__init__.py +83 -0
- defessential-0.1.7.dist-info/METADATA +41 -0
- defessential-0.1.7.dist-info/RECORD +104 -0
- defessential-0.1.7.dist-info/WHEEL +6 -0
- defessential-0.1.7.dist-info/licenses/LICENSE +674 -0
- 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,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
|