DefEssential 0.1.8__25-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 +511 -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 +242 -0
- DefEssential/IO/WebSocket/WebSocket.py +288 -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.8.dist-info/METADATA +86 -0
- defessential-0.1.8.dist-info/RECORD +104 -0
- defessential-0.1.8.dist-info/WHEEL +6 -0
- defessential-0.1.8.dist-info/licenses/LICENSE +674 -0
- defessential-0.1.8.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
|
+
"""Read ASGI request bodies without waiting for the full upload and monitor disconnects."""
|
|
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
|
+
"""Use a2wsgi for environment conversion and apply backpressure during byte transmission."""
|
|
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
|
+
"""Bridge synchronous handler I/O to the event loop and release waits on cancellation."""
|
|
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
|
+
"""Run a synchronous application on Uvicorn through a2wsgi."""
|
|
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
|
+
# Do not import optional dependencies when using the built-in server.
|
|
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
|
+
"""Expose an asyncio connection as a socket to the existing HTTP/WebSocket handler."""
|
|
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
|
+
"""Run the existing RFC 6455 handler on Uvicorn's upgraded connection."""
|
|
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
|
+
# The standard handler requires the nominal socket type but uses only the file and send contracts below.
|
|
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
|