DefEssential 0.1.8__23-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 +82 -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,288 @@
1
+ from logging import getLogger
2
+ from struct import (
3
+ pack,
4
+ unpack,
5
+ )
6
+ from threading import Lock
7
+ from typing import (
8
+ Optional,
9
+ Union,
10
+ )
11
+
12
+ from ...Request import Request
13
+ from .WebSocketError import WebSocketError
14
+ from .WebSocketFrame import WebSocketFrame
15
+ from .WebSocketFrameReader import WebSocketFrameReader
16
+ from .WebSocketHandshake import WebSocketHandshake
17
+
18
+ __all__ = (
19
+ 'WebSocket',
20
+ )
21
+
22
+
23
+ class WebSocket:
24
+ """Exchange WebSocket messages over an upgraded HTTP connection.
25
+
26
+ Call receive() from a single thread. send(), ping(), and close() may also be
27
+ called from other threads.
28
+ """
29
+
30
+ logger = getLogger('DefEssential.IO.WebSocket')
31
+
32
+ def __init__(
33
+ self,
34
+ request: Request,
35
+ limit: int,
36
+ ):
37
+ if limit <= 0:
38
+ raise ValueError('Message size limit must be positive')
39
+ self.request = request
40
+ self.limit = limit
41
+ self.handshake = WebSocketHandshake(request)
42
+ self.frames = WebSocketFrameReader(
43
+ request.reader,
44
+ limit,
45
+ )
46
+ self.lock = Lock()
47
+ self.accepted = False
48
+ self.close_sent = False
49
+ self.closed = False
50
+ self.code: Optional[int] = None
51
+ self.reason = ''
52
+ return
53
+
54
+ @property
55
+ def protocols(self) -> list[str]:
56
+ return self.handshake.protocols
57
+
58
+ def accept(
59
+ self,
60
+ protocol: Optional[str] = None,
61
+ ) -> None:
62
+ """Validate the handshake and send a 101 response. On failure, a regular error response is sent because the connection has not been upgraded."""
63
+ if self.accepted:
64
+ raise RuntimeError('WebSocket has already been accepted')
65
+ self.request.writer.upgrade(self.handshake.response(protocol))
66
+ self.accepted = True
67
+ self.logger.debug('WebSocket upgraded: request_id=%s status=101', self.request.id)
68
+ return
69
+
70
+ def valid_code(
71
+ self,
72
+ code: int,
73
+ ) -> bool:
74
+ return code in (
75
+ 1000,
76
+ 1001,
77
+ 1002,
78
+ 1003,
79
+ 1007,
80
+ 1008,
81
+ 1009,
82
+ 1010,
83
+ 1011,
84
+ 1012,
85
+ 1013,
86
+ 1014,
87
+ ) or 3000 <= code <= 4999
88
+
89
+ def write(
90
+ self,
91
+ frame: WebSocketFrame,
92
+ ) -> None:
93
+ with self.lock:
94
+ if not self.accepted:
95
+ raise RuntimeError('WebSocket must be accepted first')
96
+ if self.close_sent:
97
+ raise RuntimeError('Cannot send after the WebSocket close frame')
98
+ if frame.opcode == WebSocketFrame.CLOSE:
99
+ self.close_sent = True
100
+ self.request.writer.write(frame.encode())
101
+ self.request.writer.flush()
102
+ return
103
+
104
+ def send(
105
+ self,
106
+ message: Union[str, bytes],
107
+ ) -> None:
108
+ """Send str values as text messages and bytes values as binary messages."""
109
+ if isinstance(message, str):
110
+ frame = WebSocketFrame(
111
+ WebSocketFrame.TEXT,
112
+ message.encode('utf-8'),
113
+ )
114
+ elif isinstance(message, bytes):
115
+ frame = WebSocketFrame(
116
+ WebSocketFrame.BINARY,
117
+ message,
118
+ )
119
+ else:
120
+ raise TypeError('WebSocket messages must be str or bytes')
121
+ self.write(frame)
122
+ return
123
+
124
+ def ping(
125
+ self,
126
+ data: bytes = b'',
127
+ ) -> None:
128
+ self.write(WebSocketFrame(
129
+ WebSocketFrame.PING,
130
+ data,
131
+ ))
132
+ return
133
+
134
+ def close_frame(
135
+ self,
136
+ code: int,
137
+ reason: str,
138
+ ) -> WebSocketFrame:
139
+ return WebSocketFrame(
140
+ WebSocketFrame.CLOSE,
141
+ pack('!H', code) + reason.encode('utf-8'),
142
+ )
143
+
144
+ def receive_close(
145
+ self,
146
+ frame: WebSocketFrame,
147
+ ) -> None:
148
+ payload = frame.payload
149
+ if len(payload) == 1:
150
+ raise WebSocketError(
151
+ 1002,
152
+ 'Invalid close frame',
153
+ )
154
+ code = unpack('!H', payload[:2])[0] if payload else None
155
+ if code is not None and not self.valid_code(code):
156
+ raise WebSocketError(
157
+ 1002,
158
+ 'Invalid close code',
159
+ )
160
+ try:
161
+ reason = payload[2:].decode('utf-8')
162
+ except UnicodeDecodeError as error:
163
+ raise WebSocketError(
164
+ 1007,
165
+ 'Invalid close reason',
166
+ ) from error
167
+ self.code = code
168
+ self.reason = reason
169
+ self.closed = True
170
+ with self.lock:
171
+ echo = not self.close_sent
172
+ if echo:
173
+ self.write(WebSocketFrame(
174
+ WebSocketFrame.CLOSE,
175
+ payload[:2],
176
+ ))
177
+ self.logger.debug('WebSocket closed: request_id=%s code=%s', self.request.id, self.code)
178
+ return
179
+
180
+ def fail(
181
+ self,
182
+ error: WebSocketError,
183
+ ) -> None:
184
+ self.logger.warning(
185
+ 'WebSocket protocol error: request_id=%s code=%s',
186
+ self.request.id,
187
+ error.code,
188
+ )
189
+ self.code = error.code
190
+ self.reason = error.reason
191
+ self.closed = True
192
+ with self.lock:
193
+ send = not self.close_sent
194
+ if send:
195
+ self.write(self.close_frame(
196
+ error.code,
197
+ error.reason,
198
+ ))
199
+ self.logger.debug('WebSocket failed: request_id=%s code=%s', self.request.id, error.code)
200
+ return
201
+
202
+ def receive(self) -> Union[str, bytes, None]:
203
+ """Return the next message, or None once the connection starts closing.
204
+
205
+ Automatically reply to ping frames with pong. On a protocol violation,
206
+ send a close code and return None. Propagate ClientDisconnected if the
207
+ TCP connection is lost.
208
+ """
209
+ if not self.accepted:
210
+ raise RuntimeError('WebSocket must be accepted first')
211
+ if self.closed:
212
+ return None
213
+ opcode: Optional[int] = None
214
+ parts = []
215
+ size = 0
216
+ try:
217
+ while True:
218
+ frame = self.frames.read()
219
+ if frame.opcode == WebSocketFrame.CLOSE:
220
+ self.receive_close(frame)
221
+ return None
222
+ if frame.opcode == WebSocketFrame.PING:
223
+ with self.lock:
224
+ reply = not self.close_sent
225
+ if reply:
226
+ self.write(WebSocketFrame(
227
+ WebSocketFrame.PONG,
228
+ frame.payload,
229
+ ))
230
+ continue
231
+ if frame.opcode == WebSocketFrame.PONG:
232
+ continue
233
+ if frame.opcode == WebSocketFrame.CONTINUATION:
234
+ if opcode is None:
235
+ raise WebSocketError(
236
+ 1002,
237
+ 'Unexpected continuation frame',
238
+ )
239
+ elif opcode is not None:
240
+ raise WebSocketError(
241
+ 1002,
242
+ 'Expected a continuation frame',
243
+ )
244
+ else:
245
+ opcode = frame.opcode
246
+ size += len(frame.payload)
247
+ if size > self.limit:
248
+ raise WebSocketError(
249
+ 1009,
250
+ 'Message too big',
251
+ )
252
+ parts.append(frame.payload)
253
+ if frame.final:
254
+ break
255
+ data = b''.join(parts)
256
+ if opcode == WebSocketFrame.BINARY:
257
+ return data
258
+ try:
259
+ return data.decode('utf-8')
260
+ except UnicodeDecodeError as error:
261
+ raise WebSocketError(
262
+ 1007,
263
+ 'Invalid UTF-8 text',
264
+ ) from error
265
+ except WebSocketError as error:
266
+ self.fail(error)
267
+ return None
268
+
269
+ def close(
270
+ self,
271
+ code: int = 1000,
272
+ reason: str = '',
273
+ ) -> None:
274
+ """Send a close frame and discard messages until the client's close frame arrives."""
275
+ if not self.valid_code(code):
276
+ raise ValueError('Invalid WebSocket close code')
277
+ if len(reason.encode('utf-8')) > 123:
278
+ raise ValueError('WebSocket close reason must be at most 123 bytes')
279
+ with self.lock:
280
+ send = not self.close_sent
281
+ if send:
282
+ self.write(self.close_frame(
283
+ code,
284
+ reason,
285
+ ))
286
+ while not self.closed:
287
+ self.receive()
288
+ return
@@ -0,0 +1,17 @@
1
+ __all__ = (
2
+ 'WebSocketError',
3
+ )
4
+
5
+
6
+ class WebSocketError(Exception):
7
+ """Represent a client WebSocket protocol violation with a close code."""
8
+
9
+ def __init__(
10
+ self,
11
+ code: int,
12
+ reason: str,
13
+ ):
14
+ super().__init__(reason)
15
+ self.code = code
16
+ self.reason = reason
17
+ return
@@ -0,0 +1,68 @@
1
+ from struct import pack
2
+
3
+ __all__ = (
4
+ 'WebSocketFrame',
5
+ )
6
+
7
+
8
+ class WebSocketFrame:
9
+ """Represent a WebSocket frame and encode it as unmasked bytes for server transmission."""
10
+
11
+ CONTINUATION = 0x0
12
+ TEXT = 0x1
13
+ BINARY = 0x2
14
+ CLOSE = 0x8
15
+ PING = 0x9
16
+ PONG = 0xA
17
+ OPCODES = (
18
+ CONTINUATION,
19
+ TEXT,
20
+ BINARY,
21
+ CLOSE,
22
+ PING,
23
+ PONG,
24
+ )
25
+
26
+ def __init__(
27
+ self,
28
+ opcode: int,
29
+ payload: bytes = b'',
30
+ final: bool = True,
31
+ ):
32
+ if opcode not in self.OPCODES:
33
+ raise ValueError('Unknown WebSocket opcode')
34
+ if opcode >= self.CLOSE and (not final or len(payload) > 125):
35
+ raise ValueError('Control frames must be final and at most 125 bytes')
36
+ self.opcode = opcode
37
+ self.payload = payload
38
+ self.final = final
39
+ return
40
+
41
+ @property
42
+ def control(self) -> bool:
43
+ return self.opcode >= self.CLOSE
44
+
45
+ def encode(self) -> bytes:
46
+ first = (0x80 if self.final else 0) | self.opcode
47
+ size = len(self.payload)
48
+ if size < 126:
49
+ header = pack(
50
+ '!BB',
51
+ first,
52
+ size,
53
+ )
54
+ elif size < 65536:
55
+ header = pack(
56
+ '!BBH',
57
+ first,
58
+ 126,
59
+ size,
60
+ )
61
+ else:
62
+ header = pack(
63
+ '!BBQ',
64
+ first,
65
+ 127,
66
+ size,
67
+ )
68
+ return header + self.payload
@@ -0,0 +1,98 @@
1
+ from struct import unpack
2
+
3
+ from ...Error import ClientError
4
+ from ..RequestReader import (
5
+ ClientDisconnected,
6
+ RequestReader,
7
+ )
8
+ from .WebSocketError import WebSocketError
9
+ from .WebSocketFrame import WebSocketFrame
10
+
11
+ __all__ = (
12
+ 'WebSocketFrameReader',
13
+ )
14
+
15
+
16
+ class WebSocketFrameReader:
17
+ """Read client frames one at a time from an upgraded connection and validate frame rules."""
18
+
19
+ def __init__(
20
+ self,
21
+ reader: RequestReader,
22
+ limit: int,
23
+ ):
24
+ self.reader = reader
25
+ self.limit = limit
26
+ return
27
+
28
+ def exact(
29
+ self,
30
+ size: int,
31
+ ) -> bytes:
32
+ try:
33
+ return self.reader.exact(size)
34
+ except ClientError as error:
35
+ # RequestReader.exact() treats EOF as a body error, but EOF after an upgrade means disconnection.
36
+ raise ClientDisconnected(error.reason) from error
37
+
38
+ def unmask(
39
+ self,
40
+ data: bytes,
41
+ mask: bytes,
42
+ ) -> bytes:
43
+ size = len(data)
44
+ key = (mask * (size // 4 + 1))[:size]
45
+ return (int.from_bytes(data, 'big') ^ int.from_bytes(key, 'big')).to_bytes(
46
+ size,
47
+ 'big',
48
+ )
49
+
50
+ def read(self) -> WebSocketFrame:
51
+ first, second = self.exact(2)
52
+ if first & 0x70:
53
+ raise WebSocketError(
54
+ 1002,
55
+ 'Reserved bits must be zero',
56
+ )
57
+ final = bool(first & 0x80)
58
+ opcode = first & 0x0F
59
+ if opcode not in WebSocketFrame.OPCODES:
60
+ raise WebSocketError(
61
+ 1002,
62
+ 'Unknown opcode',
63
+ )
64
+ if not second & 0x80:
65
+ raise WebSocketError(
66
+ 1002,
67
+ 'Client frames must be masked',
68
+ )
69
+ size = second & 0x7F
70
+ if size == 126:
71
+ size = unpack('!H', self.exact(2))[0]
72
+ elif size == 127:
73
+ size = unpack('!Q', self.exact(8))[0]
74
+ if size >> 63:
75
+ raise WebSocketError(
76
+ 1002,
77
+ 'Invalid payload length',
78
+ )
79
+ control = opcode >= WebSocketFrame.CLOSE
80
+ if control and (not final or size > 125):
81
+ raise WebSocketError(
82
+ 1002,
83
+ 'Control frames must be final and at most 125 bytes',
84
+ )
85
+ if not control and size > self.limit:
86
+ raise WebSocketError(
87
+ 1009,
88
+ 'Message too big',
89
+ )
90
+ mask = self.exact(4)
91
+ return WebSocketFrame(
92
+ opcode,
93
+ self.unmask(
94
+ self.exact(size),
95
+ mask,
96
+ ),
97
+ final,
98
+ )
@@ -0,0 +1,94 @@
1
+ from base64 import (
2
+ b64decode,
3
+ b64encode,
4
+ )
5
+ from hashlib import sha1
6
+ from typing import Optional
7
+
8
+ from ...Error import ClientError
9
+ from ...Request import Request
10
+ from ...Response import Response
11
+
12
+ __all__ = (
13
+ 'WebSocketHandshake',
14
+ )
15
+
16
+
17
+ class WebSocketHandshake:
18
+ """Validate a WebSocket opening handshake request and build a 101 response."""
19
+
20
+ GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
21
+ VERSION = '13'
22
+
23
+ def __init__(
24
+ self,
25
+ request: Request,
26
+ ):
27
+ self.request = request
28
+ return
29
+
30
+ def tokens(
31
+ self,
32
+ name: str,
33
+ ) -> list[str]:
34
+ value = self.request.header(name) or ''
35
+ return [token.strip() for token in value.split(',') if token.strip()]
36
+
37
+ @property
38
+ def protocols(self) -> list[str]:
39
+ """Return the subprotocols requested by the client in request order."""
40
+ return self.tokens('Sec-WebSocket-Protocol')
41
+
42
+ def validate(self) -> str:
43
+ """Validate the handshake request and return the Sec-WebSocket-Key."""
44
+ if self.request.method != 'GET':
45
+ raise ClientError('WebSocket handshake requires GET')
46
+ if self.request.writer.version != 'HTTP/1.1':
47
+ raise ClientError('WebSocket handshake requires HTTP/1.1')
48
+ if 'websocket' not in (token.lower() for token in self.tokens('Upgrade')):
49
+ raise ClientError(
50
+ 'WebSocket upgrade is required',
51
+ status=426,
52
+ headers={'Upgrade': 'websocket'},
53
+ )
54
+ if 'upgrade' not in (token.lower() for token in self.tokens('Connection')):
55
+ raise ClientError('Connection: Upgrade is required')
56
+ if self.request.header('Sec-WebSocket-Version') != self.VERSION:
57
+ raise ClientError(
58
+ 'Unsupported WebSocket version',
59
+ status=426,
60
+ headers={'Sec-WebSocket-Version': self.VERSION},
61
+ )
62
+ key = self.request.header('Sec-WebSocket-Key')
63
+ try:
64
+ valid = key is not None and len(b64decode(key, validate=True)) == 16
65
+ except ValueError:
66
+ valid = False
67
+ if key is None or not valid:
68
+ raise ClientError('Invalid Sec-WebSocket-Key')
69
+ return key
70
+
71
+ def accept_key(
72
+ self,
73
+ key: str,
74
+ ) -> str:
75
+ return b64encode(sha1((key + self.GUID).encode('ascii')).digest()).decode('ascii')
76
+
77
+ def response(
78
+ self,
79
+ protocol: Optional[str] = None,
80
+ ) -> Response:
81
+ key = self.validate()
82
+ if protocol is not None and protocol not in self.protocols:
83
+ raise ValueError('Selected subprotocol was not requested by the client')
84
+ headers = {
85
+ 'Upgrade': 'websocket',
86
+ 'Connection': 'Upgrade',
87
+ 'Sec-WebSocket-Accept': self.accept_key(key),
88
+ }
89
+ if protocol is not None:
90
+ headers['Sec-WebSocket-Protocol'] = protocol
91
+ return Response(
92
+ status=101,
93
+ headers=headers,
94
+ )
@@ -0,0 +1,13 @@
1
+ from .WebSocket import WebSocket
2
+ from .WebSocketError import WebSocketError
3
+ from .WebSocketFrame import WebSocketFrame
4
+ from .WebSocketFrameReader import WebSocketFrameReader
5
+ from .WebSocketHandshake import WebSocketHandshake
6
+
7
+ __all__ = (
8
+ 'WebSocket',
9
+ 'WebSocketError',
10
+ 'WebSocketFrame',
11
+ 'WebSocketFrameReader',
12
+ 'WebSocketHandshake',
13
+ )
@@ -0,0 +1,19 @@
1
+ from .StreamWriter import StreamWriter
2
+
3
+ __all__ = (
4
+ 'ChunkedStreamWriter',
5
+ )
6
+
7
+
8
+ class ChunkedStreamWriter(StreamWriter):
9
+ """Write directly using HTTP/1.1 chunked framing."""
10
+
11
+ chunked = True
12
+
13
+ def chunk(
14
+ self,
15
+ data: bytes,
16
+ ) -> None:
17
+ self.write(data)
18
+ self.flush()
19
+ return
@@ -0,0 +1,41 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+ __all__ = (
5
+ 'ServerSentEvents',
6
+ )
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class ServerSentEvents:
11
+ """Encode UTF-8 SSE events for both direct writes and iterable output."""
12
+
13
+ data: str = ''
14
+ event: Optional[str] = None
15
+ id: Optional[str] = None
16
+ retry: Optional[int] = None
17
+
18
+ def retry_line(self) -> str:
19
+ """Return a validated reconnection delay field."""
20
+ if self.retry is None:
21
+ return ''
22
+ if isinstance(self.retry, bool) or not isinstance(self.retry, int) or self.retry < 0:
23
+ raise ValueError('SSE retry must be a non-negative integer')
24
+ return 'retry: ' + str(self.retry) + '\n'
25
+
26
+ def encode(self) -> bytes:
27
+ lines = []
28
+ for name, value in (
29
+ ('event', self.event),
30
+ ('id', self.id),
31
+ ):
32
+ if value is None:
33
+ continue
34
+ if '\r' in value or '\n' in value or (name == 'id' and '\x00' in value):
35
+ raise ValueError('Invalid SSE ' + name)
36
+ lines.append(name + ': ' + value)
37
+ if self.retry is not None:
38
+ lines.append(self.retry_line().rstrip('\n'))
39
+ data = self.data.replace('\r\n', '\n').replace('\r', '\n')
40
+ lines.extend('data: ' + line for line in data.split('\n'))
41
+ return ('\n'.join(lines) + '\n\n').encode('utf-8')
@@ -0,0 +1,67 @@
1
+ from typing import (
2
+ Mapping,
3
+ Optional,
4
+ Union,
5
+ )
6
+
7
+ from ..ResponseWriter import ResponseWriter
8
+ from .ServerSentEvents import ServerSentEvents
9
+ from .StreamWriter import StreamWriter
10
+
11
+ __all__ = (
12
+ 'ServerSentEventsWriter',
13
+ )
14
+
15
+
16
+ class ServerSentEventsWriter:
17
+ """Wrap a plain or chunked stream to send SSE events immediately."""
18
+
19
+ def __init__(
20
+ self,
21
+ writer: Union[ResponseWriter, StreamWriter],
22
+ ):
23
+ self.writer = StreamWriter(writer) if isinstance(writer, ResponseWriter) else writer
24
+ return
25
+
26
+ def begin(
27
+ self,
28
+ headers: Optional[Mapping[str, str]] = None,
29
+ ) -> None:
30
+ properties = dict(headers) if headers is not None else {}
31
+ if not any(key.lower() == 'cache-control' for key in properties):
32
+ properties['Cache-Control'] = 'no-cache'
33
+ self.writer.begin(
34
+ headers=properties,
35
+ format='text/event-stream',
36
+ charset='utf-8',
37
+ )
38
+ return
39
+
40
+ def emit(
41
+ self,
42
+ data: str,
43
+ event: Optional[str] = None,
44
+ id: Optional[str] = None,
45
+ retry: Optional[int] = None,
46
+ ) -> None:
47
+ self.writer.write(ServerSentEvents(
48
+ data=data,
49
+ event=event,
50
+ id=id,
51
+ retry=retry,
52
+ ).encode())
53
+ self.writer.flush()
54
+ return
55
+
56
+ def retry(
57
+ self,
58
+ ms: int,
59
+ ) -> None:
60
+ frame = ServerSentEvents(retry=ms).retry_line()
61
+ self.writer.write((frame + '\n').encode('utf-8'))
62
+ self.writer.flush()
63
+ return
64
+
65
+ def end(self) -> None:
66
+ self.writer.end()
67
+ return