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