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,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,137 @@
1
+ from logging import getLogger
2
+ from socket import socket
3
+ from typing import (
4
+ Any,
5
+ Optional,
6
+ )
7
+
8
+ from ....Application import Application
9
+ from ....Startup import startup
10
+ from ...Server import Server as ServerAdapter
11
+
12
+ __all__ = (
13
+ 'Server',
14
+ )
15
+
16
+
17
+ class Server(ServerAdapter):
18
+ """a2wsgi로 동기 애플리케이션을 Uvicorn에서 실행한다."""
19
+
20
+ logger = getLogger('DefEssential.Server')
21
+
22
+ def __init__(
23
+ self,
24
+ host: str,
25
+ port: int,
26
+ application: Application,
27
+ *,
28
+ wsgi_workers: int = 10,
29
+ send_queue_size: int = 10,
30
+ **options: Any,
31
+ ):
32
+ super().__init__(
33
+ host,
34
+ port,
35
+ application,
36
+ )
37
+ if wsgi_workers < 1 or send_queue_size < 1:
38
+ raise ValueError('wsgi_workers and send_queue_size must be at least 1')
39
+ if options.get('workers') not in (
40
+ None,
41
+ 1,
42
+ ) or options.get('reload', False):
43
+ raise ValueError('Application instances require workers=1 and reload=False')
44
+ if options.get('interface', 'asgi3') != 'asgi3':
45
+ raise ValueError('a2wsgi requires interface=asgi3')
46
+ if any(key == 'ws' or key.startswith('ws_') for key in options):
47
+ raise ValueError('The adapter manages WebSocket framing; configure limits and ping in WebSocket handlers')
48
+ self.wsgi_workers = wsgi_workers
49
+ self.send_queue_size = send_queue_size
50
+ self.options = options
51
+ return
52
+
53
+ def run(self) -> None:
54
+ # 기본 서버를 사용할 때 선택적 의존성을 가져오지 않는다.
55
+ from .HTTP import Middleware
56
+ from .WebSocket import WebSocketProtocol
57
+ from uvicorn import (
58
+ Config,
59
+ Server as UvicornServer,
60
+ )
61
+
62
+ middleware = Middleware(
63
+ self.application,
64
+ workers=self.wsgi_workers,
65
+ queue_size=self.send_queue_size,
66
+ )
67
+ try:
68
+ application = self.application
69
+
70
+ class NativeWebSocketProtocol(WebSocketProtocol):
71
+ def __init__(
72
+ self,
73
+ config,
74
+ server_state,
75
+ app_state,
76
+ ):
77
+ super().__init__(
78
+ config,
79
+ server_state,
80
+ app_state,
81
+ application,
82
+ middleware.executor,
83
+ )
84
+ return
85
+
86
+ options = dict(self.options)
87
+ options['workers'] = 1
88
+ options['interface'] = 'asgi3'
89
+ logger = self.logger
90
+
91
+ class NativeServer(UvicornServer):
92
+ async def startup(
93
+ self,
94
+ sockets: Optional[list[socket]] = None,
95
+ ) -> None:
96
+ try:
97
+ await super().startup(sockets=sockets)
98
+ except SystemExit:
99
+ logger.error('Server startup aborted: adapter=Uvicorn', exc_info=True)
100
+ raise
101
+ if self.started:
102
+ for listener in self.servers:
103
+ for bound_socket in listener.sockets or ():
104
+ logger.info(
105
+ 'Server ready: adapter=Uvicorn address=%s',
106
+ bound_socket.getsockname(),
107
+ )
108
+ return
109
+
110
+ server = NativeServer(Config(
111
+ middleware,
112
+ host=self.host,
113
+ port=self.port,
114
+ ws=NativeWebSocketProtocol,
115
+ **options,
116
+ ))
117
+ startup(
118
+ self.application,
119
+ 'Uvicorn',
120
+ self.host,
121
+ self.port,
122
+ )
123
+ self.application.start()
124
+ try:
125
+ self.logger.info('Server starting: adapter=Uvicorn host=%s port=%s', self.host, self.port)
126
+ try:
127
+ server.run()
128
+ except Exception:
129
+ self.logger.exception('Server execution failed: adapter=Uvicorn')
130
+ raise
131
+ finally:
132
+ self.logger.info('Server stopping: adapter=Uvicorn')
133
+ self.application.stop()
134
+ self.logger.info('Server stopped: adapter=Uvicorn')
135
+ finally:
136
+ middleware.executor.shutdown(wait=True)
137
+ return
@@ -0,0 +1,222 @@
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
+ logger = getLogger('DefEssential.Server')
88
+
89
+ def __init__(
90
+ self,
91
+ config: Any,
92
+ server_state: Any,
93
+ app_state: dict[str, Any],
94
+ application: Application,
95
+ executor: ThreadPoolExecutor,
96
+ ):
97
+ self.application = application
98
+ self.executor = executor
99
+ self.state = server_state
100
+ self.portal = Portal(get_running_loop())
101
+ self.reader = StreamReader(limit=65536)
102
+ self.writable = Event()
103
+ self.writable.set()
104
+ self.transport: Transport
105
+ self.task = None
106
+ self.upgraded = False
107
+ return
108
+
109
+ def connection_made(
110
+ self,
111
+ transport,
112
+ ) -> None:
113
+ if not isinstance(transport, Transport):
114
+ raise TypeError('A stream transport is required')
115
+ self.transport = transport
116
+ self.reader.set_transport(transport)
117
+ self.state.connections.add(self)
118
+ self.task = self.portal.loop.create_task(self.run())
119
+ self.state.tasks.add(self.task)
120
+ self.task.add_done_callback(self.state.tasks.discard)
121
+ return
122
+
123
+ def data_received(
124
+ self,
125
+ data: bytes,
126
+ ) -> None:
127
+ self.reader.feed_data(data)
128
+ return
129
+
130
+ def eof_received(self) -> None:
131
+ self.reader.feed_eof()
132
+ return
133
+
134
+ def connection_lost(
135
+ self,
136
+ error,
137
+ ) -> None:
138
+ self.state.connections.discard(self)
139
+ if error is None:
140
+ self.reader.feed_eof()
141
+ else:
142
+ self.reader.set_exception(error)
143
+ self.writable.set()
144
+ return
145
+
146
+ async def readline(
147
+ self,
148
+ size: int,
149
+ ) -> bytes:
150
+ data = bytearray()
151
+ while size < 0 or len(data) < size:
152
+ part = await self.reader.read(1)
153
+ if not part:
154
+ break
155
+ data.extend(part)
156
+ if part == b'\n':
157
+ break
158
+ return bytes(data)
159
+
160
+ def pause_writing(self) -> None:
161
+ self.writable.clear()
162
+ return
163
+
164
+ def resume_writing(self) -> None:
165
+ self.writable.set()
166
+ return
167
+
168
+ async def write(
169
+ self,
170
+ data: bytes,
171
+ ) -> None:
172
+ await self.writable.wait()
173
+ if self.transport.is_closing():
174
+ raise ClientDisconnected('WebSocket connection is closed')
175
+ self.transport.write(data)
176
+ if data.startswith(b'HTTP/1.1 101 '):
177
+ self.upgraded = True
178
+ return
179
+
180
+ def shutdown(self) -> None:
181
+ if self.upgraded and not self.transport.is_closing():
182
+ self.transport.write(WebSocketFrame(
183
+ WebSocketFrame.CLOSE,
184
+ b'\x03\xe9',
185
+ ).encode())
186
+ self.transport.close()
187
+ self.reader.feed_eof()
188
+ return
189
+
190
+ async def run(self) -> None:
191
+ def handle() -> None:
192
+ server = ServerContext(
193
+ self.transport.get_extra_info('sockname'),
194
+ ServerRequestHandler,
195
+ )
196
+ # 표준 처리기는 socket의 명목 타입을 요구하지만 실제로는 아래 파일/송신 계약만 사용한다.
197
+ connection = Connection(self)
198
+ server.app = self.application
199
+ ServerRequestHandler(
200
+ cast(socket, connection),
201
+ self.transport.get_extra_info('peername'),
202
+ server,
203
+ )
204
+ return
205
+
206
+ worker = self.portal.loop.run_in_executor(
207
+ self.executor,
208
+ handle,
209
+ )
210
+ try:
211
+ await shield(worker)
212
+ except CancelledError:
213
+ self.portal.close()
214
+ with suppress(ClientDisconnected):
215
+ await worker
216
+ raise
217
+ except Exception:
218
+ self.logger.exception('WebSocket connection handling failed: adapter=Uvicorn')
219
+ finally:
220
+ self.portal.close()
221
+ self.transport.close()
222
+ return
@@ -0,0 +1,5 @@
1
+ from .Server import Server
2
+
3
+ __all__ = (
4
+ 'Server',
5
+ )
@@ -0,0 +1,3 @@
1
+ """필요한 서버 구현을 각 하위 패키지에서 선택해 가져온다."""
2
+
3
+ __all__ = ()