DefEssential 0.1.6__19-py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. DefEssential/Adapter/Implements/Gunicorn/Server.py +134 -0
  2. DefEssential/Adapter/Implements/Gunicorn/Transport.py +171 -0
  3. DefEssential/Adapter/Implements/Gunicorn/__init__.py +5 -0
  4. DefEssential/Adapter/Implements/Python/Server.py +245 -0
  5. DefEssential/Adapter/Implements/Python/__init__.py +5 -0
  6. DefEssential/Adapter/Implements/Uvicorn/HTTP.py +256 -0
  7. DefEssential/Adapter/Implements/Uvicorn/Portal.py +65 -0
  8. DefEssential/Adapter/Implements/Uvicorn/Server.py +102 -0
  9. DefEssential/Adapter/Implements/Uvicorn/WebSocket.py +220 -0
  10. DefEssential/Adapter/Implements/Uvicorn/__init__.py +5 -0
  11. DefEssential/Adapter/Implements/__init__.py +3 -0
  12. DefEssential/Adapter/RequestHandler.py +70 -0
  13. DefEssential/Adapter/Server.py +54 -0
  14. DefEssential/Adapter/WSGI.py +219 -0
  15. DefEssential/Adapter/__init__.py +5 -0
  16. DefEssential/Application.py +579 -0
  17. DefEssential/Body.py +50 -0
  18. DefEssential/Content.py +18 -0
  19. DefEssential/Credentials.py +245 -0
  20. DefEssential/CrossOriginResourceSharing.py +107 -0
  21. DefEssential/Descriptor/DataObject.py +409 -0
  22. DefEssential/Descriptor/Types.py +248 -0
  23. DefEssential/Descriptor/Value.py +188 -0
  24. DefEssential/Descriptor/__init__.py +172 -0
  25. DefEssential/Error.py +66 -0
  26. DefEssential/Extension.py +184 -0
  27. DefEssential/Extensions/File.py +89 -0
  28. DefEssential/Extensions/FileSystem.py +184 -0
  29. DefEssential/Extensions/Metrics.py +500 -0
  30. DefEssential/Extensions/Ping.py +89 -0
  31. DefEssential/Extensions/Specifications.py +328 -0
  32. DefEssential/Extensions/Status.html +207 -0
  33. DefEssential/Extensions/Status.py +104 -0
  34. DefEssential/Extensions/Swagger.html +77 -0
  35. DefEssential/Extensions/Swagger.py +111 -0
  36. DefEssential/Extensions/Utils/Routes.py +42 -0
  37. DefEssential/Extensions/Utils/__init__.py +43 -0
  38. DefEssential/Extensions/__init__.py +21 -0
  39. DefEssential/IO/Readers/ChunkedStream.py +106 -0
  40. DefEssential/IO/Readers/ContentReader.py +36 -0
  41. DefEssential/IO/Readers/ContentReaders.py +241 -0
  42. DefEssential/IO/Readers/__init__.py +25 -0
  43. DefEssential/IO/RequestReader.py +125 -0
  44. DefEssential/IO/ResponseWriter.py +241 -0
  45. DefEssential/IO/WebSocket/WebSocket.py +282 -0
  46. DefEssential/IO/WebSocket/WebSocketError.py +17 -0
  47. DefEssential/IO/WebSocket/WebSocketFrame.py +68 -0
  48. DefEssential/IO/WebSocket/WebSocketFrameReader.py +98 -0
  49. DefEssential/IO/WebSocket/WebSocketHandshake.py +94 -0
  50. DefEssential/IO/WebSocket/__init__.py +13 -0
  51. DefEssential/IO/Writers/ChunkedStreamWriter.py +19 -0
  52. DefEssential/IO/Writers/ServerSentEvents.py +41 -0
  53. DefEssential/IO/Writers/ServerSentEventsWriter.py +67 -0
  54. DefEssential/IO/Writers/StreamWriter.py +57 -0
  55. DefEssential/IO/Writers/__init__.py +11 -0
  56. DefEssential/IO/__init__.py +7 -0
  57. DefEssential/Request.py +110 -0
  58. DefEssential/RequestRunner.py +261 -0
  59. DefEssential/Response.py +87 -0
  60. DefEssential/ResponseSpecification.py +23 -0
  61. DefEssential/Responses/ResponseFile.py +108 -0
  62. DefEssential/Responses/__init__.py +63 -0
  63. DefEssential/Route.py +55 -0
  64. DefEssential/Router.py +48 -0
  65. DefEssential/Runners/RunExtension.py +101 -0
  66. DefEssential/Runners/RunFactory.py +26 -0
  67. DefEssential/Runners/RunOptions.py +77 -0
  68. DefEssential/Runners/RunRequest.py +121 -0
  69. DefEssential/Runners/__init__.py +5 -0
  70. DefEssential/Startup.py +104 -0
  71. DefEssential/Utils/FileSystemObject/Configuration.py +15 -0
  72. DefEssential/Utils/FileSystemObject/Connection.py +57 -0
  73. DefEssential/Utils/FileSystemObject/File.py +41 -0
  74. DefEssential/Utils/FileSystemObject/Implements/FileSystem/Configuration.py +15 -0
  75. DefEssential/Utils/FileSystemObject/Implements/FileSystem/Connection.py +115 -0
  76. DefEssential/Utils/FileSystemObject/Implements/FileSystem/File.py +77 -0
  77. DefEssential/Utils/FileSystemObject/Implements/FileSystem/__init__.py +11 -0
  78. DefEssential/Utils/FileSystemObject/Implements/__init__.py +3 -0
  79. DefEssential/Utils/FileSystemObject/__init__.py +11 -0
  80. DefEssential/Utils/Loader.py +81 -0
  81. DefEssential/Utils/__init__.py +3 -0
  82. DefEssential/Validator/Pattern.py +36 -0
  83. DefEssential/Validator/Patterns/Array.py +30 -0
  84. DefEssential/Validator/Patterns/Common.py +92 -0
  85. DefEssential/Validator/Patterns/Compare.py +180 -0
  86. DefEssential/Validator/Patterns/Condition.py +60 -0
  87. DefEssential/Validator/Patterns/DataObject.py +85 -0
  88. DefEssential/Validator/Patterns/DateTime.py +111 -0
  89. DefEssential/Validator/Patterns/Number.py +43 -0
  90. DefEssential/Validator/Patterns/Object.py +150 -0
  91. DefEssential/Validator/Patterns/Size.py +110 -0
  92. DefEssential/Validator/Patterns/String.py +85 -0
  93. DefEssential/Validator/Patterns/Type.py +350 -0
  94. DefEssential/Validator/Patterns/__init__.py +137 -0
  95. DefEssential/Validator/Validate.py +29 -0
  96. DefEssential/Validator/Validator.py +30 -0
  97. DefEssential/Validator/__init__.py +11 -0
  98. DefEssential/Validators.py +255 -0
  99. DefEssential/__init__.py +83 -0
  100. defessential-0.1.6.dist-info/METADATA +41 -0
  101. defessential-0.1.6.dist-info/RECORD +104 -0
  102. defessential-0.1.6.dist-info/WHEEL +6 -0
  103. defessential-0.1.6.dist-info/licenses/LICENSE +674 -0
  104. defessential-0.1.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,282 @@
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
+ """업그레이드된 HTTP 연결에서 WebSocket 메시지를 주고받는다.
25
+
26
+ receive()는 한 스레드에서만 호출한다. send(), ping(), close()는 다른 스레드에서도 호출할 수 있다.
27
+ """
28
+
29
+ logger = getLogger('DefEssential.IO.WebSocket')
30
+
31
+ def __init__(
32
+ self,
33
+ request: Request,
34
+ limit: int,
35
+ ):
36
+ if limit <= 0:
37
+ raise ValueError('Message size limit must be positive')
38
+ self.request = request
39
+ self.limit = limit
40
+ self.handshake = WebSocketHandshake(request)
41
+ self.frames = WebSocketFrameReader(
42
+ request.reader,
43
+ limit,
44
+ )
45
+ self.lock = Lock()
46
+ self.accepted = False
47
+ self.close_sent = False
48
+ self.closed = False
49
+ self.code: Optional[int] = None
50
+ self.reason = ''
51
+ return
52
+
53
+ @property
54
+ def protocols(self) -> list[str]:
55
+ return self.handshake.protocols
56
+
57
+ def accept(
58
+ self,
59
+ protocol: Optional[str] = None,
60
+ ) -> None:
61
+ """핸드셰이크를 검사하고 101 응답을 보낸다. 실패하면 101 전이므로 일반 오류 응답이 나간다."""
62
+ if self.accepted:
63
+ raise RuntimeError('WebSocket has already been accepted')
64
+ self.request.writer.upgrade(self.handshake.response(protocol))
65
+ self.accepted = True
66
+ return
67
+
68
+ def valid_code(
69
+ self,
70
+ code: int,
71
+ ) -> bool:
72
+ return code in (
73
+ 1000,
74
+ 1001,
75
+ 1002,
76
+ 1003,
77
+ 1007,
78
+ 1008,
79
+ 1009,
80
+ 1010,
81
+ 1011,
82
+ 1012,
83
+ 1013,
84
+ 1014,
85
+ ) or 3000 <= code <= 4999
86
+
87
+ def write(
88
+ self,
89
+ frame: WebSocketFrame,
90
+ ) -> None:
91
+ with self.lock:
92
+ if not self.accepted:
93
+ raise RuntimeError('WebSocket must be accepted first')
94
+ if self.close_sent:
95
+ raise RuntimeError('Cannot send after the WebSocket close frame')
96
+ if frame.opcode == WebSocketFrame.CLOSE:
97
+ self.close_sent = True
98
+ self.request.writer.write(frame.encode())
99
+ self.request.writer.flush()
100
+ return
101
+
102
+ def send(
103
+ self,
104
+ message: Union[str, bytes],
105
+ ) -> None:
106
+ """str은 text 메시지로, bytes는 binary 메시지로 보낸다."""
107
+ if isinstance(message, str):
108
+ frame = WebSocketFrame(
109
+ WebSocketFrame.TEXT,
110
+ message.encode('utf-8'),
111
+ )
112
+ elif isinstance(message, bytes):
113
+ frame = WebSocketFrame(
114
+ WebSocketFrame.BINARY,
115
+ message,
116
+ )
117
+ else:
118
+ raise TypeError('WebSocket messages must be str or bytes')
119
+ self.write(frame)
120
+ return
121
+
122
+ def ping(
123
+ self,
124
+ data: bytes = b'',
125
+ ) -> None:
126
+ self.write(WebSocketFrame(
127
+ WebSocketFrame.PING,
128
+ data,
129
+ ))
130
+ return
131
+
132
+ def close_frame(
133
+ self,
134
+ code: int,
135
+ reason: str,
136
+ ) -> WebSocketFrame:
137
+ return WebSocketFrame(
138
+ WebSocketFrame.CLOSE,
139
+ pack('!H', code) + reason.encode('utf-8'),
140
+ )
141
+
142
+ def receive_close(
143
+ self,
144
+ frame: WebSocketFrame,
145
+ ) -> None:
146
+ payload = frame.payload
147
+ if len(payload) == 1:
148
+ raise WebSocketError(
149
+ 1002,
150
+ 'Invalid close frame',
151
+ )
152
+ code = unpack('!H', payload[:2])[0] if payload else None
153
+ if code is not None and not self.valid_code(code):
154
+ raise WebSocketError(
155
+ 1002,
156
+ 'Invalid close code',
157
+ )
158
+ try:
159
+ reason = payload[2:].decode('utf-8')
160
+ except UnicodeDecodeError as error:
161
+ raise WebSocketError(
162
+ 1007,
163
+ 'Invalid close reason',
164
+ ) from error
165
+ self.code = code
166
+ self.reason = reason
167
+ self.closed = True
168
+ with self.lock:
169
+ echo = not self.close_sent
170
+ if echo:
171
+ self.write(WebSocketFrame(
172
+ WebSocketFrame.CLOSE,
173
+ payload[:2],
174
+ ))
175
+ return
176
+
177
+ def fail(
178
+ self,
179
+ error: WebSocketError,
180
+ ) -> None:
181
+ self.logger.warning(
182
+ 'WebSocket protocol error: %s',
183
+ error.reason,
184
+ )
185
+ self.code = error.code
186
+ self.reason = error.reason
187
+ self.closed = True
188
+ with self.lock:
189
+ send = not self.close_sent
190
+ if send:
191
+ self.write(self.close_frame(
192
+ error.code,
193
+ error.reason,
194
+ ))
195
+ return
196
+
197
+ def receive(self) -> Union[str, bytes, None]:
198
+ """다음 메시지를 반환한다. 연결이 닫히는 과정에 들어가면 None을 반환한다.
199
+
200
+ ping에는 자동으로 pong을 보낸다. 프로토콜 위반은 닫기 코드를 보낸 뒤 None을 반환하고,
201
+ TCP 연결이 끊기면 ClientDisconnected가 전파된다.
202
+ """
203
+ if not self.accepted:
204
+ raise RuntimeError('WebSocket must be accepted first')
205
+ if self.closed:
206
+ return None
207
+ opcode: Optional[int] = None
208
+ parts = []
209
+ size = 0
210
+ try:
211
+ while True:
212
+ frame = self.frames.read()
213
+ if frame.opcode == WebSocketFrame.CLOSE:
214
+ self.receive_close(frame)
215
+ return None
216
+ if frame.opcode == WebSocketFrame.PING:
217
+ with self.lock:
218
+ reply = not self.close_sent
219
+ if reply:
220
+ self.write(WebSocketFrame(
221
+ WebSocketFrame.PONG,
222
+ frame.payload,
223
+ ))
224
+ continue
225
+ if frame.opcode == WebSocketFrame.PONG:
226
+ continue
227
+ if frame.opcode == WebSocketFrame.CONTINUATION:
228
+ if opcode is None:
229
+ raise WebSocketError(
230
+ 1002,
231
+ 'Unexpected continuation frame',
232
+ )
233
+ elif opcode is not None:
234
+ raise WebSocketError(
235
+ 1002,
236
+ 'Expected a continuation frame',
237
+ )
238
+ else:
239
+ opcode = frame.opcode
240
+ size += len(frame.payload)
241
+ if size > self.limit:
242
+ raise WebSocketError(
243
+ 1009,
244
+ 'Message too big',
245
+ )
246
+ parts.append(frame.payload)
247
+ if frame.final:
248
+ break
249
+ data = b''.join(parts)
250
+ if opcode == WebSocketFrame.BINARY:
251
+ return data
252
+ try:
253
+ return data.decode('utf-8')
254
+ except UnicodeDecodeError as error:
255
+ raise WebSocketError(
256
+ 1007,
257
+ 'Invalid UTF-8 text',
258
+ ) from error
259
+ except WebSocketError as error:
260
+ self.fail(error)
261
+ return None
262
+
263
+ def close(
264
+ self,
265
+ code: int = 1000,
266
+ reason: str = '',
267
+ ) -> None:
268
+ """닫기 프레임을 보내고 클라이언트의 닫기 프레임을 받을 때까지 남은 메시지를 버린다."""
269
+ if not self.valid_code(code):
270
+ raise ValueError('Invalid WebSocket close code')
271
+ if len(reason.encode('utf-8')) > 123:
272
+ raise ValueError('WebSocket close reason must be at most 123 bytes')
273
+ with self.lock:
274
+ send = not self.close_sent
275
+ if send:
276
+ self.write(self.close_frame(
277
+ code,
278
+ reason,
279
+ ))
280
+ while not self.closed:
281
+ self.receive()
282
+ return
@@ -0,0 +1,17 @@
1
+ __all__ = (
2
+ 'WebSocketError',
3
+ )
4
+
5
+
6
+ class WebSocketError(Exception):
7
+ """클라이언트의 WebSocket 프로토콜 위반을 닫기 코드와 함께 표현한다."""
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
+ """WebSocket 프레임 하나를 표현하고 서버가 보내는 마스크 없는 바이트로 인코딩한다."""
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
+ """업그레이드된 연결에서 클라이언트 프레임을 하나씩 읽고 프레임 규칙을 검사한다."""
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()는 EOF를 본문 오류로 보고하지만, 업그레이드 뒤의 EOF는 연결 종료다.
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
+ """WebSocket 열기 핸드셰이크 요청을 검증하고 101 응답을 만든다."""
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
+ """클라이언트가 요청한 서브프로토콜을 요청 순서대로 반환한다."""
40
+ return self.tokens('Sec-WebSocket-Protocol')
41
+
42
+ def validate(self) -> str:
43
+ """핸드셰이크 요청을 검사하고 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