DefEssential 0.1.8__27-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 +511 -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 +242 -0
  45. DefEssential/IO/WebSocket/WebSocket.py +288 -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.8.dist-info/METADATA +86 -0
  101. defessential-0.1.8.dist-info/RECORD +104 -0
  102. defessential-0.1.8.dist-info/WHEEL +6 -0
  103. defessential-0.1.8.dist-info/licenses/LICENSE +674 -0
  104. 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
@@ -0,0 +1,5 @@
1
+ from .Server import Server
2
+
3
+ __all__ = (
4
+ 'Server',
5
+ )
@@ -0,0 +1,3 @@
1
+ """Import the required server implementations from their respective subpackages."""
2
+
3
+ __all__ = ()