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,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