DefEssential 0.1.6__19-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 +134 -0
- DefEssential/Adapter/Implements/Gunicorn/Transport.py +171 -0
- DefEssential/Adapter/Implements/Gunicorn/__init__.py +5 -0
- DefEssential/Adapter/Implements/Python/Server.py +245 -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 +102 -0
- DefEssential/Adapter/Implements/Uvicorn/WebSocket.py +220 -0
- DefEssential/Adapter/Implements/Uvicorn/__init__.py +5 -0
- DefEssential/Adapter/Implements/__init__.py +3 -0
- DefEssential/Adapter/RequestHandler.py +70 -0
- DefEssential/Adapter/Server.py +54 -0
- DefEssential/Adapter/WSGI.py +219 -0
- DefEssential/Adapter/__init__.py +5 -0
- DefEssential/Application.py +579 -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 +184 -0
- DefEssential/Extensions/File.py +89 -0
- DefEssential/Extensions/FileSystem.py +184 -0
- DefEssential/Extensions/Metrics.py +500 -0
- DefEssential/Extensions/Ping.py +89 -0
- DefEssential/Extensions/Specifications.py +328 -0
- DefEssential/Extensions/Status.html +207 -0
- DefEssential/Extensions/Status.py +104 -0
- DefEssential/Extensions/Swagger.html +77 -0
- DefEssential/Extensions/Swagger.py +111 -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 +282 -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 +87 -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 +101 -0
- DefEssential/Runners/RunFactory.py +26 -0
- DefEssential/Runners/RunOptions.py +77 -0
- DefEssential/Runners/RunRequest.py +121 -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.6.dist-info/METADATA +41 -0
- defessential-0.1.6.dist-info/RECORD +104 -0
- defessential-0.1.6.dist-info/WHEEL +6 -0
- defessential-0.1.6.dist-info/licenses/LICENSE +674 -0
- defessential-0.1.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
from asyncio import (
|
|
2
|
+
CancelledError,
|
|
3
|
+
Queue,
|
|
4
|
+
create_task,
|
|
5
|
+
get_running_loop,
|
|
6
|
+
shield,
|
|
7
|
+
)
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
9
|
+
from contextlib import suppress
|
|
10
|
+
from typing import Optional
|
|
11
|
+
from a2wsgi.asgi_typing import (
|
|
12
|
+
Receive,
|
|
13
|
+
Scope,
|
|
14
|
+
Send,
|
|
15
|
+
)
|
|
16
|
+
from a2wsgi.wsgi import (
|
|
17
|
+
Body,
|
|
18
|
+
build_environ,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from ....Application import Application
|
|
22
|
+
from ....IO.RequestReader import ClientDisconnected
|
|
23
|
+
from ...WSGI import WSGIApplication
|
|
24
|
+
from .Portal import Portal
|
|
25
|
+
|
|
26
|
+
__all__ = (
|
|
27
|
+
'Input',
|
|
28
|
+
'Middleware',
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Input(Body):
|
|
33
|
+
"""ASGI 본문을 전체 업로드 대기 없이 읽으며 연결 종료도 감시한다."""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
receive: Receive,
|
|
38
|
+
portal: Portal,
|
|
39
|
+
capacity: int,
|
|
40
|
+
):
|
|
41
|
+
super().__init__(
|
|
42
|
+
portal.loop,
|
|
43
|
+
receive,
|
|
44
|
+
)
|
|
45
|
+
self.portal = portal
|
|
46
|
+
self.messages: Queue[bytes] = Queue(maxsize=capacity)
|
|
47
|
+
self.finished = False
|
|
48
|
+
self.disconnected = False
|
|
49
|
+
self.failure: Optional[Exception] = None
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
async def pump(self) -> None:
|
|
53
|
+
try:
|
|
54
|
+
while True:
|
|
55
|
+
message = await self.receive()
|
|
56
|
+
if message['type'] == 'http.disconnect':
|
|
57
|
+
self.disconnected = True
|
|
58
|
+
await self.messages.put(b'')
|
|
59
|
+
return
|
|
60
|
+
if message['type'] != 'http.request':
|
|
61
|
+
raise RuntimeError('Expected an HTTP request body event')
|
|
62
|
+
if message['body']:
|
|
63
|
+
await self.messages.put(message['body'])
|
|
64
|
+
if not message.get('more_body', False):
|
|
65
|
+
await self.messages.put(b'')
|
|
66
|
+
except Exception as error:
|
|
67
|
+
self.failure = error
|
|
68
|
+
await self.messages.put(b'')
|
|
69
|
+
raise
|
|
70
|
+
|
|
71
|
+
async def take(
|
|
72
|
+
self,
|
|
73
|
+
size: int,
|
|
74
|
+
) -> bytes:
|
|
75
|
+
if self.failure is not None:
|
|
76
|
+
raise self.failure
|
|
77
|
+
if self.disconnected:
|
|
78
|
+
raise ClientDisconnected('Client disconnected while receiving request')
|
|
79
|
+
while not self.buffer and not self.finished:
|
|
80
|
+
data = await self.messages.get()
|
|
81
|
+
if not data:
|
|
82
|
+
if self.failure is not None:
|
|
83
|
+
raise self.failure
|
|
84
|
+
if self.disconnected:
|
|
85
|
+
raise ClientDisconnected('Client disconnected while receiving request')
|
|
86
|
+
self.finished = True
|
|
87
|
+
else:
|
|
88
|
+
self.buffer.extend(data)
|
|
89
|
+
if size < 0:
|
|
90
|
+
while not self.finished:
|
|
91
|
+
data = await self.messages.get()
|
|
92
|
+
if not data:
|
|
93
|
+
if self.failure is not None:
|
|
94
|
+
raise self.failure
|
|
95
|
+
if self.disconnected:
|
|
96
|
+
raise ClientDisconnected('Client disconnected while receiving request')
|
|
97
|
+
self.finished = True
|
|
98
|
+
else:
|
|
99
|
+
self.buffer.extend(data)
|
|
100
|
+
size = len(self.buffer)
|
|
101
|
+
data = bytes(self.buffer[:size])
|
|
102
|
+
del self.buffer[:size]
|
|
103
|
+
return data
|
|
104
|
+
|
|
105
|
+
def read(
|
|
106
|
+
self,
|
|
107
|
+
size: int = -1,
|
|
108
|
+
) -> bytes:
|
|
109
|
+
if size == 0:
|
|
110
|
+
return b''
|
|
111
|
+
return self.portal.call(self.take(size))
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def has_more(self) -> bool:
|
|
115
|
+
return bool(self.buffer) or not self.finished
|
|
116
|
+
|
|
117
|
+
def readline(
|
|
118
|
+
self,
|
|
119
|
+
limit: int = -1,
|
|
120
|
+
) -> bytes:
|
|
121
|
+
data = bytearray()
|
|
122
|
+
while limit < 0 or len(data) < limit:
|
|
123
|
+
part = self.read(1)
|
|
124
|
+
if not part:
|
|
125
|
+
break
|
|
126
|
+
data.extend(part)
|
|
127
|
+
if part == b'\n':
|
|
128
|
+
break
|
|
129
|
+
return bytes(data)
|
|
130
|
+
|
|
131
|
+
def readlines(
|
|
132
|
+
self,
|
|
133
|
+
hint: int = -1,
|
|
134
|
+
) -> list[bytes]:
|
|
135
|
+
lines = []
|
|
136
|
+
total = 0
|
|
137
|
+
while self.has_more:
|
|
138
|
+
line = self.readline()
|
|
139
|
+
if not line:
|
|
140
|
+
break
|
|
141
|
+
lines.append(line)
|
|
142
|
+
total += len(line)
|
|
143
|
+
if hint > 0 and total >= hint:
|
|
144
|
+
break
|
|
145
|
+
return lines
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class Middleware:
|
|
149
|
+
"""a2wsgi의 환경 변환을 사용하고 바이트 전송은 즉시 역압을 적용한다."""
|
|
150
|
+
|
|
151
|
+
def __init__(
|
|
152
|
+
self,
|
|
153
|
+
application: Application,
|
|
154
|
+
workers: int,
|
|
155
|
+
queue_size: int,
|
|
156
|
+
):
|
|
157
|
+
self.application = WSGIApplication(application)
|
|
158
|
+
self.executor = ThreadPoolExecutor(
|
|
159
|
+
max_workers=workers,
|
|
160
|
+
thread_name_prefix='DefEssential.Uvicorn',
|
|
161
|
+
)
|
|
162
|
+
self.queue_size = queue_size
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
async def __call__(
|
|
166
|
+
self,
|
|
167
|
+
scope: Scope,
|
|
168
|
+
receive: Receive,
|
|
169
|
+
send: Send,
|
|
170
|
+
) -> None:
|
|
171
|
+
if scope['type'] == 'lifespan':
|
|
172
|
+
while True:
|
|
173
|
+
message = await receive()
|
|
174
|
+
if message['type'] == 'lifespan.startup':
|
|
175
|
+
await send({'type': 'lifespan.startup.complete'})
|
|
176
|
+
elif message['type'] == 'lifespan.shutdown':
|
|
177
|
+
await send({'type': 'lifespan.shutdown.complete'})
|
|
178
|
+
return
|
|
179
|
+
else:
|
|
180
|
+
raise RuntimeError('Unexpected lifespan event')
|
|
181
|
+
if scope['type'] != 'http':
|
|
182
|
+
raise RuntimeError('WebSocket connections require the native protocol')
|
|
183
|
+
portal = Portal(get_running_loop())
|
|
184
|
+
body = Input(
|
|
185
|
+
receive,
|
|
186
|
+
portal,
|
|
187
|
+
self.queue_size,
|
|
188
|
+
)
|
|
189
|
+
environ = build_environ(
|
|
190
|
+
scope,
|
|
191
|
+
body,
|
|
192
|
+
)
|
|
193
|
+
pump = create_task(body.pump())
|
|
194
|
+
|
|
195
|
+
async def transmit(message) -> None:
|
|
196
|
+
if body.failure is not None:
|
|
197
|
+
raise body.failure
|
|
198
|
+
if body.disconnected:
|
|
199
|
+
raise ClientDisconnected('Client disconnected while sending response')
|
|
200
|
+
await send(message)
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
def write(data: bytes) -> None:
|
|
204
|
+
portal.call(transmit({
|
|
205
|
+
'type': 'http.response.body',
|
|
206
|
+
'body': data,
|
|
207
|
+
'more_body': True,
|
|
208
|
+
}))
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
def start_response(
|
|
212
|
+
status: str,
|
|
213
|
+
headers: list[tuple[str, str]],
|
|
214
|
+
):
|
|
215
|
+
portal.call(transmit({
|
|
216
|
+
'type': 'http.response.start',
|
|
217
|
+
'status': int(status.split(' ', 1)[0]),
|
|
218
|
+
'headers': [
|
|
219
|
+
(
|
|
220
|
+
key.lower().encode('latin-1'),
|
|
221
|
+
value.encode('latin-1'),
|
|
222
|
+
) for key, value in headers
|
|
223
|
+
],
|
|
224
|
+
}))
|
|
225
|
+
return write
|
|
226
|
+
|
|
227
|
+
def run() -> None:
|
|
228
|
+
for data in self.application(
|
|
229
|
+
environ,
|
|
230
|
+
start_response,
|
|
231
|
+
):
|
|
232
|
+
write(data)
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
worker = portal.loop.run_in_executor(
|
|
236
|
+
self.executor,
|
|
237
|
+
run,
|
|
238
|
+
)
|
|
239
|
+
try:
|
|
240
|
+
await shield(worker)
|
|
241
|
+
await transmit({
|
|
242
|
+
'type': 'http.response.body',
|
|
243
|
+
'body': b'',
|
|
244
|
+
'more_body': False,
|
|
245
|
+
})
|
|
246
|
+
except CancelledError:
|
|
247
|
+
portal.close()
|
|
248
|
+
with suppress(ClientDisconnected):
|
|
249
|
+
await worker
|
|
250
|
+
raise
|
|
251
|
+
finally:
|
|
252
|
+
portal.close()
|
|
253
|
+
pump.cancel()
|
|
254
|
+
with suppress(CancelledError):
|
|
255
|
+
await pump
|
|
256
|
+
return
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from asyncio import (
|
|
2
|
+
AbstractEventLoop,
|
|
3
|
+
run_coroutine_threadsafe,
|
|
4
|
+
)
|
|
5
|
+
from concurrent.futures import (
|
|
6
|
+
CancelledError,
|
|
7
|
+
Future,
|
|
8
|
+
)
|
|
9
|
+
from threading import Lock
|
|
10
|
+
from typing import (
|
|
11
|
+
Any,
|
|
12
|
+
Coroutine,
|
|
13
|
+
TypeVar,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from ....IO.RequestReader import ClientDisconnected
|
|
17
|
+
|
|
18
|
+
__all__ = (
|
|
19
|
+
'Portal',
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
Result = TypeVar('Result')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Portal:
|
|
27
|
+
"""동기 핸들러의 입출력을 이벤트 루프에 전달하고 취소 시 대기를 해제한다."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
loop: AbstractEventLoop,
|
|
32
|
+
):
|
|
33
|
+
self.loop = loop
|
|
34
|
+
self.lock = Lock()
|
|
35
|
+
self.pending: set[Future[Any]] = set()
|
|
36
|
+
self.closed = False
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
def call(
|
|
40
|
+
self,
|
|
41
|
+
coroutine: Coroutine[Any, Any, Result],
|
|
42
|
+
) -> Result:
|
|
43
|
+
with self.lock:
|
|
44
|
+
if self.closed:
|
|
45
|
+
coroutine.close()
|
|
46
|
+
raise ClientDisconnected('Connection is closed')
|
|
47
|
+
future = run_coroutine_threadsafe(
|
|
48
|
+
coroutine,
|
|
49
|
+
self.loop,
|
|
50
|
+
)
|
|
51
|
+
self.pending.add(future)
|
|
52
|
+
try:
|
|
53
|
+
return future.result()
|
|
54
|
+
except CancelledError as error:
|
|
55
|
+
raise ClientDisconnected('Connection was cancelled') from error
|
|
56
|
+
finally:
|
|
57
|
+
with self.lock:
|
|
58
|
+
self.pending.discard(future)
|
|
59
|
+
|
|
60
|
+
def close(self) -> None:
|
|
61
|
+
with self.lock:
|
|
62
|
+
self.closed = True
|
|
63
|
+
for future in self.pending:
|
|
64
|
+
future.cancel()
|
|
65
|
+
return
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from ....Application import Application
|
|
4
|
+
from ....Startup import startup
|
|
5
|
+
from ...Server import Server as ServerAdapter
|
|
6
|
+
|
|
7
|
+
__all__ = (
|
|
8
|
+
'Server',
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Server(ServerAdapter):
|
|
13
|
+
"""a2wsgi로 동기 애플리케이션을 Uvicorn에서 실행한다."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
host: str,
|
|
18
|
+
port: int,
|
|
19
|
+
application: Application,
|
|
20
|
+
*,
|
|
21
|
+
wsgi_workers: int = 10,
|
|
22
|
+
send_queue_size: int = 10,
|
|
23
|
+
**options: Any,
|
|
24
|
+
):
|
|
25
|
+
super().__init__(
|
|
26
|
+
host,
|
|
27
|
+
port,
|
|
28
|
+
application,
|
|
29
|
+
)
|
|
30
|
+
if wsgi_workers < 1 or send_queue_size < 1:
|
|
31
|
+
raise ValueError('wsgi_workers and send_queue_size must be at least 1')
|
|
32
|
+
if options.get('workers') not in (
|
|
33
|
+
None,
|
|
34
|
+
1,
|
|
35
|
+
) or options.get('reload', False):
|
|
36
|
+
raise ValueError('Application instances require workers=1 and reload=False')
|
|
37
|
+
if options.get('interface', 'asgi3') != 'asgi3':
|
|
38
|
+
raise ValueError('a2wsgi requires interface=asgi3')
|
|
39
|
+
if any(key == 'ws' or key.startswith('ws_') for key in options):
|
|
40
|
+
raise ValueError('The adapter manages WebSocket framing; configure limits and ping in WebSocket handlers')
|
|
41
|
+
self.wsgi_workers = wsgi_workers
|
|
42
|
+
self.send_queue_size = send_queue_size
|
|
43
|
+
self.options = options
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
def run(self) -> None:
|
|
47
|
+
# 기본 서버를 사용할 때 선택적 의존성을 가져오지 않는다.
|
|
48
|
+
from .HTTP import Middleware
|
|
49
|
+
from .WebSocket import WebSocketProtocol
|
|
50
|
+
from uvicorn import (
|
|
51
|
+
Config,
|
|
52
|
+
Server as UvicornServer,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
middleware = Middleware(
|
|
56
|
+
self.application,
|
|
57
|
+
workers=self.wsgi_workers,
|
|
58
|
+
queue_size=self.send_queue_size,
|
|
59
|
+
)
|
|
60
|
+
try:
|
|
61
|
+
application = self.application
|
|
62
|
+
|
|
63
|
+
class NativeWebSocketProtocol(WebSocketProtocol):
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
config,
|
|
67
|
+
server_state,
|
|
68
|
+
app_state,
|
|
69
|
+
):
|
|
70
|
+
super().__init__(
|
|
71
|
+
config,
|
|
72
|
+
server_state,
|
|
73
|
+
app_state,
|
|
74
|
+
application,
|
|
75
|
+
middleware.executor,
|
|
76
|
+
)
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
options = dict(self.options)
|
|
80
|
+
options['workers'] = 1
|
|
81
|
+
options['interface'] = 'asgi3'
|
|
82
|
+
server = UvicornServer(Config(
|
|
83
|
+
middleware,
|
|
84
|
+
host=self.host,
|
|
85
|
+
port=self.port,
|
|
86
|
+
ws=NativeWebSocketProtocol,
|
|
87
|
+
**options,
|
|
88
|
+
))
|
|
89
|
+
startup(
|
|
90
|
+
self.application,
|
|
91
|
+
'Uvicorn',
|
|
92
|
+
self.host,
|
|
93
|
+
self.port,
|
|
94
|
+
)
|
|
95
|
+
self.application.start()
|
|
96
|
+
try:
|
|
97
|
+
server.run()
|
|
98
|
+
finally:
|
|
99
|
+
self.application.stop()
|
|
100
|
+
finally:
|
|
101
|
+
middleware.executor.shutdown(wait=True)
|
|
102
|
+
return
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from asyncio import (
|
|
2
|
+
CancelledError,
|
|
3
|
+
Event,
|
|
4
|
+
Protocol,
|
|
5
|
+
StreamReader,
|
|
6
|
+
Transport,
|
|
7
|
+
get_running_loop,
|
|
8
|
+
shield,
|
|
9
|
+
)
|
|
10
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
11
|
+
from contextlib import suppress
|
|
12
|
+
from logging import getLogger
|
|
13
|
+
from socket import socket
|
|
14
|
+
from socketserver import BaseServer
|
|
15
|
+
from typing import (
|
|
16
|
+
Any,
|
|
17
|
+
cast,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from ....Application import Application
|
|
21
|
+
from ....IO.RequestReader import ClientDisconnected
|
|
22
|
+
from ....IO.WebSocket.WebSocketFrame import WebSocketFrame
|
|
23
|
+
from ..Python.Server import ServerRequestHandler
|
|
24
|
+
from .Portal import Portal
|
|
25
|
+
|
|
26
|
+
__all__ = (
|
|
27
|
+
'ServerContext',
|
|
28
|
+
'Connection',
|
|
29
|
+
'WebSocketProtocol',
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ServerContext(BaseServer):
|
|
34
|
+
app: Application
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Connection:
|
|
38
|
+
"""기존 HTTP/WebSocket 처리기에 asyncio 연결을 소켓 형태로 제공한다."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
protocol: 'WebSocketProtocol',
|
|
43
|
+
):
|
|
44
|
+
self.protocol = protocol
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
def makefile(
|
|
48
|
+
self,
|
|
49
|
+
mode: str,
|
|
50
|
+
buffering: int = -1,
|
|
51
|
+
) -> 'Connection':
|
|
52
|
+
if mode != 'rb' or buffering not in (
|
|
53
|
+
-1,
|
|
54
|
+
0,
|
|
55
|
+
):
|
|
56
|
+
raise ValueError('Only a binary input file is supported')
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def read(
|
|
60
|
+
self,
|
|
61
|
+
size: int = -1,
|
|
62
|
+
/,
|
|
63
|
+
) -> bytes:
|
|
64
|
+
return self.protocol.portal.call(self.protocol.reader.read(size))
|
|
65
|
+
|
|
66
|
+
def readline(
|
|
67
|
+
self,
|
|
68
|
+
size: int = -1,
|
|
69
|
+
/,
|
|
70
|
+
) -> bytes:
|
|
71
|
+
return self.protocol.portal.call(self.protocol.readline(size))
|
|
72
|
+
|
|
73
|
+
def sendall(
|
|
74
|
+
self,
|
|
75
|
+
data: bytes,
|
|
76
|
+
) -> None:
|
|
77
|
+
self.protocol.portal.call(self.protocol.write(data))
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
def close(self) -> None:
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class WebSocketProtocol(Protocol):
|
|
85
|
+
"""Uvicorn의 upgrade 연결에서 기존 RFC 6455 처리기를 그대로 실행한다."""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
config: Any,
|
|
90
|
+
server_state: Any,
|
|
91
|
+
app_state: dict[str, Any],
|
|
92
|
+
application: Application,
|
|
93
|
+
executor: ThreadPoolExecutor,
|
|
94
|
+
):
|
|
95
|
+
self.application = application
|
|
96
|
+
self.executor = executor
|
|
97
|
+
self.state = server_state
|
|
98
|
+
self.portal = Portal(get_running_loop())
|
|
99
|
+
self.reader = StreamReader(limit=65536)
|
|
100
|
+
self.writable = Event()
|
|
101
|
+
self.writable.set()
|
|
102
|
+
self.transport: Transport
|
|
103
|
+
self.task = None
|
|
104
|
+
self.upgraded = False
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
def connection_made(
|
|
108
|
+
self,
|
|
109
|
+
transport,
|
|
110
|
+
) -> None:
|
|
111
|
+
if not isinstance(transport, Transport):
|
|
112
|
+
raise TypeError('A stream transport is required')
|
|
113
|
+
self.transport = transport
|
|
114
|
+
self.reader.set_transport(transport)
|
|
115
|
+
self.state.connections.add(self)
|
|
116
|
+
self.task = self.portal.loop.create_task(self.run())
|
|
117
|
+
self.state.tasks.add(self.task)
|
|
118
|
+
self.task.add_done_callback(self.state.tasks.discard)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
def data_received(
|
|
122
|
+
self,
|
|
123
|
+
data: bytes,
|
|
124
|
+
) -> None:
|
|
125
|
+
self.reader.feed_data(data)
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
def eof_received(self) -> None:
|
|
129
|
+
self.reader.feed_eof()
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
def connection_lost(
|
|
133
|
+
self,
|
|
134
|
+
error,
|
|
135
|
+
) -> None:
|
|
136
|
+
self.state.connections.discard(self)
|
|
137
|
+
if error is None:
|
|
138
|
+
self.reader.feed_eof()
|
|
139
|
+
else:
|
|
140
|
+
self.reader.set_exception(error)
|
|
141
|
+
self.writable.set()
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
async def readline(
|
|
145
|
+
self,
|
|
146
|
+
size: int,
|
|
147
|
+
) -> bytes:
|
|
148
|
+
data = bytearray()
|
|
149
|
+
while size < 0 or len(data) < size:
|
|
150
|
+
part = await self.reader.read(1)
|
|
151
|
+
if not part:
|
|
152
|
+
break
|
|
153
|
+
data.extend(part)
|
|
154
|
+
if part == b'\n':
|
|
155
|
+
break
|
|
156
|
+
return bytes(data)
|
|
157
|
+
|
|
158
|
+
def pause_writing(self) -> None:
|
|
159
|
+
self.writable.clear()
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
def resume_writing(self) -> None:
|
|
163
|
+
self.writable.set()
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
async def write(
|
|
167
|
+
self,
|
|
168
|
+
data: bytes,
|
|
169
|
+
) -> None:
|
|
170
|
+
await self.writable.wait()
|
|
171
|
+
if self.transport.is_closing():
|
|
172
|
+
raise ClientDisconnected('WebSocket connection is closed')
|
|
173
|
+
self.transport.write(data)
|
|
174
|
+
if data.startswith(b'HTTP/1.1 101 '):
|
|
175
|
+
self.upgraded = True
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
def shutdown(self) -> None:
|
|
179
|
+
if self.upgraded and not self.transport.is_closing():
|
|
180
|
+
self.transport.write(WebSocketFrame(
|
|
181
|
+
WebSocketFrame.CLOSE,
|
|
182
|
+
b'\x03\xe9',
|
|
183
|
+
).encode())
|
|
184
|
+
self.transport.close()
|
|
185
|
+
self.reader.feed_eof()
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
async def run(self) -> None:
|
|
189
|
+
def handle() -> None:
|
|
190
|
+
server = ServerContext(
|
|
191
|
+
self.transport.get_extra_info('sockname'),
|
|
192
|
+
ServerRequestHandler,
|
|
193
|
+
)
|
|
194
|
+
# 표준 처리기는 socket의 명목 타입을 요구하지만 실제로는 아래 파일/송신 계약만 사용한다.
|
|
195
|
+
connection = Connection(self)
|
|
196
|
+
server.app = self.application
|
|
197
|
+
ServerRequestHandler(
|
|
198
|
+
cast(socket, connection),
|
|
199
|
+
self.transport.get_extra_info('peername'),
|
|
200
|
+
server,
|
|
201
|
+
)
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
worker = self.portal.loop.run_in_executor(
|
|
205
|
+
self.executor,
|
|
206
|
+
handle,
|
|
207
|
+
)
|
|
208
|
+
try:
|
|
209
|
+
await shield(worker)
|
|
210
|
+
except CancelledError:
|
|
211
|
+
self.portal.close()
|
|
212
|
+
with suppress(ClientDisconnected):
|
|
213
|
+
await worker
|
|
214
|
+
raise
|
|
215
|
+
except Exception:
|
|
216
|
+
getLogger('DefEssential.Server').exception('WebSocket connection handling failed')
|
|
217
|
+
finally:
|
|
218
|
+
self.portal.close()
|
|
219
|
+
self.transport.close()
|
|
220
|
+
return
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
from ..Application import Application
|
|
5
|
+
from ..Error import Error
|
|
6
|
+
from ..IO.RequestReader import ClientDisconnected
|
|
7
|
+
from ..IO.ResponseWriter import ResponseWriter
|
|
8
|
+
from ..Request import Request
|
|
9
|
+
from ..Response import Response
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
'handle_request',
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def handle_request(
|
|
17
|
+
application: Application,
|
|
18
|
+
create_request: Callable[[], Request],
|
|
19
|
+
writer: ResponseWriter,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""전송 계층과 무관하게 오류 응답과 자원 정리를 수행한다."""
|
|
22
|
+
logger = getLogger('DefEssential.Server')
|
|
23
|
+
response = None
|
|
24
|
+
try:
|
|
25
|
+
try:
|
|
26
|
+
request = create_request()
|
|
27
|
+
response = application(request)
|
|
28
|
+
except ClientDisconnected:
|
|
29
|
+
raise
|
|
30
|
+
except Error as error:
|
|
31
|
+
if writer.started:
|
|
32
|
+
raise
|
|
33
|
+
response = Response(
|
|
34
|
+
status=error.status,
|
|
35
|
+
headers=error.headers,
|
|
36
|
+
body=error.reason.encode('utf-8'),
|
|
37
|
+
format='text/plain',
|
|
38
|
+
charset='utf-8',
|
|
39
|
+
)
|
|
40
|
+
except Exception:
|
|
41
|
+
if writer.started:
|
|
42
|
+
raise
|
|
43
|
+
logger.exception('Unexpected error while handling request')
|
|
44
|
+
response = Response(
|
|
45
|
+
status=500,
|
|
46
|
+
body=b'Internal Server Error',
|
|
47
|
+
)
|
|
48
|
+
if response is not None:
|
|
49
|
+
writer.response(
|
|
50
|
+
response,
|
|
51
|
+
filtered=True,
|
|
52
|
+
)
|
|
53
|
+
elif not writer.ended:
|
|
54
|
+
raise RuntimeError('Response was neither returned nor completed successfully')
|
|
55
|
+
except ConnectionError as error:
|
|
56
|
+
logger.warning('Client disconnected: %s', error)
|
|
57
|
+
raise
|
|
58
|
+
except OSError:
|
|
59
|
+
logger.warning('Connection closed while sending response', exc_info=True)
|
|
60
|
+
raise
|
|
61
|
+
except Exception:
|
|
62
|
+
logger.exception('Response transmission failed: closing connection without another error response')
|
|
63
|
+
raise
|
|
64
|
+
finally:
|
|
65
|
+
if response is not None:
|
|
66
|
+
try:
|
|
67
|
+
response.close()
|
|
68
|
+
except Exception:
|
|
69
|
+
logger.exception('Response body cleanup failed')
|
|
70
|
+
return
|