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/Reactor/__init__.py
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.Reactor - Low-level event reactor base class
|
|
3
|
+
=================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Reactor import Reactor
|
|
7
|
+
|
|
8
|
+
class MyEventLoop(Reactor):
|
|
9
|
+
def again(self, id, after=None): ...
|
|
10
|
+
def io(self, handle, cb): ...
|
|
11
|
+
@property
|
|
12
|
+
def is_running(self): ...
|
|
13
|
+
def next_tick(self, cb): ...
|
|
14
|
+
def one_tick(self): ...
|
|
15
|
+
def recurring(self, after, cb): ...
|
|
16
|
+
def remove(self, remove): ...
|
|
17
|
+
def reset(self): ...
|
|
18
|
+
def start(self): ...
|
|
19
|
+
def stop(self): ...
|
|
20
|
+
def timer(self, after, cb): ...
|
|
21
|
+
def watch(self, handle, read, write): ...
|
|
22
|
+
|
|
23
|
+
# Or import the module, as in Google Python Style Guide
|
|
24
|
+
|
|
25
|
+
import Pyjo.Reactor
|
|
26
|
+
|
|
27
|
+
class MyEventLoop(Pyjo.Reactor.object):
|
|
28
|
+
def again(self, id, after=None): ...
|
|
29
|
+
def io(self, handle, cb): ...
|
|
30
|
+
@property
|
|
31
|
+
def is_running(self): ...
|
|
32
|
+
def next_tick(self, cb): ...
|
|
33
|
+
def one_tick(self): ...
|
|
34
|
+
def recurring(self, after, cb): ...
|
|
35
|
+
def remove(self, remove): ...
|
|
36
|
+
def reset(self): ...
|
|
37
|
+
def start(self): ...
|
|
38
|
+
def stop(self): ...
|
|
39
|
+
def timer(self, after, cb): ...
|
|
40
|
+
def watch(self, handle, read, write): ...
|
|
41
|
+
|
|
42
|
+
:mod:`Pyjo.Reactor` is an abstract base class for low-level event reactors,
|
|
43
|
+
like :mod:`Pyjo.Reactor.Asyncio`.
|
|
44
|
+
|
|
45
|
+
Events
|
|
46
|
+
------
|
|
47
|
+
|
|
48
|
+
:mod:`Pyjo.Reactor` inherits all events from :mod:`Pyjo.EventEmitter` and can
|
|
49
|
+
emit the following new ones.
|
|
50
|
+
|
|
51
|
+
error
|
|
52
|
+
~~~~~
|
|
53
|
+
::
|
|
54
|
+
|
|
55
|
+
reactor.on('error', lambda reactor, err: ...)
|
|
56
|
+
|
|
57
|
+
Emitted for exceptions caught in callbacks, fatal if unhandled. Note that if
|
|
58
|
+
this event is unhandled or fails it might kill your program, so you need to be
|
|
59
|
+
careful. ::
|
|
60
|
+
|
|
61
|
+
reactor.on('error', lambda reactor, err: print(f'Something very bad happened: {err}'))
|
|
62
|
+
|
|
63
|
+
Alternative reactors
|
|
64
|
+
--------------------
|
|
65
|
+
|
|
66
|
+
:mod:`Pyjo.Reactor` is a namespace that other distributions can add reactors
|
|
67
|
+
to, and the ``PYJO_REACTOR`` environment variable selects one for
|
|
68
|
+
:mod:`Pyjo.IOLoop`. A reactor on another implementation of the asyncio event
|
|
69
|
+
loop, like uvloop, is a subclass of :mod:`Pyjo.Reactor.Asyncio`, and one on
|
|
70
|
+
an event loop that is not asyncio's, like the ones of Trio or gevent,
|
|
71
|
+
implements the methods of :mod:`Pyjo.Reactor`.
|
|
72
|
+
|
|
73
|
+
A reactor is a module named ``Pyjo.Reactor.<Name>``, with a function ``new``
|
|
74
|
+
that constructs it and the class as ``object``, as the modules of Pyjoyment
|
|
75
|
+
have them. ::
|
|
76
|
+
|
|
77
|
+
# Pyjo/Reactor/MyLoop.py of another distribution
|
|
78
|
+
import Pyjo.Reactor.Asyncio
|
|
79
|
+
|
|
80
|
+
import asyncio
|
|
81
|
+
import selectors
|
|
82
|
+
|
|
83
|
+
class Pyjo_Reactor_MyLoop(Pyjo.Reactor.Asyncio.object):
|
|
84
|
+
def __init__(self, loop=None):
|
|
85
|
+
super().__init__(loop=loop or asyncio.SelectorEventLoop(selectors.SelectSelector()))
|
|
86
|
+
|
|
87
|
+
new = Pyjo_Reactor_MyLoop.new
|
|
88
|
+
object = Pyjo_Reactor_MyLoop
|
|
89
|
+
|
|
90
|
+
The distribution contains the module and no ``__init__.py`` of ``Pyjo`` or
|
|
91
|
+
``Pyjo.Reactor``: those of Pyjoyment have the base class and keep the
|
|
92
|
+
namespace open with :func:`pkgutil.extend_path`, and another one would
|
|
93
|
+
replace them when both are installed in the same directory. Its package
|
|
94
|
+
depends on Pyjoyment and holds only the files of its own reactor::
|
|
95
|
+
|
|
96
|
+
Pyjo/
|
|
97
|
+
Reactor/
|
|
98
|
+
MyLoop.py
|
|
99
|
+
|
|
100
|
+
The reactor is found when the distribution is installed next to Pyjoyment or
|
|
101
|
+
in another directory of :data:`sys.path`, and the environment variable
|
|
102
|
+
selects it::
|
|
103
|
+
|
|
104
|
+
PYJO_REACTOR=Pyjo.Reactor.MyLoop
|
|
105
|
+
|
|
106
|
+
See also
|
|
107
|
+
--------
|
|
108
|
+
|
|
109
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
110
|
+
|
|
111
|
+
Ported from `Mojo::Reactor <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Reactor.pm>`_
|
|
112
|
+
of Mojolicious 9.49.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
import Pyjo.EventEmitter
|
|
116
|
+
|
|
117
|
+
from Pyjo.Loader import load_class
|
|
118
|
+
|
|
119
|
+
import builtins
|
|
120
|
+
import collections
|
|
121
|
+
import concurrent.futures
|
|
122
|
+
import os
|
|
123
|
+
import sys
|
|
124
|
+
import threading
|
|
125
|
+
import weakref
|
|
126
|
+
|
|
127
|
+
from collections.abc import Awaitable, Callable, Coroutine, Generator
|
|
128
|
+
from pkgutil import extend_path
|
|
129
|
+
from typing import Any, Protocol, Self
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
type Anything = builtins.object
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class HasFileno(Protocol):
|
|
136
|
+
def fileno(self) -> int: ...
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# A handle to watch: an object with a fileno() method, or a file descriptor
|
|
140
|
+
type Handle = HasFileno | int
|
|
141
|
+
|
|
142
|
+
# A callback of an I/O watcher gets the reactor and whether the handle is writable
|
|
143
|
+
type IOCallback = Callable[[Any, bool], Anything]
|
|
144
|
+
|
|
145
|
+
# A callback of a timer gets the reactor
|
|
146
|
+
type TimerCallback = Callable[[Any], Anything]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Not in Mojolicious: what the futures of concurrent.futures and asyncio have in common
|
|
150
|
+
class Future[T](Protocol):
|
|
151
|
+
def add_done_callback(self, fn: Callable[[Any], object], /) -> None: ...
|
|
152
|
+
|
|
153
|
+
def cancel(self) -> bool: ...
|
|
154
|
+
|
|
155
|
+
def cancelled(self) -> bool: ...
|
|
156
|
+
|
|
157
|
+
def done(self) -> bool: ...
|
|
158
|
+
|
|
159
|
+
def exception(self) -> BaseException | None: ...
|
|
160
|
+
|
|
161
|
+
def result(self) -> T: ...
|
|
162
|
+
|
|
163
|
+
def set_exception(self, exception: BaseException, /) -> None: ...
|
|
164
|
+
|
|
165
|
+
def set_result(self, result: T, /) -> None: ...
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
_DETECTED: dict[str, str] = {}
|
|
169
|
+
|
|
170
|
+
# Not in Mojolicious: a namespace that other distributions can add reactors to
|
|
171
|
+
__path__ = extend_path(__path__, __name__)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class Pyjo_Reactor(Pyjo.EventEmitter.object):
|
|
175
|
+
"""
|
|
176
|
+
:mod:`Pyjo.Reactor` inherits all methods from :mod:`Pyjo.EventEmitter` and
|
|
177
|
+
implements the following new ones.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def __init__(self) -> None:
|
|
181
|
+
super().__init__()
|
|
182
|
+
# Not in Mojolicious: the thread that made the reactor, where it runs
|
|
183
|
+
self._thread = threading.get_ident()
|
|
184
|
+
|
|
185
|
+
# Not in Mojolicious: the coroutines that run_coroutine runs, which wait for promises, and which only these
|
|
186
|
+
# promises refer to otherwise, through the objects that they wait for, which the reactor refers to weakly, in a
|
|
187
|
+
# cycle that the garbage collector would free, as the reactor of asyncio keeps its tasks
|
|
188
|
+
self._coroutines: set[Coroutine[Any, Any, Any]] = set()
|
|
189
|
+
_ALL.add(self)
|
|
190
|
+
|
|
191
|
+
def again(self, id: str, after: float | None = None) -> None:
|
|
192
|
+
"""::
|
|
193
|
+
|
|
194
|
+
reactor.again(id)
|
|
195
|
+
reactor.again(id, 0.5)
|
|
196
|
+
|
|
197
|
+
Restart timer and optionally change the invocation time. Meant to be
|
|
198
|
+
overloaded in a subclass. Note that this method requires an active
|
|
199
|
+
timer.
|
|
200
|
+
"""
|
|
201
|
+
raise NotImplementedError('Method "again" not implemented by subclass')
|
|
202
|
+
|
|
203
|
+
# Not in Mojolicious: what awaiting a promise of Pyjoyment waits on
|
|
204
|
+
def awaitable(self, promise: Any) -> Awaitable[None]:
|
|
205
|
+
"""::
|
|
206
|
+
|
|
207
|
+
awaitable = reactor.awaitable(promise)
|
|
208
|
+
|
|
209
|
+
Return what ``await promise`` waits on, which completes when the
|
|
210
|
+
promise is fulfilled or rejected. The base class gives one that
|
|
211
|
+
:meth:`run_coroutine` drives, without asyncio, and a reactor of an event
|
|
212
|
+
loop gives one of its loop, so that a promise can be awaited in the
|
|
213
|
+
tasks of that loop.
|
|
214
|
+
"""
|
|
215
|
+
return _Awaitable(promise)
|
|
216
|
+
|
|
217
|
+
# Not in Mojolicious: how a coroutine runs as a promise, as Mojo's async sub
|
|
218
|
+
def run_coroutine(
|
|
219
|
+
self, coro: Coroutine[Any, Any, Any], resolve: Callable[[Any], Anything], reject: Callable[[Any], Anything]
|
|
220
|
+
) -> None:
|
|
221
|
+
"""::
|
|
222
|
+
|
|
223
|
+
reactor.run_coroutine(coro, resolve, reject)
|
|
224
|
+
|
|
225
|
+
Run a coroutine and call ``resolve`` with what it returns or
|
|
226
|
+
``reject`` with the exception it raises. The base class runs it at
|
|
227
|
+
once, until it waits for a promise, as Mojo runs an ``async sub``,
|
|
228
|
+
and continues it when the promise is fulfilled or rejected, so that it
|
|
229
|
+
can await the promises of Pyjoyment without asyncio, and keeps it
|
|
230
|
+
until it ends, as the reactor of asyncio keeps its tasks. A reactor of an
|
|
231
|
+
event loop runs it as a task of its loop, where it can await anything
|
|
232
|
+
of that loop.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
coroutines = self._coroutines
|
|
236
|
+
coroutines.add(coro)
|
|
237
|
+
|
|
238
|
+
def step() -> None:
|
|
239
|
+
try:
|
|
240
|
+
waiting = coro.send(None)
|
|
241
|
+
except StopIteration as e:
|
|
242
|
+
coroutines.discard(coro)
|
|
243
|
+
resolve(e.value)
|
|
244
|
+
return
|
|
245
|
+
except Exception as e:
|
|
246
|
+
coroutines.discard(coro)
|
|
247
|
+
reject(e)
|
|
248
|
+
return
|
|
249
|
+
then = getattr(waiting, 'then', None)
|
|
250
|
+
if not callable(then):
|
|
251
|
+
coroutines.discard(coro)
|
|
252
|
+
coro.close()
|
|
253
|
+
reject(TypeError(f'{_name(type(self))} cannot wait for {waiting!r}'))
|
|
254
|
+
return
|
|
255
|
+
then(resume, resume)
|
|
256
|
+
|
|
257
|
+
def resume(*values: Any) -> None:
|
|
258
|
+
step()
|
|
259
|
+
|
|
260
|
+
step()
|
|
261
|
+
|
|
262
|
+
@classmethod
|
|
263
|
+
def detect(cls) -> str:
|
|
264
|
+
"""::
|
|
265
|
+
|
|
266
|
+
name = Pyjo.Reactor.detect()
|
|
267
|
+
|
|
268
|
+
Detect and load the best reactor implementation available, will try
|
|
269
|
+
the value of the ``PYJO_REACTOR`` environment variable or
|
|
270
|
+
:mod:`Pyjo.Reactor.Asyncio`, and return the name of its module. ::
|
|
271
|
+
|
|
272
|
+
# Instantiate best reactor implementation available
|
|
273
|
+
reactor = sys.modules[Pyjo.Reactor.detect()].new()
|
|
274
|
+
"""
|
|
275
|
+
# Left out from Mojolicious: Mojo::Reactor::EV, as asyncio is the event loop
|
|
276
|
+
default = 'Pyjo.Reactor.Asyncio'
|
|
277
|
+
tried = os.environ.get('PYJO_REACTOR') or default
|
|
278
|
+
# Not in Mojolicious: the error of the default reactor, which may not load, as asyncio without SYSTEMROOT on
|
|
279
|
+
# Windows, where Mojo::Reactor::Poll always does; the first one, as a module that failed is not imported again
|
|
280
|
+
if tried not in _DETECTED:
|
|
281
|
+
_DETECTED[tried] = default if (e := load_class(tried)) else tried
|
|
282
|
+
if tried == default and isinstance(e, Exception):
|
|
283
|
+
raise e
|
|
284
|
+
if isinstance(e := load_class(_DETECTED[tried]), Exception):
|
|
285
|
+
raise e
|
|
286
|
+
return _DETECTED[tried]
|
|
287
|
+
|
|
288
|
+
# Not in Mojolicious: a future that a promise turns into
|
|
289
|
+
def future(self) -> Future[Any]:
|
|
290
|
+
"""::
|
|
291
|
+
|
|
292
|
+
future = reactor.future()
|
|
293
|
+
|
|
294
|
+
Return a new future, which :meth:`Pyjo.Promise.Pyjo_Promise.to_future`
|
|
295
|
+
turns a promise into. The base class gives a
|
|
296
|
+
:class:`concurrent.futures.Future`, which needs no event loop and
|
|
297
|
+
can be waited for in another thread, and a reactor of an event loop
|
|
298
|
+
gives one of its loop, so that it can be awaited in the tasks of that
|
|
299
|
+
loop.
|
|
300
|
+
"""
|
|
301
|
+
return concurrent.futures.Future()
|
|
302
|
+
|
|
303
|
+
def io(self, handle: Handle, cb: IOCallback) -> Self:
|
|
304
|
+
"""::
|
|
305
|
+
|
|
306
|
+
reactor = reactor.io(handle, cb)
|
|
307
|
+
|
|
308
|
+
Watch handle for I/O events, invoking the callback whenever handle
|
|
309
|
+
becomes readable or writable. Meant to be overloaded in a subclass. ::
|
|
310
|
+
|
|
311
|
+
# Callback will be executed twice if handle becomes readable and writable
|
|
312
|
+
reactor.io(handle, lambda reactor, writable: print('Handle is writable' if writable else 'Handle is readable'))
|
|
313
|
+
"""
|
|
314
|
+
raise NotImplementedError('Method "io" not implemented by subclass')
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def is_running(self) -> bool:
|
|
318
|
+
"""::
|
|
319
|
+
|
|
320
|
+
boolean = reactor.is_running
|
|
321
|
+
|
|
322
|
+
Check if reactor is running. Meant to be overloaded in a subclass.
|
|
323
|
+
"""
|
|
324
|
+
raise NotImplementedError('Method "is_running" not implemented by subclass')
|
|
325
|
+
|
|
326
|
+
def next_tick(self, cb: TimerCallback) -> None:
|
|
327
|
+
"""::
|
|
328
|
+
|
|
329
|
+
none = reactor.next_tick(cb)
|
|
330
|
+
|
|
331
|
+
Execute callback as soon as possible, but not before returning or other
|
|
332
|
+
callbacks that have been registered with this method, always returns
|
|
333
|
+
``None``. Meant to be overloaded in a subclass.
|
|
334
|
+
"""
|
|
335
|
+
raise NotImplementedError('Method "next_tick" not implemented by subclass')
|
|
336
|
+
|
|
337
|
+
def one_tick(self) -> None:
|
|
338
|
+
"""::
|
|
339
|
+
|
|
340
|
+
reactor.one_tick()
|
|
341
|
+
|
|
342
|
+
Run reactor until an event occurs. Note that this method can recurse
|
|
343
|
+
back into the reactor, so you need to be careful. Meant to be
|
|
344
|
+
overloaded in a subclass. ::
|
|
345
|
+
|
|
346
|
+
# Don't block longer than 0.5 seconds
|
|
347
|
+
id = reactor.timer(0.5, lambda reactor: None)
|
|
348
|
+
reactor.one_tick()
|
|
349
|
+
reactor.remove(id)
|
|
350
|
+
"""
|
|
351
|
+
raise NotImplementedError('Method "one_tick" not implemented by subclass')
|
|
352
|
+
|
|
353
|
+
def recurring(self, after: float, cb: TimerCallback) -> str:
|
|
354
|
+
"""::
|
|
355
|
+
|
|
356
|
+
id = reactor.recurring(0.25, cb)
|
|
357
|
+
|
|
358
|
+
Create a new recurring timer, invoking the callback repeatedly after a
|
|
359
|
+
given amount of time in seconds. Meant to be overloaded in a subclass.
|
|
360
|
+
"""
|
|
361
|
+
raise NotImplementedError('Method "recurring" not implemented by subclass')
|
|
362
|
+
|
|
363
|
+
def remove(self, remove: Handle | str) -> bool:
|
|
364
|
+
"""::
|
|
365
|
+
|
|
366
|
+
boolean = reactor.remove(handle)
|
|
367
|
+
boolean = reactor.remove(id)
|
|
368
|
+
|
|
369
|
+
Remove handle or timer. Meant to be overloaded in a subclass.
|
|
370
|
+
"""
|
|
371
|
+
raise NotImplementedError('Method "remove" not implemented by subclass')
|
|
372
|
+
|
|
373
|
+
def reset(self) -> None:
|
|
374
|
+
"""::
|
|
375
|
+
|
|
376
|
+
reactor.reset()
|
|
377
|
+
|
|
378
|
+
Remove all handles and timers. Meant to be overloaded in a subclass.
|
|
379
|
+
"""
|
|
380
|
+
raise NotImplementedError('Method "reset" not implemented by subclass')
|
|
381
|
+
|
|
382
|
+
def start(self) -> None:
|
|
383
|
+
"""::
|
|
384
|
+
|
|
385
|
+
reactor.start()
|
|
386
|
+
|
|
387
|
+
Start watching for I/O and timer events, this will block until
|
|
388
|
+
:meth:`stop` is called. Note that some reactors stop automatically if
|
|
389
|
+
there are no events being watched anymore. Meant to be overloaded in a
|
|
390
|
+
subclass. ::
|
|
391
|
+
|
|
392
|
+
# Start reactor only if it is not running already
|
|
393
|
+
if not reactor.is_running:
|
|
394
|
+
reactor.start()
|
|
395
|
+
"""
|
|
396
|
+
raise NotImplementedError('Method "start" not implemented by subclass')
|
|
397
|
+
|
|
398
|
+
def stop(self) -> None:
|
|
399
|
+
"""::
|
|
400
|
+
|
|
401
|
+
reactor.stop()
|
|
402
|
+
|
|
403
|
+
Stop watching for I/O and timer events. Meant to be overloaded in a
|
|
404
|
+
subclass.
|
|
405
|
+
"""
|
|
406
|
+
raise NotImplementedError('Method "stop" not implemented by subclass')
|
|
407
|
+
|
|
408
|
+
def timer(self, after: float, cb: TimerCallback) -> str:
|
|
409
|
+
"""::
|
|
410
|
+
|
|
411
|
+
id = reactor.timer(0.5, cb)
|
|
412
|
+
|
|
413
|
+
Create a new timer, invoking the callback after a given amount of time
|
|
414
|
+
in seconds. Meant to be overloaded in a subclass.
|
|
415
|
+
"""
|
|
416
|
+
raise NotImplementedError('Method "timer" not implemented by subclass')
|
|
417
|
+
|
|
418
|
+
def watch(self, handle: Handle, read: bool, write: bool) -> Self:
|
|
419
|
+
"""::
|
|
420
|
+
|
|
421
|
+
reactor = reactor.watch(handle, readable, writable)
|
|
422
|
+
|
|
423
|
+
Change I/O events to watch handle for with true and false values.
|
|
424
|
+
Meant to be overloaded in a subclass. Note that this method requires an
|
|
425
|
+
active I/O watcher. ::
|
|
426
|
+
|
|
427
|
+
# Watch only for readable events
|
|
428
|
+
reactor.watch(handle, True, False)
|
|
429
|
+
|
|
430
|
+
# Watch only for writable events
|
|
431
|
+
reactor.watch(handle, False, True)
|
|
432
|
+
|
|
433
|
+
# Watch for readable and writable events
|
|
434
|
+
reactor.watch(handle, True, True)
|
|
435
|
+
|
|
436
|
+
# Pause watching for events
|
|
437
|
+
reactor.watch(handle, False, False)
|
|
438
|
+
"""
|
|
439
|
+
raise NotImplementedError('Method "watch" not implemented by subclass')
|
|
440
|
+
|
|
441
|
+
# Not in Mojolicious: run the cleanups that finalizers deferred at the next tick, in a way that is safe in the middle
|
|
442
|
+
# of what the reactor does, as the garbage collector runs a finalizer there
|
|
443
|
+
def _wake(self) -> None:
|
|
444
|
+
pass
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# Not in Mojolicious: the cleanups of the finalizers that the garbage collector ran in the middle of what an event loop
|
|
448
|
+
# does, as a socket that an object closes there, or an acceptor that it removes, changes the structures that the event
|
|
449
|
+
# loop or asyncio are changing; Perl runs a finalizer where the last reference goes, at the end of a statement. Each
|
|
450
|
+
# thread has its own, which the reactors of the thread run
|
|
451
|
+
_DEFERRED: dict[int | None, collections.deque[Callable[[], Anything]]] = {}
|
|
452
|
+
|
|
453
|
+
# The reactors that run the deferred cleanups
|
|
454
|
+
_ALL: 'weakref.WeakSet[Pyjo_Reactor]' = weakref.WeakSet()
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# The reactor is the one of the object, none for an object without one
|
|
458
|
+
def _finalize( # pyright: ignore[reportUnusedFunction]
|
|
459
|
+
cb: Callable[[], Anything], reactor: Pyjo_Reactor | None = None
|
|
460
|
+
) -> None:
|
|
461
|
+
# The cleanup runs at once, as Mojo's DESTROY runs, unless the garbage collector interrupted the code of an event
|
|
462
|
+
# loop, in a frame of Pyjo.IOLoop, of a reactor, of asyncio or of selectors, below the finalizer that calls it, or
|
|
463
|
+
# ran in another thread than the one of the reactor of the object, the main one for an object without a reactor, as
|
|
464
|
+
# a thread of name resolution, while that thread may be in the middle of what its event loop does, which the frames
|
|
465
|
+
# of this thread do not show
|
|
466
|
+
thread = threading.main_thread().ident if reactor is None else reactor._thread # pyright: ignore[reportPrivateUsage]
|
|
467
|
+
if threading.get_ident() == thread:
|
|
468
|
+
prefixes = _prefixes()
|
|
469
|
+
# The frame below the finalizer, none when the interpreter frees the object outside of any code
|
|
470
|
+
frame = sys._getframe(1).f_back # pyright: ignore[reportPrivateUsage]
|
|
471
|
+
while frame is not None and not frame.f_code.co_filename.startswith(prefixes):
|
|
472
|
+
frame = frame.f_back
|
|
473
|
+
if frame is None:
|
|
474
|
+
cb()
|
|
475
|
+
return
|
|
476
|
+
_DEFERRED.setdefault(thread, collections.deque()).append(cb)
|
|
477
|
+
for reactor in list(_ALL):
|
|
478
|
+
if reactor._thread == thread: # pyright: ignore[reportPrivateUsage]
|
|
479
|
+
reactor._wake() # pyright: ignore[reportPrivateUsage]
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _drain(every: bool = False) -> None: # pyright: ignore[reportUnusedFunction]
|
|
483
|
+
# The cleanups of the thread, or of every thread, as at the end of the interpreter; they fail as a finalizer fails,
|
|
484
|
+
# without an exception
|
|
485
|
+
queues = list(_DEFERRED.values()) if every else [_DEFERRED.get(threading.get_ident())]
|
|
486
|
+
for queue in queues:
|
|
487
|
+
while queue:
|
|
488
|
+
try:
|
|
489
|
+
cb = queue.popleft()
|
|
490
|
+
except IndexError:
|
|
491
|
+
break
|
|
492
|
+
try:
|
|
493
|
+
cb()
|
|
494
|
+
except Exception:
|
|
495
|
+
pass
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _prefixes() -> tuple[str, ...]:
|
|
499
|
+
# The directories of the event loops, and of asyncio and selectors when they are loaded
|
|
500
|
+
pyjo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
501
|
+
prefixes = [os.path.join(pyjo, 'IOLoop') + os.sep, os.path.join(pyjo, 'Reactor') + os.sep]
|
|
502
|
+
if (asyncio := sys.modules.get('asyncio')) is not None and (file := getattr(asyncio, '__file__', None)):
|
|
503
|
+
prefixes.append(os.path.dirname(file) + os.sep)
|
|
504
|
+
if (selectors := sys.modules.get('selectors')) is not None and (file := getattr(selectors, '__file__', None)):
|
|
505
|
+
prefixes.append(file)
|
|
506
|
+
return tuple(prefixes)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# What the base class gives to await: it waits for the promise itself
|
|
510
|
+
class _Awaitable:
|
|
511
|
+
def __init__(self, promise: Any) -> None:
|
|
512
|
+
self.promise = promise
|
|
513
|
+
|
|
514
|
+
def __await__(self) -> Generator[Any, Any, None]:
|
|
515
|
+
yield self.promise
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _name(cls: type) -> str:
|
|
519
|
+
return f'{cls.__module__}.{cls.__qualname__}'
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
detect = Pyjo_Reactor.detect
|
|
523
|
+
new = Pyjo_Reactor.new
|
|
524
|
+
object = Pyjo_Reactor
|
|
525
|
+
Reactor = Pyjo_Reactor
|
Pyjo/SSE.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Pyjo.SSE - Server-Sent Events
|
|
3
|
+
=============================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.SSE import build_event, parse_event
|
|
7
|
+
|
|
8
|
+
:mod:`Pyjo.SSE` implements the Server-Sent Events protocol. Note that this
|
|
9
|
+
module is **EXPERIMENTAL** and may change without warning!
|
|
10
|
+
|
|
11
|
+
An event is a :class:`dict` with the keys ``type``, ``text``, ``id`` and,
|
|
12
|
+
when it is built, ``comment``.
|
|
13
|
+
|
|
14
|
+
Functions
|
|
15
|
+
---------
|
|
16
|
+
|
|
17
|
+
:mod:`Pyjo.SSE` implements the following functions, which can be imported
|
|
18
|
+
individually.
|
|
19
|
+
|
|
20
|
+
See also
|
|
21
|
+
--------
|
|
22
|
+
|
|
23
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
24
|
+
|
|
25
|
+
Ported from `Mojo::SSE <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/SSE.pm>`_
|
|
26
|
+
of Mojolicious 9.49.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import Pyjo.String.Unicode
|
|
30
|
+
|
|
31
|
+
from Pyjo.Util import decode, encode
|
|
32
|
+
|
|
33
|
+
import re
|
|
34
|
+
|
|
35
|
+
from collections.abc import Mapping
|
|
36
|
+
from typing import Any, TypedDict
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = ['Event', 'build_event', 'parse_event']
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Event(TypedDict):
|
|
43
|
+
"""A parsed Server-Sent Event."""
|
|
44
|
+
|
|
45
|
+
id: str | None
|
|
46
|
+
type: str
|
|
47
|
+
text: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
SPLIT_RE = re.compile(r'(?:\x0d\x0a|(?<!\x0d)\x0a|\x0d(?!\x0a))')
|
|
51
|
+
EVENT_RE = re.compile(rb'(.*?)(?:(?:\x0d\x0a|(?<!\x0d)\x0a|\x0d(?!\x0a)){2})', re.S)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_event(event: Mapping[str, Any]) -> bytes:
|
|
55
|
+
r"""::
|
|
56
|
+
|
|
57
|
+
data = build_event({'type': 'foo', 'text': 'bar', 'id': 23})
|
|
58
|
+
|
|
59
|
+
Build Server-Sent Event.
|
|
60
|
+
"""
|
|
61
|
+
data = _split(event.get('text'))
|
|
62
|
+
comment = _split(event.get('comment'))
|
|
63
|
+
|
|
64
|
+
parts: list[str] = []
|
|
65
|
+
if len(comment):
|
|
66
|
+
parts.extend(f': {line}' for line in comment)
|
|
67
|
+
else:
|
|
68
|
+
if event.get('type') is not None:
|
|
69
|
+
parts.append(f'event: {event["type"]}')
|
|
70
|
+
parts.extend(f'data: {line}' for line in data)
|
|
71
|
+
if event.get('id') is not None:
|
|
72
|
+
parts.append(f'id: {event["id"]}')
|
|
73
|
+
|
|
74
|
+
return encode('UTF-8', '\x0d\x0a'.join([*parts, '', '']))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Unlike Mojolicious: the buffer is a bytearray, which parse_event changes, where Mojo takes a reference to a string
|
|
78
|
+
def parse_event(buffer: bytearray) -> Event | None:
|
|
79
|
+
r"""::
|
|
80
|
+
|
|
81
|
+
event = parse_event(bytearray(b'data: bar\r\n\r\n'))
|
|
82
|
+
|
|
83
|
+
Parse Server-Sent Event from a :class:`bytearray`, which loses the parsed
|
|
84
|
+
event. Returns ``None`` if no complete event was found.
|
|
85
|
+
"""
|
|
86
|
+
event: Event = {'id': None, 'type': 'message', 'text': ''}
|
|
87
|
+
|
|
88
|
+
# A copy is matched, as the bytearray cannot be resized while a regular expression scans it
|
|
89
|
+
while m := EVENT_RE.match(bytes(buffer)):
|
|
90
|
+
del buffer[: m.end()]
|
|
91
|
+
|
|
92
|
+
# Skip lines with encoding errors
|
|
93
|
+
if (lines := decode('UTF-8', m[1])) is None:
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
# Skip comments
|
|
97
|
+
if re.match(r'\s*:', lines):
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
first = 0
|
|
101
|
+
for line in _split(lines):
|
|
102
|
+
if m := re.fullmatch(r'event(?::\s*(\S.*))?', line):
|
|
103
|
+
event['type'] = m[1] if m[1] is not None else 'message'
|
|
104
|
+
elif m := re.fullmatch(r'data(?::\s*(.*))?', line):
|
|
105
|
+
event['text'] += ('\n' if first else '') + (m[1] if m[1] is not None else '')
|
|
106
|
+
first += 1
|
|
107
|
+
elif m := re.fullmatch(r'id(?::\s*(.*))?', line):
|
|
108
|
+
event['id'] = m[1]
|
|
109
|
+
|
|
110
|
+
return event
|
|
111
|
+
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# Perl's split of the lines, which leaves out the empty ones at the end
|
|
116
|
+
def _split(text: Any) -> list[str]:
|
|
117
|
+
if text is None:
|
|
118
|
+
return []
|
|
119
|
+
return [str(line) for line in Pyjo.String.Unicode.u(str(text)).split(SPLIT_RE) if line is not None]
|