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.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- 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
|