Pyjoyment 0.1.0__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 (200) hide show
  1. Pyjo/App/ASGI.py +735 -0
  2. Pyjo/App/CGI.py +773 -0
  3. Pyjo/App/FastCGI.py +631 -0
  4. Pyjo/App/Lambda.py +987 -0
  5. Pyjo/App/WSGI.py +427 -0
  6. Pyjo/App/__init__.py +409 -0
  7. Pyjo/Asset/File.py +377 -0
  8. Pyjo/Asset/Memory.py +235 -0
  9. Pyjo/Asset/__init__.py +200 -0
  10. Pyjo/Base.py +215 -0
  11. Pyjo/Cache.py +119 -0
  12. Pyjo/Collection.py +570 -0
  13. Pyjo/Content/MultiPart.py +325 -0
  14. Pyjo/Content/Single.py +193 -0
  15. Pyjo/Content/__init__.py +806 -0
  16. Pyjo/Cookie/Request.py +90 -0
  17. Pyjo/Cookie/Response.py +294 -0
  18. Pyjo/Cookie/__init__.py +143 -0
  19. Pyjo/DOM/CSS.py +1019 -0
  20. Pyjo/DOM/HTML.py +550 -0
  21. Pyjo/DOM/__init__.py +1329 -0
  22. Pyjo/Date.py +294 -0
  23. Pyjo/EventEmitter.py +522 -0
  24. Pyjo/Exception.py +494 -0
  25. Pyjo/File.py +959 -0
  26. Pyjo/GetOpt.py +605 -0
  27. Pyjo/Headers.py +760 -0
  28. Pyjo/HelloWorld.py +77 -0
  29. Pyjo/Home.py +109 -0
  30. Pyjo/IOLoop/ChildProcess.py +591 -0
  31. Pyjo/IOLoop/Client.py +736 -0
  32. Pyjo/IOLoop/Datagram.py +982 -0
  33. Pyjo/IOLoop/Messages.py +664 -0
  34. Pyjo/IOLoop/Server.py +458 -0
  35. Pyjo/IOLoop/Stdio.py +446 -0
  36. Pyjo/IOLoop/Stream.py +644 -0
  37. Pyjo/IOLoop/Subprocess.py +419 -0
  38. Pyjo/IOLoop/TLS.py +360 -0
  39. Pyjo/IOLoop/__init__.py +1036 -0
  40. Pyjo/IOLoop/certs/README.md +41 -0
  41. Pyjo/IOLoop/certs/ca.crt +20 -0
  42. Pyjo/IOLoop/certs/server.crt +21 -0
  43. Pyjo/IOLoop/certs/server.key +28 -0
  44. Pyjo/JSON/Pointer.py +135 -0
  45. Pyjo/JSON/RPC.py +772 -0
  46. Pyjo/JSON/Schema.py +2506 -0
  47. Pyjo/JSON/__init__.py +573 -0
  48. Pyjo/Loader.py +342 -0
  49. Pyjo/Log.py +723 -0
  50. Pyjo/Message/Request.py +725 -0
  51. Pyjo/Message/Response.py +476 -0
  52. Pyjo/Message/__init__.py +830 -0
  53. Pyjo/Parameters.py +472 -0
  54. Pyjo/Path.py +419 -0
  55. Pyjo/Promise.py +894 -0
  56. Pyjo/Prove.py +1305 -0
  57. Pyjo/Reactor/Asyncio.py +854 -0
  58. Pyjo/Reactor/Poll.py +439 -0
  59. Pyjo/Reactor/__init__.py +525 -0
  60. Pyjo/SSE.py +119 -0
  61. Pyjo/Server/ASGI.py +486 -0
  62. Pyjo/Server/CGI.py +259 -0
  63. Pyjo/Server/Daemon.py +1007 -0
  64. Pyjo/Server/FastCGI.py +517 -0
  65. Pyjo/Server/Hot/__init__.py +687 -0
  66. Pyjo/Server/Hot/__main__.py +5 -0
  67. Pyjo/Server/Lambda.py +641 -0
  68. Pyjo/Server/Prefork.py +917 -0
  69. Pyjo/Server/Prethread.py +394 -0
  70. Pyjo/Server/Reload/Backend/Poll.py +114 -0
  71. Pyjo/Server/Reload/Backend/__init__.py +126 -0
  72. Pyjo/Server/Reload/__init__.py +482 -0
  73. Pyjo/Server/Reload/__main__.py +5 -0
  74. Pyjo/Server/Stdio.py +228 -0
  75. Pyjo/Server/WSGI.py +349 -0
  76. Pyjo/Server/__init__.py +396 -0
  77. Pyjo/String/Bytes.py +532 -0
  78. Pyjo/String/HTML.py +210 -0
  79. Pyjo/String/Mixin.py +333 -0
  80. Pyjo/String/Template.py +470 -0
  81. Pyjo/String/Unicode.py +553 -0
  82. Pyjo/String/__init__.py +0 -0
  83. Pyjo/TOML.py +232 -0
  84. Pyjo/Template.py +1290 -0
  85. Pyjo/Test/Pyjo.py +1454 -0
  86. Pyjo/Test/__init__.py +1233 -0
  87. Pyjo/Test/pytest_plugin.py +305 -0
  88. Pyjo/Transaction/HTTP.py +374 -0
  89. Pyjo/Transaction/WebSocket.py +709 -0
  90. Pyjo/Transaction/__init__.py +429 -0
  91. Pyjo/URL/Template.py +459 -0
  92. Pyjo/URL/__init__.py +651 -0
  93. Pyjo/Upload.py +139 -0
  94. Pyjo/UserAgent/CookieJar.py +391 -0
  95. Pyjo/UserAgent/Proxy.py +138 -0
  96. Pyjo/UserAgent/Server.py +239 -0
  97. Pyjo/UserAgent/Transactor.py +766 -0
  98. Pyjo/UserAgent/__init__.py +1243 -0
  99. Pyjo/Util.py +3567 -0
  100. Pyjo/WebSocket.py +332 -0
  101. Pyjo/YAML.py +1167 -0
  102. Pyjo/__init__.py +441 -0
  103. Pyjo/__main__.py +35 -0
  104. Pyjo/py.typed +0 -0
  105. Pyjoyment/Command/Author/__init__.py +20 -0
  106. Pyjoyment/Command/Author/generate/__init__.py +106 -0
  107. Pyjoyment/Command/Author/generate/app.py +299 -0
  108. Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
  109. Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
  110. Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
  111. Pyjoyment/Command/Author/generate/lite_app.py +113 -0
  112. Pyjoyment/Command/Author/generate/plugin.py +267 -0
  113. Pyjoyment/Command/Author/generate/pyproject.py +123 -0
  114. Pyjoyment/Command/Author/inflate.py +101 -0
  115. Pyjoyment/Command/__init__.py +332 -0
  116. Pyjoyment/Command/asgi.py +92 -0
  117. Pyjoyment/Command/cgi.py +90 -0
  118. Pyjoyment/Command/daemon.py +160 -0
  119. Pyjoyment/Command/eval.py +128 -0
  120. Pyjoyment/Command/fastcgi.py +99 -0
  121. Pyjoyment/Command/get.py +305 -0
  122. Pyjoyment/Command/lambda.py +117 -0
  123. Pyjoyment/Command/prefork.py +181 -0
  124. Pyjoyment/Command/prethread.py +126 -0
  125. Pyjoyment/Command/routes.py +133 -0
  126. Pyjoyment/Command/stdio.py +92 -0
  127. Pyjoyment/Command/version.py +141 -0
  128. Pyjoyment/Command/wsgi.py +93 -0
  129. Pyjoyment/Commands.py +546 -0
  130. Pyjoyment/Controller.py +1307 -0
  131. Pyjoyment/HelloWorld.py +56 -0
  132. Pyjoyment/Lite.py +494 -0
  133. Pyjoyment/Plugin/AllowedHosts.py +205 -0
  134. Pyjoyment/Plugin/Config.py +248 -0
  135. Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
  136. Pyjoyment/Plugin/EPLRenderer.py +134 -0
  137. Pyjoyment/Plugin/EPRenderer.py +212 -0
  138. Pyjoyment/Plugin/HeaderCondition.py +98 -0
  139. Pyjoyment/Plugin/Health.py +325 -0
  140. Pyjoyment/Plugin/JSONConfig.py +172 -0
  141. Pyjoyment/Plugin/Mount.py +223 -0
  142. Pyjoyment/Plugin/PyRenderer.py +209 -0
  143. Pyjoyment/Plugin/TOMLConfig.py +141 -0
  144. Pyjoyment/Plugin/TagHelpers.py +959 -0
  145. Pyjoyment/Plugin/YAMLConfig.py +178 -0
  146. Pyjoyment/Plugin/__init__.py +80 -0
  147. Pyjoyment/Plugins.py +215 -0
  148. Pyjoyment/Renderer.py +673 -0
  149. Pyjoyment/Routes/Match.py +251 -0
  150. Pyjoyment/Routes/Pattern.py +443 -0
  151. Pyjoyment/Routes/Route.py +827 -0
  152. Pyjoyment/Routes/__init__.py +419 -0
  153. Pyjoyment/Sessions.py +255 -0
  154. Pyjoyment/Static.py +421 -0
  155. Pyjoyment/Types.py +238 -0
  156. Pyjoyment/Validator/Validation.py +395 -0
  157. Pyjoyment/Validator/__init__.py +304 -0
  158. Pyjoyment/__init__.py +1083 -0
  159. Pyjoyment/__main__.py +5 -0
  160. Pyjoyment/py.typed +0 -0
  161. Pyjoyment/resources/public/favicon.ico +0 -0
  162. Pyjoyment/resources/public/favicon.svg +345 -0
  163. Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
  164. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
  165. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
  166. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
  167. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
  168. Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
  169. Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
  170. Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
  171. Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
  172. Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
  173. Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
  174. Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
  175. Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
  176. Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
  177. Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
  178. Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
  179. Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
  180. Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
  181. Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
  182. Pyjoyment/resources/public/pyjo/logo.png +0 -0
  183. Pyjoyment/resources/public/pyjo/logo.svg +346 -0
  184. Pyjoyment/resources/public/pyjo/notfound.png +0 -0
  185. Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
  186. Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
  187. Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
  188. Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
  189. Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
  190. Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
  191. Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
  192. Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
  193. Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
  194. Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
  195. Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
  196. pyjoyment-0.1.0.dist-info/METADATA +376 -0
  197. pyjoyment-0.1.0.dist-info/RECORD +200 -0
  198. pyjoyment-0.1.0.dist-info/WHEEL +4 -0
  199. pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
  200. pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,709 @@
