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
Pyjo/Promise.py ADDED
@@ -0,0 +1,894 @@
1
+ """
2
+ Pyjo.Promise - Promises/A+
3
+ ==========================
4
+ ::
5
+
6
+ import Pyjo.UserAgent
7
+
8
+ from Pyjo.Promise import Promise
9
+
10
+ # Wrap continuation-passing style APIs with promises
11
+ ua = Pyjo.UserAgent.new()
12
+
13
+ def get_p(url):
14
+ promise = Promise()
15
+
16
+ def cb(ua, tx):
17
+ err = tx.error
18
+ if not err or err.get('code'):
19
+ promise.resolve(tx)
20
+ else:
21
+ promise.reject(err['message'])
22
+
23
+ ua.get(url, cb)
24
+ return promise
25
+
26
+ # Or import the module, as in Google Python Style Guide
27
+
28
+ import Pyjo.Promise
29
+ import Pyjo.UserAgent
30
+
31
+ # Wrap continuation-passing style APIs with promises
32
+ ua = Pyjo.UserAgent.new()
33
+
34
+ def get_p(url):
35
+ promise = Pyjo.Promise.new()
36
+
37
+ def cb(ua, tx):
38
+ err = tx.error
39
+ if not err or err.get('code'):
40
+ promise.resolve(tx)
41
+ else:
42
+ promise.reject(err['message'])
43
+
44
+ ua.get(url, cb)
45
+ return promise
46
+
47
+ # Perform non-blocking operations sequentially
48
+ def mojo_cb(mojo):
49
+ print(mojo.res.code)
50
+ return get_p('https://metacpan.org')
51
+
52
+ get_p('https://mojolicious.org').then(mojo_cb).then(
53
+ lambda cpan: print(cpan.res.code)
54
+ ).catch(lambda err: warn(f'Something went wrong: {err}')).wait()
55
+
56
+ # Synchronize non-blocking operations (all)
57
+ mojo = get_p('https://mojolicious.org')
58
+ cpan = get_p('https://metacpan.org')
59
+ Pyjo.Promise.all(mojo, cpan).then(
60
+ lambda mojo, cpan: print(mojo[0].res.code, cpan[0].res.code)
61
+ ).catch(lambda err: warn(f'Something went wrong: {err}')).wait()
62
+
63
+ # Synchronize non-blocking operations (race)
64
+ mojo = get_p('https://mojolicious.org')
65
+ cpan = get_p('https://metacpan.org')
66
+ Pyjo.Promise.race(mojo, cpan).then(
67
+ lambda tx: print(tx.req.url, 'won!')
68
+ ).catch(lambda err: warn(f'Something went wrong: {err}')).wait()
69
+
70
+ # Await a promise in a coroutine
71
+ @Pyjo.Promise.coroutine
72
+ async def code(url):
73
+ tx = await get_p(url)
74
+ return tx.res.code
75
+
76
+ code('https://mojolicious.org').then(print).wait()
77
+
78
+ :mod:`Pyjo.Promise` is a Python implementation of
79
+ `Promises/A+ <https://promisesaplus.com>`_ and a superset of
80
+ `ES6 Promises <https://duckduckgo.com/?q=\\\\mdn%20Promise>`_.
81
+
82
+ States
83
+ ------
84
+
85
+ A promise is an object representing the eventual completion or failure of a
86
+ non-blocking operation. It allows non-blocking functions to return values,
87
+ like blocking functions. But instead of immediately returning the final value,
88
+ the non-blocking function returns a promise to supply the value at some point
89
+ in the future.
90
+
91
+ A promise can be in one of three states:
92
+
93
+ pending
94
+ Initial state, neither fulfilled nor rejected.
95
+
96
+ fulfilled
97
+ Meaning that the operation completed successfully.
98
+
99
+ rejected
100
+ Meaning that the operation failed.
101
+
102
+ A pending promise can either be fulfilled with a value or rejected with a
103
+ reason. When either happens, the associated handlers queued up by a promise's
104
+ :meth:`~Pyjo_Promise.then` method are called.
105
+
106
+ Values
107
+ ------
108
+
109
+ A promise is fulfilled or rejected with any number of values, and the
110
+ handlers get them as arguments. What a handler returns is the value of the new
111
+ promise, ``None`` is no value, and a promise is adopted, so a handler that
112
+ passes several values on returns a promise of them. ::
113
+
114
+ promise.then(lambda *values: Pyjo.Promise.resolve(*values))
115
+
116
+ Coroutines
117
+ ----------
118
+
119
+ ``await promise`` waits for the promise in a coroutine, and gives its value,
120
+ a tuple of several values, or ``None`` for none. A rejection raises the
121
+ exception the promise was rejected with, or a :mod:`Pyjo.Exception` of the
122
+ first reason. A coroutine is a promise with :meth:`~Pyjo_Promise.new` or
123
+ :func:`coroutine`. The reactor of the event loop runs it: a coroutine awaits the
124
+ promises of Pyjoyment with any reactor, and with :mod:`Pyjo.Reactor.Asyncio`
125
+ anything of asyncio too, and a promise can be awaited in any asyncio task.
126
+ Another awaitable, as a future of asyncio, is a promise with
127
+ :meth:`~Pyjo_Promise.new` too, and :meth:`~Pyjo_Promise.to_future` turns a
128
+ promise into a future of the reactor. ::
129
+
130
+ async def main():
131
+ title = (await get_p('https://mojolicious.org')).res.dom().at('title').text
132
+
133
+ Debugging
134
+ ---------
135
+
136
+ You can set the ``PYJO_PROMISE_DEBUG`` environment variable to get some
137
+ advanced diagnostics information printed to ``sys.stderr``. ::
138
+
139
+ PYJO_PROMISE_DEBUG=1
140
+
141
+ See also
142
+ --------
143
+
144
+ :mod:`Pyjoyment`, https://mojolicious.org.
145
+
146
+ Ported from `Mojo::Promise <https://github.com/mojolicious/mojo/blob/v9.52/lib/Mojo/Promise.pm>`_
147
+ of Mojolicious 9.52.
148
+
149
+ Functions
150
+ ---------
151
+ """
152
+
153
+ import Pyjo.Base
154
+ import Pyjo.Exception
155
+ import Pyjo.IOLoop
156
+ import Pyjo.Reactor
157
+
158
+ from Pyjo.Util import warn
159
+
160
+ import atexit
161
+ import builtins
162
+ import collections
163
+ import concurrent.futures
164
+ import functools
165
+ import os
166
+ import socket
167
+ import warnings
168
+ import weakref
169
+
170
+ from collections.abc import Awaitable, Callable, Coroutine, Generator
171
+ from typing import TYPE_CHECKING, Any, Concatenate, cast
172
+
173
+
174
+ type Anything = builtins.object
175
+
176
+ # A handler gets the values of the promise
177
+ type Handler = Callable[..., Any]
178
+
179
+ DEBUG = bool(os.environ.get('PYJO_PROMISE_DEBUG'))
180
+
181
+
182
+ class _hybridmethod[**P, R]:
183
+ """A method that is called on the class or on an object, as Perl's can be."""
184
+
185
+ def __init__(self, func: Callable[Concatenate[Any, P], R]) -> None:
186
+ self.func = func
187
+ self.__doc__ = func.__doc__
188
+
189
+ def __get__(self, obj: Anything, objtype: type | None = None) -> Callable[P, R]:
190
+ def bound(*args: P.args, **kwargs: P.kwargs) -> R:
191
+ return self.func(obj if obj is not None else objtype, *args, **kwargs)
192
+
193
+ bound.__doc__ = self.__doc__
194
+ return bound
195
+
196
+
197
+ class Pyjo_Promise(Pyjo.Base.object):
198
+ """
199
+ :mod:`Pyjo.Promise` inherits all methods from :mod:`Pyjo.Base` and
200
+ implements the following new ones.
201
+ """
202
+
203
+ def __init__(
204
+ self, cb: Callable[[Handler, Handler], Anything] | Awaitable[Any] | concurrent.futures.Future[Any] | None = None
205
+ ) -> None:
206
+ """::
207
+
208
+ promise = Pyjo.Promise.new()
209
+ promise = Pyjo.Promise.new(lambda resolve, reject: ...)
210
+ promise = Pyjo.Promise.new(coroutine)
211
+ promise = Pyjo.Promise.new(future)
212
+
213
+ Construct a new :mod:`Pyjo.Promise` object. ::
214
+
215
+ # Wrap a continuation-passing style API
216
+ def cb(resolve, reject):
217
+ Pyjo.IOLoop.timer(5, lambda loop: resolve('Lucky!') if random.randint(0, 1) else reject('Unlucky!'))
218
+
219
+ promise = Pyjo.Promise.new(cb)
220
+
221
+ A coroutine is run by the reactor of :attr:`ioloop`, and the promise
222
+ is fulfilled with what it returns, or rejected with the exception it
223
+ raises. ::
224
+
225
+ # Wrap a coroutine
226
+ async def lucky():
227
+ await Pyjo.Promise.timer(5)
228
+ return 'Lucky!'
229
+
230
+ promise = Pyjo.Promise.new(lucky())
231
+
232
+ Another awaitable, like an :class:`asyncio.Future` or an
233
+ :class:`asyncio.Task`, is awaited in a coroutine, so with a reactor
234
+ that can await it, as :mod:`Pyjo.Reactor.Asyncio`. ::
235
+
236
+ # Wrap a future of asyncio
237
+ future = asyncio.get_running_loop().run_in_executor(None, time.sleep, 1)
238
+ promise = Pyjo.Promise.new(future)
239
+
240
+ A :class:`concurrent.futures.Future`, which is done in another thread
241
+ or process, is waited for with any reactor, which the thread that
242
+ completes the future wakes up. ::
243
+
244
+ # Wrap a future of a pool of threads
245
+ with concurrent.futures.ThreadPoolExecutor() as executor:
246
+ promise = Pyjo.Promise.new(executor.submit(time.sleep, 1))
247
+ promise.wait()
248
+ """
249
+ self._debug: Pyjo.Exception.object | None = None
250
+ self._handled = False
251
+ self._ioloop: weakref.ref[Pyjo.IOLoop.object] | None = None
252
+ self._reject: list[Handler] = []
253
+ self._resolve: list[Handler] = []
254
+ self._results: tuple[Any, ...] | None = None
255
+ self._status = ''
256
+
257
+ if DEBUG:
258
+ self._debug = Pyjo.Exception.new().trace()
259
+
260
+ # Not in Mojolicious: a future of concurrent.futures, done in another thread
261
+ if isinstance(cb, concurrent.futures.Future):
262
+ _wait_future(self, cast(concurrent.futures.Future[Any], cb))
263
+
264
+ # Not in Mojolicious: a coroutine, or another awaitable in one, as a future of asyncio
265
+ elif isinstance(cb, Awaitable):
266
+ coro = cast(Coroutine[Any, Any, Any], cb) if isinstance(cb, Coroutine) else _await(cast(Awaitable[Any], cb))
267
+
268
+ def resolve(value: Any) -> None:
269
+ self.resolve() if value is None else self.resolve(value)
270
+
271
+ self.ioloop.reactor.run_coroutine(coro, resolve, lambda e: self.reject(e))
272
+
273
+ elif cb is not None:
274
+ cb(lambda *values: self.resolve(*values), lambda *values: self.reject(*values))
275
+
276
+ @property
277
+ def ioloop(self) -> Pyjo.IOLoop.object:
278
+ """::
279
+
280
+ loop = promise.ioloop
281
+ promise.ioloop = Pyjo.IOLoop.new()
282
+
283
+ Event loop object to control, defaults to the global
284
+ :mod:`Pyjo.IOLoop` singleton. Note that this attribute is weakened.
285
+ """
286
+ if self._ioloop is None:
287
+ self._ioloop = weakref.ref(Pyjo.IOLoop.singleton)
288
+ if (loop := self._ioloop()) is None:
289
+ raise ReferenceError('weakly-referenced object no longer exists')
290
+ return loop
291
+
292
+ @ioloop.setter
293
+ def ioloop(self, value: Pyjo.IOLoop.object) -> None:
294
+ self._ioloop = weakref.ref(value)
295
+
296
+ def __await__(self) -> Generator[Any, Any, Any]:
297
+ """::
298
+
299
+ value = await promise
300
+
301
+ Wait for the promise in a coroutine and return its value, a tuple of
302
+ several values, or ``None`` for none, or raise the exception it was
303
+ rejected with, or a :mod:`Pyjo.Exception` of the first reason.
304
+ """
305
+ # Mojo's AWAIT_* methods
306
+ self._handled = True
307
+ yield from self.ioloop.reactor.awaitable(self).__await__()
308
+
309
+ results: tuple[Any, ...] = self._results or ()
310
+ if self._status == 'resolve':
311
+ return _result(results)
312
+ raise _exception(results)
313
+
314
+ if not TYPE_CHECKING:
315
+ # Not in Mojolicious: an attribute of the value taken from the promise itself, as of the result of a request of
316
+ # get_p without await; out of the sight of type checkers, which would take any attribute of a promise for one
317
+ def __getattr__(self, name: str) -> Any:
318
+ message = f"'{type(self).__name__}' object has no attribute '{name}'"
319
+ if not name.startswith('_'):
320
+ message += ', a promise gives its value to await or to a callback of then'
321
+ raise AttributeError(message, name=name, obj=self)
322
+
323
+ def __del__(self) -> None:
324
+ if getattr(self, '_handled', True) or getattr(self, '_status', '') != 'reject' or self._results is None:
325
+ return
326
+ try:
327
+ warnings.warn(f'Unhandled rejected promise: {" ".join(str(r) for r in self._results)}', stacklevel=2)
328
+ if DEBUG and (debug := self._debug) is not None:
329
+ debug.message = '-- Destroyed promise\n'
330
+ debug.verbose = True
331
+ warn(debug.to_str())
332
+ except Exception:
333
+ pass
334
+
335
+ @classmethod
336
+ def all(cls, *promises: 'Pyjo_Promise') -> 'Pyjo_Promise':
337
+ """::
338
+
339
+ new = Pyjo.Promise.all(*promises)
340
+
341
+ Returns a new :mod:`Pyjo.Promise` object that either fulfills when all
342
+ of the passed :mod:`Pyjo.Promise` objects have fulfilled or rejects as
343
+ soon as one of them rejects. If the returned promise fulfills, it is
344
+ fulfilled with the values from the fulfilled promises in the same order
345
+ as the passed promises, a list of the values of each.
346
+ """
347
+ return _all(2, promises)
348
+
349
+ @classmethod
350
+ def all_settled(cls, *promises: 'Pyjo_Promise') -> 'Pyjo_Promise':
351
+ """::
352
+
353
+ new = Pyjo.Promise.all_settled(*promises)
354
+
355
+ Returns a new :mod:`Pyjo.Promise` object that fulfills when all of the
356
+ passed :mod:`Pyjo.Promise` objects have fulfilled or rejected, with
357
+ dicts that describe the outcome of each promise.
358
+ """
359
+ return _all(0, promises)
360
+
361
+ @classmethod
362
+ def any(cls, *promises: 'Pyjo_Promise') -> 'Pyjo_Promise':
363
+ """::
364
+
365
+ new = Pyjo.Promise.any(*promises)
366
+
367
+ Returns a new :mod:`Pyjo.Promise` object that fulfills as soon as one
368
+ of the passed :mod:`Pyjo.Promise` objects fulfills, with the value from
369
+ that promise.
370
+ """
371
+ return _all(3, promises)
372
+
373
+ def catch(self, cb: Handler) -> 'Pyjo_Promise':
374
+ """::
375
+
376
+ new = promise.catch(lambda *reason: ...)
377
+
378
+ Appends a rejection handler callback to the promise, and returns a new
379
+ :mod:`Pyjo.Promise` object resolving to the return value of the
380
+ callback if it is called, or to its original fulfillment value if the
381
+ promise is instead fulfilled. ::
382
+
383
+ # Longer version
384
+ new = promise.then(None, lambda *reason: ...)
385
+
386
+ # Pass along the rejection reason
387
+ def cb(*reason):
388
+ warn(f'Something went wrong: {reason[0]}')
389
+ return Pyjo.Promise.reject(*reason)
390
+
391
+ promise.catch(cb)
392
+
393
+ # Change the rejection reason
394
+ promise.catch(lambda *reason: f'This is bad: {reason[0]}')
395
+ """
396
+ return self.then(None, cb)
397
+
398
+ def clone(self) -> 'Pyjo_Promise':
399
+ """::
400
+
401
+ new = promise.clone()
402
+
403
+ Return a new :mod:`Pyjo.Promise` object cloned from this promise that
404
+ is still pending.
405
+ """
406
+ new = type(self)()
407
+ new.ioloop = self.ioloop
408
+ return new
409
+
410
+ def lastly(self, cb: Callable[[], Any]) -> 'Pyjo_Promise':
411
+ """::
412
+
413
+ new = promise.lastly(lambda: ...)
414
+
415
+ Appends a fulfillment and rejection handler to the promise, and returns
416
+ a new :mod:`Pyjo.Promise` object resolving to the original fulfillment
417
+ value or rejection reason. ``finally_`` is an alias, the name of
418
+ Mojolicious with an underscore. ::
419
+
420
+ # Do something on fulfillment and rejection
421
+ promise.lastly(lambda: print('We are done!'))
422
+ """
423
+ # Renamed from Mojolicious: finally, a keyword of Python, as Bluebird names it
424
+ return self._finally(True, cb)
425
+
426
+ # The name of Mojolicious, with an underscore as finally is a keyword of Python
427
+ finally_ = lastly
428
+
429
+ @classmethod
430
+ def map(cls, cb: Callable[[Any], 'Pyjo_Promise'], *items: Any, concurrency: int = 0) -> 'Pyjo_Promise':
431
+ """::
432
+
433
+ new = Pyjo.Promise.map(lambda item: ..., *items)
434
+ new = Pyjo.Promise.map(lambda item: ..., *items, concurrency=3)
435
+
436
+ Apply a function that returns a :mod:`Pyjo.Promise` to each item in a
437
+ list of items while optionally limiting concurrency. Returns a
438
+ :mod:`Pyjo.Promise` that collects the results in the same manner as
439
+ :meth:`all`. If any item's promise is rejected, any remaining items
440
+ which have not yet been mapped will not be. ::
441
+
442
+ # Perform 3 requests at a time concurrently
443
+ Pyjo.Promise.map(lambda url: ua.get_p(url), *urls, concurrency=3).then(
444
+ lambda *results: [print(r[0].res.dom().at('title').text) for r in results]
445
+ )
446
+
447
+ These options are currently available:
448
+
449
+ concurrency
450
+ ::
451
+
452
+ concurrency=3
453
+
454
+ The maximum number of items that are in progress at the same time.
455
+ """
456
+ if not concurrency or len(items) <= concurrency:
457
+ return cls.all(*(cb(item) for item in items))
458
+
459
+ # Deques, whose popleft takes the first item as shift of Perl does, where pop(0) of a list moves all the others
460
+ rest = collections.deque(items[concurrency:])
461
+ start = [cb(item) for item in items[:concurrency]]
462
+ wait = [start[0].clone() for _ in rest]
463
+ waiting = collections.deque(wait)
464
+
465
+ def start_next(*values: Any) -> None:
466
+ if not len(rest):
467
+ return
468
+ item = rest.popleft()
469
+ chain = waiting.popleft()
470
+
471
+ def resolve(*values: Any) -> None:
472
+ chain.resolve(*values)
473
+ start_next()
474
+
475
+ def reject(*values: Any) -> None:
476
+ chain.reject(*values)
477
+ rest.clear()
478
+
479
+ cb(item).then(resolve, reject)
480
+
481
+ for promise in start:
482
+ promise.then(start_next, lambda *values: None)
483
+
484
+ return cls.all(*start, *wait)
485
+
486
+ @classmethod
487
+ def race(cls, *promises: 'Pyjo_Promise') -> 'Pyjo_Promise':
488
+ """::
489
+
490
+ new = Pyjo.Promise.race(*promises)
491
+
492
+ Returns a new :mod:`Pyjo.Promise` object that fulfills or rejects as
493
+ soon as one of the passed :mod:`Pyjo.Promise` objects fulfills or
494
+ rejects, with the value or reason from that promise.
495
+ """
496
+ return _all(1, promises)
497
+
498
+ @_hybridmethod
499
+ def reject(self_or_cls: 'Pyjo_Promise | type[Pyjo_Promise]', *reason: Any) -> 'Pyjo_Promise': # noqa: N805
500
+ """::
501
+
502
+ new = Pyjo.Promise.reject(*reason)
503
+ promise = promise.reject(*reason)
504
+
505
+ Build rejected :mod:`Pyjo.Promise` object or reject the promise with one
506
+ or more rejection reasons. ::
507
+
508
+ # Longer version
509
+ promise = Pyjo.Promise.new().reject(*reason)
510
+ """
511
+ return Pyjo_Promise._settle(self_or_cls, 'reject', reason)
512
+
513
+ @_hybridmethod
514
+ def resolve(self_or_cls: 'Pyjo_Promise | type[Pyjo_Promise]', *value: Any) -> 'Pyjo_Promise': # noqa: N805
515
+ """::
516
+
517
+ new = Pyjo.Promise.resolve(*value)
518
+ promise = promise.resolve(*value)
519
+
520
+ Build resolved :mod:`Pyjo.Promise` object or resolve the promise with
521
+ one or more fulfillment values. ::
522
+
523
+ # Longer version
524
+ promise = Pyjo.Promise.new().resolve(*value)
525
+ """
526
+ return Pyjo_Promise._settle(self_or_cls, 'resolve', value)
527
+
528
+ def then(self, resolve: Handler | None = None, reject: Handler | None = None) -> 'Pyjo_Promise':
529
+ """::
530
+
531
+ new = promise.then(lambda *value: ...)
532
+ new = promise.then(lambda *value: ..., lambda *reason: ...)
533
+ new = promise.then(None, lambda *reason: ...)
534
+
535
+ Appends fulfillment and rejection handlers to the promise, and returns
536
+ a new :mod:`Pyjo.Promise` object resolving to the return value of the
537
+ called handler. ::
538
+
539
+ # Pass along the fulfillment value or rejection reason
540
+ def value_cb(*value):
541
+ print(f'The result is {value[0]}')
542
+ return Pyjo.Promise.resolve(*value)
543
+
544
+ def reason_cb(*reason):
545
+ warn(f'Something went wrong: {reason[0]}')
546
+ return Pyjo.Promise.reject(*reason)
547
+
548
+ promise.then(value_cb, reason_cb)
549
+
550
+ # Change the fulfillment value or rejection reason
551
+ promise.then(lambda *value: f'This is good: {value[0]}', lambda *reason: f'This is bad: {reason[0]}')
552
+ """
553
+ new = self.clone()
554
+ self._handled = True
555
+ self._resolve.append(lambda *results: _then_cb(new, resolve, 'resolve', results))
556
+ self._reject.append(lambda *results: _then_cb(new, reject, 'reject', results))
557
+
558
+ if self._results is not None:
559
+ self._defer()
560
+
561
+ return new
562
+
563
+ @_hybridmethod
564
+ def timer(self_or_cls: 'Pyjo_Promise | type[Pyjo_Promise]', after: float, *value: Any) -> 'Pyjo_Promise': # noqa: N805
565
+ """::
566
+
567
+ new = Pyjo.Promise.timer(5, 'Success!')
568
+ promise = promise.timer(5, 'Success!')
569
+ promise = promise.timer(5)
570
+
571
+ Create a new :mod:`Pyjo.Promise` object with a timer or attach a timer
572
+ to an existing promise. The promise will be resolved after the given
573
+ amount of time in seconds with or without a value, unless it has already
574
+ been settled. Note that this does not delay the resolution of an existing
575
+ promise, to add a delay to a promise chain you have to return a new timer
576
+ promise instead. ::
577
+
578
+ # Wait 5 seconds between two steps
579
+ promise.then(lambda *_: Pyjo.Promise.timer(5)).then(lambda *_: print('Five seconds later'))
580
+ """
581
+ return _timer(self_or_cls, 'resolve', after, value)
582
+
583
+ @_hybridmethod
584
+ def timeout(self_or_cls: 'Pyjo_Promise | type[Pyjo_Promise]', after: float, *reason: Any) -> 'Pyjo_Promise': # noqa: N805
585
+ """::
586
+
587
+ new = Pyjo.Promise.timeout(5, 'Timeout!')
588
+ promise = promise.timeout(5, 'Timeout!')
589
+ promise = promise.timeout(5)
590
+
591
+ Create a new :mod:`Pyjo.Promise` object with a timeout or attach a
592
+ timeout to an existing promise. The promise will be rejected after the
593
+ given amount of time in seconds with a reason, which defaults to
594
+ ``Promise timeout``, unless it has already been settled.
595
+ """
596
+ return _timer(self_or_cls, 'reject', after, reason)
597
+
598
+ # Not in Mojolicious: a future of the reactor
599
+ def to_future(self) -> Pyjo.Reactor.Future[Any]:
600
+ """::
601
+
602
+ future = promise.to_future()
603
+
604
+ Turn promise into a future of the reactor of :attr:`ioloop`, an
605
+ :class:`asyncio.Future` of its loop with :mod:`Pyjo.Reactor.Asyncio`,
606
+ or a :class:`concurrent.futures.Future` with a reactor without asyncio,
607
+ as :mod:`Pyjo.Reactor.Poll`. The future is done when the promise is
608
+ fulfilled or rejected, with what ``await promise`` gives: its value, a
609
+ tuple of several values, or ``None`` for none, or the exception it was
610
+ rejected with, or a :mod:`Pyjo.Exception` of the first reason. ::
611
+
612
+ # Print the value when the future is done
613
+ future = Pyjo.Promise.timer(1, 'Done!').to_future()
614
+ future.add_done_callback(lambda future: print(future.result()))
615
+ """
616
+ future = self.ioloop.reactor.future()
617
+
618
+ def resolve(*values: Any) -> None:
619
+ if not future.done():
620
+ future.set_result(_result(values))
621
+
622
+ def reject(*reason: Any) -> None:
623
+ if not future.done():
624
+ future.set_exception(_exception(reason))
625
+
626
+ self.then(resolve, reject)
627
+ return future
628
+
629
+ def wait(self) -> None:
630
+ """::
631
+
632
+ promise.wait()
633
+
634
+ Start :attr:`ioloop` and stop it again once the promise has been
635
+ fulfilled or rejected, does nothing when :attr:`ioloop` is already
636
+ running.
637
+ """
638
+ if (loop := self.ioloop).is_running:
639
+ return
640
+ done = False
641
+
642
+ def finish() -> None:
643
+ nonlocal done
644
+ done = True
645
+ loop.stop()
646
+
647
+ self._finally(False, finish).catch(lambda *reason: None)
648
+ while not done:
649
+ loop.start()
650
+
651
+ @staticmethod
652
+ def _settle(
653
+ self_or_cls: 'Pyjo_Promise | type[Pyjo_Promise]', status: str, results: tuple[Any, ...]
654
+ ) -> 'Pyjo_Promise':
655
+ first = results[0] if len(results) else None
656
+ thenable = first is not None and not isinstance(first, type) and callable(getattr(first, 'then', None))
657
+ if isinstance(self_or_cls, type):
658
+ if thenable and status == 'resolve' and isinstance(first, Pyjo_Promise):
659
+ return first
660
+ promise = self_or_cls.new()
661
+ else:
662
+ promise = self_or_cls
663
+
664
+ if thenable and status == 'resolve':
665
+
666
+ def resolve(*values: Any) -> None:
667
+ promise.resolve(*values)
668
+
669
+ def reject(*values: Any) -> None:
670
+ promise.reject(*values)
671
+
672
+ cast(Any, first).then(resolve, reject)
673
+
674
+ elif promise._results is None:
675
+ promise._results, promise._status = results, status
676
+ promise._defer()
677
+
678
+ return promise
679
+
680
+ def _defer(self) -> None:
681
+ if (results := self._results) is None:
682
+ return
683
+ cbs = self._resolve if self._status == 'resolve' else self._reject
684
+ self._resolve, self._reject = [], []
685
+
686
+ def run(loop: Any) -> None:
687
+ for cb in cbs:
688
+ cb(*results)
689
+
690
+ self.ioloop.next_tick(run)
691
+
692
+ def _finally(self, handled: bool, cb: Callable[[], Any]) -> 'Pyjo_Promise':
693
+ new = self.clone()
694
+
695
+ def finally_cb(*results: Any) -> 'Pyjo_Promise':
696
+ value = cb()
697
+ return (new.resolve() if value is None else new.resolve(value)).then(lambda *values: _values(results))
698
+
699
+ before = self._handled
700
+ self.catch(finally_cb)
701
+ then = self.then(finally_cb)
702
+ if not before and not handled:
703
+ self._handled = False
704
+
705
+ return then
706
+
707
+
708
+ # Not in Mojolicious: a decorator of a coroutine function, as Mojo's async sub
709
+ def coroutine[**P](func: Callable[P, Coroutine[Any, Any, Any]]) -> Callable[P, Pyjo_Promise]:
710
+ """::
711
+
712
+ @Pyjo.Promise.coroutine
713
+ async def title(url):
714
+ tx = await ua.get_p(url)
715
+ return tx.res.dom().at('title').text
716
+
717
+ title('https://mojolicious.org').then(print).wait()
718
+
719
+ Turn a coroutine function into a function that returns a
720
+ :mod:`Pyjo.Promise` of the coroutine, as ``async sub`` does in Mojo.
721
+ """
722
+
723
+ @functools.wraps(func)
724
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Pyjo_Promise:
725
+ return Pyjo_Promise.new(func(*args, **kwargs))
726
+
727
+ return wrapper
728
+
729
+
730
+ def _all(type: int, promises: tuple[Pyjo_Promise, ...]) -> Pyjo_Promise:
731
+ all = promises[0].clone()
732
+ results: list[Any] = [None] * len(promises)
733
+ remaining = len(promises)
734
+
735
+ def resolve_all(*values: Any) -> None:
736
+ all.resolve(*values)
737
+
738
+ def reject_all(*values: Any) -> None:
739
+ all.reject(*values)
740
+
741
+ for i, promise in enumerate(promises):
742
+ # "race"
743
+ if type == 1:
744
+ promise.then(resolve_all, reject_all)
745
+
746
+ # "all"
747
+ elif type == 2:
748
+
749
+ def fulfilled(*values: Any, i: int = i) -> None:
750
+ nonlocal remaining
751
+ results[i] = list(values)
752
+ remaining -= 1
753
+ if remaining <= 0:
754
+ all.resolve(*results)
755
+
756
+ promise.then(fulfilled, reject_all)
757
+
758
+ # "any"
759
+ elif type == 3:
760
+
761
+ def rejected(*values: Any, i: int = i) -> None:
762
+ nonlocal remaining
763
+ results[i] = list(values)
764
+ remaining -= 1
765
+ if remaining <= 0:
766
+ all.reject(*results)
767
+
768
+ promise.then(resolve_all, rejected)
769
+
770
+ # "all_settled"
771
+ else:
772
+
773
+ def settled_fulfilled(*values: Any, i: int = i) -> None:
774
+ nonlocal remaining
775
+ results[i] = {'status': 'fulfilled', 'value': list(values)}
776
+ remaining -= 1
777
+ if remaining <= 0:
778
+ all.resolve(*results)
779
+
780
+ def settled_rejected(*values: Any, i: int = i) -> None:
781
+ nonlocal remaining
782
+ results[i] = {'status': 'rejected', 'reason': list(values)}
783
+ remaining -= 1
784
+ if remaining <= 0:
785
+ all.resolve(*results)
786
+
787
+ promise.then(settled_fulfilled, settled_rejected)
788
+
789
+ return all
790
+
791
+
792
+ def _then_cb(new: Pyjo_Promise, cb: Handler | None, method: str, results: tuple[Any, ...]) -> Pyjo_Promise:
793
+ if cb is None:
794
+ return new.resolve(*results) if method == 'resolve' else new.reject(*results)
795
+
796
+ try:
797
+ res = cb(*results)
798
+ except Exception as e:
799
+ return new.reject(e)
800
+ # Not in Mojolicious: a callback of Python returns one value, None is no value
801
+ return new.resolve() if res is None else new.resolve(res)
802
+
803
+
804
+ def _timer(
805
+ self_or_cls: Pyjo_Promise | type[Pyjo_Promise], method: str, after: float, results: tuple[Any, ...]
806
+ ) -> Pyjo_Promise:
807
+ promise = self_or_cls.new() if isinstance(self_or_cls, type) else self_or_cls
808
+ if method == 'reject' and not len(results):
809
+ results = ('Promise timeout',)
810
+
811
+ def timer(loop: Any) -> None:
812
+ promise.resolve(*results) if method == 'resolve' else promise.reject(*results)
813
+
814
+ promise.ioloop.timer(after, timer)
815
+ return promise
816
+
817
+
818
+ # A future of concurrent.futures settles the promise in the thread of the reactor, which a socket pair wakes up from the
819
+ # thread that completes the future, as the resolver of Pyjo.IOLoop.Client does, so that it works with any reactor
820
+ def _wait_future(promise: Pyjo_Promise, future: concurrent.futures.Future[Any]) -> None:
821
+ reactor = promise.ioloop.reactor
822
+ reader, writer = socket.socketpair()
823
+ reader.setblocking(False)
824
+ _pairs.update((reader, writer))
825
+
826
+ def ready(reactor: Any, writable: bool) -> None:
827
+ reactor.remove(reader)
828
+ _pairs.difference_update((reader, writer))
829
+ reader.close()
830
+ writer.close()
831
+ if future.cancelled():
832
+ promise.reject(concurrent.futures.CancelledError())
833
+ elif (e := future.exception()) is not None:
834
+ promise.reject(e)
835
+ elif (value := future.result()) is None:
836
+ promise.resolve()
837
+ else:
838
+ promise.resolve(value)
839
+
840
+ reactor.io(reader, ready).watch(reader, True, False)
841
+ future.add_done_callback(lambda future: writer.send(b'\x00'))
842
+
843
+
844
+ # The socket pairs of the futures that are not done when the program ends, after the threads of concurrent.futures, whose
845
+ # event loop does not run again to see them done
846
+ _pairs: set[socket.socket] = set()
847
+
848
+
849
+ @atexit.register
850
+ def _close_pairs() -> None:
851
+ for handle in list(_pairs):
852
+ handle.close()
853
+ _pairs.clear()
854
+
855
+
856
+ # An awaitable in a coroutine, which the reactor runs
857
+ async def _await(awaitable: Awaitable[Any]) -> Any:
858
+ return await awaitable
859
+
860
+
861
+ # The exception of a rejection: the reason, or an exception of it
862
+ def _exception(reason: tuple[Any, ...]) -> BaseException:
863
+ if len(reason) and isinstance(reason[0], BaseException):
864
+ return reason[0]
865
+ return Pyjo.Exception.new(reason[0] if len(reason) else None)
866
+
867
+
868
+ # The value of a fulfillment: none, one, or a tuple of several
869
+ def _result(values: tuple[Any, ...]) -> Any:
870
+ return None if not len(values) else values[0] if len(values) == 1 else values
871
+
872
+
873
+ # The value of a handler that passes values on: none, one, or a promise of several
874
+ def _values(values: tuple[Any, ...]) -> Any:
875
+ if not len(values):
876
+ return None
877
+ if len(values) == 1:
878
+ return values[0]
879
+ return Pyjo_Promise.resolve(*values)
880
+
881
+
882
+ new = Pyjo_Promise.new
883
+ object = Pyjo_Promise
884
+ Promise = Pyjo_Promise
885
+
886
+ all = Pyjo_Promise.all
887
+ all_settled = Pyjo_Promise.all_settled
888
+ any = Pyjo_Promise.any
889
+ map = Pyjo_Promise.map
890
+ race = Pyjo_Promise.race
891
+ reject = Pyjo_Promise.reject
892
+ resolve = Pyjo_Promise.resolve
893
+ timer = Pyjo_Promise.timer
894
+ timeout = Pyjo_Promise.timeout