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.
Files changed (104) hide show
  1. DefEssential/Adapter/Implements/Gunicorn/Server.py +134 -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 +245 -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 +102 -0
  9. DefEssential/Adapter/Implements/Uvicorn/WebSocket.py +220 -0
  10. DefEssential/Adapter/Implements/Uvicorn/__init__.py +5 -0
  11. DefEssential/Adapter/Implements/__init__.py +3 -0
  12. DefEssential/Adapter/RequestHandler.py +70 -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 +579 -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 +184 -0
  27. DefEssential/Extensions/File.py +89 -0
  28. DefEssential/Extensions/FileSystem.py +184 -0
  29. DefEssential/Extensions/Metrics.py +500 -0
  30. DefEssential/Extensions/Ping.py +89 -0
  31. DefEssential/Extensions/Specifications.py +328 -0
  32. DefEssential/Extensions/Status.html +207 -0
  33. DefEssential/Extensions/Status.py +104 -0
  34. DefEssential/Extensions/Swagger.html +77 -0
  35. DefEssential/Extensions/Swagger.py +111 -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 +282 -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 +87 -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 +101 -0
  66. DefEssential/Runners/RunFactory.py +26 -0
  67. DefEssential/Runners/RunOptions.py +77 -0
  68. DefEssential/Runners/RunRequest.py +121 -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.6.dist-info/METADATA +41 -0
  101. defessential-0.1.6.dist-info/RECORD +104 -0
  102. defessential-0.1.6.dist-info/WHEEL +6 -0
  103. defessential-0.1.6.dist-info/licenses/LICENSE +674 -0
  104. 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,5 @@
1
+ from .Server import Server
2
+
3
+ __all__ = (
4
+ 'Server',
5
+ )
@@ -0,0 +1,3 @@
1
+ """필요한 서버 구현을 각 하위 패키지에서 선택해 가져온다."""
2
+
3
+ __all__ = ()
@@ -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