1
+ r"""
2
+ Pyjo.Transaction.WebSocket - WebSocket transaction
3
+ ==================================================
4
+ ::
5
+
6
+ from Pyjo.Transaction.WebSocket import WebSocket
7
+
8
+ # Send and receive WebSocket messages
9
+ ws = WebSocket()
10
+ ws.send('Hello World!')
11
+ ws.on('message', lambda ws, msg: print(f'Message: {msg}'))
12
+ ws.on('finish', lambda ws, code, reason: print(f'WebSocket closed with status {code}.'))
13
+
14
+ # Or import the module, as in Google Python Style Guide
15
+
16
+ import Pyjo.Transaction.WebSocket
17
+
18
+ # Send and receive WebSocket messages
19
+ ws = Pyjo.Transaction.WebSocket.new()
20
+ ws.send('Hello World!')
21
+ ws.on('message', lambda ws, msg: print(f'Message: {msg}'))
22
+ ws.on('finish', lambda ws, code, reason: print(f'WebSocket closed with status {code}.'))
23
+
24
+ :mod:`Pyjo.Transaction.WebSocket` is a container for WebSocket transactions,
25
+ based on `RFC 6455 <https://tools.ietf.org/html/rfc6455>`_ and
26
+ `RFC 7692 <https://tools.ietf.org/html/rfc7692>`_.
27
+
28
+ Events
29
+ ------
30
+
31
+ :mod:`Pyjo.Transaction.WebSocket` inherits all events from
32
+ :mod:`Pyjo.Transaction` and can emit the following new ones.
33
+
34
+ binary
35
+ ~~~~~~
36
+ ::
37
+
38
+ ws.on('binary', lambda ws, data: ...)
39
+
40
+ Emitted when a complete WebSocket binary message has been received. ::
41
+
42
+ ws.on('binary', lambda ws, data: print(f'Binary: {data}'))
43
+
44
+ drain
45
+ ~~~~~
46
+ ::
47
+
48
+ ws.on('drain', lambda ws: ...)
49
+
50
+ Emitted once all data has been sent. ::
51
+
52
+ ws.on('drain', lambda ws: ws.send(str(time.time())))
53
+
54
+ finish
55
+ ~~~~~~
56
+ ::
57
+
58
+ ws.on('finish', lambda ws, code, reason: ...)
59
+
60
+ Emitted when the WebSocket connection has been closed.
61
+
62
+ frame
63
+ ~~~~~
64
+ ::
65
+
66
+ ws.on('frame', lambda ws, frame: ...)
67
+
68
+ Emitted when a WebSocket frame has been received. ::
69
+
70
+ @ws.on('frame')
71
+ def frame(ws, frame):
72
+ print(f'FIN: {frame[0]}')
73
+ print(f'RSV1: {frame[1]}')
74
+ print(f'RSV2: {frame[2]}')
75
+ print(f'RSV3: {frame[3]}')
76
+ print(f'Opcode: {frame[4]}')
77
+ print(f'Payload: {frame[5]}')
78
+
79
+ json
80
+ ~~~~
81
+ ::
82
+
83
+ ws.on('json', lambda ws, json: ...)
84
+
85
+ Emitted when a complete WebSocket message has been received, all text and
86
+ binary messages will be automatically JSON decoded. Note that this event only
87
+ gets emitted when it has at least one subscriber. ::
88
+
89
+ ws.on('json', lambda ws, data: print(f'Message: {data["msg"]}'))
90
+
91
+ message
92
+ ~~~~~~~
93
+ ::
94
+
95
+ ws.on('message', lambda ws, msg: ...)
96
+
97
+ Emitted when a complete WebSocket message has been received, text messages
98
+ will be automatically decoded. Note that this event only gets emitted when it
99
+ has at least one subscriber. ::
100
+
101
+ ws.on('message', lambda ws, msg: print(f'Message: {msg}'))
102
+
103
+ resume
104
+ ~~~~~~
105
+ ::
106
+
107
+ tx.on('resume', lambda tx: ...)
108
+
109
+ Emitted when transaction is resumed.
110
+
111
+ text
112
+ ~~~~
113
+ ::
114
+
115
+ ws.on('text', lambda ws, data: ...)
116
+
117
+ Emitted when a complete WebSocket text message has been received. ::
118
+
119
+ ws.on('text', lambda ws, data: print(f'Text: {data}'))
120
+
121
+ See also
122
+ --------
123
+
124
+ :mod:`Pyjoyment`, https://mojolicious.org.
125
+
126
+ Ported from `Mojo::Transaction::WebSocket <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Transaction/WebSocket.pm>`_
127
+ of Mojolicious 9.49.
128
+ """
129
+
130
+ import Pyjo.IOLoop
131
+ import Pyjo.Message.Request
132
+ import Pyjo.Message.Response
133
+ import Pyjo.Transaction
134
+ import Pyjo.Transaction.HTTP
135
+ import Pyjo.WebSocket
136
+
137
+ from Pyjo.JSON import encode_json, j
138
+ from Pyjo.Util import decode, encode, trim
139
+ from Pyjo.WebSocket import WS_BINARY, WS_CLOSE, WS_PING, WS_PONG, WS_TEXT, Frame
140
+
141
+ import os
142
+ import struct
143
+ import weakref
144
+ import zlib
145
+
146
+ from collections.abc import Callable, Mapping, Sequence
147
+ from typing import Any, Self
148
+
149
+
150
+ class Pyjo_Transaction_WebSocket(Pyjo.Transaction.object):
151
+ """
152
+ :mod:`Pyjo.Transaction.WebSocket` inherits all methods from
153
+ :mod:`Pyjo.Transaction` and implements the following new ones.
154
+ """
155
+
156
+ def __init__(
157
+ self,
158
+ *,
159
+ compressed: bool | None = None,
160
+ established: bool | None = None,
161
+ handshake: Pyjo.Transaction.HTTP.object | None = None,
162
+ masked: bool | None = None,
163
+ max_websocket_size: int | None = None,
164
+ **kwargs: Any,
165
+ ) -> None:
166
+ """::
167
+
168
+ ws = Pyjo.Transaction.WebSocket.new()
169
+ ws = Pyjo.Transaction.WebSocket.new(compressed=True)
170
+
171
+ Construct a new :mod:`Pyjo.Transaction.WebSocket` object.
172
+ """
173
+ super().__init__(**kwargs)
174
+
175
+ self.compressed = compressed
176
+ """::
177
+
178
+ boolean = ws.compressed
179
+ ws.compressed = boolean
180
+
181
+ Compress messages with ``permessage-deflate`` extension.
182
+ """
183
+
184
+ self.established = established
185
+ """::
186
+
187
+ boolean = ws.established
188
+ ws.established = boolean
189
+
190
+ WebSocket connection established.
191
+ """
192
+
193
+ self.handshake = handshake if handshake is not None else Pyjo.Transaction.HTTP.new()
194
+ """::
195
+
196
+ handshake = ws.handshake
197
+ ws.handshake = Pyjo.Transaction.HTTP.new()
198
+
199
+ The original handshake transaction, usually a
200
+ :mod:`Pyjo.Transaction.HTTP` object.
201
+ """
202
+
203
+ self.masked = masked
204
+ """::
205
+
206
+ boolean = ws.masked
207
+ ws.masked = boolean
208
+
209
+ Mask outgoing frames with XOR cipher and a random 32-bit key.
210
+ """
211
+
212
+ self._max_websocket_size: int | None = max_websocket_size
213
+
214
+ # The state of the transaction, as the keys of Mojo's hash
215
+ self._close: list[Any] | None = None
216
+ self._closing = False
217
+ self._heartbeat: _Heartbeat | None = None
218
+ self._inflate: Any = None
219
+ self._message: bytes | None = None
220
+ self._op: int | None = None
221
+ self._pmc = False
222
+ self._read = bytearray()
223
+ self._write: bytes | None = None
224
+
225
+ @property
226
+ def max_websocket_size(self) -> int:
227
+ """::
228
+
229
+ size = ws.max_websocket_size
230
+ ws.max_websocket_size = 1024
231
+
232
+ Maximum WebSocket message size in bytes, defaults to the value of the
233
+ ``PYJO_MAX_WEBSOCKET_SIZE`` environment variable or ``262144``
234
+ (256KiB).
235
+ """
236
+ if self._max_websocket_size is None:
237
+ self._max_websocket_size = int(os.environ.get('PYJO_MAX_WEBSOCKET_SIZE') or 262144)
238
+ return self._max_websocket_size
239
+
240
+ @max_websocket_size.setter
241
+ def max_websocket_size(self, value: int) -> None:
242
+ self._max_websocket_size = value
243
+
244
+ def build_message(self, frame: str | Mapping[str, Any]) -> Frame:
245
+ """::
246
+
247
+ frame = ws.build_message({'binary': data})
248
+ frame = ws.build_message({'text': data})
249
+ frame = ws.build_message({'json': {'test': [1, 2, 3]}})
250
+ frame = ws.build_message(chars)
251
+
252
+ Build WebSocket message.
253
+ """
254
+ # Text
255
+ message = {'text': encode('UTF-8', frame)} if isinstance(frame, str) else dict(frame)
256
+
257
+ # JSON
258
+ if 'json' in message:
259
+ message['text'] = encode_json(message['json'])
260
+
261
+ # Raw text or binary
262
+ op, payload = (WS_TEXT, message['text']) if 'text' in message else (WS_BINARY, message['binary'])
263
+
264
+ # "permessage-deflate" extension
265
+ if not self.compressed:
266
+ return (1, 0, 0, 0, op, payload)
267
+ # A new context for each message, as deflateReset gives
268
+ deflate = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15, 8)
269
+ out = deflate.compress(payload) + deflate.flush(zlib.Z_SYNC_FLUSH)
270
+
271
+ return (1, 1, 0, 0, op, out[:-4])
272
+
273
+ def client_read(self, chunk: bytes) -> None:
274
+ """::
275
+
276
+ ws.client_read(data)
277
+
278
+ Read data client-side, used to implement user agents such as
279
+ :mod:`Pyjo.UserAgent`.
280
+ """
281
+ self.server_read(chunk)
282
+
283
+ def client_write(self) -> bytes:
284
+ """::
285
+
286
+ data = ws.client_write()
287
+
288
+ Write data client-side, used to implement user agents such as
289
+ :mod:`Pyjo.UserAgent`.
290
+ """
291
+ return self.server_write()
292
+
293
+ def closed(self) -> Self:
294
+ """::
295
+
296
+ ws = ws.closed()
297
+
298
+ Same as :meth:`Pyjo.Transaction.Pyjo_Transaction.completed`, but also
299
+ indicates that all transaction data has been sent.
300
+ """
301
+ self.completed()
302
+ args = list(self._close) if self._close else [1006]
303
+ return self.emit('finish', *(args if len(args) > 1 else [*args, None]))
304
+
305
+ @property
306
+ def connection(self) -> str | None:
307
+ """::
308
+
309
+ id = ws.connection
310
+
311
+ Connection identifier.
312
+ """
313
+ return self.handshake.connection
314
+
315
+ # Not in Mojolicious: setting it sets the handshake's, which Mojo's getter ignores
316
+ @connection.setter
317
+ def connection(self, value: str | None) -> None:
318
+ self.handshake.connection = value
319
+
320
+ def finish(self, code: int | None = None, reason: str | None = None) -> Self:
321
+ """::
322
+
323
+ ws = ws.finish()
324
+ ws = ws.finish(1000)
325
+ ws = ws.finish(1003, 'Cannot accept data!')
326
+
327
+ Close WebSocket connection gracefully.
328
+ """
329
+ close = self._close = [code, reason] if reason is not None else [code] if code is not None else []
330
+ payload = struct.pack('>H', close[0]) if close and close[0] else b''
331
+ if reason is not None:
332
+ payload += encode('UTF-8', reason)
333
+ if not close:
334
+ close.append(1005)
335
+ elif close[0] is None:
336
+ close[0] = 1005
337
+ self.send((1, 0, 0, 0, WS_CLOSE, payload))._closing = True
338
+
339
+ return self
340
+
341
+ # Not in Mojolicious: the pings that ASGI servers and other WebSocket servers send, so that an application does not
342
+ # repeat a recurring timer and its end for every WebSocket
343
+ def heartbeat(self, interval: float | None = 20, timeout: float | None = 20) -> Self:
344
+ """::
345
+
346
+ ws = ws.heartbeat()
347
+ ws = ws.heartbeat(60, 10)
348
+ ws = ws.heartbeat(None)
349
+
350
+ Send a ``Ping`` frame every ``interval`` seconds while the WebSocket
351
+ connection is established, so that the inactivity timeout of the
352
+ connection does not close a WebSocket without messages, and close the
353
+ connection, with the status ``1006`` of the event ``finish``, when the
354
+ peer sends no frame, as the ``Pong`` that answers it, within
355
+ ``timeout`` seconds. The timers are of :data:`Pyjo.IOLoop.singleton`,
356
+ whose connection :attr:`connection` is. An ``interval`` of ``None``
357
+ or ``0`` stops the pings, and a ``timeout`` of ``None`` or ``0`` waits
358
+ for no frame. ::
359
+
360
+ # Keep the WebSocket of a route alive
361
+ @websocket('/echo')
362
+ def echo(c):
363
+ c.tx.heartbeat()
364
+ c.on('message', lambda c, msg: c.send(f'echo: {msg}'))
365
+ """
366
+ if (heartbeat := self._heartbeat) is not None:
367
+ self._heartbeat = None
368
+ heartbeat.stop()
369
+ if interval:
370
+ self._heartbeat = _Heartbeat(self, interval, timeout)
371
+ return self
372
+
373
+ @property
374
+ def is_websocket(self) -> bool:
375
+ """::
376
+
377
+ boolean = ws.is_websocket
378
+
379
+ True, this is a :mod:`Pyjo.Transaction.WebSocket` object.
380
+ """
381
+ return True
382
+
383
+ @property
384
+ def kept_alive(self) -> bool | None:
385
+ """::
386
+
387
+ boolean = ws.kept_alive
388
+
389
+ Connection has been kept alive.
390
+ """
391
+ return self.handshake.kept_alive
392
+
393
+ # Not in Mojolicious: setting it sets the handshake's, which Mojo's getter ignores
394
+ @kept_alive.setter
395
+ def kept_alive(self, value: bool | None) -> None:
396
+ self.handshake.kept_alive = value
397
+
398
+ @property
399
+ def local_address(self) -> str | None:
400
+ """::
401
+
402
+ address = ws.local_address
403
+
404
+ Local interface address.
405
+ """
406
+ return self.handshake.local_address
407
+
408
+ # Not in Mojolicious: setting it sets the handshake's, which Mojo's getter ignores
409
+ @local_address.setter
410
+ def local_address(self, value: str | None) -> None:
411
+ self.handshake.local_address = value
412
+
413
+ @property
414
+ def local_port(self) -> int | None:
415
+ """::
416
+
417
+ port = ws.local_port
418
+
419
+ Local interface port.
420
+ """
421
+ return self.handshake.local_port
422
+
423
+ # Not in Mojolicious: setting it sets the handshake's, which Mojo's getter ignores
424
+ @local_port.setter
425
+ def local_port(self, value: int | None) -> None:
426
+ self.handshake.local_port = value
427
+
428
+ def parse_message(self, frame: Sequence[Any]) -> object:
429
+ """::
430
+
431
+ ws.parse_message((fin, rsv1, rsv2, rsv3, op, payload))
432
+
433
+ Parse WebSocket message.
434
+ """
435
+ self.emit('frame', frame)
436
+
437
+ # Ping/Pong
438
+ op, payload = frame[4], bytes(frame[5])
439
+ if op == WS_PING:
440
+ return self.send((1, 0, 0, 0, WS_PONG, payload))
441
+ if op == WS_PONG:
442
+ return None
443
+
444
+ # Close
445
+ if op == WS_CLOSE:
446
+ if not len(payload) >= 2:
447
+ return self.finish()
448
+ return self.finish(struct.unpack('>H', payload[:2])[0], decode('UTF-8', payload[2:]))
449
+
450
+ # Append chunk and check message size
451
+ if self._op is None:
452
+ self._op, self._pmc = op, bool(self.compressed and frame[1])
453
+ self._message = (self._message or b'') + payload
454
+ max = self.max_websocket_size
455
+ if len(self._message) > max:
456
+ return self.finish(1009)
457
+
458
+ # No FIN bit (Continuation)
459
+ if not frame[0]:
460
+ return None
461
+
462
+ # "permessage-deflate" extension (handshake and RSV1)
463
+ msg, self._message = self._message, None
464
+ if self.compressed and self._pmc:
465
+ if self._inflate is None:
466
+ self._inflate = zlib.decompressobj(-15)
467
+ out = self._inflate.decompress(msg + b'\x00\x00\xff\xff', max)
468
+ if len(self._inflate.unconsumed_tail):
469
+ return self.finish(1009)
470
+ msg = out
471
+
472
+ if self.has_subscribers('json'):
473
+ self.emit('json', j(msg))
474
+ op, self._op = self._op, None
475
+ self.emit('text' if op == WS_TEXT else 'binary', msg)
476
+ if self.has_subscribers('message'):
477
+ self.emit('message', decode('UTF-8', msg) if op == WS_TEXT else msg)
478
+ return None
479
+
480
+ @property
481
+ def protocol(self) -> str | None:
482
+ """::
483
+
484
+ proto = ws.protocol
485
+
486
+ Return negotiated subprotocol or ``None``.
487
+ """
488
+ return self.res.headers.sec_websocket_protocol
489
+
490
+ @property
491
+ def remote_address(self) -> str | None:
492
+ """::
493
+
494
+ address = ws.remote_address
495
+
496
+ Remote interface address.
497
+ """
498
+ return self.handshake.remote_address
499
+
500
+ # Not in Mojolicious: setting it sets the handshake's, which Mojo's getter ignores
501
+ @remote_address.setter
502
+ def remote_address(self, value: str | None) -> None:
503
+ self.handshake.remote_address = value
504
+
505
+ @property
506
+ def remote_port(self) -> int | None:
507
+ """::
508
+
509
+ port = ws.remote_port
510
+
511
+ Remote interface port.
512
+ """
513
+ return self.handshake.remote_port
514
+
515
+ # Not in Mojolicious: setting it sets the handshake's, which Mojo's getter ignores
516
+ @remote_port.setter
517
+ def remote_port(self, value: int | None) -> None:
518
+ self.handshake.remote_port = value
519
+
520
+ @property
521
+ def req(self) -> Pyjo.Message.Request.object:
522
+ """::
523
+
524
+ req = ws.req
525
+
526
+ Handshake request, usually a :mod:`Pyjo.Message.Request` object.
527
+ """
528
+ return self.handshake.req
529
+
530
+ # Not in Mojolicious: setting it sets the handshake's, which Mojo's getter ignores
531
+ @req.setter
532
+ def req(self, value: Pyjo.Message.Request.object) -> None:
533
+ self.handshake.req = value
534
+
535
+ @property
536
+ def res(self) -> Pyjo.Message.Response.object:
537
+ """::
538
+
539
+ res = ws.res
540
+
541
+ Handshake response, usually a :mod:`Pyjo.Message.Response` object.
542
+ """
543
+ return self.handshake.res
544
+
545
+ # Not in Mojolicious: setting it sets the handshake's, which Mojo's getter ignores
546
+ @res.setter
547
+ def res(self, value: Pyjo.Message.Response.object) -> None:
548
+ self.handshake.res = value
549
+
550
+ def resume(self) -> Self:
551
+ """::
552
+
553
+ ws = ws.resume()
554
+
555
+ Resume :attr:`handshake` transaction.
556
+ """
557
+ self.handshake.resume()
558
+ return self
559
+
560
+ def send(self, msg: str | Mapping[str, Any] | Sequence[Any], cb: Callable[..., object] | None = None) -> Self:
561
+ """::
562
+
563
+ ws = ws.send({'binary': data})
564
+ ws = ws.send({'text': data})
565
+ ws = ws.send({'json': {'test': [1, 2, 3]}})
566
+ ws = ws.send((fin, rsv1, rsv2, rsv3, op, payload))
567
+ ws = ws.send(chars)
568
+ ws = ws.send(chars, lambda ws: ...)
569
+
570
+ Send message or frame non-blocking via WebSocket, the optional drain
571
+ callback will be executed once all data has been written. ::
572
+
573
+ # Send "Ping" frame
574
+ from Pyjo.WebSocket import WS_PING
575
+ ws.send((1, 0, 0, 0, WS_PING, b'Hello World!'))
576
+ """
577
+ if cb is not None:
578
+ self.once('drain', cb)
579
+ frame = self.build_message(msg) if isinstance(msg, str | Mapping) else msg
580
+ fin, rsv1, rsv2, rsv3, op, payload = frame
581
+ self._write = (self._write or b'') + Pyjo.WebSocket.build_frame(self.masked, fin, rsv1, rsv2, rsv3, op, payload)
582
+ return self.emit('resume')
583
+
584
+ def server_read(self, chunk: bytes) -> None:
585
+ """::
586
+
587
+ ws.server_read(data)
588
+
589
+ Read data server-side, used to implement web servers such as
590
+ :mod:`Pyjo.Server.Daemon`.
591
+ """
592
+ self._read += chunk
593
+ max = self.max_websocket_size
594
+ while frame := Pyjo.WebSocket.parse_frame(self._read, max):
595
+ if not isinstance(frame, tuple):
596
+ self.finish(1009)
597
+ break
598
+ self.parse_message(frame)
599
+
600
+ self.emit('resume')
601
+
602
+ def server_write(self) -> bytes:
603
+ """::
604
+
605
+ data = ws.server_write()
606
+
607
+ Write data server-side, used to implement web servers such as
608
+ :mod:`Pyjo.Server.Daemon`.
609
+ """
610
+ if self._write is None:
611
+ self._write = b''
612
+ if not len(self._write):
613
+ self.emit('drain')
614
+ if not len(self._write or b'') and self._closing:
615
+ self.completed()
616
+ write, self._write = self._write or b'', None
617
+ return write
618
+
619
+ def with_compression(self) -> None:
620
+ """::
621
+
622
+ ws.with_compression()
623
+
624
+ Negotiate ``permessage-deflate`` extension for this WebSocket
625
+ connection.
626
+ """
627
+ # "permessage-deflate" extension
628
+ if 'permessage-deflate' in (self.req.headers.sec_websocket_extensions or ''):
629
+ self.compressed = True
630
+ self.res.headers.sec_websocket_extensions = 'permessage-deflate'
631
+
632
+ def with_protocols(self, *protos: str) -> str | None:
633
+ """::
634
+
635
+ proto = ws.with_protocols('v2.proto', 'v1.proto')
636
+
637
+ Negotiate subprotocol for this WebSocket connection.
638
+ """
639
+ offered = {trim(p) for p in (self.req.headers.sec_websocket_protocol or '').split(',')}
640
+ if (proto := next((p for p in protos if p in offered), None)) is None:
641
+ return None
642
+
643
+ self.res.headers.sec_websocket_protocol = proto
644
+ return proto
645
+
646
+
647
+ # The pings of a WebSocket, whose timers refer to it weakly, and which end with it
648
+ class _Heartbeat:
649
+ def __init__(self, ws: Pyjo_Transaction_WebSocket, interval: float, timeout: float | None) -> None:
650
+ self.loop = Pyjo.IOLoop.object.singleton()
651
+ self.pong: str | None = None
652
+ self.timeout = timeout
653
+ self.ws = weakref.ref(ws)
654
+ ref = weakref.ref(self)
655
+
656
+ def ping(loop: Any) -> None:
657
+ if (this := ref()) is not None:
658
+ this.ping()
659
+
660
+ def frame(ws: Any, frame: Any) -> None:
661
+ if (this := ref()) is not None:
662
+ this.frame()
663
+
664
+ def finish(ws: Any, *args: Any) -> None:
665
+ if (this := ref()) is not None:
666
+ this.stop()
667
+
668
+ self.events = [ws.on('frame', frame), ws.on('finish', finish)]
669
+ self.timer = self.loop.recurring(interval, ping)
670
+
671
+ # A frame of the peer, which is there
672
+ def frame(self) -> None:
673
+ if (pong := self.pong) is not None:
674
+ self.pong = None
675
+ self.loop.remove(pong)
676
+
677
+ # The connection of a peer that has not answered
678
+ def gone(self) -> None:
679
+ self.pong = None
680
+ if (ws := self.ws()) is not None and (connection := ws.connection) is not None:
681
+ self.loop.remove(connection)
682
+
683
+ def ping(self) -> None:
684
+ if (ws := self.ws()) is None:
685
+ return self.stop()
686
+ if not ws.established or self.pong is not None:
687
+ return
688
+ ws.send((1, 0, 0, 0, WS_PING, b''))
689
+ if self.timeout:
690
+ ref = weakref.ref(self)
691
+
692
+ def gone(loop: Any) -> None:
693
+ if (this := ref()) is not None:
694
+ this.gone()
695
+
696
+ self.pong = self.loop.timer(self.timeout, gone)
697
+
698
+ def stop(self) -> None:
699
+ self.loop.remove(self.timer)
700
+ if (pong := self.pong) is not None:
701
+ self.pong = None
702
+ self.loop.remove(pong)
703
+ if (ws := self.ws()) is not None:
704
+ ws.unsubscribe('frame', self.events[0]).unsubscribe('finish', self.events[1])
705
+
706
+
707
+ new = Pyjo_Transaction_WebSocket.new
708
+ object = Pyjo_Transaction_WebSocket
709
+ WebSocket = Pyjo_Transaction_WebSocket