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/IOLoop/Client.py ADDED
@@ -0,0 +1,736 @@
1
+ """
2
+ Pyjo.IOLoop.Client - Non-blocking TCP/IP and UNIX domain socket client
3
+ ======================================================================
4
+ ::
5
+
6
+ from Pyjo.IOLoop.Client import Client
7
+
8
+ # Create socket connection
9
+ client = Client()
10
+ client.on('connect', lambda client, handle: ...)
11
+ client.on('error', lambda client, err: ...)
12
+ client.connect(address='example.com', port=80)
13
+
14
+ # Or import the module, as in Google Python Style Guide
15
+
16
+ import Pyjo.IOLoop.Client
17
+
18
+ # Create socket connection
19
+ client = Pyjo.IOLoop.Client.new()
20
+ client.on('connect', lambda client, handle: ...)
21
+ client.on('error', lambda client, err: ...)
22
+ client.connect(address='example.com', port=80)
23
+
24
+ # Start reactor if necessary
25
+ if not client.reactor.is_running:
26
+ client.reactor.start()
27
+
28
+ :mod:`Pyjo.IOLoop.Client` opens TCP/IP and UNIX domain socket connections for
29
+ :mod:`Pyjo.IOLoop`.
30
+
31
+ The end of a ``with`` block closes the connection.
32
+
33
+ Events
34
+ ------
35
+
36
+ :mod:`Pyjo.IOLoop.Client` inherits all events from :mod:`Pyjo.EventEmitter`
37
+ and can emit the following new ones.
38
+
39
+ connect
40
+ ~~~~~~~
41
+ ::
42
+
43
+ client.on('connect', lambda client, handle: ...)
44
+
45
+ Emitted once the connection is established, with its :class:`socket.socket`,
46
+ or the :class:`ssl.SSLSocket` once TLS has been negotiated.
47
+
48
+ error
49
+ ~~~~~
50
+ ::
51
+
52
+ client.on('error', lambda client, err: ...)
53
+
54
+ Emitted if an error occurs on the connection, fatal if unhandled. The error is
55
+ a message, or the :class:`OSError` of the system.
56
+
57
+ See also
58
+ --------
59
+
60
+ :mod:`Pyjoyment`, https://mojolicious.org.
61
+
62
+ Ported from `Mojo::IOLoop::Client <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/IOLoop/Client.pm>`_
63
+ of Mojolicious 9.49.
64
+ """
65
+
66
+ import Pyjo.EventEmitter
67
+ import Pyjo.IOLoop
68
+ import Pyjo.IOLoop.TLS
69
+ import Pyjo.Reactor
70
+
71
+ import concurrent.futures
72
+ import errno
73
+ import os
74
+ import socket
75
+ import sys
76
+ import threading
77
+ import time
78
+ import weakref
79
+
80
+ from collections.abc import Callable, Generator
81
+ from types import TracebackType
82
+ from typing import Any, Literal, Self
83
+
84
+
85
+ # Non-blocking name resolution, which Mojo does with Net::DNS::Native, is a thread of the standard library here
86
+ NNR = not os.environ.get('PYJO_NO_NNR')
87
+ _executor: concurrent.futures.ThreadPoolExecutor | None = None
88
+
89
+ # SOCKS5 support, which Mojo does with IO::Socket::Socks, is a client of its own here
90
+ SOCKS = not os.environ.get('PYJO_NO_SOCKS')
91
+
92
+ # The replies of a SOCKS5 server, with the messages of IO::Socket::Socks
93
+ _SOCKS_REPLIES = {
94
+ 1: 'General failure',
95
+ 2: 'Not allowed',
96
+ 3: 'Network unreachable',
97
+ 4: 'Host unreachable',
98
+ 5: 'Connection refused',
99
+ 6: 'TTL expired',
100
+ 7: 'Command not supported',
101
+ 8: 'Address not supported',
102
+ }
103
+
104
+ type AddrInfo = list[tuple[Any, Any, Any, Any, Any]]
105
+
106
+
107
+ # The threads of the pool are stopped before a fork, as a process with threads may deadlock in its child, which Python
108
+ # warns about, and the child would have a pool without its threads; the next resolution starts the pool again
109
+ def _before_fork() -> None:
110
+ global _executor
111
+ if (executor := _executor) is not None:
112
+ _executor = None
113
+ executor.shutdown(wait=True)
114
+
115
+ # Linux lists a thread that has been joined for a moment more, which Python 3.15 counts as a thread when it
116
+ # forks, so the fork waits a little for the threads of the system to be those of Python
117
+ tasks = '/proc/self/task'
118
+ deadline = time.monotonic() + 0.1
119
+ while (
120
+ os.path.isdir(tasks) and len(os.listdir(tasks)) > threading.active_count() and time.monotonic() < deadline
121
+ ):
122
+ time.sleep(0.001)
123
+
124
+
125
+ if hasattr(os, 'register_at_fork'):
126
+ os.register_at_fork(before=_before_fork)
127
+
128
+
129
+ class _Resolver:
130
+ """A name resolution in a thread, which makes a socket readable once it is done, for Pyjo.IOLoop.Datagram too."""
131
+
132
+ def __init__(self, host: str, port: int, type: int = socket.SOCK_STREAM, family: int = 0) -> None:
133
+ global _executor
134
+ if _executor is None:
135
+ _executor = concurrent.futures.ThreadPoolExecutor(max_workers=5, thread_name_prefix='Pyjo.IOLoop.Client')
136
+ self.handle, self._writer = socket.socketpair()
137
+ self.handle.setblocking(False)
138
+ self.future = _executor.submit(
139
+ socket.getaddrinfo,
140
+ host,
141
+ port,
142
+ family,
143
+ type,
144
+ socket.IPPROTO_TCP if type == socket.SOCK_STREAM else socket.IPPROTO_UDP,
145
+ )
146
+ self.future.add_done_callback(self._done)
147
+
148
+ def _done(self, future: Any) -> None:
149
+ try:
150
+ self._writer.send(b'.')
151
+ except OSError:
152
+ pass
153
+
154
+ def close(self) -> None:
155
+ self.future.cancel()
156
+ self.handle.close()
157
+ self._writer.close()
158
+
159
+
160
+ class _SocksError(Exception):
161
+ """An error of the SOCKS5 handshake, with the message of IO::Socket::Socks."""
162
+
163
+
164
+ class _Socks:
165
+ """
166
+ A SOCKS5 handshake on a socket that does not block, as the client of
167
+ IO::Socket::Socks does it: the proxy resolves the name, and the user and
168
+ the password of RFC 1929 are offered next to no authentication.
169
+ """
170
+
171
+ def __init__(self, handle: socket.socket, address: str, port: int, user: str | None, password: str | None) -> None:
172
+ self.handle = handle
173
+ self._steps = self._handshake(address, port, user, password)
174
+ self._done = False
175
+
176
+ def step(self) -> Literal['read', 'write'] | None:
177
+ """What the handshake waits for, or ``None`` once it is done."""
178
+ if self._done:
179
+ return None
180
+ try:
181
+ return next(self._steps)
182
+ except StopIteration:
183
+ self._done = True
184
+ return None
185
+
186
+ def _handshake(
187
+ self, address: str, port: int, user: str | None, password: str | None
188
+ ) -> Generator[Literal['read', 'write']]:
189
+ # The auth mechanisms
190
+ methods = b'\x00\x02' if user else b'\x00'
191
+ yield from self._send(b'\x05' + bytes([len(methods)]) + methods)
192
+ _, method = yield from self._read(2)
193
+ if method == 0xFF:
194
+ raise _SocksError('No valid auth mechanisms')
195
+
196
+ # The user and the password
197
+ if method != 0x00:
198
+ name, secret = (user or '').encode(), (password or '').encode()
199
+ yield from self._send(b'\x01' + bytes([len(name)]) + name + bytes([len(secret)]) + secret)
200
+ _, status = yield from self._read(2)
201
+ if status != 0x00:
202
+ raise _SocksError('Authentication failed with SOCKS5 proxy')
203
+
204
+ # The command, with the name that the proxy resolves
205
+ host = _encode_host(address)
206
+ yield from self._send(b'\x05\x01\x00\x03' + bytes([len(host)]) + host + port.to_bytes(2, 'big'))
207
+
208
+ # The reply
209
+ _, reply, _, atyp = yield from self._read(4)
210
+ if atyp == 0x03:
211
+ (length,) = yield from self._read(1)
212
+ yield from self._read(length)
213
+ elif atyp == 0x01:
214
+ yield from self._read(4)
215
+ elif atyp == 0x04:
216
+ yield from self._read(16)
217
+ else:
218
+ raise _SocksError(f'Unsupported address type returned by socks server: {atyp}')
219
+ yield from self._read(2)
220
+ if reply != 0x00:
221
+ raise _SocksError(_SOCKS_REPLIES.get(reply, _SOCKS_REPLIES[1]))
222
+
223
+ def _read(self, length: int) -> Generator[Literal['read', 'write'], None, bytes]:
224
+ data = b''
225
+ while len(data) < length:
226
+ try:
227
+ chunk = self.handle.recv(length - len(data))
228
+ except (BlockingIOError, InterruptedError):
229
+ yield 'read'
230
+ continue
231
+ except OSError as e:
232
+ raise _SocksError(f'read: {e.strerror or e}') from e
233
+ if not len(chunk):
234
+ raise _SocksError('Socket closed by remote side')
235
+ data += chunk
236
+ return data
237
+
238
+ def _send(self, data: bytes) -> Generator[Literal['read', 'write']]:
239
+ while len(data):
240
+ try:
241
+ data = data[self.handle.send(data) :]
242
+ except (BlockingIOError, InterruptedError):
243
+ yield 'write'
244
+ except OSError as e:
245
+ raise _SocksError(f'send: {e.strerror or e}') from e
246
+
247
+
248
+ class Pyjo_IOLoop_Client(Pyjo.EventEmitter.object):
249
+ """
250
+ :mod:`Pyjo.IOLoop.Client` inherits all methods from
251
+ :mod:`Pyjo.EventEmitter` and implements the following new ones.
252
+ """
253
+
254
+ def __init__(self, *, reactor: Pyjo.Reactor.object | None = None) -> None:
255
+ """::
256
+
257
+ client = Pyjo.IOLoop.Client.new()
258
+ client = Pyjo.IOLoop.Client.new(reactor=Pyjo.Reactor.Poll.new())
259
+
260
+ Construct a new :mod:`Pyjo.IOLoop.Client` object.
261
+ """
262
+ super().__init__()
263
+ self._addresses: AddrInfo = []
264
+ self._dns: _Resolver | None = None
265
+ self._socks_handshake: _Socks | None = None
266
+ self._handle: Any = None
267
+ self._reactor: weakref.ref[Pyjo.Reactor.object] | None = None
268
+ self._timer: str | None = None
269
+ if reactor is not None:
270
+ self.reactor = reactor
271
+
272
+ def __del__(self) -> None:
273
+ # Module globals may be gone at the end of the interpreter
274
+ try:
275
+ if not sys.is_finalizing():
276
+ reactor = self._reactor() if self._reactor is not None else None
277
+ Pyjo.Reactor._finalize(self.close, reactor) # pyright: ignore[reportPrivateUsage]
278
+ except Exception:
279
+ pass
280
+
281
+ # Not in Mojolicious: a context manager, which closes the connection where the block ends, as PyPy frees the object
282
+ # only when its garbage collector runs
283
+ def __enter__(self) -> Self:
284
+ return self
285
+
286
+ def __exit__(
287
+ self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
288
+ ) -> None:
289
+ self.close()
290
+
291
+ @property
292
+ def reactor(self) -> Pyjo.Reactor.object:
293
+ """::
294
+
295
+ reactor = client.reactor
296
+ client.reactor = Pyjo.Reactor.Poll.new()
297
+
298
+ Low-level event reactor, defaults to the ``reactor`` attribute value of
299
+ the global :mod:`Pyjo.IOLoop` singleton. Note that this attribute is
300
+ weakened.
301
+ """
302
+ if self._reactor is None:
303
+ self._reactor = weakref.ref(Pyjo.IOLoop.singleton.reactor)
304
+ if (reactor := self._reactor()) is None:
305
+ raise ReferenceError('weakly-referenced object no longer exists')
306
+ return reactor
307
+
308
+ @reactor.setter
309
+ def reactor(self, value: Pyjo.Reactor.object) -> None:
310
+ self._reactor = weakref.ref(value)
311
+
312
+ @classmethod
313
+ def can_nnr(cls) -> bool:
314
+ """::
315
+
316
+ boolean = Pyjo.IOLoop.Client.object.can_nnr()
317
+
318
+ True if non-blocking name resolution support is enabled, as it is
319
+ unless the ``PYJO_NO_NNR`` environment variable is set. The names are
320
+ resolved in a thread.
321
+ """
322
+ return NNR
323
+
324
+ @classmethod
325
+ def can_socks(cls) -> bool:
326
+ """::
327
+
328
+ boolean = Pyjo.IOLoop.Client.object.can_socks()
329
+
330
+ True if SOCKS5 support is enabled, as it is unless the
331
+ ``PYJO_NO_SOCKS`` environment variable is set.
332
+ """
333
+ return SOCKS
334
+
335
+ # Not in Mojolicious: what DESTROY does, which Python does when it frees the client, PyPy only later
336
+ def close(self) -> None:
337
+ """::
338
+
339
+ client.close()
340
+
341
+ Stop connecting and close a socket that is not connected yet, as it
342
+ happens when the client is freed.
343
+ """
344
+ handle = self._handle
345
+ self._cleanup()
346
+ if handle is not None:
347
+ handle.close()
348
+
349
+ def connect(self, **options: Any) -> None:
350
+ """::
351
+
352
+ client.connect(address='127.0.0.1', port=3000)
353
+
354
+ Open a socket connection to a remote host.
355
+
356
+ These options are currently available:
357
+
358
+ address
359
+ ::
360
+
361
+ address='mojolicious.org'
362
+
363
+ Address or host name of the peer to connect to, defaults to
364
+ ``127.0.0.1``.
365
+
366
+ handle
367
+ ::
368
+
369
+ handle=handle
370
+
371
+ Use an already prepared :class:`socket.socket` object.
372
+
373
+ path
374
+ ::
375
+
376
+ path='/tmp/myapp.sock'
377
+
378
+ Path of UNIX domain socket to connect to.
379
+
380
+ port
381
+ ::
382
+
383
+ port=80
384
+
385
+ Port to connect to, defaults to ``80`` or ``443`` with ``tls``
386
+ option.
387
+
388
+ source_address
389
+ ::
390
+
391
+ source_address=('127.0.0.1', 0)
392
+
393
+ Local address and port to connect from.
394
+
395
+ socks_address
396
+ ::
397
+
398
+ socks_address='127.0.0.1'
399
+
400
+ Address or host name of SOCKS5 proxy server to use for connection.
401
+
402
+ socks_pass
403
+ ::
404
+
405
+ socks_pass='secr3t'
406
+
407
+ Password to use for SOCKS5 authentication.
408
+
409
+ socks_port
410
+ ::
411
+
412
+ socks_port=9050
413
+
414
+ Port of SOCKS5 proxy server to use for connection.
415
+
416
+ socks_user
417
+ ::
418
+
419
+ socks_user='sri'
420
+
421
+ Username to use for SOCKS5 authentication.
422
+
423
+ timeout
424
+ ::
425
+
426
+ timeout=15
427
+
428
+ Maximum amount of time in seconds establishing connection may take
429
+ before getting canceled, defaults to ``10``.
430
+
431
+ tls
432
+ ::
433
+
434
+ tls=True
435
+
436
+ Enable TLS.
437
+
438
+ tls_ca
439
+ ::
440
+
441
+ tls_ca='/etc/tls/ca.crt'
442
+
443
+ Path to TLS certificate authority file.
444
+
445
+ tls_cert
446
+ ::
447
+
448
+ tls_cert='/etc/tls/client.crt'
449
+
450
+ Path to the TLS certificate file.
451
+
452
+ tls_context
453
+ ::
454
+
455
+ tls_context=ssl.create_default_context()
456
+
457
+ The :class:`ssl.SSLContext` to use instead of the one that the
458
+ other options make.
459
+
460
+ tls_hostname
461
+ ::
462
+
463
+ tls_hostname='mojolicious.org'
464
+
465
+ Name of the server for SNI and the verification of its
466
+ certificate instead of ``address``, an empty one for no SNI.
467
+
468
+ tls_key
469
+ ::
470
+
471
+ tls_key='/etc/tls/client.key'
472
+
473
+ Path to the TLS key file.
474
+ """
475
+ args = dict(options)
476
+
477
+ # Timeout
478
+ ref = weakref.ref(self)
479
+ reactor = self.reactor
480
+
481
+ def timeout(reactor: Any) -> None:
482
+ if (client := ref()) is not None:
483
+ client.emit('error', 'Connect timeout')
484
+
485
+ self._timer = reactor.timer(args.get('timeout') or 10, timeout)
486
+
487
+ # Blocking name resolution
488
+ for key in ('address', 'socks_address'):
489
+ if args.get(key):
490
+ args[key] = str(args[key]).replace('[', '').replace(']', '')
491
+ if not args.get('address'):
492
+ args['address'] = '127.0.0.1'
493
+ address = args.get('socks_address') or args['address']
494
+ if not NNR or args.get('handle') or args.get('path'):
495
+
496
+ def connect(reactor: Any) -> None:
497
+ if (client := ref()) is not None:
498
+ client._connect(args)
499
+
500
+ return reactor.next_tick(connect)
501
+
502
+ # Non-blocking name resolution
503
+ dns = self._dns = _Resolver(address, _port(args))
504
+
505
+ def resolved(reactor: Any, writable: bool) -> None:
506
+ if (client := ref()) is None:
507
+ return
508
+ reactor.remove(dns.handle)
509
+ client._dns = None
510
+ try:
511
+ args['addr_info'] = dns.future.result()
512
+ except OSError as e:
513
+ client.emit('error', f"Can't resolve: {e.strerror or e}")
514
+ return
515
+ finally:
516
+ dns.close()
517
+ client._connect(args)
518
+
519
+ reactor.io(dns.handle, resolved).watch(dns.handle, True, False)
520
+
521
+ def _cleanup(self) -> None:
522
+ reactor = self._reactor() if self._reactor is not None else None
523
+ if (dns := self._dns) is not None:
524
+ self._dns = None
525
+ # The reactor forgets the socket before it is closed, as epoll does and asyncio does not
526
+ if reactor is not None:
527
+ try:
528
+ reactor.remove(dns.handle)
529
+ except Exception:
530
+ pass
531
+ dns.close()
532
+ if reactor is None:
533
+ return
534
+ for name in ('_timer', '_handle'):
535
+ if (value := getattr(self, name)) is not None:
536
+ setattr(self, name, None)
537
+ try:
538
+ reactor.remove(value)
539
+ except Exception:
540
+ pass
541
+
542
+ def _connect(self, args: dict[str, Any]) -> None:
543
+ path = args.get('path')
544
+ handle = self._handle = args.get('handle')
545
+
546
+ if handle is None:
547
+ try:
548
+ # UNIX domain socket
549
+ if path:
550
+ # Not in Mojolicious: Python has no UNIX domain sockets on Windows
551
+ if sys.platform == 'win32':
552
+ raise OSError(errno.EAFNOSUPPORT, 'UNIX domain sockets are not supported')
553
+ handle = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
554
+ handle.setblocking(False)
555
+ _connect_ex(handle, path)
556
+
557
+ # IP socket, as IO::Socket::IP tries the addresses of the name one after another
558
+ else:
559
+ self._addresses = list(
560
+ args.get('addr_info')
561
+ or socket.getaddrinfo(
562
+ args.get('socks_address') or args['address'],
563
+ _port(args),
564
+ type=socket.SOCK_STREAM,
565
+ proto=socket.IPPROTO_TCP,
566
+ )
567
+ )
568
+ handle = self._next(args)
569
+ except OSError as e:
570
+ self.emit('error', f"Can't connect: {e.strerror or e}")
571
+ return
572
+ self._handle = handle
573
+ handle.setblocking(False)
574
+
575
+ if path:
576
+ self._try_socks(args)
577
+ else:
578
+ self._wait(self._ready, handle, args)
579
+
580
+ # The next address of the name that a socket can connect from, as IO::Socket::IP tries them all
581
+ def _next(self, args: dict[str, Any]) -> socket.socket:
582
+ error: OSError | None = None
583
+ while len(self._addresses):
584
+ family, type, proto, _, sockaddr = self._addresses.pop(0)
585
+ handle = socket.socket(family, type, proto)
586
+ try:
587
+ handle.setblocking(False)
588
+ # Unlike Mojolicious: socket_options of IO::Socket::IP is source_address, as socket.create_connection has
589
+ # it
590
+ if (source := args.get('source_address')) is not None:
591
+ handle.bind(source)
592
+ _connect_ex(handle, sockaddr)
593
+ except OSError as e:
594
+ handle.close()
595
+ error = e
596
+ continue
597
+ return handle
598
+ raise error or OSError(errno.EADDRNOTAVAIL, os.strerror(errno.EADDRNOTAVAIL))
599
+
600
+ def _ready(self, args: dict[str, Any]) -> None:
601
+ handle = self._handle
602
+ if handle is None:
603
+ return
604
+
605
+ # Socket changes in between attempts and needs to be re-added for epoll/kqueue
606
+ if err := handle.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR):
607
+ self.reactor.remove(handle)
608
+ handle.close()
609
+ self._handle = None
610
+ if not len(self._addresses):
611
+ self.emit('error', OSError(err, os.strerror(err)))
612
+ return
613
+ try:
614
+ handle = self._handle = self._next(args)
615
+ except OSError as e:
616
+ self.emit('error', e)
617
+ return
618
+ return self._wait(self._ready, handle, args)
619
+
620
+ try:
621
+ handle.getpeername()
622
+ except OSError as e:
623
+ self.emit('error', e)
624
+ return
625
+
626
+ # Disable Nagle's algorithm
627
+ try:
628
+ handle.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
629
+ except OSError:
630
+ pass
631
+
632
+ self._try_socks(args)
633
+
634
+ def _socks(self, args: dict[str, Any]) -> None:
635
+ handle, socks = self._handle, self._socks_handshake
636
+ if handle is None or socks is None:
637
+ return
638
+ try:
639
+ want = socks.step()
640
+ except _SocksError as e:
641
+ self.emit('error', str(e))
642
+ return
643
+
644
+ # Connected
645
+ if want is None:
646
+ self._socks_handshake = None
647
+ return self._try_tls(args)
648
+
649
+ # Switch between reading and writing
650
+ self.reactor.watch(handle, True, want == 'write')
651
+
652
+ def _try_socks(self, args: dict[str, Any]) -> None:
653
+ handle = self._handle
654
+ if not args.get('socks_address'):
655
+ return self._try_tls(args)
656
+ if not SOCKS:
657
+ self.emit('error', 'SOCKS support is disabled')
658
+ return
659
+
660
+ port = int(args.get('port') or (443 if args.get('tls') else 80))
661
+ socks = self._socks_handshake = _Socks(
662
+ handle, args['address'], port, args.get('socks_user'), args.get('socks_pass')
663
+ )
664
+ reactor = self.reactor
665
+ reactor.remove(handle)
666
+ try:
667
+ socks.step()
668
+ except _SocksError as e:
669
+ # Unlike Mojolicious: the error of the handshake, not "SOCKS upgrade failed", which Mojo gives where the
670
+ # handshake fails before it would block for the first time, so that the message depends on timing
671
+ self.emit('error', str(e))
672
+ return
673
+
674
+ self._wait(self._socks, handle, args)
675
+
676
+ def _try_tls(self, args: dict[str, Any]) -> None:
677
+ handle = self._handle
678
+ if not args.get('tls'):
679
+ self._cleanup()
680
+ self.emit('connect', handle)
681
+ return
682
+ reactor = self.reactor
683
+ reactor.remove(handle)
684
+ self._handle = None
685
+
686
+ # Start TLS handshake
687
+ ref = weakref.ref(self)
688
+
689
+ def upgrade(tls: Any, handle: Any) -> None:
690
+ if (client := ref()) is not None:
691
+ client._cleanup()
692
+ client.emit('connect', handle)
693
+
694
+ def error(tls: Any, err: Any) -> None:
695
+ if (client := ref()) is not None:
696
+ client.emit('error', err)
697
+
698
+ tls = Pyjo.IOLoop.TLS.new(handle)
699
+ tls.reactor = reactor
700
+ tls.on('upgrade', upgrade)
701
+ tls.on('error', error)
702
+ tls.negotiate(**args)
703
+
704
+ def _wait(self, next: Callable[[dict[str, Any]], None], handle: Any, args: dict[str, Any]) -> None:
705
+ ref = weakref.ref(self)
706
+ name = next.__name__
707
+
708
+ def cb(reactor: Any, writable: bool) -> None:
709
+ if (client := ref()) is not None:
710
+ getattr(client, name)(args)
711
+
712
+ self.reactor.io(handle, cb).watch(handle, False, True)
713
+
714
+
715
+ # The name of a host for SOCKS5, as IDNA where it is not ASCII
716
+ def _encode_host(address: str) -> bytes:
717
+ try:
718
+ return address.encode('ascii')
719
+ except UnicodeEncodeError:
720
+ return address.encode('idna')
721
+
722
+
723
+ # A connection that does not block
724
+ def _connect_ex(handle: socket.socket, address: Any) -> None:
725
+ err = handle.connect_ex(address)
726
+ if err and err not in (errno.EINPROGRESS, errno.EWOULDBLOCK, errno.EAGAIN):
727
+ raise OSError(err, os.strerror(err))
728
+
729
+
730
+ def _port(args: dict[str, Any]) -> int:
731
+ return int(args.get('socks_port') or args.get('port') or (443 if args.get('tls') else 80))
732
+
733
+
734
+ new = Pyjo_IOLoop_Client.new
735
+ object = Pyjo_IOLoop_Client
736
+ Client = Pyjo_IOLoop_Client