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/__init__.py
ADDED
|
@@ -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
|