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
Pyjo/IOLoop/Messages.py
ADDED
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.IOLoop.Messages - Messages on a stream
|
|
3
|
+
===========================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.IOLoop.Messages import Messages
|
|
7
|
+
from Pyjo.IOLoop.Stdio import Stdio
|
|
8
|
+
|
|
9
|
+
# Echo the lines of the standard input in capitals
|
|
10
|
+
messages = Messages(Stdio())
|
|
11
|
+
messages.on('message', lambda messages, message: messages.send(message.upper()))
|
|
12
|
+
|
|
13
|
+
# Or import the module, as in Google Python Style Guide
|
|
14
|
+
|
|
15
|
+
import Pyjo.IOLoop.Messages
|
|
16
|
+
import Pyjo.IOLoop.Stdio
|
|
17
|
+
|
|
18
|
+
# Echo the lines of the standard input in capitals
|
|
19
|
+
messages = Pyjo.IOLoop.Messages.new(Pyjo.IOLoop.Stdio.new())
|
|
20
|
+
messages.on('message', lambda messages, message: messages.send(message.upper()))
|
|
21
|
+
messages.on('close', lambda messages: messages.stream.reactor.stop())
|
|
22
|
+
messages.start()
|
|
23
|
+
|
|
24
|
+
# The messages of the Language Server Protocol, framed by Content-Length headers
|
|
25
|
+
messages = Pyjo.IOLoop.Messages.new(stream, framing=Pyjo.IOLoop.Messages.ContentLength())
|
|
26
|
+
|
|
27
|
+
# The standard output and input of a server in a child process
|
|
28
|
+
child = Pyjo.IOLoop.ChildProcess.new().spawn(['./server.py'])
|
|
29
|
+
messages = Pyjo.IOLoop.Messages.new(child.stdout, output=child.stdin)
|
|
30
|
+
messages.send('{"jsonrpc": "2.0", "method": "update", "params": [1]}')
|
|
31
|
+
|
|
32
|
+
# Start reactor if necessary
|
|
33
|
+
if not messages.stream.reactor.is_running:
|
|
34
|
+
messages.stream.reactor.start()
|
|
35
|
+
|
|
36
|
+
:mod:`Pyjo.IOLoop.Messages` is a stream of messages on a
|
|
37
|
+
:mod:`Pyjo.IOLoop.Stream`, a stream of bytes, as a connection of TCP or of a
|
|
38
|
+
Unix domain socket, the standard input and output of :mod:`Pyjo.IOLoop.Stdio`,
|
|
39
|
+
or the standard output and input of a :mod:`Pyjo.IOLoop.ChildProcess`. The
|
|
40
|
+
framing of the messages tells where one ends and the next begins, as the line
|
|
41
|
+
of :class:`Lines`, the default, the headers of :class:`ContentLength` or the
|
|
42
|
+
length of :class:`LengthPrefix`. A message is :class:`bytes`, and the frame of
|
|
43
|
+
a message is written as a whole, so that the frames of two messages never
|
|
44
|
+
interleave.
|
|
45
|
+
|
|
46
|
+
A message that would be larger than :attr:`~Pyjo_IOLoop_Messages.max_message_size`
|
|
47
|
+
closes the stream gracefully with an error, as the stream can not tell where
|
|
48
|
+
the next message begins: nothing more is read, and the messages that have been
|
|
49
|
+
sent are written.
|
|
50
|
+
|
|
51
|
+
A framing is an object with the method ``decode``, which takes the next
|
|
52
|
+
message that is whole from the front of a :class:`bytearray` of what has been
|
|
53
|
+
read and returns it, or ``None`` until it is whole, raising a
|
|
54
|
+
:mod:`Pyjo.Exception` for a message that is larger than the maximum size, and
|
|
55
|
+
the method ``encode``, which returns the frame of a message. A framing keeps what it knows of the stream that it decodes, as the
|
|
56
|
+
length of the message whose headers it has read, so each stream of messages
|
|
57
|
+
has one of its own. ::
|
|
58
|
+
|
|
59
|
+
# Messages of netstrings, as 3:abc,
|
|
60
|
+
class Netstrings:
|
|
61
|
+
def decode(self, buffer, max_size):
|
|
62
|
+
if (colon := buffer.find(b':')) < 0:
|
|
63
|
+
return None
|
|
64
|
+
length = int(buffer[:colon])
|
|
65
|
+
if max_size and length > max_size:
|
|
66
|
+
raise Pyjo.Exception.new('Maximum message size exceeded')
|
|
67
|
+
if len(buffer) < colon + length + 2:
|
|
68
|
+
return None
|
|
69
|
+
message = bytes(buffer[colon + 1 : colon + 1 + length])
|
|
70
|
+
del buffer[: colon + length + 2]
|
|
71
|
+
return message
|
|
72
|
+
|
|
73
|
+
def encode(self, message):
|
|
74
|
+
return b'%d:%s,' % (len(message), message)
|
|
75
|
+
|
|
76
|
+
messages = Pyjo.IOLoop.Messages.new(stream, framing=Netstrings())
|
|
77
|
+
|
|
78
|
+
Events
|
|
79
|
+
------
|
|
80
|
+
|
|
81
|
+
:mod:`Pyjo.IOLoop.Messages` inherits all events from :mod:`Pyjo.EventEmitter`
|
|
82
|
+
and can emit the following new ones.
|
|
83
|
+
|
|
84
|
+
close
|
|
85
|
+
~~~~~
|
|
86
|
+
::
|
|
87
|
+
|
|
88
|
+
messages.on('close', lambda messages: ...)
|
|
89
|
+
|
|
90
|
+
Emitted once the stream has been closed, or the input, when the input and the
|
|
91
|
+
output are two, as the peer ends its output when it has no more to send.
|
|
92
|
+
|
|
93
|
+
end
|
|
94
|
+
~~~
|
|
95
|
+
::
|
|
96
|
+
|
|
97
|
+
messages.on('end', lambda messages: ...)
|
|
98
|
+
|
|
99
|
+
Emitted once the input has ended, as the event ``end`` of
|
|
100
|
+
:mod:`Pyjo.IOLoop.Stdio`, while messages can still be sent, as the responses of
|
|
101
|
+
the requests that are in progress. Without a subscriber of this event, the
|
|
102
|
+
stream closes gracefully, once all messages have been written; with one, the
|
|
103
|
+
subscriber closes it. ::
|
|
104
|
+
|
|
105
|
+
messages.on('end', lambda messages: messages.close_gracefully())
|
|
106
|
+
|
|
107
|
+
error
|
|
108
|
+
~~~~~
|
|
109
|
+
::
|
|
110
|
+
|
|
111
|
+
messages.on('error', lambda messages, err: ...)
|
|
112
|
+
|
|
113
|
+
Emitted if an error occurs on the stream, as a message that is larger than the
|
|
114
|
+
maximum size, fatal if unhandled. ::
|
|
115
|
+
|
|
116
|
+
messages.on('error', lambda messages, err: print(f'This looks bad: {err}'))
|
|
117
|
+
|
|
118
|
+
message
|
|
119
|
+
~~~~~~~
|
|
120
|
+
::
|
|
121
|
+
|
|
122
|
+
messages.on('message', lambda messages, message: ...)
|
|
123
|
+
|
|
124
|
+
Emitted for each message that has arrived, without its framing. ::
|
|
125
|
+
|
|
126
|
+
messages.on('message', lambda messages, message: print(f'Message: {message!r}'))
|
|
127
|
+
|
|
128
|
+
timeout
|
|
129
|
+
~~~~~~~
|
|
130
|
+
::
|
|
131
|
+
|
|
132
|
+
messages.on('timeout', lambda messages: ...)
|
|
133
|
+
|
|
134
|
+
Emitted if the stream has been inactive for too long and will get closed
|
|
135
|
+
automatically, as the event ``timeout`` of :mod:`Pyjo.IOLoop.Stream`.
|
|
136
|
+
|
|
137
|
+
See also
|
|
138
|
+
--------
|
|
139
|
+
|
|
140
|
+
:mod:`Pyjo.IOLoop.Stream`, :mod:`Pyjo.IOLoop.Stdio`, :mod:`Pyjoyment`,
|
|
141
|
+
https://mojolicious.org.
|
|
142
|
+
|
|
143
|
+
It has no counterpart in Mojolicious, and the framings follow the transport
|
|
144
|
+
stdio of the Model Context Protocol, the base protocol of the Language Server
|
|
145
|
+
Protocol, and Native Messaging of the extensions of the browsers.
|
|
146
|
+
|
|
147
|
+
Classes
|
|
148
|
+
-------
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
import Pyjo.Base
|
|
152
|
+
import Pyjo.EventEmitter
|
|
153
|
+
import Pyjo.Exception
|
|
154
|
+
import Pyjo.IOLoop.Stream
|
|
155
|
+
import Pyjo.Promise
|
|
156
|
+
|
|
157
|
+
import os
|
|
158
|
+
import re
|
|
159
|
+
import sys
|
|
160
|
+
|
|
161
|
+
from collections.abc import AsyncIterator, Callable
|
|
162
|
+
from typing import Any, Literal, Protocol, Self
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class Framing(Protocol):
|
|
166
|
+
"""
|
|
167
|
+
The framing of the messages of a stream, as :class:`Lines`.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def decode(self, buffer: bytearray, max_size: int) -> bytes | None:
|
|
171
|
+
"""::
|
|
172
|
+
|
|
173
|
+
message = framing.decode(buffer, 16777216)
|
|
174
|
+
|
|
175
|
+
Take the next message that is whole from the front of ``buffer`` and
|
|
176
|
+
return it, without its framing, or ``None`` until it is whole, or raise
|
|
177
|
+
a :mod:`Pyjo.Exception` for a message that is larger than ``max_size``,
|
|
178
|
+
unless it is ``0``.
|
|
179
|
+
"""
|
|
180
|
+
...
|
|
181
|
+
|
|
182
|
+
def encode(self, message: bytes) -> bytes:
|
|
183
|
+
"""::
|
|
184
|
+
|
|
185
|
+
frame = framing.encode(b'Hello')
|
|
186
|
+
|
|
187
|
+
Return the frame of a message.
|
|
188
|
+
"""
|
|
189
|
+
...
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Lines(Pyjo.Base.object):
|
|
193
|
+
"""
|
|
194
|
+
Messages of one line each, as the transport stdio of the Model Context
|
|
195
|
+
Protocol and many protocols of text have them, which end with a line feed,
|
|
196
|
+
or a carriage return and a line feed, which :attr:`terminator` writes. A
|
|
197
|
+
message has no line feed. ::
|
|
198
|
+
|
|
199
|
+
framing = Pyjo.IOLoop.Messages.Lines.new()
|
|
200
|
+
framing = Pyjo.IOLoop.Messages.Lines.new(terminator=b'\\r\\n')
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
def __init__(self, terminator: bytes = b'\n') -> None:
|
|
204
|
+
self.terminator = terminator
|
|
205
|
+
"""::
|
|
206
|
+
|
|
207
|
+
terminator = framing.terminator
|
|
208
|
+
framing.terminator = b'\\r\\n'
|
|
209
|
+
|
|
210
|
+
Terminator of the lines that are written, defaults to ``b'\\n'``. Both
|
|
211
|
+
are read.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
# The part of the buffer that has no line feed, which the next decode does not search again
|
|
215
|
+
self._searched = 0
|
|
216
|
+
|
|
217
|
+
def decode(self, buffer: bytearray, max_size: int) -> bytes | None:
|
|
218
|
+
if (end := buffer.find(b'\n', self._searched)) < 0:
|
|
219
|
+
self._searched = len(buffer)
|
|
220
|
+
|
|
221
|
+
# A line that is longer already, without a carriage return that may end it
|
|
222
|
+
if max_size and len(buffer) - buffer.endswith(b'\r') > max_size:
|
|
223
|
+
raise Pyjo.Exception.new('Maximum message size exceeded')
|
|
224
|
+
return None
|
|
225
|
+
message = bytes(buffer[: end - 1 if end and buffer[end - 1] == 13 else end])
|
|
226
|
+
if max_size and len(message) > max_size:
|
|
227
|
+
raise Pyjo.Exception.new('Maximum message size exceeded')
|
|
228
|
+
del buffer[: end + 1]
|
|
229
|
+
self._searched = 0
|
|
230
|
+
return message
|
|
231
|
+
|
|
232
|
+
def encode(self, message: bytes) -> bytes:
|
|
233
|
+
if b'\n' in message:
|
|
234
|
+
raise Pyjo.Exception.new('Message with a line feed')
|
|
235
|
+
return message + self.terminator
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class ContentLength(Pyjo.Base.object):
|
|
239
|
+
"""
|
|
240
|
+
Messages with headers, of which ``Content-Length`` has their size, as the
|
|
241
|
+
base protocol of the Language Server Protocol and of the Debug Adapter
|
|
242
|
+
Protocol have them. The other headers, as ``Content-Type``, are read and
|
|
243
|
+
not written. ::
|
|
244
|
+
|
|
245
|
+
framing = Pyjo.IOLoop.Messages.ContentLength.new()
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
max_header_size = 8192
|
|
249
|
+
"""::
|
|
250
|
+
|
|
251
|
+
size = framing.max_header_size
|
|
252
|
+
framing.max_header_size = 1024
|
|
253
|
+
|
|
254
|
+
Maximum size of the headers of a message in bytes, defaults to ``8192``.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
def __init__(self) -> None:
|
|
258
|
+
# The size of the message whose headers have been read
|
|
259
|
+
self._length: int | None = None
|
|
260
|
+
|
|
261
|
+
def decode(self, buffer: bytearray, max_size: int) -> bytes | None:
|
|
262
|
+
if (length := self._length) is None:
|
|
263
|
+
if (end := buffer.find(b'\r\n\r\n')) < 0:
|
|
264
|
+
if len(buffer) > self.max_header_size:
|
|
265
|
+
raise Pyjo.Exception.new('Maximum header size exceeded')
|
|
266
|
+
return None
|
|
267
|
+
headers = bytes(buffer[:end]).decode('latin-1')
|
|
268
|
+
if not (match := re.search(r'(?im)^content-length:[ \t]*(\d+)[ \t]*$', headers)):
|
|
269
|
+
raise Pyjo.Exception.new('Message without Content-Length')
|
|
270
|
+
length = int(match[1])
|
|
271
|
+
if max_size and length > max_size:
|
|
272
|
+
raise Pyjo.Exception.new('Maximum message size exceeded')
|
|
273
|
+
del buffer[: end + 4]
|
|
274
|
+
self._length = length
|
|
275
|
+
if len(buffer) < length:
|
|
276
|
+
return None
|
|
277
|
+
message = bytes(buffer[:length])
|
|
278
|
+
del buffer[:length]
|
|
279
|
+
self._length = None
|
|
280
|
+
return message
|
|
281
|
+
|
|
282
|
+
def encode(self, message: bytes) -> bytes:
|
|
283
|
+
return b'Content-Length: %d\r\n\r\n%s' % (len(message), message)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class LengthPrefix(Pyjo.Base.object):
|
|
287
|
+
"""
|
|
288
|
+
Messages that follow their size, as an unsigned integer of :attr:`size`
|
|
289
|
+
bytes, as Native Messaging of the extensions of the browsers has them, with
|
|
290
|
+
4 bytes in the native byte order. ::
|
|
291
|
+
|
|
292
|
+
framing = Pyjo.IOLoop.Messages.LengthPrefix.new()
|
|
293
|
+
framing = Pyjo.IOLoop.Messages.LengthPrefix.new(size=2, byteorder='big')
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
def __init__(self, size: int = 4, byteorder: Literal['little', 'big'] = sys.byteorder) -> None:
|
|
297
|
+
self.size = size
|
|
298
|
+
"""::
|
|
299
|
+
|
|
300
|
+
size = framing.size
|
|
301
|
+
framing.size = 2
|
|
302
|
+
|
|
303
|
+
Size of the length of a message in bytes, defaults to ``4``.
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
self.byteorder: Literal['little', 'big'] = byteorder
|
|
307
|
+
"""::
|
|
308
|
+
|
|
309
|
+
byteorder = framing.byteorder
|
|
310
|
+
framing.byteorder = 'big'
|
|
311
|
+
|
|
312
|
+
Byte order of the length of a message, ``'little'`` or ``'big'``,
|
|
313
|
+
defaults to the native one, :data:`sys.byteorder`.
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
def decode(self, buffer: bytearray, max_size: int) -> bytes | None:
|
|
317
|
+
size = self.size
|
|
318
|
+
if len(buffer) < size:
|
|
319
|
+
return None
|
|
320
|
+
length = int.from_bytes(buffer[:size], self.byteorder)
|
|
321
|
+
if max_size and length > max_size:
|
|
322
|
+
raise Pyjo.Exception.new('Maximum message size exceeded')
|
|
323
|
+
if len(buffer) < size + length:
|
|
324
|
+
return None
|
|
325
|
+
message = bytes(buffer[size : size + length])
|
|
326
|
+
del buffer[: size + length]
|
|
327
|
+
return message
|
|
328
|
+
|
|
329
|
+
def encode(self, message: bytes) -> bytes:
|
|
330
|
+
try:
|
|
331
|
+
return len(message).to_bytes(self.size, self.byteorder) + message
|
|
332
|
+
except OverflowError:
|
|
333
|
+
raise Pyjo.Exception.new('Message too large for its length') from None
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class Pyjo_IOLoop_Messages(Pyjo.EventEmitter.object):
|
|
337
|
+
"""
|
|
338
|
+
:mod:`Pyjo.IOLoop.Messages` inherits all methods from
|
|
339
|
+
:mod:`Pyjo.EventEmitter` and implements the following new ones.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
def __init__(
|
|
343
|
+
self,
|
|
344
|
+
stream: Pyjo.IOLoop.Stream.object,
|
|
345
|
+
*,
|
|
346
|
+
framing: Framing | None = None,
|
|
347
|
+
output: Pyjo.IOLoop.Stream.object | None = None,
|
|
348
|
+
) -> None:
|
|
349
|
+
"""::
|
|
350
|
+
|
|
351
|
+
messages = Pyjo.IOLoop.Messages.new(stream)
|
|
352
|
+
messages = Pyjo.IOLoop.Messages.new(stream, framing=Pyjo.IOLoop.Messages.ContentLength())
|
|
353
|
+
messages = Pyjo.IOLoop.Messages.new(child.stdout, output=child.stdin)
|
|
354
|
+
|
|
355
|
+
Construct a new :mod:`Pyjo.IOLoop.Messages` object, which reads and
|
|
356
|
+
writes ``stream``, or reads it and writes ``output``.
|
|
357
|
+
"""
|
|
358
|
+
super().__init__()
|
|
359
|
+
|
|
360
|
+
self.framing: Framing = framing if framing is not None else Lines()
|
|
361
|
+
"""::
|
|
362
|
+
|
|
363
|
+
framing = messages.framing
|
|
364
|
+
messages.framing = Pyjo.IOLoop.Messages.ContentLength()
|
|
365
|
+
|
|
366
|
+
Framing of the messages, defaults to a :class:`Lines` object.
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
self.stream = stream
|
|
370
|
+
"""::
|
|
371
|
+
|
|
372
|
+
stream = messages.stream
|
|
373
|
+
|
|
374
|
+
:mod:`Pyjo.IOLoop.Stream` object that the messages are read from, and
|
|
375
|
+
written to, unless :attr:`output` is another one.
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
self.output = output if output is not None else stream
|
|
379
|
+
"""::
|
|
380
|
+
|
|
381
|
+
stream = messages.output
|
|
382
|
+
|
|
383
|
+
:mod:`Pyjo.IOLoop.Stream` object that the messages are written to,
|
|
384
|
+
defaults to :attr:`stream`.
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
self._buffer = bytearray()
|
|
388
|
+
self._closed = False
|
|
389
|
+
self._failed = False
|
|
390
|
+
self._max_message_size: int | None = None
|
|
391
|
+
|
|
392
|
+
# The stream refers to this object until it closes, which the subscribers that it keeps drop
|
|
393
|
+
self._subscribed: list[tuple[Pyjo.IOLoop.Stream.object, str, Callable[..., Any]]] = []
|
|
394
|
+
|
|
395
|
+
def read(stream: Any, chunk: bytes) -> None:
|
|
396
|
+
self._read(chunk)
|
|
397
|
+
|
|
398
|
+
def end(stream: Any) -> None:
|
|
399
|
+
self._end()
|
|
400
|
+
|
|
401
|
+
def close(stream: Pyjo.IOLoop.Stream.object) -> None:
|
|
402
|
+
self._close(stream)
|
|
403
|
+
|
|
404
|
+
def error(stream: Any, e: Exception) -> None:
|
|
405
|
+
self.emit('error', e)
|
|
406
|
+
|
|
407
|
+
def timeout(stream: Any) -> None:
|
|
408
|
+
self.emit('timeout')
|
|
409
|
+
|
|
410
|
+
self._subscribe(stream, 'read', read)
|
|
411
|
+
self._subscribe(stream, 'end', end)
|
|
412
|
+
for handle in (stream, self.output) if self.output is not stream else (stream,):
|
|
413
|
+
self._subscribe(handle, 'close', close)
|
|
414
|
+
self._subscribe(handle, 'error', error)
|
|
415
|
+
self._subscribe(handle, 'timeout', timeout)
|
|
416
|
+
|
|
417
|
+
def __aiter__(self) -> AsyncIterator[bytes]:
|
|
418
|
+
"""::
|
|
419
|
+
|
|
420
|
+
async for message in messages:
|
|
421
|
+
...
|
|
422
|
+
|
|
423
|
+
Same as :meth:`iterate`.
|
|
424
|
+
"""
|
|
425
|
+
return self.iterate()
|
|
426
|
+
|
|
427
|
+
def close(self) -> None:
|
|
428
|
+
"""::
|
|
429
|
+
|
|
430
|
+
messages.close()
|
|
431
|
+
|
|
432
|
+
Close the stream immediately.
|
|
433
|
+
"""
|
|
434
|
+
self.stream.close()
|
|
435
|
+
self.output.close()
|
|
436
|
+
|
|
437
|
+
def close_gracefully(self) -> None:
|
|
438
|
+
"""::
|
|
439
|
+
|
|
440
|
+
messages.close_gracefully()
|
|
441
|
+
|
|
442
|
+
Close the stream gracefully, once all messages have been written, or,
|
|
443
|
+
when the input and the output are two, as the standard input and output
|
|
444
|
+
of a child process, the output, and the input is read until the peer
|
|
445
|
+
ends it, so that the responses of the peer still come in. ::
|
|
446
|
+
|
|
447
|
+
# The last request of a server of the standard input and output, and its response
|
|
448
|
+
messages.send(request).close_gracefully()
|
|
449
|
+
"""
|
|
450
|
+
self.output.close_gracefully()
|
|
451
|
+
|
|
452
|
+
def close_gracefully_p(self) -> Pyjo.Promise.object:
|
|
453
|
+
"""::
|
|
454
|
+
|
|
455
|
+
promise = messages.close_gracefully_p()
|
|
456
|
+
|
|
457
|
+
Same as :meth:`close_gracefully`, but returns a :mod:`Pyjo.Promise`
|
|
458
|
+
object, which is fulfilled once the stream has been closed, also when it
|
|
459
|
+
is closed already. ::
|
|
460
|
+
|
|
461
|
+
await messages.send_p('Bye!')
|
|
462
|
+
await messages.close_gracefully_p()
|
|
463
|
+
"""
|
|
464
|
+
if self._closed:
|
|
465
|
+
return Pyjo.Promise.resolve()
|
|
466
|
+
promise = self.once_p('close')
|
|
467
|
+
self.close_gracefully()
|
|
468
|
+
return promise
|
|
469
|
+
|
|
470
|
+
def iterate(self) -> AsyncIterator[bytes]:
|
|
471
|
+
"""::
|
|
472
|
+
|
|
473
|
+
async for message in messages.iterate():
|
|
474
|
+
...
|
|
475
|
+
|
|
476
|
+
The messages as an asynchronous iterator, until the end of the stream,
|
|
477
|
+
which :meth:`message_p` takes. ::
|
|
478
|
+
|
|
479
|
+
# Echo in capitals, one message after another
|
|
480
|
+
async for message in messages:
|
|
481
|
+
await messages.send_p(message.upper())
|
|
482
|
+
"""
|
|
483
|
+
return self._messages().iterate()
|
|
484
|
+
|
|
485
|
+
@property
|
|
486
|
+
def max_message_size(self) -> int:
|
|
487
|
+
"""::
|
|
488
|
+
|
|
489
|
+
size = messages.max_message_size
|
|
490
|
+
messages.max_message_size = 1024
|
|
491
|
+
|
|
492
|
+
Maximum size of a message in bytes, defaults to the value of the
|
|
493
|
+
``PYJO_MAX_MESSAGE_SIZE`` environment variable or ``16777216``
|
|
494
|
+
(16MiB). Setting the value to ``0`` will allow messages of indefinite
|
|
495
|
+
size.
|
|
496
|
+
"""
|
|
497
|
+
if self._max_message_size is None:
|
|
498
|
+
size = os.environ.get('PYJO_MAX_MESSAGE_SIZE')
|
|
499
|
+
self._max_message_size = int(size) if size is not None else 16777216
|
|
500
|
+
return self._max_message_size
|
|
501
|
+
|
|
502
|
+
@max_message_size.setter
|
|
503
|
+
def max_message_size(self, value: int) -> None:
|
|
504
|
+
self._max_message_size = value
|
|
505
|
+
|
|
506
|
+
def message_p(self) -> Pyjo.Promise.object:
|
|
507
|
+
"""::
|
|
508
|
+
|
|
509
|
+
promise = messages.message_p()
|
|
510
|
+
|
|
511
|
+
Return a :mod:`Pyjo.Promise` object, which is fulfilled with the next
|
|
512
|
+
message, or ``None`` at the end of the stream, the event ``close`` or
|
|
513
|
+
``end``, or when it is closed already, and rejected with the error of
|
|
514
|
+
the event ``error``, or a :class:`TimeoutError` when the stream has been
|
|
515
|
+
inactive for too long. The stream
|
|
516
|
+
reads only while a promise waits, as :meth:`~Pyjo.IOLoop.Stream.Pyjo_IOLoop_Stream.read_p`
|
|
517
|
+
of :mod:`Pyjo.IOLoop.Stream` does, and the subscribers of the event
|
|
518
|
+
``message`` get the messages that it reads then. A loop of them handles
|
|
519
|
+
the messages one after another, where the events handle them at the
|
|
520
|
+
same time, as :meth:`~Pyjo.JSON.RPC.Pyjo_JSON_RPC.serve` of
|
|
521
|
+
:mod:`Pyjo.JSON.RPC` does. ::
|
|
522
|
+
|
|
523
|
+
while (message := await messages.message_p()) is not None:
|
|
524
|
+
print(message)
|
|
525
|
+
"""
|
|
526
|
+
return self._messages().next_p()
|
|
527
|
+
|
|
528
|
+
def once_p(self, name: str) -> Pyjo.Promise.object:
|
|
529
|
+
"""::
|
|
530
|
+
|
|
531
|
+
promise = messages.once_p('close')
|
|
532
|
+
|
|
533
|
+
Same as :meth:`~Pyjo.EventEmitter.Pyjo_EventEmitter.once_p` of
|
|
534
|
+
:mod:`Pyjo.EventEmitter`, but fulfilled at once for the event
|
|
535
|
+
``close`` of a stream that is closed already, as it comes no more.
|
|
536
|
+
"""
|
|
537
|
+
if name == 'close' and self._closed:
|
|
538
|
+
return Pyjo.Promise.resolve()
|
|
539
|
+
return super().once_p(name)
|
|
540
|
+
|
|
541
|
+
def send(self, message: bytes | str, cb: Callable[[Self], Any] | None = None) -> Self:
|
|
542
|
+
"""::
|
|
543
|
+
|
|
544
|
+
messages = messages.send(b'Hello')
|
|
545
|
+
messages = messages.send('Hello')
|
|
546
|
+
messages = messages.send('Hello', lambda messages: ...)
|
|
547
|
+
|
|
548
|
+
Send a message, :class:`bytes`, or :class:`str` that is encoded in
|
|
549
|
+
UTF-8, the optional drain callback will be executed once all data has
|
|
550
|
+
been written.
|
|
551
|
+
"""
|
|
552
|
+
if isinstance(message, str):
|
|
553
|
+
message = message.encode()
|
|
554
|
+
frame = self.framing.encode(message)
|
|
555
|
+
self.output.write(frame, (lambda stream: cb(self)) if cb is not None else None)
|
|
556
|
+
return self
|
|
557
|
+
|
|
558
|
+
def send_p(self, message: bytes | str) -> Pyjo.Promise.object:
|
|
559
|
+
"""::
|
|
560
|
+
|
|
561
|
+
promise = messages.send_p(b'Hello')
|
|
562
|
+
|
|
563
|
+
Same as :meth:`send`, but returns a :mod:`Pyjo.Promise` object, which
|
|
564
|
+
is fulfilled once all data has been written, and rejected if the stream
|
|
565
|
+
closes before, or is closed already, or with a :class:`TimeoutError`
|
|
566
|
+
when it has been inactive for too long. ::
|
|
567
|
+
|
|
568
|
+
await messages.send_p('Hello')
|
|
569
|
+
"""
|
|
570
|
+
if self._closed:
|
|
571
|
+
return Pyjo.Promise.reject(Pyjo.Exception.new('Stream closed before all data has been written'))
|
|
572
|
+
promise = Pyjo.Promise.new()
|
|
573
|
+
|
|
574
|
+
def closed(messages: Any) -> None:
|
|
575
|
+
self.unsubscribe('timeout', inactive)
|
|
576
|
+
promise.reject(Pyjo.Exception.new('Stream closed before all data has been written'))
|
|
577
|
+
|
|
578
|
+
def timeout(messages: Any) -> None:
|
|
579
|
+
self.unsubscribe('close', wrapper)
|
|
580
|
+
promise.reject(TimeoutError('Inactivity timeout'))
|
|
581
|
+
|
|
582
|
+
wrapper = self.once('close', closed)
|
|
583
|
+
inactive = self.once('timeout', timeout)
|
|
584
|
+
|
|
585
|
+
def drained(messages: Any) -> None:
|
|
586
|
+
self.unsubscribe('close', wrapper).unsubscribe('timeout', inactive)
|
|
587
|
+
promise.resolve()
|
|
588
|
+
|
|
589
|
+
self.send(message, drained)
|
|
590
|
+
return self._hold(promise)
|
|
591
|
+
|
|
592
|
+
def start(self) -> None:
|
|
593
|
+
"""::
|
|
594
|
+
|
|
595
|
+
messages.start()
|
|
596
|
+
|
|
597
|
+
Start or resume reading messages from the stream.
|
|
598
|
+
"""
|
|
599
|
+
self.stream.start()
|
|
600
|
+
|
|
601
|
+
def stop(self) -> None:
|
|
602
|
+
"""::
|
|
603
|
+
|
|
604
|
+
messages.stop()
|
|
605
|
+
|
|
606
|
+
Stop reading messages from the stream.
|
|
607
|
+
"""
|
|
608
|
+
self.stream.stop()
|
|
609
|
+
|
|
610
|
+
# The pull of the messages, which a stream that is closed already has ended
|
|
611
|
+
def _messages(self) -> Any:
|
|
612
|
+
pull = self._pull('message', True)
|
|
613
|
+
if self._closed:
|
|
614
|
+
pull.end()
|
|
615
|
+
return pull
|
|
616
|
+
|
|
617
|
+
def _close(self, stream: Pyjo.IOLoop.Stream.object) -> None:
|
|
618
|
+
# An output of its own that is closed, while the input is still read until the peer ends it
|
|
619
|
+
if self._closed or stream is not self.stream:
|
|
620
|
+
return
|
|
621
|
+
self._closed = True
|
|
622
|
+
for emitter, name, cb in self._subscribed:
|
|
623
|
+
emitter.unsubscribe(name, cb)
|
|
624
|
+
self._subscribed = []
|
|
625
|
+
if self.output is not self.stream:
|
|
626
|
+
self.output.close_gracefully()
|
|
627
|
+
self.emit('close')
|
|
628
|
+
|
|
629
|
+
def _end(self) -> None:
|
|
630
|
+
if self._failed or not self.has_subscribers('end'):
|
|
631
|
+
return self.close_gracefully()
|
|
632
|
+
self.emit('end')
|
|
633
|
+
|
|
634
|
+
def _read(self, chunk: bytes) -> None:
|
|
635
|
+
if self._failed:
|
|
636
|
+
return
|
|
637
|
+
buffer = self._buffer
|
|
638
|
+
buffer += chunk
|
|
639
|
+
while not self._closed:
|
|
640
|
+
try:
|
|
641
|
+
message = self.framing.decode(buffer, self.max_message_size)
|
|
642
|
+
|
|
643
|
+
# Nothing more is read, as where the next message begins is unknown, and what has been sent is written
|
|
644
|
+
except Pyjo.Exception.object as e:
|
|
645
|
+
self._failed = True
|
|
646
|
+
buffer.clear()
|
|
647
|
+
self.stream.stop()
|
|
648
|
+
try:
|
|
649
|
+
self.emit('error', e)
|
|
650
|
+
finally:
|
|
651
|
+
self.close_gracefully()
|
|
652
|
+
return
|
|
653
|
+
if message is None:
|
|
654
|
+
return
|
|
655
|
+
self.emit('message', message)
|
|
656
|
+
|
|
657
|
+
def _subscribe(self, emitter: Pyjo.IOLoop.Stream.object, name: str, cb: Callable[..., Any]) -> None:
|
|
658
|
+
emitter.on(name, cb)
|
|
659
|
+
self._subscribed.append((emitter, name, cb))
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
new = Pyjo_IOLoop_Messages.new
|
|
663
|
+
object = Pyjo_IOLoop_Messages
|
|
664
|
+
Messages = Pyjo_IOLoop_Messages
|