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,1036 @@
1
+ r"""
2
+ Pyjo.IOLoop - Minimalistic event loop
3
+ =====================================
4
+ ::
5
+
6
+ import Pyjo.IOLoop
7
+
8
+ # Listen on port 3000
9
+ @Pyjo.IOLoop.server(port=3000)
10
+ def server(loop, stream, id):
11
+ @stream.on
12
+ def read(stream, chunk):
13
+ # Process input chunk
14
+ print(chunk)
15
+
16
+ # Write response
17
+ stream.write(b'HTTP/1.1 200 OK')
18
+
19
+ # Connect to port 3000
20
+ @Pyjo.IOLoop.client(port=3000)
21
+ def id(loop, err, stream):
22
+ @stream.on
23
+ def read(stream, chunk):
24
+ # Process input
25
+ print(f'Input: {chunk}')
26
+
27
+ # Write request
28
+ stream.write(b'GET / HTTP/1.1\x0d\x0a\x0d\x0a')
29
+
30
+ # Add a timer
31
+ Pyjo.IOLoop.timer(5, lambda loop: loop.remove(id))
32
+
33
+ # Start event loop if necessary
34
+ if not Pyjo.IOLoop.is_running():
35
+ Pyjo.IOLoop.start()
36
+
37
+ :mod:`Pyjo.IOLoop` is a very minimalistic event loop based on
38
+ :mod:`Pyjo.Reactor`, it has been reduced to the absolute minimal feature set
39
+ required to build solid and scalable non-blocking clients and servers.
40
+
41
+ Depending on operating system, the default per-process and system-wide file
42
+ descriptor limits are often very low and need to be tuned for better
43
+ scalability.
44
+
45
+ The event loop is resilient to time jumps, as it uses a monotonic clock. A
46
+ TLS certificate and key are also built right in, to make writing test servers
47
+ as easy as possible.
48
+
49
+ What Mojo calls on the class, as ``Mojo::IOLoop->timer``, is a function of
50
+ the module that works with the :data:`singleton`, as ``Pyjo.IOLoop.timer``.
51
+ Each thread has a :data:`singleton` of its own, as each thread of Perl has a
52
+ copy of the interpreter, so that the threads run event loops of their own at
53
+ the same time, and the functions of the module work with the one of the
54
+ thread that calls them.
55
+ A method that takes a callback takes it also as the keyword argument ``cb``,
56
+ and is a decorator without it, and the decorated name is what the method
57
+ returns. ::
58
+
59
+ @Pyjo.IOLoop.timer(3)
60
+ def id(loop):
61
+ print('Timeout!')
62
+
63
+ The end of a ``with`` block removes everything, as :meth:`~Pyjo_IOLoop.reset`
64
+ does.
65
+
66
+ Events
67
+ ------
68
+
69
+ :mod:`Pyjo.IOLoop` inherits all events from :mod:`Pyjo.EventEmitter` and can
70
+ emit the following new ones.
71
+
72
+ finish
73
+ ~~~~~~
74
+ ::
75
+
76
+ loop.on('finish', lambda loop: ...)
77
+
78
+ Emitted when the event loop wants to shut down gracefully and is just waiting
79
+ for all existing connections to be closed.
80
+
81
+ reset
82
+ ~~~~~
83
+ ::
84
+
85
+ loop.on('reset', lambda loop: ...)
86
+
87
+ Emitted when the event loop is reset, this usually happens after the process
88
+ is forked to clean up resources that cannot be shared.
89
+
90
+ Debugging
91
+ ---------
92
+
93
+ You can set the ``PYJO_IOLOOP_DEBUG`` environment variable to get some
94
+ advanced diagnostics information printed to ``sys.stderr``. ::
95
+
96
+ PYJO_IOLOOP_DEBUG=1
97
+
98
+ See also
99
+ --------
100
+
101
+ :mod:`Pyjoyment`, https://mojolicious.org.
102
+
103
+ Ported from `Mojo::IOLoop <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/IOLoop.pm>`_
104
+ of Mojolicious 9.49.
105
+ """
106
+
107
+ import Pyjo.EventEmitter
108
+ import Pyjo.Exception
109
+ import Pyjo.IOLoop.Client
110
+ import Pyjo.IOLoop.Datagram
111
+ import Pyjo.IOLoop.Server
112
+ import Pyjo.IOLoop.Stream
113
+ import Pyjo.Reactor
114
+
115
+ from Pyjo.Util import _call_hint, md5_sum, warn # pyright: ignore[reportPrivateUsage]
116
+
117
+ import atexit
118
+ import builtins
119
+ import gc
120
+ import os
121
+ import random
122
+ import sys
123
+ import threading
124
+ import time
125
+ import weakref
126
+
127
+ from collections.abc import Callable, Iterator
128
+ from types import TracebackType
129
+ from typing import TYPE_CHECKING, Any, Concatenate, Self, overload
130
+
131
+
132
+ if TYPE_CHECKING:
133
+ import Pyjo.IOLoop.ChildProcess
134
+ import Pyjo.IOLoop.Subprocess
135
+ import Pyjo.Promise
136
+
137
+ from Pyjo.IOLoop.Subprocess import Child, Parent
138
+
139
+
140
+ type Anything = builtins.object
141
+
142
+ # A callback of a timer gets the loop
143
+ type TimerCallback = Callable[[Any], Anything]
144
+
145
+ # A callback of a server gets the loop, the stream and the id of the connection
146
+ type ServerCallback = Callable[[Any, Any, str], Anything]
147
+
148
+ # A callback of a client gets the loop, an error and the stream
149
+ type ClientCallback = Callable[[Any, Any, Any], Anything]
150
+
151
+ # A callback of a datagram socket gets the loop, the socket and its id
152
+ type DatagramCallback = Callable[[Any, Any, str], Anything]
153
+
154
+ DEBUG = bool(os.environ.get('PYJO_IOLOOP_DEBUG'))
155
+
156
+ # The states that reset(freeze=True) keeps
157
+ _FROZEN: list[dict[str, Any]] = []
158
+
159
+ # Not in Mojolicious: the singleton of each thread, as each thread of Perl has a copy of the interpreter with its own
160
+ _local = threading.local()
161
+
162
+ # Not in Mojolicious: the event loops that exist, which are reset at the end of the interpreter
163
+ _LOOPS: 'weakref.WeakSet[Pyjo_IOLoop]' = weakref.WeakSet()
164
+
165
+
166
+ class Pyjo_IOLoop(Pyjo.EventEmitter.object):
167
+ """
168
+ :mod:`Pyjo.IOLoop` inherits all methods from :mod:`Pyjo.EventEmitter` and
169
+ implements the following new ones.
170
+ """
171
+
172
+ def __init__(
173
+ self, *, max_accepts: int = 0, max_connections: int = 1000, reactor: Pyjo.Reactor.object | None = None
174
+ ) -> None:
175
+ """::
176
+
177
+ loop = Pyjo.IOLoop.new()
178
+ loop = Pyjo.IOLoop.new(max_connections=100)
179
+
180
+ Construct a new :mod:`Pyjo.IOLoop` object.
181
+ """
182
+ super().__init__()
183
+
184
+ self.max_accepts = max_accepts
185
+ """::
186
+
187
+ max = loop.max_accepts
188
+ loop.max_accepts = 1000
189
+
190
+ The maximum number of connections this event loop is allowed to
191
+ accept, before shutting down gracefully without interrupting existing
192
+ connections, defaults to ``0``. Setting the value to ``0`` will allow
193
+ this event loop to accept new connections indefinitely. Note that up to
194
+ half of this value can be subtracted randomly to improve load balancing
195
+ between multiple server processes, and to make sure that not all of
196
+ them restart at the same time.
197
+ """
198
+
199
+ self.max_connections = max_connections
200
+ """::
201
+
202
+ max = loop.max_connections
203
+ loop.max_connections = 100
204
+
205
+ The maximum number of accepted connections this event loop is allowed
206
+ to handle concurrently, before stopping to accept new incoming
207
+ connections, defaults to ``1000``.
208
+ """
209
+
210
+ _LOOPS.add(self)
211
+ self._accepting = False
212
+ self._accepts: int | None = None
213
+ self._acceptors: dict[str, Any] = {}
214
+ self._in: dict[str, dict[str, Any]] = {}
215
+ self._out: dict[str, dict[str, Any]] = {}
216
+ self._reactor = reactor
217
+ self._stop = 0
218
+ self._delay: str | None = None
219
+
220
+ # Not in Mojolicious: a context manager, which removes everything, as reset does, where the block ends, as PyPy
221
+ # frees the object only when its garbage collector runs
222
+ def __enter__(self) -> Self:
223
+ return self
224
+
225
+ def __exit__(
226
+ self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
227
+ ) -> None:
228
+ self.reset()
229
+
230
+ @property
231
+ def reactor(self) -> Pyjo.Reactor.object:
232
+ """::
233
+
234
+ reactor = loop.reactor
235
+ loop.reactor = Pyjo.Reactor.Asyncio.new()
236
+
237
+ Low-level event reactor, usually a :mod:`Pyjo.Reactor.Asyncio` object
238
+ with a default subscriber to the event ``error`` of
239
+ :mod:`Pyjo.Reactor`. ::
240
+
241
+ # Watch if handle becomes readable or writable
242
+ Pyjo.IOLoop.singleton.reactor.io(handle, lambda reactor, writable: print(
243
+ 'Handle is writable' if writable else 'Handle is readable'
244
+ ))
245
+
246
+ # Change to watching only if handle becomes writable
247
+ Pyjo.IOLoop.singleton.reactor.watch(handle, False, True)
248
+
249
+ # Remove handle again
250
+ Pyjo.IOLoop.singleton.reactor.remove(handle)
251
+ """
252
+ if self._reactor is None:
253
+ name = Pyjo.Reactor.detect()
254
+ if DEBUG:
255
+ warn(f'-- Reactor initialized ({name})')
256
+ reactor: Pyjo.Reactor.object = sys.modules[name].new()
257
+
258
+ def catch(reactor: Pyjo.Reactor.object, err: Any) -> None:
259
+ warn(f'{_name(type(reactor))}: {err}')
260
+
261
+ self._reactor = reactor.catch(catch)
262
+ return self._reactor
263
+
264
+ @reactor.setter
265
+ def reactor(self, value: Pyjo.Reactor.object) -> None:
266
+ self._reactor = value
267
+
268
+ def acceptor(self, acceptor: Any) -> Any:
269
+ """::
270
+
271
+ server = Pyjo.IOLoop.acceptor(id)
272
+ server = loop.acceptor(id)
273
+ id = loop.acceptor(Pyjo.IOLoop.Server.new())
274
+
275
+ Get :mod:`Pyjo.IOLoop.Server` object for id or turn object into an
276
+ acceptor.
277
+ """
278
+ # Find acceptor for id
279
+ if isinstance(acceptor, str):
280
+ return self._acceptors.get(acceptor)
281
+
282
+ # Connect acceptor with reactor
283
+ id = self._id()
284
+ acceptor.reactor = self.reactor
285
+ self._acceptors[id] = acceptor
286
+
287
+ # Allow new acceptor to get picked up
288
+ self._not_accepting()._maybe_accepting()
289
+
290
+ return id
291
+
292
+ # Unlike Mojolicious: the callback comes before the options, which are keyword arguments, or is the keyword
293
+ # argument cb
294
+ @overload
295
+ def client(self, cb: ClientCallback, **options: Any) -> str: ...
296
+
297
+ @overload
298
+ def client(self, **options: Any) -> Callable[[ClientCallback], str]: ...
299
+
300
+ def client(self, cb: ClientCallback | None = None, **options: Any) -> Any:
301
+ """::
302
+
303
+ id = Pyjo.IOLoop.client(cb, address='127.0.0.1', port=3000)
304
+ id = loop.client(cb, address='127.0.0.1', port=3000)
305
+ id = loop.client(cb=cb, address='127.0.0.1', port=3000)
306
+
307
+ @loop.client(address='127.0.0.1', port=3000)
308
+ def id(loop, err, stream):
309
+ ...
310
+
311
+ Open a TCP/IP or UNIX domain socket connection with
312
+ :mod:`Pyjo.IOLoop.Client` and create a stream object (usually
313
+ :mod:`Pyjo.IOLoop.Stream`), takes the same arguments as
314
+ :meth:`Pyjo.IOLoop.Client.connect`.
315
+ """
316
+ # Not in Mojolicious: a decorator
317
+ if cb is None:
318
+
319
+ def decorator(cb: ClientCallback) -> str:
320
+ return self.client(cb, **options)
321
+
322
+ return decorator
323
+
324
+ id = self._id()
325
+ client: Any = Pyjo.IOLoop.Client.new(reactor=self.reactor)
326
+ self._out[id] = {'client': client}
327
+
328
+ ref = weakref.ref(self)
329
+
330
+ def connect(client: Any, handle: Any) -> None:
331
+ if (loop := ref()) is None:
332
+ return
333
+ del loop._out[id]['client']
334
+ stream = Pyjo.IOLoop.Stream.new(handle)
335
+ loop._stream(stream, id)
336
+ cb(loop, None, stream)
337
+
338
+ def error(client: Any, err: Any) -> None:
339
+ if (loop := ref()) is None:
340
+ return
341
+ loop._remove(id)
342
+ cb(loop, err, None)
343
+
344
+ client.on('connect', connect)
345
+ client.on('error', error)
346
+ client.connect(**options)
347
+
348
+ return id
349
+
350
+ # Not in Mojolicious: the stream of a connection as a promise, as connect of promise-socket
351
+ def client_p(self, **options: Any) -> 'Pyjo.Promise.object':
352
+ """::
353
+
354
+ promise = Pyjo.IOLoop.client_p(address='127.0.0.1', port=3000)
355
+ promise = loop.client_p(address='127.0.0.1', port=3000)
356
+
357
+ Same as :meth:`client`, but returns a :mod:`Pyjo.Promise` object, which
358
+ is fulfilled with the stream object of the connection, and rejected
359
+ with its error. The stream keeps what arrives for its promises, as
360
+ :meth:`~Pyjo.IOLoop.Stream.Pyjo_IOLoop_Stream.read_p`, from the
361
+ connection on, as a coroutine that awaits the promise goes on later,
362
+ when a peer may have sent something already. ::
363
+
364
+ stream = await Pyjo.IOLoop.client_p(address='127.0.0.1', port=3000)
365
+ await stream.write_p(b'Hello!')
366
+ reply = await stream.read_p()
367
+ """
368
+ # Imported here, as Pyjo.Promise uses Pyjo.IOLoop
369
+ import Pyjo.Promise
370
+
371
+ promise = Pyjo.Promise.new()
372
+ promise.ioloop = self
373
+
374
+ def connected(loop: Any, err: Any, stream: Any) -> None:
375
+ if err is not None:
376
+ promise.reject(err)
377
+ return
378
+ stream._chunks() # pyright: ignore[reportPrivateUsage]
379
+ promise.resolve(stream)
380
+
381
+ self.client(connected, **options)
382
+ return promise
383
+
384
+ # Not in Mojolicious: datagram sockets
385
+ @overload
386
+ def datagram(self, cb: DatagramCallback, **options: Any) -> str: ...
387
+
388
+ @overload
389
+ def datagram(self, **options: Any) -> Callable[[DatagramCallback], str]: ...
390
+
391
+ def datagram(self, cb: DatagramCallback | None = None, **options: Any) -> Any:
392
+ """::
393
+
394
+ id = Pyjo.IOLoop.datagram(cb, port=9999)
395
+ id = loop.datagram(cb, address='127.0.0.1', port=9999)
396
+ id = loop.datagram(cb=cb)
397
+
398
+ @loop.datagram(port=9999)
399
+ def id(loop, datagram, id):
400
+ ...
401
+
402
+ Open a datagram socket with :mod:`Pyjo.IOLoop.Datagram`, bound with the
403
+ options of :meth:`Pyjo.IOLoop.Datagram.Pyjo_IOLoop_Datagram.bind` if
404
+ any are given, which the event loop watches until it is closed. The
405
+ callback gets the loop, the datagram socket and its id, which
406
+ :meth:`remove` takes. ::
407
+
408
+ # Echo server
409
+ @Pyjo.IOLoop.datagram(address='127.0.0.1', port=9999)
410
+ def id(loop, datagram, id):
411
+ datagram.on('message', lambda datagram, data, peer: datagram.send(data, peer))
412
+ """
413
+ # Not in Mojolicious: a decorator
414
+ if cb is None:
415
+
416
+ def decorator(cb: DatagramCallback) -> str:
417
+ return self.datagram(cb, **options)
418
+
419
+ return decorator
420
+
421
+ datagram = Pyjo.IOLoop.Datagram.new()
422
+ datagram.reactor = self.reactor
423
+ if options:
424
+ datagram.bind(**options)
425
+ id = self._stream(datagram, self._id())
426
+ cb(self, datagram, id)
427
+
428
+ return id
429
+
430
+ @property
431
+ def is_running(self) -> bool:
432
+ """::
433
+
434
+ boolean = Pyjo.IOLoop.is_running()
435
+ boolean = loop.is_running
436
+
437
+ Check if event loop is running.
438
+ """
439
+ return self.reactor.is_running
440
+
441
+ # Not in Mojolicious: what readiness answers while the connections are closing, as Mojo keeps the count private
442
+ @property
443
+ def is_stopping(self) -> bool:
444
+ """::
445
+
446
+ boolean = Pyjo.IOLoop.is_stopping()
447
+ boolean = loop.is_stopping
448
+
449
+ Check if event loop is stopping gracefully, from :meth:`stop_gracefully`,
450
+ its delay too, until :meth:`reset`.
451
+ """
452
+ return bool(self._stop) or self._delay is not None
453
+
454
+ @overload
455
+ def next_tick(self, cb: TimerCallback) -> None: ...
456
+
457
+ @overload
458
+ def next_tick(self) -> Callable[[TimerCallback], None]: ...
459
+
460
+ def next_tick(self, cb: TimerCallback | None = None) -> Any:
461
+ """::
462
+
463
+ none = Pyjo.IOLoop.next_tick(lambda loop: ...)
464
+ none = loop.next_tick(lambda loop: ...)
465
+
466
+ Execute callback as soon as possible, but not before returning or other
467
+ callbacks that have been registered with this method, always returns
468
+ ``None``. ::
469
+
470
+ # Perform operation on next reactor tick
471
+ Pyjo.IOLoop.next_tick(lambda loop: ...)
472
+ """
473
+ # Not in Mojolicious: a decorator
474
+ if cb is None:
475
+
476
+ def decorator(cb: TimerCallback) -> None:
477
+ return self.next_tick(cb)
478
+
479
+ return decorator
480
+
481
+ ref = weakref.ref(self)
482
+ callback = cb
483
+
484
+ def next_tick(reactor: Pyjo.Reactor.object) -> None:
485
+ callback(ref())
486
+
487
+ return self.reactor.next_tick(next_tick)
488
+
489
+ def one_tick(self) -> None:
490
+ """::
491
+
492
+ Pyjo.IOLoop.one_tick()
493
+ loop.one_tick()
494
+
495
+ Run event loop until an event occurs. ::
496
+
497
+ # Don't block longer than 0.5 seconds
498
+ id = Pyjo.IOLoop.timer(0.5, lambda loop: None)
499
+ Pyjo.IOLoop.one_tick()
500
+ Pyjo.IOLoop.remove(id)
501
+ """
502
+ if self.is_running:
503
+ Pyjo.Exception.object.throw('Pyjo.IOLoop already running')
504
+ self.reactor.one_tick()
505
+
506
+ @overload
507
+ def recurring(self, after: float, cb: TimerCallback) -> str: ...
508
+
509
+ @overload
510
+ def recurring(self, after: float) -> Callable[[TimerCallback], str]: ...
511
+
512
+ def recurring(self, after: float, cb: TimerCallback | None = None) -> Any:
513
+ """::
514
+
515
+ id = Pyjo.IOLoop.recurring(3, lambda loop: ...)
516
+ id = loop.recurring(0, lambda loop: ...)
517
+ id = loop.recurring(0.25, lambda loop: ...)
518
+
519
+ Create a new recurring timer, invoking the callback repeatedly after a
520
+ given amount of time in seconds. ::
521
+
522
+ # Perform operation every 5 seconds
523
+ Pyjo.IOLoop.recurring(5, lambda loop: ...)
524
+ """
525
+ return self._timer('recurring', after, cb)
526
+
527
+ def remove(self, id: str, *, close: bool = True) -> None:
528
+ """::
529
+
530
+ Pyjo.IOLoop.remove(id)
531
+ loop.remove(id)
532
+ loop.remove(id, close=False)
533
+
534
+ Remove anything with an id, connections will be dropped gracefully by
535
+ allowing them to finish writing all data in their write buffers. An
536
+ acceptor keeps its listen socket open with ``close=False``, so that it
537
+ can be turned into an acceptor again.
538
+ """
539
+ c = self._in.get(id) or self._out.get(id)
540
+ if c and (stream := c.get('stream')):
541
+ return stream.close_gracefully()
542
+ self._remove(id, close)
543
+
544
+ def reset(self, *, freeze: bool = False) -> None:
545
+ """::
546
+
547
+ Pyjo.IOLoop.reset()
548
+ loop.reset()
549
+ loop.reset(freeze=True)
550
+
551
+ Remove everything and stop the event loop.
552
+
553
+ These options are currently available:
554
+
555
+ freeze
556
+ ::
557
+
558
+ freeze=True
559
+
560
+ Freeze the current state of the event loop in time before
561
+ resetting it. This will prevent active connections from getting
562
+ closed immediately, which can help with many unintended side
563
+ effects when processes are forked.
564
+ """
565
+ self.emit('reset').stop()
566
+ if freeze:
567
+ _FROZEN.append(dict(self.__dict__))
568
+ self._reactor = None
569
+ else:
570
+ self.reactor.reset()
571
+
572
+ acceptors, connections = [*self._acceptors.values()], [*self._in.values(), *self._out.values()]
573
+ self._accepting = False
574
+ self._acceptors = {}
575
+ self._events = {}
576
+ self._in = {}
577
+ self._out = {}
578
+ self._stop = 0
579
+ self._delay = None
580
+
581
+ # Perl closes what it frees at once, Python later, as PyPy does when its garbage collector runs
582
+ if not freeze:
583
+ for acceptor in acceptors:
584
+ acceptor.close()
585
+ for connection in connections:
586
+ for value in connection.values():
587
+ value.close()
588
+
589
+ # Unlike Mojolicious: the callback comes before the options, which are keyword arguments, or is the keyword
590
+ # argument cb
591
+ @overload
592
+ def server(self, cb: ServerCallback, **options: Any) -> str: ...
593
+
594
+ @overload
595
+ def server(self, **options: Any) -> Callable[[ServerCallback], str]: ...
596
+
597
+ def server(self, cb: ServerCallback | None = None, **options: Any) -> Any:
598
+ """::
599
+
600
+ id = Pyjo.IOLoop.server(cb, port=3000)
601
+ id = loop.server(cb, port=3000)
602
+ id = loop.server(cb=cb, port=3000)
603
+
604
+ @loop.server(port=3000)
605
+ def id(loop, stream, id):
606
+ ...
607
+
608
+ Accept TCP/IP and UNIX domain socket connections with
609
+ :mod:`Pyjo.IOLoop.Server` and create stream objects (usually
610
+ :mod:`Pyjo.IOLoop.Stream`), takes the same arguments as
611
+ :meth:`Pyjo.IOLoop.Server.listen`. ::
612
+
613
+ # Listen on random port
614
+ id = Pyjo.IOLoop.server(lambda loop, stream, id: ..., address='127.0.0.1')
615
+ port = Pyjo.IOLoop.acceptor(id).port
616
+ """
617
+ # Not in Mojolicious: a decorator
618
+ if cb is None:
619
+
620
+ def decorator(cb: ServerCallback) -> str:
621
+ return self.server(cb, **options)
622
+
623
+ return decorator
624
+
625
+ server: Any = Pyjo.IOLoop.Server.new()
626
+ ref = weakref.ref(self)
627
+
628
+ def accept(server: Any, handle: Any) -> None:
629
+ if (loop := ref()) is None:
630
+ return
631
+ stream = Pyjo.IOLoop.Stream.new(handle)
632
+ id = loop._stream(stream, loop._id(), True)
633
+ cb(loop, stream, id)
634
+
635
+ # Enforce connection limit (randomize to improve load balancing)
636
+ if max := loop.max_accepts:
637
+ if loop._accepts is None:
638
+ loop._accepts = max - int(random.random() * max / 2)
639
+ loop._accepts -= 1
640
+ if loop._accepts <= 0:
641
+ loop.stop_gracefully()
642
+
643
+ # Stop accepting if connection limit has been reached
644
+ if loop._limit():
645
+ loop._not_accepting()
646
+
647
+ server.on('accept', accept)
648
+ server.listen(**options)
649
+
650
+ return self.acceptor(server)
651
+
652
+ @classmethod
653
+ def singleton(cls) -> 'Pyjo_IOLoop':
654
+ """::
655
+
656
+ loop = Pyjo.IOLoop.object.singleton()
657
+ loop = Pyjo.IOLoop.singleton
658
+
659
+ The global :mod:`Pyjo.IOLoop` singleton, used to access a single shared
660
+ event loop object from everywhere inside the thread, which is also the
661
+ :data:`singleton` of the module. Each thread has one of its own. ::
662
+
663
+ # Many methods also allow you to take shortcuts
664
+ Pyjo.IOLoop.timer(2, lambda loop: Pyjo.IOLoop.stop())
665
+ Pyjo.IOLoop.start()
666
+
667
+ # Restart active timer
668
+ id = Pyjo.IOLoop.timer(3, lambda loop: print('Timeout!'))
669
+ Pyjo.IOLoop.singleton.reactor.again(id)
670
+
671
+ # Watch a file descriptor if it becomes readable
672
+ Pyjo.IOLoop.singleton.reactor.io(fd, lambda reactor, writable: print(
673
+ 'Handle is writable' if writable else 'Handle is readable'
674
+ )).watch(fd, True, False)
675
+ """
676
+ try:
677
+ return _local.singleton
678
+ except AttributeError:
679
+ _local.singleton = loop = Pyjo_IOLoop()
680
+ return loop
681
+
682
+ def spawn(self, args: Any, **options: Any) -> 'Pyjo.IOLoop.ChildProcess.object':
683
+ """::
684
+
685
+ child = Pyjo.IOLoop.spawn(['ls', '-l'])
686
+ child = loop.spawn(['php-cgi', 'index.php'], cwd='/var/www')
687
+
688
+ Run a program with :mod:`Pyjo.IOLoop.ChildProcess` on the reactor of
689
+ the event loop, with the options of
690
+ :meth:`Pyjo.IOLoop.ChildProcess.Pyjo_IOLoop_ChildProcess.spawn`. ::
691
+
692
+ # The output of a program, and its exit code
693
+ child = Pyjo.IOLoop.spawn(['ls', '-l'])
694
+ child.stdout.on('read', lambda stream, chunk: print(chunk))
695
+ child.on('close', lambda child, code, signal: print(f'Exited with {code}'))
696
+ """
697
+ # Not in Mojolicious: child processes, inspired by child_process of Node.js, imported here, as
698
+ # Pyjo.IOLoop.ChildProcess uses Pyjo.IOLoop
699
+ import Pyjo.IOLoop.ChildProcess
700
+
701
+ return Pyjo.IOLoop.ChildProcess.new(reactor=self.reactor).spawn(args, **options)
702
+
703
+ def start(self) -> None:
704
+ """::
705
+
706
+ Pyjo.IOLoop.start()
707
+ loop.start()
708
+
709
+ Start the event loop, this will block until :meth:`stop` is called.
710
+ Note that some reactors stop automatically if there are no events being
711
+ watched anymore. ::
712
+
713
+ # Start event loop only if it is not running already
714
+ if not Pyjo.IOLoop.is_running():
715
+ Pyjo.IOLoop.start()
716
+ """
717
+ if self.is_running:
718
+ Pyjo.Exception.object.throw('Pyjo.IOLoop already running')
719
+ self.reactor.start()
720
+
721
+ def stop(self) -> None:
722
+ """::
723
+
724
+ Pyjo.IOLoop.stop()
725
+ loop.stop()
726
+
727
+ Stop the event loop, this will not interrupt any existing connections
728
+ and the event loop can be restarted by running :meth:`start` again.
729
+ """
730
+ self.reactor.stop()
731
+
732
+ # Not in Mojolicious: the delay, while which the event loop serves as before and is_stopping is true already, so that
733
+ # the readiness of a server fails before it stops accepting
734
+ def stop_gracefully(self, delay: float = 0) -> None:
735
+ """::
736
+
737
+ Pyjo.IOLoop.stop_gracefully()
738
+ loop.stop_gracefully()
739
+ loop.stop_gracefully(delay=5)
740
+
741
+ Stop accepting new connections and wait for already accepted
742
+ connections to be closed, before stopping the event loop. With a
743
+ ``delay`` in seconds, the event loop keeps accepting them for that
744
+ long, while :attr:`is_stopping` is true already, so that a load
745
+ balancer that checks the readiness of the server sends no more of
746
+ them. A call without a delay ends the delay of an earlier one.
747
+ """
748
+ if delay > 0:
749
+ if self._delay is None and not self._stop:
750
+ self._delay = self.timer(delay, lambda loop: loop.stop_gracefully())
751
+ return
752
+ if (timer := self._delay) is not None:
753
+ self._delay = None
754
+ self.remove(timer)
755
+ self._not_accepting()
756
+ self._stop += 1
757
+ if not len(self.emit('finish')._in):
758
+ self.stop()
759
+
760
+ def stream(self, stream: Any) -> Any:
761
+ """::
762
+
763
+ stream = Pyjo.IOLoop.stream(id)
764
+ stream = loop.stream(id)
765
+ id = loop.stream(Pyjo.IOLoop.Stream.new(handle))
766
+
767
+ Get :mod:`Pyjo.IOLoop.Stream` object for id or turn object into a
768
+ connection. ::
769
+
770
+ # Increase inactivity timeout for connection to 300 seconds
771
+ Pyjo.IOLoop.stream(id).timeout = 300
772
+ """
773
+ if stream is None:
774
+ return None
775
+ if not isinstance(stream, str):
776
+ return self._stream(stream, self._id())
777
+ c = self._in.get(stream) or self._out.get(stream) or {}
778
+ return c.get('stream')
779
+
780
+ def subprocess(
781
+ self, child: 'Child | None' = None, parent: 'Parent | None' = None
782
+ ) -> 'Pyjo.IOLoop.Subprocess.object':
783
+ """::
784
+
785
+ subprocess = Pyjo.IOLoop.subprocess()
786
+ subprocess = loop.subprocess()
787
+ subprocess = loop.subprocess(lambda subprocess: ..., lambda subprocess, err, *results: ...)
788
+
789
+ Build :mod:`Pyjo.IOLoop.Subprocess` object to perform computationally
790
+ expensive operations in subprocesses, without blocking the event loop.
791
+ Callbacks will be passed along to
792
+ :meth:`Pyjo.IOLoop.Subprocess.Pyjo_IOLoop_Subprocess.run`. ::
793
+
794
+ # Operation that would block the event loop for 5 seconds
795
+ Pyjo.IOLoop.subprocess().run_p(blocking).then(lambda *results: print(f'I {results[0]} {results[1]}!')).catch(
796
+ lambda err: print(f'Subprocess error: {err}')
797
+ )
798
+ """
799
+ # Not in Mojolicious: imported here, as Pyjo.IOLoop.Subprocess uses Pyjo.Promise, which uses Pyjo.IOLoop
800
+ import Pyjo.IOLoop.Subprocess
801
+
802
+ subprocess = Pyjo.IOLoop.Subprocess.new(ioloop=self)
803
+ if child is not None and parent is not None:
804
+ return subprocess.run(child, parent)
805
+ return subprocess
806
+
807
+ @overload
808
+ def timer(self, after: float, cb: TimerCallback) -> str: ...
809
+
810
+ @overload
811
+ def timer(self, after: float) -> Callable[[TimerCallback], str]: ...
812
+
813
+ def timer(self, after: float, cb: TimerCallback | None = None) -> Any:
814
+ """::
815
+
816
+ id = Pyjo.IOLoop.timer(3, lambda loop: ...)
817
+ id = loop.timer(0, lambda loop: ...)
818
+ id = loop.timer(0.25, lambda loop: ...)
819
+
820
+ Create a new timer, invoking the callback after a given amount of time
821
+ in seconds. ::
822
+
823
+ # Perform operation in 5 seconds
824
+ Pyjo.IOLoop.timer(5, lambda loop: ...)
825
+ """
826
+ return self._timer('timer', after, cb)
827
+
828
+ def _id(self) -> str:
829
+ while True:
830
+ id = md5_sum(f'c{time.monotonic()}{random.random()}'.encode())
831
+ if id not in self._in and id not in self._out and id not in self._acceptors:
832
+ return id
833
+
834
+ def _limit(self) -> bool:
835
+ return True if self._stop else len(self._in) >= self.max_connections
836
+
837
+ # Not in Mojolicious: the acceptors of a copy that are still there, as the garbage collector may free another server
838
+ # meanwhile, whose finalizer removes its acceptor and closes its socket, which a new one may then reuse
839
+ def _each_acceptor(self) -> Iterator[Any]:
840
+ for id, acceptor in list(self._acceptors.items()):
841
+ if self._acceptors.get(id) is acceptor:
842
+ yield acceptor
843
+
844
+ def _maybe_accepting(self) -> None:
845
+ if self._accepting or self._limit():
846
+ return
847
+ for acceptor in self._each_acceptor():
848
+ acceptor.start()
849
+ self._accepting = True
850
+
851
+ def _not_accepting(self) -> Self:
852
+ if not self._accepting:
853
+ return self
854
+ self._accepting = False
855
+ for acceptor in self._each_acceptor():
856
+ acceptor.stop()
857
+ return self
858
+
859
+ def _remove(self, id: str, close: bool = True) -> None:
860
+ # Timer
861
+ if self.reactor.remove(id):
862
+ return
863
+
864
+ # Acceptor
865
+ if (acceptor := self._acceptors.pop(id, None)) is not None:
866
+ # Perl closes the acceptor it frees at once, Python later; Not in Mojolicious: one that a server keeps to
867
+ # resume it later, as Perl does not free it
868
+ if close:
869
+ acceptor.close()
870
+ else:
871
+ acceptor.stop()
872
+ return self._not_accepting()._maybe_accepting()
873
+
874
+ # Connection
875
+ if (connection := self._in.pop(id, None) or self._out.pop(id, None)) is None:
876
+ return
877
+ # Perl closes the client it frees at once, Python later
878
+ if (client := connection.get('client')) is not None:
879
+ client.close()
880
+ if self._stop and not len(self._in):
881
+ return self.stop()
882
+ self._maybe_accepting()
883
+ if DEBUG:
884
+ warn(f'-- {id} <<< {os.getpid()} ({len(self._in)}:{len(self._out)})')
885
+
886
+ def _stream(self, stream: Any, id: str, server: bool = False) -> str:
887
+ # Connect stream with reactor
888
+ stream.reactor = self.reactor
889
+ (self._in if server else self._out).setdefault(id, {})['stream'] = stream
890
+ if DEBUG:
891
+ warn(f'-- {id} >>> {os.getpid()} ({len(self._in)}:{len(self._out)})')
892
+
893
+ ref = weakref.ref(self)
894
+
895
+ def close(stream: Any) -> None:
896
+ if (loop := ref()) is not None:
897
+ loop._remove(id)
898
+
899
+ stream.on('close', close)
900
+ stream.start()
901
+
902
+ return id
903
+
904
+ def _timer(self, method: str, after: float, cb: TimerCallback | None) -> Any:
905
+ # Not in Mojolicious: a decorator
906
+ if cb is None:
907
+
908
+ def decorator(cb: TimerCallback) -> Any:
909
+ return self._timer(method, after, cb)
910
+
911
+ return decorator
912
+
913
+ ref = weakref.ref(self)
914
+ callback = cb
915
+
916
+ def timer(reactor: Pyjo.Reactor.object) -> None:
917
+ try:
918
+ callback(ref())
919
+ except TypeError as e:
920
+ # Not in Mojolicious: the arguments of a callback, which Perl does not check
921
+ _call_hint(e, 'the callback of a timer gets the event loop')
922
+ raise
923
+
924
+ return getattr(self.reactor, method)(after, timer)
925
+
926
+
927
+ def _name(cls: type) -> str:
928
+ return f'{cls.__module__}.{cls.__qualname__}'
929
+
930
+
931
+ new = Pyjo_IOLoop.new
932
+ object = Pyjo_IOLoop
933
+ IOLoop = Pyjo_IOLoop
934
+
935
+ # Left out from Mojolicious: $SIG{PIPE} = 'IGNORE', which Python does already
936
+
937
+ singleton: Pyjo_IOLoop
938
+ """::
939
+
940
+ loop = Pyjo.IOLoop.singleton
941
+
942
+ The global :mod:`Pyjo.IOLoop` singleton, as :meth:`Pyjo_IOLoop.singleton`
943
+ gives it, the one of the thread.
944
+ """
945
+
946
+ # Initialize singleton reactor early
947
+ Pyjo_IOLoop.singleton().reactor
948
+
949
+
950
+ # Not in Mojolicious: Perl closes the handles that are left at the end of the interpreter, Python warns that they were
951
+ # not closed, as __del__ does nothing while it finalizes, so the connections of the event loops are closed and the
952
+ # objects that close theirs are freed before; registered after the reactor, so that it runs before the event loops of
953
+ # asyncio are closed
954
+ @atexit.register
955
+ def _cleanup() -> None:
956
+ def close(*objects: Any) -> None:
957
+ for obj in objects:
958
+ try:
959
+ obj.close()
960
+ except Exception:
961
+ pass
962
+
963
+ # The cleanups that finalizers deferred, before and after the event loops and the garbage collector, whose
964
+ # finalizers run in this frame of Pyjo.IOLoop, and so are deferred too
965
+ Pyjo.Reactor._drain(every=True) # pyright: ignore[reportPrivateUsage]
966
+
967
+ for loop in list(_LOOPS):
968
+ try:
969
+ loop.reset()
970
+ except Exception:
971
+ pass
972
+
973
+ # The frozen state too, which Perl frees then
974
+ for state in _FROZEN:
975
+ close(*state.get('_acceptors', {}).values())
976
+ for connection in [*state.get('_in', {}).values(), *state.get('_out', {}).values()]:
977
+ close(*connection.values())
978
+ _FROZEN.clear()
979
+ gc.collect()
980
+ Pyjo.Reactor._drain(every=True) # pyright: ignore[reportPrivateUsage]
981
+
982
+
983
+ # Not in Mojolicious: the singleton is found in the thread that asks for it, as a module has the same attributes in all
984
+ # threads
985
+ def __getattr__(name: str) -> Any:
986
+ if name == 'singleton':
987
+ return Pyjo_IOLoop.singleton()
988
+ raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
989
+
990
+
991
+ # What Mojo calls on the class works with the singleton, the one of the thread that calls it
992
+ def _shortcut[**P, R](method: Callable[Concatenate[Pyjo_IOLoop, P], R]) -> Callable[P, R]:
993
+ def shortcut(*args: P.args, **kwargs: P.kwargs) -> R:
994
+ return method(Pyjo_IOLoop.singleton(), *args, **kwargs)
995
+
996
+ return shortcut
997
+
998
+
999
+ acceptor = _shortcut(Pyjo_IOLoop.acceptor)
1000
+ client = _shortcut(Pyjo_IOLoop.client)
1001
+ client_p = _shortcut(Pyjo_IOLoop.client_p)
1002
+ datagram = _shortcut(Pyjo_IOLoop.datagram)
1003
+ next_tick = _shortcut(Pyjo_IOLoop.next_tick)
1004
+ one_tick = _shortcut(Pyjo_IOLoop.one_tick)
1005
+ recurring = _shortcut(Pyjo_IOLoop.recurring)
1006
+ remove = _shortcut(Pyjo_IOLoop.remove)
1007
+ reset = _shortcut(Pyjo_IOLoop.reset)
1008
+ server = _shortcut(Pyjo_IOLoop.server)
1009
+ spawn = _shortcut(Pyjo_IOLoop.spawn)
1010
+ start = _shortcut(Pyjo_IOLoop.start)
1011
+ stop = _shortcut(Pyjo_IOLoop.stop)
1012
+ stop_gracefully = _shortcut(Pyjo_IOLoop.stop_gracefully)
1013
+ stream = _shortcut(Pyjo_IOLoop.stream)
1014
+ subprocess = _shortcut(Pyjo_IOLoop.subprocess)
1015
+ timer = _shortcut(Pyjo_IOLoop.timer)
1016
+
1017
+
1018
+ def is_running() -> bool:
1019
+ """::
1020
+
1021
+ boolean = Pyjo.IOLoop.is_running()
1022
+
1023
+ Check if the :data:`singleton` is running.
1024
+ """
1025
+ return Pyjo_IOLoop.singleton().is_running
1026
+
1027
+
1028
+ # Not in Mojolicious: the counterpart of is_running
1029
+ def is_stopping() -> bool:
1030
+ """::
1031
+
1032
+ boolean = Pyjo.IOLoop.is_stopping()
1033
+
1034
+ Check if the :data:`singleton` is stopping gracefully.
1035
+ """
1036
+ return Pyjo_IOLoop.singleton().is_stopping