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,854 @@
1
+ """
2
+ Pyjo.Reactor.Asyncio - Low-level event reactor on asyncio
3
+ =========================================================
4
+ ::
5
+
6
+ from Pyjo.Reactor.Asyncio import Asyncio
7
+
8
+ # Watch if handle becomes readable or writable
9
+ reactor = Asyncio()
10
+ reactor.io(first, lambda reactor, writable: print(
11
+ 'First handle is writable' if writable else 'First handle is readable'
12
+ ))
13
+
14
+ # Or import the module, as in Google Python Style Guide
15
+
16
+ import Pyjo.Reactor.Asyncio
17
+
18
+ # Watch if handle becomes readable or writable
19
+ reactor = Pyjo.Reactor.Asyncio.new()
20
+ reactor.io(first, lambda reactor, writable: print(
21
+ 'First handle is writable' if writable else 'First handle is readable'
22
+ ))
23
+
24
+ # Change to watching only if handle becomes writable
25
+ reactor.watch(first, False, True)
26
+
27
+ # Watch a file descriptor if it becomes readable
28
+ reactor.io(fd, lambda reactor, writable: print(
29
+ 'Second handle is writable' if writable else 'Second handle is readable'
30
+ )).watch(fd, True, False)
31
+
32
+ # Add a timer
33
+ def timeout(reactor):
34
+ reactor.remove(first)
35
+ reactor.remove(fd)
36
+ print('Timeout!')
37
+
38
+ reactor.timer(15, timeout)
39
+
40
+ # Start reactor if necessary
41
+ if not reactor.is_running:
42
+ reactor.start()
43
+
44
+ :mod:`Pyjo.Reactor.Asyncio` is a low-level event reactor based on
45
+ :mod:`asyncio`. It watches handles and runs timers in an asyncio event loop,
46
+ so coroutines, tasks and other users of the loop run along with it.
47
+
48
+ A reactor uses the event loop it is given, or else the loop that is running,
49
+ as inside :func:`asyncio.run`, or a new event loop of its own, and moves its
50
+ handles and timers to the loop that is running. In a loop that runs
51
+ already, :meth:`start` and :meth:`one_tick` return right away, as asyncio
52
+ does not nest event loops, and the handles and timers are watched by that
53
+ loop.
54
+
55
+ The loop watches handles with ``add_reader`` and ``add_writer``, which the
56
+ default event loop of asyncio on Windows, :class:`asyncio.ProactorEventLoop`,
57
+ does not have. There, a loop of its own is an
58
+ :class:`asyncio.SelectorEventLoop`, and a loop that the reactor is given or
59
+ that is running has to be one too, as of
60
+ ``asyncio.run(main(), loop_factory=asyncio.SelectorEventLoop)``. The
61
+ :class:`asyncio.SelectorEventLoop` of Windows watches sockets only, not pipes
62
+ or files, up to 512 of them, with :func:`select.select`.
63
+
64
+ In the debug mode of asyncio, as of ``PYTHONDEVMODE``, a loop logs a callback
65
+ that takes longer than its ``slow_callback_duration``, 0.1 seconds. The
66
+ ``PYJO_ASYNCIO_SLOW_CALLBACK`` environment variable sets it in seconds for a
67
+ loop that the reactor makes, as on a machine that is busy, where TLS and an
68
+ application of a server take longer. ::
69
+
70
+ PYTHONDEVMODE=1 PYJO_ASYNCIO_SLOW_CALLBACK=5 python myapp.py daemon
71
+
72
+ The end of a ``with`` block closes the event loop that the reactor made.
73
+
74
+ See also
75
+ --------
76
+
77
+ :mod:`Pyjoyment`, https://mojolicious.org.
78
+
79
+ Ported from `Mojo::Reactor::Poll <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Reactor/Poll.pm>`_
80
+ of Mojolicious 9.49.
81
+ """
82
+
83
+ import Pyjo.Exception
84
+ import Pyjo.Reactor
85
+
86
+ from Pyjo.Reactor import Handle, IOCallback, TimerCallback
87
+ from Pyjo.Util import _call_hint, md5_sum # pyright: ignore[reportPrivateUsage]
88
+
89
+ import asyncio
90
+ import atexit
91
+ import collections
92
+ import os
93
+ import random
94
+ import sys
95
+ import time
96
+ import weakref
97
+
98
+ from collections.abc import Awaitable, Callable, Coroutine, Generator
99
+ from types import TracebackType
100
+ from typing import Any, Self, TypedDict
101
+
102
+
103
+ class _IO(TypedDict):
104
+ cb: IOCallback
105
+ read: bool
106
+ write: bool
107
+
108
+
109
+ class _Timer(TypedDict):
110
+ cb: TimerCallback | None
111
+ after: float
112
+ recurring: bool
113
+ handle: asyncio.Handle
114
+ deadline: float
115
+
116
+
117
+ class Pyjo_Reactor_Asyncio(Pyjo.Reactor.object):
118
+ """
119
+ :mod:`Pyjo.Reactor.Asyncio` inherits all methods from
120
+ :mod:`Pyjo.Reactor` and implements the following new ones.
121
+ """
122
+
123
+ def __init__(self, loop: asyncio.AbstractEventLoop | None = None) -> None:
124
+ """::
125
+
126
+ reactor = Pyjo.Reactor.Asyncio.new()
127
+ reactor = Pyjo.Reactor.Asyncio.new(loop=asyncio.new_event_loop())
128
+
129
+ Construct a new :mod:`Pyjo.Reactor.Asyncio` object.
130
+ """
131
+ super().__init__()
132
+
133
+ # Not in Mojolicious: the asyncio event loop
134
+ self._fixed = loop is not None
135
+ self._loop = loop
136
+ self._own: asyncio.AbstractEventLoop | None = None
137
+ self._finalizer: weakref.finalize[..., Any] | None = None
138
+
139
+ self._error: BaseException | None = None
140
+ self._io: dict[int, _IO] = {}
141
+ # A deque, whose popleft takes the first callback as shift of Perl does, where pop(0) of a list moves all the others
142
+ self._next_tick: collections.deque[TimerCallback] = collections.deque()
143
+ self._next_timer: str | None = None
144
+ self._one = 0
145
+ self._run = 0
146
+ self._running = 0
147
+ self._tasks: set[asyncio.Task[Any]] = set()
148
+ self._timers: dict[str, _Timer] = {}
149
+ _REACTORS.add(self)
150
+
151
+ # Not in Mojolicious: a context manager, which closes the event loop that the reactor made where the block ends, as
152
+ # PyPy frees the object only when its garbage collector runs
153
+ def __enter__(self) -> Self:
154
+ return self
155
+
156
+ def __exit__(
157
+ self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
158
+ ) -> None:
159
+ self.close()
160
+
161
+ # The cleanups that finalizers deferred at the next tick, with call_soon_threadsafe of the event loop, which only
162
+ # appends to a queue, where the timers of the reactor would change its structures, and works from any thread, as the
163
+ # garbage collector runs finalizers in any of them; the reactor stops after them when it watches nothing anymore, as
164
+ # after an event
165
+ def _wake(self) -> None:
166
+ loop = self._loop
167
+ if loop is not None and not loop.is_closed():
168
+ loop.call_soon_threadsafe(Pyjo_Reactor_Asyncio._weak_drain, weakref.ref(self))
169
+
170
+ @staticmethod
171
+ def _weak_drain(ref: 'weakref.ref[Any]') -> None:
172
+ Pyjo.Reactor._drain() # pyright: ignore[reportPrivateUsage]
173
+ if (self := ref()) is not None and self._run and (not self._running or not self._watching()):
174
+ self._stop()
175
+
176
+ def _close_loop(self) -> None:
177
+ if (loop := self._own) is not None:
178
+ _cancel(loop)
179
+ _close(loop)
180
+
181
+ # Not in Mojolicious: a loop of its own, which is closed with the reactor by weakref.finalize, which the garbage
182
+ # collector calls before the finalizers of the objects that it frees, as the loop warns when it is freed before the
183
+ # reactor closes it, as among the objects that a free-threaded Python frees only when the garbage collector runs
184
+ def _own_loop(self) -> asyncio.AbstractEventLoop:
185
+ loop = self._own = _new_event_loop()
186
+ _OWN.add(loop)
187
+ self._finalizer = weakref.finalize(self, _close, loop)
188
+ _register()
189
+ return loop
190
+
191
+ # Not in Mojolicious: the asyncio event loop
192
+ @property
193
+ def loop(self) -> asyncio.AbstractEventLoop:
194
+ """::
195
+
196
+ loop = reactor.loop
197
+
198
+ The :mod:`asyncio` event loop of the reactor: the one it is given, or
199
+ else the loop that is running, as inside :func:`asyncio.run`, or a
200
+ loop of its own. A reactor that is not given a loop follows the loop
201
+ that is running, and moves its handles and timers to it, unless it
202
+ has tasks in another loop, which cannot move. In the child of a fork,
203
+ it moves to a new loop of its own, as the selector of the loop of the
204
+ parent is not the child's.
205
+ """
206
+ return self._follow()
207
+
208
+ # The loop that is running, which a reactor that is not given a loop follows, or the one of the reactor
209
+ def _follow(self) -> asyncio.AbstractEventLoop:
210
+ loop = self._loop
211
+ if self._fixed and loop is not None:
212
+ return loop
213
+
214
+ try:
215
+ running = asyncio.get_running_loop()
216
+ except RuntimeError:
217
+ running = None
218
+
219
+ if running is not None:
220
+ if running is not loop and not len(self._tasks):
221
+ self._move(running)
222
+ elif loop is None or loop.is_closed() or (loop is not self._own and not loop.is_running()):
223
+ if self._own is None or self._own.is_closed():
224
+ self._own_loop()
225
+ assert self._own is not None
226
+ self._move(self._own)
227
+
228
+ assert self._loop is not None
229
+ return self._loop
230
+
231
+ # The child of a fork moves to a new loop, as the selector of the old one, epoll or kqueue, is the one of the parent;
232
+ # the old loop is kept, not closed, which would close the files of the parent
233
+ def _fork(self) -> None:
234
+ if self._fixed or self._loop is None:
235
+ return
236
+ _FORKED.append(self._loop)
237
+ if self._finalizer is not None:
238
+ self._finalizer.detach()
239
+ self._loop = self._own = None
240
+ self._move(self._own_loop())
241
+
242
+ # Move the handles and timers to another loop
243
+ def _move(self, new: asyncio.AbstractEventLoop) -> None:
244
+ old, self._loop = self._loop, new
245
+ alive = old is not None and not old.is_closed()
246
+ for id, timer in self._timers.items():
247
+ after = timer['after']
248
+ if old is not None and alive and after > 0:
249
+ after = max(0.0, timer['deadline'] - old.time())
250
+ timer['handle'].cancel()
251
+ timer['handle'], timer['deadline'] = self._schedule(new, after, id)
252
+ for fd, io in self._io.items():
253
+ if old is not None and alive:
254
+ if io['read']:
255
+ old.remove_reader(fd)
256
+ if io['write']:
257
+ old.remove_writer(fd)
258
+ if io['read']:
259
+ self._add(new, fd, False)
260
+ if io['write']:
261
+ self._add(new, fd, True)
262
+
263
+ def again(self, id: str, after: float | None = None) -> None:
264
+ """::
265
+
266
+ reactor.again(id)
267
+ reactor.again(id, 0.5)
268
+
269
+ Restart timer and optionally change the invocation time. Note that this
270
+ method requires an active timer.
271
+ """
272
+ if (timer := self._timers.get(id)) is None:
273
+ Pyjo.Exception.object.throw('Timer not active')
274
+ if after is not None:
275
+ timer['after'] = after
276
+
277
+ # Not in Mojolicious: a later time only moves the deadline, which the timer checks when it is due, as a stream
278
+ # restarts its timer on every read and write, and a handle of asyncio cancelled and scheduled again each time
279
+ # is what costs, where Mojo sets the time of the timer
280
+ loop = self.loop
281
+ after, handle = timer['after'], timer['handle']
282
+ deadline = loop.time() + after
283
+ if (
284
+ after > 0
285
+ and isinstance(handle, asyncio.TimerHandle)
286
+ and not handle.cancelled()
287
+ and handle.when() <= deadline
288
+ ):
289
+ timer['deadline'] = deadline
290
+ return
291
+ handle.cancel()
292
+ timer['handle'], timer['deadline'] = self._schedule(loop, after, id)
293
+
294
+ # Not in Mojolicious: a future of the loop
295
+ def awaitable(self, promise: Any) -> Awaitable[None]:
296
+ """::
297
+
298
+ future = reactor.awaitable(promise)
299
+
300
+ Return a future of :attr:`loop` that is done when the promise is
301
+ fulfilled or rejected, so that ``await promise`` works in any task of
302
+ the loop.
303
+ """
304
+ future: asyncio.Future[None] = self.loop.create_future()
305
+
306
+ def settle(*values: Any) -> None:
307
+ if not future.done():
308
+ future.set_result(None)
309
+
310
+ promise.then(settle, settle)
311
+ return future
312
+
313
+ # Not in Mojolicious: the release of the event loop of its own, which Perl does in DESTROY, also at the end of a with
314
+ # block
315
+ def close(self) -> None:
316
+ """::
317
+
318
+ reactor.close()
319
+
320
+ Close the event loop that the reactor made for itself, as the end of a
321
+ ``with`` block does, after the tasks that are left in it are cancelled
322
+ and have ended, as :func:`asyncio.run` does, and as the end of the
323
+ program does; an event loop that it was given, or that was running, is
324
+ left to its owner.
325
+ """
326
+ self._close_loop()
327
+
328
+ # Not in Mojolicious: a task of the loop
329
+ def run_coroutine(
330
+ self, coro: Coroutine[Any, Any, Any], resolve: Callable[[Any], Any], reject: Callable[[Any], Any]
331
+ ) -> None:
332
+ """::
333
+
334
+ reactor.run_coroutine(coro, resolve, reject)
335
+
336
+ Run a coroutine as a task of :attr:`loop`, where it can await anything
337
+ of the loop, and call ``resolve`` with what it returns or ``reject``
338
+ with the exception it raises. A task that is not done is something to
339
+ watch, so that :meth:`start` does not stop before it, and each step of
340
+ the coroutine is an event, which ends :meth:`one_tick`.
341
+ """
342
+ task = self.loop.create_task(_steps(coro, weakref.WeakMethod(self._step)))
343
+ self._tasks.add(task)
344
+
345
+ def done(task: asyncio.Task[Any]) -> None:
346
+ self._tasks.discard(task)
347
+ if task.cancelled():
348
+ # A task cancelled before its first step never starts the coroutine, which would warn that it was never
349
+ # awaited
350
+ coro.close()
351
+ reject(asyncio.CancelledError())
352
+ elif (e := task.exception()) is not None:
353
+ reject(e)
354
+ else:
355
+ resolve(task.result())
356
+
357
+ task.add_done_callback(done)
358
+
359
+ # Not in Mojolicious: a future of the loop
360
+ def future(self) -> asyncio.Future[Any]:
361
+ """::
362
+
363
+ future = reactor.future()
364
+
365
+ Return a new :class:`asyncio.Future` of :attr:`loop`, which
366
+ :meth:`Pyjo.Promise.Pyjo_Promise.to_future` turns a promise into.
367
+ """
368
+ return self.loop.create_future()
369
+
370
+ def io(self, handle: Handle, cb: IOCallback) -> Self:
371
+ """::
372
+
373
+ reactor = reactor.io(handle, cb)
374
+
375
+ Watch handle for I/O events, invoking the callback whenever handle
376
+ becomes readable or writable. ::
377
+
378
+ # Callback will be executed twice if handle becomes readable and writable
379
+ reactor.io(handle, lambda reactor, writable: print('Handle is writable' if writable else 'Handle is readable'))
380
+ """
381
+ self._io[_fileno(handle)] = {'cb': cb, 'read': False, 'write': False}
382
+ return self.watch(handle, True, True)
383
+
384
+ @property
385
+ def is_running(self) -> bool:
386
+ """::
387
+
388
+ boolean = reactor.is_running
389
+
390
+ Check if reactor is running.
391
+ """
392
+ return bool(self._running)
393
+
394
+ def next_tick(self, cb: TimerCallback) -> None:
395
+ """::
396
+
397
+ none = reactor.next_tick(cb)
398
+
399
+ Execute callback as soon as possible, but not before returning or other
400
+ callbacks that have been registered with this method, always returns
401
+ ``None``.
402
+ """
403
+ self._next_tick.append(cb)
404
+ if self._next_timer is None:
405
+ self._next_timer = self.timer(0, Pyjo_Reactor_Asyncio._next)
406
+
407
+ # Not in Mojolicious: a reactor that follows the loop that is running moves there with the timer of the next tick,
408
+ # which a loop that stopped in the middle of a tick, as in Pyjo.Promise.wait, leaves where nothing runs it; its
409
+ # own loop that runs, as most of the time, needs no move
410
+ elif not self._fixed and (loop := self._loop) is not None and not loop.is_running():
411
+ self._follow()
412
+ return None
413
+
414
+ def one_tick(self) -> None:
415
+ """::
416
+
417
+ reactor.one_tick()
418
+
419
+ Run reactor until an event occurs or no events are being watched
420
+ anymore. ::
421
+
422
+ # Don't block longer than 0.5 seconds
423
+ id = reactor.timer(0.5, lambda reactor: None)
424
+ reactor.one_tick()
425
+ reactor.remove(id)
426
+ """
427
+ # Just one tick
428
+ running = self._running
429
+ if not running:
430
+ self._running = 1
431
+
432
+ try:
433
+ # Stop automatically if there is nothing to watch
434
+ if not self._watching():
435
+ return self.stop()
436
+
437
+ # Unlike Mojolicious: asyncio does not nest event loops, the one that runs already watches the events
438
+ loop = self.loop
439
+ if loop.is_running():
440
+ return
441
+
442
+ # Wait for one event
443
+ self._one += 1
444
+ try:
445
+ loop.run_forever()
446
+ finally:
447
+ self._one -= 1
448
+
449
+ # An exception of an unhandled error event
450
+ if (error := self._error) is not None:
451
+ self._error = None
452
+ raise error
453
+ finally:
454
+ if not running:
455
+ self._running = running
456
+
457
+ def recurring(self, after: float, cb: TimerCallback) -> str:
458
+ """::
459
+
460
+ id = reactor.recurring(0.25, cb)
461
+
462
+ Create a new recurring timer, invoking the callback repeatedly after a
463
+ given amount of time in seconds.
464
+ """
465
+ return self._timer(True, after, cb)
466
+
467
+ def remove(self, remove: Handle | str) -> bool:
468
+ """::
469
+
470
+ boolean = reactor.remove(handle)
471
+ boolean = reactor.remove(id)
472
+
473
+ Remove handle or timer.
474
+ """
475
+ if isinstance(remove, str):
476
+ if (timer := self._timers.pop(remove, None)) is None:
477
+ return False
478
+ timer['handle'].cancel()
479
+ return True
480
+ fd = _fileno(remove)
481
+ if (io := self._io.pop(fd, None)) is None:
482
+ return False
483
+ self._unwatch(fd, io)
484
+ return True
485
+
486
+ def reset(self) -> None:
487
+ """::
488
+
489
+ reactor.reset()
490
+
491
+ Remove all handles and timers.
492
+ """
493
+ for fd, io in self._io.items():
494
+ self._unwatch(fd, io)
495
+ for timer in self._timers.values():
496
+ timer['handle'].cancel()
497
+ self._events = {}
498
+ self._io = {}
499
+ self._next_tick.clear()
500
+ self._next_timer = None
501
+ self._running = 0
502
+ self._tasks = set()
503
+ self._timers = {}
504
+ self._stop()
505
+
506
+ def start(self) -> None:
507
+ """::
508
+
509
+ reactor.start()
510
+
511
+ Start watching for I/O and timer events, this will block until
512
+ :meth:`stop` is called or no events are being watched anymore. ::
513
+
514
+ # Start reactor only if it is not running already
515
+ if not reactor.is_running:
516
+ reactor.start()
517
+ """
518
+ running = self._running
519
+ self._running = running + 1
520
+ try:
521
+ # Unlike Mojolicious: asyncio does not nest event loops, the one that runs already watches the events
522
+ while self._running and not (loop := self.loop).is_running():
523
+ # Stop automatically if there is nothing to watch
524
+ if not self._watching():
525
+ return self.stop()
526
+
527
+ # Not in Mojolicious: the loop runs until the reactor stops, where Mojo runs one tick after another, as
528
+ # an asyncio loop that stops and runs again for every event is what costs
529
+ self._run += 1
530
+ try:
531
+ loop.run_forever()
532
+ finally:
533
+ self._run -= 1
534
+
535
+ # An exception of an unhandled error event
536
+ if (error := self._error) is not None:
537
+ self._error = None
538
+ raise error
539
+ finally:
540
+ self._running = running
541
+
542
+ def stop(self) -> None:
543
+ """::
544
+
545
+ reactor.stop()
546
+
547
+ Stop watching for I/O and timer events.
548
+ """
549
+ self._running = 0
550
+ self._stop()
551
+
552
+ def timer(self, after: float, cb: TimerCallback) -> str:
553
+ """::
554
+
555
+ id = reactor.timer(0.5, cb)
556
+
557
+ Create a new timer, invoking the callback after a given amount of time
558
+ in seconds.
559
+ """
560
+ return self._timer(False, after, cb)
561
+
562
+ def watch(self, handle: Handle, read: bool, write: bool) -> Self:
563
+ """::
564
+
565
+ reactor = reactor.watch(handle, readable, writable)
566
+
567
+ Change I/O events to watch handle for with true and false values. Note
568
+ that this method requires an active I/O watcher. ::
569
+
570
+ # Watch only for readable events
571
+ reactor.watch(handle, True, False)
572
+
573
+ # Watch only for writable events
574
+ reactor.watch(handle, False, True)
575
+
576
+ # Watch for readable and writable events
577
+ reactor.watch(handle, True, True)
578
+
579
+ # Pause watching for events
580
+ reactor.watch(handle, False, False)
581
+ """
582
+ fd = _fileno(handle)
583
+ if (io := self._io.get(fd)) is None:
584
+ Pyjo.Exception.object.throw('I/O watcher not active')
585
+
586
+ # Not in Mojolicious: only the events that change are added or removed, as each is a call of the selector of
587
+ # asyncio, where Mojo sets the mask of the handle
588
+ loop = self.loop
589
+ read, write = bool(read), bool(write)
590
+ if read != io['read']:
591
+ if read:
592
+ self._add(loop, fd, False)
593
+ else:
594
+ loop.remove_reader(fd)
595
+ if write != io['write']:
596
+ if write:
597
+ self._add(loop, fd, True)
598
+ else:
599
+ loop.remove_writer(fd)
600
+ io['read'], io['write'] = read, write
601
+
602
+ return self
603
+
604
+ # Not in Mojolicious: a loop without add_reader and add_writer, as the ProactorEventLoop of Windows, which
605
+ # asyncio.run makes there, cannot watch handles, and says so
606
+ def _add(self, loop: asyncio.AbstractEventLoop, fd: int, writable: bool) -> None:
607
+ add = loop.add_writer if writable else loop.add_reader
608
+ try:
609
+ add(fd, Pyjo_Reactor_Asyncio._weak_ready, weakref.ref(self), fd, writable)
610
+ except NotImplementedError:
611
+ name = f'{type(loop).__module__}.{type(loop).__qualname__}'
612
+ Pyjo.Exception.object.throw(f'Event loop {name} cannot watch handles, an asyncio.SelectorEventLoop can')
613
+
614
+ # A timer of no time runs before the I/O of the next iteration of the loop, as the timers of Mojo's reactor run in
615
+ # the same tick, after its I/O; call_later would run it after the I/O of the next iteration
616
+ def _schedule(self, loop: asyncio.AbstractEventLoop, after: float, id: str) -> tuple[asyncio.Handle, float]:
617
+ if after <= 0:
618
+ return loop.call_soon(Pyjo_Reactor_Asyncio._weak_alarm, weakref.ref(self), id), loop.time()
619
+ handle = loop.call_later(after, Pyjo_Reactor_Asyncio._weak_alarm, weakref.ref(self), id)
620
+ return handle, handle.when()
621
+
622
+ # The loop refers to the reactor weakly, so that the reactor and its loop do not make a cycle
623
+ @staticmethod
624
+ def _weak_alarm(ref: 'weakref.ref[Any]', id: str) -> None:
625
+ if (reactor := ref()) is not None:
626
+ reactor._alarm(id)
627
+
628
+ @staticmethod
629
+ def _weak_ready(ref: 'weakref.ref[Any]', fd: int, writable: bool) -> None:
630
+ if (reactor := ref()) is not None:
631
+ reactor._ready(fd, writable)
632
+
633
+ # A timer is due
634
+ def _alarm(self, id: str) -> None:
635
+ if (timer := self._timers.get(id)) is None:
636
+ return
637
+
638
+ # Not in Mojolicious: a timer that again moved to a later time waits for it
639
+ loop = self.loop
640
+ if (left := timer['deadline'] - loop.time()) > 0:
641
+ timer['handle'] = loop.call_later(left, Pyjo_Reactor_Asyncio._weak_alarm, weakref.ref(self), id)
642
+ return
643
+
644
+ # Recurring timer
645
+ if timer['recurring']:
646
+ timer['handle'], timer['deadline'] = self._schedule(loop, timer['after'], id)
647
+
648
+ # Normal timer
649
+ else:
650
+ self.remove(id)
651
+
652
+ if (cb := timer['cb']) is not None:
653
+ self._event('Timer', cb)
654
+
655
+ # An event of a watcher or a timer, which ends one_tick, and start once the reactor stops or watches nothing
656
+ def _event(self, what: str, cb: Any, *args: Any) -> None:
657
+ try:
658
+ self._try(what, cb, *args)
659
+ except Exception as e:
660
+ # Not in Mojolicious: asyncio logs an exception of a callback, Perl dies out of one_tick, so it is kept and
661
+ # raised by one_tick or start, unless the loop runs without them
662
+ if not self._one and not self._run:
663
+ raise
664
+ self._error = e
665
+ return self._stop()
666
+ if self._one or (self._run and (not self._running or not self._watching())):
667
+ self._stop()
668
+
669
+ def _id(self) -> str:
670
+ while True:
671
+ id = md5_sum(f't{time.monotonic()}{random.random()}'.encode())
672
+ if id not in self._timers:
673
+ return id
674
+
675
+ def _next(self) -> None:
676
+ self._next_timer = None
677
+ while len(self._next_tick):
678
+ self._next_tick.popleft()(self)
679
+
680
+ # A handle is ready
681
+ def _ready(self, fd: int, writable: bool) -> None:
682
+ if (io := self._io.get(fd)) is not None:
683
+ self._event('I/O watcher', io['cb'], writable)
684
+
685
+ # A step of a coroutine, which ends one_tick
686
+ def _step(self) -> None:
687
+ if self._one and self.loop.is_running():
688
+ self.loop.stop()
689
+
690
+ def _stop(self) -> None:
691
+ if (self._one or self._run) and self.loop.is_running():
692
+ self.loop.stop()
693
+
694
+ def _timer(self, recurring: bool, after: float, cb: TimerCallback) -> str:
695
+ id = self._id()
696
+ handle, deadline = self._schedule(self.loop, after, id)
697
+ self._timers[id] = {'cb': cb, 'after': after, 'recurring': recurring, 'handle': handle, 'deadline': deadline}
698
+ return id
699
+
700
+ def _try(self, what: str, cb: Any, *args: Any) -> None:
701
+ try:
702
+ cb(self, *args)
703
+ except Exception as e:
704
+ # Not in Mojolicious: the arguments of a callback, which Perl does not check
705
+ if isinstance(e, TypeError):
706
+ _call_hint(e, 'the callbacks of a reactor get the reactor first')
707
+ # Unlike Mojolicious: the message of Python has no file and line, the exception keeps the frames
708
+ err = Pyjo.Exception.new(e)
709
+ err.message = f'{what} failed: {err.message}'
710
+ err.__cause__ = e
711
+ self.emit('error', err)
712
+
713
+ def _watching(self) -> bool:
714
+ return bool(len(self._timers) or len(self._io) or len(self._tasks))
715
+
716
+ # The handle is removed from the loop that watches it, without the move to a loop that runs, which leaves out the
717
+ # handles that are not watched anymore, so the loop of before would keep it, and select of Windows fails once the
718
+ # handle is closed
719
+ def _unwatch(self, fd: int, io: _IO) -> None:
720
+ if (loop := self._loop) is not None and not loop.is_closed():
721
+ if io['read']:
722
+ loop.remove_reader(fd)
723
+ if io['write']:
724
+ loop.remove_writer(fd)
725
+ io['read'] = io['write'] = False
726
+
727
+
728
+ def _fileno(handle: Handle) -> int:
729
+ fd = handle if isinstance(handle, int) else handle.fileno()
730
+ if fd < 0:
731
+ Pyjo.Exception.object.throw('Handle is closed')
732
+ return fd
733
+
734
+
735
+ # Not in Mojolicious: a step of a coroutine is an event of the reactor, as a step of an async sub of Mojo is a callback of
736
+ # next_tick, so that one_tick returns after it: the step of a task of asyncio is a callback of the loop that ends nothing,
737
+ # so code that waits with one_tick for what the coroutine does, as a server that waits for the response of an action,
738
+ # would wait for the next event of a watcher or a timer
739
+ async def _steps(coro: Coroutine[Any, Any, Any], step: 'weakref.WeakMethod[Callable[[], None]]') -> Any:
740
+ return await _Steps(coro, step)
741
+
742
+
743
+ class _Steps:
744
+ def __init__(self, coro: Coroutine[Any, Any, Any], step: 'weakref.WeakMethod[Callable[[], None]]') -> None:
745
+ self._coro = coro
746
+ self._step = step
747
+
748
+ def __await__(self) -> Generator[Any, Any, Any]:
749
+ coro = self._coro
750
+ method: Callable[[Any], Any] = coro.send
751
+ arg: Any = None
752
+ while True:
753
+ try:
754
+ result = method(arg)
755
+ except StopIteration as e:
756
+ self._stepped()
757
+ return e.value
758
+ except BaseException:
759
+ self._stepped()
760
+ raise
761
+ self._stepped()
762
+ try:
763
+ arg = yield result
764
+ method = coro.send
765
+ except GeneratorExit:
766
+ coro.close()
767
+ raise
768
+ except BaseException as e:
769
+ arg = e
770
+ method = coro.throw
771
+
772
+ def _stepped(self) -> None:
773
+ if (step := self._step()) is not None:
774
+ step()
775
+
776
+
777
+ # Not in Mojolicious: a loop of its own is a SelectorEventLoop on Windows, where the default of asyncio is the
778
+ # ProactorEventLoop, which has no add_reader and add_writer, with the threshold of a slow callback of
779
+ # PYJO_ASYNCIO_SLOW_CALLBACK, as asyncio has no variable of its own for it
780
+ def _new_event_loop() -> asyncio.AbstractEventLoop:
781
+ loop = asyncio.SelectorEventLoop() if sys.platform == 'win32' else asyncio.new_event_loop()
782
+ if duration := os.environ.get('PYJO_ASYNCIO_SLOW_CALLBACK'):
783
+ loop.slow_callback_duration = float(duration)
784
+ return loop
785
+
786
+
787
+ # Not in Mojolicious: the event loops that the reactors make for themselves, which are closed at the exit of the
788
+ # interpreter, before it finalizes the objects and the loops of the reactors that still exist would warn that they are
789
+ # not closed
790
+ _OWN: 'weakref.WeakSet[asyncio.AbstractEventLoop]' = weakref.WeakSet()
791
+
792
+
793
+ # Not in Mojolicious: the reactors that exist, which move to new loops in the child of a fork, and the loops of the parent,
794
+ # which the child keeps
795
+ _REACTORS: 'weakref.WeakSet[Pyjo_Reactor_Asyncio]' = weakref.WeakSet()
796
+ _FORKED: list[asyncio.AbstractEventLoop] = []
797
+
798
+
799
+ def _after_fork_in_child() -> None:
800
+ for reactor in list(_REACTORS):
801
+ try:
802
+ reactor._fork() # pyright: ignore[reportPrivateUsage]
803
+ except Exception:
804
+ pass
805
+
806
+
807
+ if hasattr(os, 'register_at_fork'):
808
+ os.register_at_fork(after_in_child=_after_fork_in_child)
809
+
810
+
811
+ def _close(loop: asyncio.AbstractEventLoop) -> None:
812
+ if not loop.is_running() and not loop.is_closed():
813
+ loop.close()
814
+
815
+
816
+ # Not in Mojolicious: the tasks that are left in a loop of its own, as a coroutine that waits for the next cycle of a loop
817
+ # that has stopped, are cancelled and run to their end before the loop closes, as asyncio.run does, so that none is
818
+ # destroyed while it is pending; not in another loop that runs, which cannot run this one, nor in the garbage collector
819
+ def _cancel(loop: asyncio.AbstractEventLoop) -> None:
820
+ if loop.is_running() or loop.is_closed():
821
+ return
822
+ try:
823
+ asyncio.get_running_loop()
824
+ return
825
+ except RuntimeError:
826
+ pass
827
+ if tasks := [task for task in asyncio.all_tasks(loop) if not task.done()]:
828
+ for task in tasks:
829
+ task.cancel()
830
+ loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
831
+
832
+
833
+ def _close_own() -> None:
834
+ for loop in list(_OWN):
835
+ _cancel(loop)
836
+ _close(loop)
837
+
838
+
839
+ # Not in Mojolicious: the loops of their own are closed at the end of the program after their tasks, by a handler that is
840
+ # registered after the first finalizer, so that it runs before the exit function of weakref.finalize, as atexit runs
841
+ # the handlers in the reverse order, which would close the loops with their tasks pending
842
+ _registered = False
843
+
844
+
845
+ def _register() -> None:
846
+ global _registered
847
+ if not _registered:
848
+ _registered = True
849
+ atexit.register(_close_own)
850
+
851
+
852
+ new = Pyjo_Reactor_Asyncio.new
853
+ object = Pyjo_Reactor_Asyncio
854
+ Asyncio = Pyjo_Reactor_Asyncio