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,1243 @@
1
+ """
2
+ Pyjo.UserAgent - Non-blocking I/O HTTP and WebSocket user agent
3
+ ===============================================================
4
+ ::
5
+
6
+ from Pyjo.UserAgent import UserAgent
7
+
8
+ # Fine grained response handling (raises on connection errors)
9
+ ua = UserAgent()
10
+ res = ua.get('docs.mojolicious.org').result
11
+ if res.is_success:
12
+ print(res.body)
13
+ elif res.is_error:
14
+ print(res.message)
15
+ elif res.code == 301:
16
+ print(res.headers.location)
17
+ else:
18
+ print('Whatever...')
19
+
20
+ # Or import the module, as in Google Python Style Guide
21
+
22
+ import Pyjo.IOLoop
23
+ import Pyjo.Promise
24
+ import Pyjo.URL
25
+ import Pyjo.UserAgent
26
+
27
+ # Fine grained response handling (raises on connection errors)
28
+ ua = Pyjo.UserAgent.new()
29
+ res = ua.get('docs.mojolicious.org').result
30
+ if res.is_success:
31
+ print(res.body)
32
+ elif res.is_error:
33
+ print(res.message)
34
+ elif res.code == 301:
35
+ print(res.headers.location)
36
+ else:
37
+ print('Whatever...')
38
+
39
+ # Say hello to the Unicode snowman and include an Accept header
40
+ print(ua.get('www.☃.net?hello=there', {'Accept': '*/*'}).result.body)
41
+
42
+ # Extract data from HTML and XML resources with CSS selectors
43
+ print(ua.get('www.perl.org').result.dom().at('title').text)
44
+
45
+ # Scrape the latest headlines from a news site
46
+ print(ua.get('blogs.perl.org').result.dom().find('h2 > a').map('text').join('\\n'))
47
+
48
+ # IPv6 PUT request with Content-Type header and content
49
+ tx = ua.put('[::1]:3000', {'Content-Type': 'text/plain'}, b'Hi!')
50
+
51
+ # Quick JSON API request with Basic authentication
52
+ url = Pyjo.URL.new('https://example.com/test.json')
53
+ url.userinfo = 'sri:☃'
54
+ value = ua.get(url).result.json()
55
+
56
+ # JSON POST (application/json) with TLS certificate authentication
57
+ ua.cert, ua.key = 'tls.crt', 'tls.key'
58
+ tx = ua.post('https://example.com', json={'top': 'secret'})
59
+
60
+ # Form POST (application/x-www-form-urlencoded)
61
+ tx = ua.post('https://metacpan.org/search', form={'q': 'mojo'})
62
+
63
+ # Search DuckDuckGo anonymously through Tor
64
+ ua.proxy.http = 'socks://127.0.0.1:9050'
65
+ print(ua.get('api.3g2upl4pq6kufc4m.onion/?q=mojolicious&format=json').result.json('/Abstract'))
66
+
67
+ # GET request via UNIX domain socket "/tmp/myapp.sock" (percent encoded slash)
68
+ print(ua.get('http+unix://%2Ftmp%2Fmyapp.sock/test').result.body)
69
+
70
+ # Follow redirects to download Mojolicious from GitHub
71
+ ua.max_redirects = 5
72
+ ua.get('https://www.github.com/mojolicious/mojo/tarball/main').result.save_to('/home/sri/mojo.tar.gz')
73
+
74
+ # Non-blocking request
75
+ ua.get('mojolicious.org', lambda ua, tx: print(tx.result.dom().at('title').text))
76
+ if not Pyjo.IOLoop.is_running():
77
+ Pyjo.IOLoop.start()
78
+
79
+ # Concurrent non-blocking requests (synchronized with promises)
80
+ mojo_promise = ua.get_p('mojolicious.org')
81
+ cpan_promise = ua.get_p('cpan.org')
82
+
83
+
84
+ @Pyjo.Promise.all(mojo_promise, cpan_promise).then
85
+ def titles(mojo, cpan):
86
+ print(mojo[0].result.dom().at('title').text)
87
+ print(cpan[0].result.dom().at('title').text)
88
+
89
+
90
+ titles.wait()
91
+
92
+
93
+ # WebSocket connection sending and receiving JSON via UNIX domain socket
94
+ @ua.websocket('ws+unix://%2Ftmp%2Fmyapp.sock/echo.json')
95
+ def echo(ua, tx):
96
+ if not tx.is_websocket:
97
+ return print('WebSocket handshake failed!')
98
+
99
+ @tx.on('json')
100
+ def json(tx, hash):
101
+ print(f'WebSocket message via JSON: {hash["msg"]}')
102
+ tx.finish()
103
+
104
+ tx.send({'json': {'msg': 'Hello World!'}})
105
+
106
+
107
+ if not Pyjo.IOLoop.is_running():
108
+ Pyjo.IOLoop.start()
109
+
110
+ :mod:`Pyjo.UserAgent` is a full featured non-blocking I/O HTTP and WebSocket
111
+ user agent, with IPv6, TLS, SNI, IDNA, HTTP/SOCKS5 proxy, UNIX domain socket,
112
+ Comet (long polling), Promises/A+, keep-alive, connection pooling, timeout,
113
+ cookie, multipart, gzip compression and multiple event loop support.
114
+
115
+ All connections will be reset automatically if a new process has been forked,
116
+ this allows multiple processes to share the same :mod:`Pyjo.UserAgent` object
117
+ safely.
118
+
119
+ Non-blocking name resolution, SOCKS5 and TLS support come with
120
+ :mod:`Pyjo.IOLoop` and the standard library. Individual features can be
121
+ disabled with the ``PYJO_NO_NNR``, ``PYJO_NO_SOCKS`` and ``PYJO_NO_TLS``
122
+ environment variables.
123
+
124
+ The end of a ``with`` block closes the connections and the server that the user
125
+ agent made.
126
+
127
+ Events
128
+ ------
129
+
130
+ :mod:`Pyjo.UserAgent` inherits all events from :mod:`Pyjo.EventEmitter` and
131
+ can emit the following new ones.
132
+
133
+ prepare
134
+ ~~~~~~~
135
+ ::
136
+
137
+ ua.on('prepare', lambda ua, tx: ...)
138
+
139
+ Emitted whenever a new transaction is being prepared, before relative URLs
140
+ are rewritten and cookies added. This includes automatically prepared proxy
141
+ ``CONNECT`` requests and followed redirects. ::
142
+
143
+ @ua.on('prepare')
144
+ def prepare(ua, tx):
145
+ if tx.req.url.host == 'mojolicious.org':
146
+ tx.req.url = Pyjo.URL.new('/mock-mojolicious')
147
+
148
+ start
149
+ ~~~~~
150
+ ::
151
+
152
+ ua.on('start', lambda ua, tx: ...)
153
+
154
+ Emitted whenever a new transaction is about to start. This includes
155
+ automatically prepared proxy ``CONNECT`` requests and followed redirects. ::
156
+
157
+ @ua.on('start')
158
+ def start(ua, tx):
159
+ tx.req.headers.header('X-Bender', 'Bite my shiny metal ass!')
160
+
161
+ Debugging
162
+ ---------
163
+
164
+ You can set the ``PYJO_CLIENT_DEBUG`` environment variable to get some
165
+ advanced diagnostics information printed to :data:`sys.stderr`. ::
166
+
167
+ PYJO_CLIENT_DEBUG=1
168
+
169
+ See also
170
+ --------
171
+
172
+ :mod:`Pyjoyment`, https://mojolicious.org.
173
+
174
+ Ported from `Mojo::UserAgent <https://github.com/mojolicious/mojo/blob/v9.53/lib/Mojo/UserAgent.pm>`_
175
+ of Mojolicious 9.53.
176
+
177
+ Classes
178
+ -------
179
+ """
180
+
181
+ import Pyjo.EventEmitter
182
+ import Pyjo.IOLoop
183
+ import Pyjo.IOLoop.Stream
184
+ import Pyjo.IOLoop.TLS
185
+ import Pyjo.Promise
186
+ import Pyjo.Reactor
187
+ import Pyjo.Transaction
188
+ import Pyjo.Transaction.HTTP
189
+ import Pyjo.UserAgent.CookieJar
190
+ import Pyjo.UserAgent.Proxy
191
+ import Pyjo.UserAgent.Server
192
+ import Pyjo.UserAgent.Transactor
193
+
194
+ from Pyjo.String.Unicode import split
195
+ from Pyjo.Util import term_escape, warn
196
+
197
+ import builtins
198
+ import os
199
+ import socket
200
+ import ssl
201
+ import sys
202
+ import warnings
203
+ import weakref
204
+
205
+ from collections.abc import Callable
206
+ from types import TracebackType
207
+ from typing import Any, Self, overload
208
+
209
+
210
+ type Anything = builtins.object
211
+
212
+ # The callback of a request gets the user agent and the transaction
213
+ type Callback = Callable[[Any, Any], Anything]
214
+
215
+ DEBUG = bool(os.environ.get('PYJO_CLIENT_DEBUG'))
216
+
217
+
218
+ class _Request:
219
+ # A method for a common HTTP method, as Mojo makes them with monkey_patch
220
+ def __init__(self, method: str, promise: bool) -> None:
221
+ self.method = method
222
+ name = method.lower()
223
+ if promise:
224
+ self.__doc__ = f"""::
225
+
226
+ promise = ua.{name}_p('http://example.com')
227
+
228
+ Same as :attr:`{name}`, but performs all requests non-blocking and
229
+ returns a :mod:`Pyjo.Promise` object instead of accepting a callback. ::
230
+
231
+ @ua.{name}_p('http://example.com', json={{'a': 'b'}}).then
232
+ def ok(tx):
233
+ print(tx.result.body)
234
+
235
+ ok.catch(lambda err: warn(f'Connection error: {{err}}')).wait()
236
+ """
237
+ else:
238
+ self.__doc__ = f"""::
239
+
240
+ tx = ua.{name}('example.com')
241
+ tx = ua.{name}('http://example.com', {{'Accept': '*/*'}}, b'Content!')
242
+ tx = ua.{name}('http://example.com', {{'Accept': '*/*'}}, form={{'a': 'b'}})
243
+ tx = ua.{name}('http://example.com', {{'Accept': '*/*'}}, json={{'a': 'b'}})
244
+
245
+ Perform blocking ``{method}`` request and return resulting
246
+ :mod:`Pyjo.Transaction.HTTP` object, takes the same arguments as
247
+ :meth:`Pyjo.UserAgent.Transactor.Pyjo_UserAgent_Transactor.tx`
248
+ (except for the ``{method}`` method, which is implied). You can also
249
+ append a callback, or pass it as ``cb``, to perform requests
250
+ non-blocking. ::
251
+
252
+ ua.{name}('http://example.com', lambda ua, tx: print(tx.result.body), json={{'a': 'b'}})
253
+ ua.{name}('http://example.com', json={{'a': 'b'}}, cb=lambda ua, tx: print(tx.result.body))
254
+ if not Pyjo.IOLoop.is_running():
255
+ Pyjo.IOLoop.start()
256
+ """
257
+ self.promise = promise
258
+
259
+ @overload
260
+ def __get__(self, ua: None, owner: type | None = None) -> Self: ...
261
+
262
+ @overload
263
+ def __get__(self, ua: 'Pyjo_UserAgent', owner: type | None = None) -> Callable[..., Any]: ...
264
+
265
+ # The arguments of the transactor come before the callback, so the method is not typed further
266
+ def __get__(self, ua: 'Pyjo_UserAgent | None', owner: type | None = None) -> Self | Callable[..., Any]:
267
+ if ua is None:
268
+ return self
269
+ method = self.method
270
+
271
+ if self.promise:
272
+
273
+ def request_p(*args: Any, **generator: Any) -> Pyjo.Promise.object:
274
+ return ua.start_p(ua.build_tx(method, *args, **generator))
275
+
276
+ return request_p
277
+
278
+ # Unlike Mojolicious: the callback is also the keyword argument cb
279
+ def request(*args: Any, cb: Callback | None = None, **generator: Any) -> Any:
280
+ args, cb = _callback(args, cb)
281
+ return ua.start(ua.build_tx(method, *args, **generator), cb)
282
+
283
+ return request
284
+
285
+
286
+ class Pyjo_UserAgent(Pyjo.EventEmitter.object):
287
+ """
288
+ :mod:`Pyjo.UserAgent` inherits all methods from :mod:`Pyjo.EventEmitter`
289
+ and implements the following new ones.
290
+ """
291
+
292
+ def __init__(
293
+ self,
294
+ *,
295
+ ca: str | None = None,
296
+ cert: str | None = None,
297
+ connect_timeout: float | None = None,
298
+ cookie_jar: 'Pyjo.UserAgent.CookieJar.object | None' = None,
299
+ inactivity_timeout: float | None = None,
300
+ insecure: bool | None = None,
301
+ ioloop: Pyjo.IOLoop.object | None = None,
302
+ key: str | None = None,
303
+ max_connections: int = 5,
304
+ max_redirects: int | None = None,
305
+ max_response_size: int | None = None,
306
+ proxy: 'Pyjo.UserAgent.Proxy.object | None' = None,
307
+ request_timeout: float | None = None,
308
+ server: 'Pyjo.UserAgent.Server.object | None' = None,
309
+ source_address: tuple[str, int] | None = None,
310
+ tls_context: ssl.SSLContext | None = None,
311
+ transactor: 'Pyjo.UserAgent.Transactor.object | None' = None,
312
+ ) -> None:
313
+ """::
314
+
315
+ ua = Pyjo.UserAgent.new()
316
+ ua = Pyjo.UserAgent.new(max_redirects=3, request_timeout=5)
317
+
318
+ Construct a new :mod:`Pyjo.UserAgent` object.
319
+ """
320
+ super().__init__()
321
+ env = os.environ
322
+
323
+ # Left out from Mojolicious: the example with set_defaults of IO::Socket::SSL, which ssl does not have
324
+ self.ca = ca if ca is not None else env.get('PYJO_CA_FILE')
325
+ """::
326
+
327
+ ca = ua.ca
328
+ ua.ca = '/etc/tls/ca.crt'
329
+
330
+ Path to TLS certificate authority file used to verify the peer
331
+ certificate, defaults to the value of the ``PYJO_CA_FILE``
332
+ environment variable.
333
+ """
334
+
335
+ self.cert = cert if cert is not None else env.get('PYJO_CERT_FILE')
336
+ """::
337
+
338
+ cert = ua.cert
339
+ ua.cert = '/etc/tls/client.crt'
340
+
341
+ Path to TLS certificate file, defaults to the value of the
342
+ ``PYJO_CERT_FILE`` environment variable.
343
+ """
344
+
345
+ self.connect_timeout: float = (
346
+ connect_timeout if connect_timeout is not None else _number(env.get('PYJO_CONNECT_TIMEOUT'), True) or 10
347
+ )
348
+ """::
349
+
350
+ timeout = ua.connect_timeout
351
+ ua.connect_timeout = 5
352
+
353
+ Maximum amount of time in seconds establishing a connection may take
354
+ before getting canceled, defaults to the value of the
355
+ ``PYJO_CONNECT_TIMEOUT`` environment variable or ``10``.
356
+ """
357
+
358
+ # Left out from Mojolicious: the example with IO::Socket::SSL::PublicSuffix, which ssl does not have
359
+ self.cookie_jar = cookie_jar if cookie_jar is not None else Pyjo.UserAgent.CookieJar.new()
360
+ """::
361
+
362
+ cookie_jar = ua.cookie_jar
363
+ ua.cookie_jar = Pyjo.UserAgent.CookieJar.new()
364
+
365
+ Cookie jar to use for requests performed by this user agent, defaults
366
+ to a :mod:`Pyjo.UserAgent.CookieJar` object. ::
367
+
368
+ # Ignore all cookies
369
+ ua.cookie_jar.ignore = lambda cookie: True
370
+
371
+ # Add custom cookie to the jar
372
+ ua.cookie_jar.add(
373
+ Pyjo.Cookie.Response.new(
374
+ name='foo', value='bar', domain='docs.mojolicious.org', path='/Mojolicious'
375
+ )
376
+ )
377
+ """
378
+
379
+ self.inactivity_timeout: float = (
380
+ inactivity_timeout
381
+ if inactivity_timeout is not None
382
+ else _number(env.get('PYJO_INACTIVITY_TIMEOUT'), False, 40)
383
+ )
384
+ """::
385
+
386
+ timeout = ua.inactivity_timeout
387
+ ua.inactivity_timeout = 15
388
+
389
+ Maximum amount of time in seconds a connection can be inactive before
390
+ getting closed, defaults to the value of the
391
+ ``PYJO_INACTIVITY_TIMEOUT`` environment variable or ``40``. Setting the
392
+ value to ``0`` will allow connections to be inactive indefinitely.
393
+ """
394
+
395
+ self.insecure = insecure if insecure is not None else env.get('PYJO_INSECURE') not in (None, '', '0')
396
+ """::
397
+
398
+ boolean = ua.insecure
399
+ ua.insecure = boolean
400
+
401
+ Do not require a valid TLS certificate to access HTTPS/WSS sites,
402
+ defaults to the value of the ``PYJO_INSECURE`` environment variable. ::
403
+
404
+ # Disable TLS certificate verification for testing
405
+ ua.insecure = True
406
+ print(ua.get('https://127.0.0.1:3000').result.code)
407
+ """
408
+
409
+ self._ioloop = ioloop
410
+
411
+ self.key = key if key is not None else env.get('PYJO_KEY_FILE')
412
+ """::
413
+
414
+ key = ua.key
415
+ ua.key = '/etc/tls/client.crt'
416
+
417
+ Path to TLS key file, defaults to the value of the ``PYJO_KEY_FILE``
418
+ environment variable.
419
+ """
420
+
421
+ self.max_connections = max_connections
422
+ """::
423
+
424
+ max = ua.max_connections
425
+ ua.max_connections = 5
426
+
427
+ Maximum number of keep-alive connections that the user agent will
428
+ retain before it starts closing the oldest ones, defaults to ``5``.
429
+ Setting the value to ``0`` will prevent any connections from being
430
+ kept alive.
431
+ """
432
+
433
+ self.max_redirects: int = (
434
+ max_redirects if max_redirects is not None else int(_number(env.get('PYJO_MAX_REDIRECTS'), True) or 0)
435
+ )
436
+ """::
437
+
438
+ max = ua.max_redirects
439
+ ua.max_redirects = 3
440
+
441
+ Maximum number of redirects the user agent will follow before it
442
+ fails, defaults to the value of the ``PYJO_MAX_REDIRECTS`` environment
443
+ variable or ``0``.
444
+ """
445
+
446
+ self.max_response_size = max_response_size
447
+ """::
448
+
449
+ max = ua.max_response_size
450
+ ua.max_response_size = 16777216
451
+
452
+ Maximum response size in bytes, defaults to the value of
453
+ :attr:`Pyjo.Message.Response.Pyjo_Message_Response.max_message_size`.
454
+ Setting the value to ``0`` will allow responses of indefinite size.
455
+ Note that increasing this value can also drastically increase memory
456
+ usage, should you for example attempt to parse an excessively large
457
+ response body with the methods ``dom`` or ``json`` of
458
+ :mod:`Pyjo.Message`.
459
+ """
460
+
461
+ self.proxy = proxy if proxy is not None else Pyjo.UserAgent.Proxy.new()
462
+ """::
463
+
464
+ proxy = ua.proxy
465
+ ua.proxy = Pyjo.UserAgent.Proxy.new()
466
+
467
+ Proxy manager, defaults to a :mod:`Pyjo.UserAgent.Proxy` object. ::
468
+
469
+ # Detect proxy servers from environment
470
+ ua.proxy.detect()
471
+
472
+ # Manually configure HTTP proxy (using CONNECT for HTTPS/WebSockets)
473
+ ua.proxy.http = ua.proxy.https = 'http://127.0.0.1:8080'
474
+
475
+ # Manually configure Tor (SOCKS5)
476
+ ua.proxy.http = ua.proxy.https = 'socks://127.0.0.1:9050'
477
+
478
+ # Manually configure UNIX domain socket (using CONNECT for HTTPS/WebSockets)
479
+ ua.proxy.http = ua.proxy.https = 'http+unix://%2Ftmp%2Fproxy.sock'
480
+ """
481
+
482
+ self.request_timeout: float = (
483
+ request_timeout if request_timeout is not None else _number(env.get('PYJO_REQUEST_TIMEOUT'), False, 0)
484
+ )
485
+ """::
486
+
487
+ timeout = ua.request_timeout
488
+ ua.request_timeout = 5
489
+
490
+ Maximum amount of time in seconds establishing a connection, sending
491
+ the request and receiving a whole response may take before getting
492
+ canceled, defaults to the value of the ``PYJO_REQUEST_TIMEOUT``
493
+ environment variable or ``0``. Setting the value to ``0`` will allow
494
+ the user agent to wait indefinitely. The timeout will reset for every
495
+ followed redirect. ::
496
+
497
+ # Total limit of 5 seconds, of which 3 seconds may be spent connecting
498
+ ua.max_redirects, ua.connect_timeout, ua.request_timeout = 0, 3, 5
499
+ """
500
+
501
+ self._server = server
502
+ self._own_server = False
503
+
504
+ # Unlike Mojolicious: socket_options of IO::Socket::IP is source_address, as socket.create_connection has it
505
+ self.source_address = source_address
506
+ """::
507
+
508
+ address = ua.source_address
509
+ ua.source_address = ('127.0.0.1', 0)
510
+
511
+ Local address and port to connect from when opening new connections,
512
+ as :func:`socket.create_connection` takes it.
513
+ """
514
+
515
+ # Unlike Mojolicious: tls_options of IO::Socket::SSL is an ssl.SSLContext
516
+ self.tls_context = tls_context
517
+ """::
518
+
519
+ context = ua.tls_context
520
+ ua.tls_context = ssl.create_default_context()
521
+
522
+ The :class:`ssl.SSLContext` to use for TLS connections instead of the
523
+ one that :attr:`ca`, :attr:`cert` and :attr:`key` make.
524
+ """
525
+
526
+ self.transactor = transactor if transactor is not None else Pyjo.UserAgent.Transactor.new()
527
+ """::
528
+
529
+ t = ua.transactor
530
+ ua.transactor = Pyjo.UserAgent.Transactor.new()
531
+
532
+ Transaction builder, defaults to a :mod:`Pyjo.UserAgent.Transactor`
533
+ object. ::
534
+
535
+ # Change name of user agent
536
+ ua.transactor.name = 'MyUA 1.0'
537
+
538
+ # Disable compression
539
+ ua.transactor.compressed = False
540
+ """
541
+
542
+ self._connections: dict[str, dict[str, Any]] = {}
543
+ self._pid: int | None = None
544
+ self._queue: dict[Pyjo.IOLoop.object, list[tuple[str, str]]] = {}
545
+
546
+ # Common HTTP methods
547
+ delete = _Request('DELETE', False)
548
+ delete_p = _Request('DELETE', True)
549
+ get = _Request('GET', False)
550
+ get_p = _Request('GET', True)
551
+ head = _Request('HEAD', False)
552
+ head_p = _Request('HEAD', True)
553
+ options = _Request('OPTIONS', False)
554
+ options_p = _Request('OPTIONS', True)
555
+ patch = _Request('PATCH', False)
556
+ patch_p = _Request('PATCH', True)
557
+ post = _Request('POST', False)
558
+ post_p = _Request('POST', True)
559
+ put = _Request('PUT', False)
560
+ put_p = _Request('PUT', True)
561
+ query = _Request('QUERY', False)
562
+ query_p = _Request('QUERY', True)
563
+
564
+ def __del__(self) -> None:
565
+ try:
566
+ if sys.is_finalizing():
567
+ return
568
+ # Not in Mojolicious: the reactor that the user agent has, without making one
569
+ reactor = getattr(getattr(self, '_ioloop', None), '_reactor', None)
570
+ Pyjo.Reactor._finalize(self._release, reactor) # pyright: ignore[reportPrivateUsage]
571
+ except Exception:
572
+ pass
573
+
574
+ # Not in Mojolicious: a context manager, which closes the connections and the server that the user agent made where
575
+ # the block ends, as PyPy frees the object only when its garbage collector runs
576
+ def __enter__(self) -> Self:
577
+ return self
578
+
579
+ def __exit__(
580
+ self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
581
+ ) -> None:
582
+ self.close()
583
+
584
+ def _release(self) -> None:
585
+ self._cleanup()
586
+ # Not in Mojolicious: the server that the user agent made is closed here, as Perl closes it when it frees it, and
587
+ # PyPy frees it only when its garbage collector runs
588
+ if self._own_server and self._server is not None:
589
+ self._server.restart()
590
+
591
+ @property
592
+ def ioloop(self) -> Pyjo.IOLoop.object:
593
+ """::
594
+
595
+ loop = ua.ioloop
596
+ ua.ioloop = Pyjo.IOLoop.new()
597
+
598
+ Event loop object to use for blocking I/O operations, defaults to a
599
+ :mod:`Pyjo.IOLoop` object.
600
+ """
601
+ if self._ioloop is None:
602
+ self._ioloop = Pyjo.IOLoop.new()
603
+ return self._ioloop
604
+
605
+ @ioloop.setter
606
+ def ioloop(self, value: Pyjo.IOLoop.object) -> None:
607
+ self._ioloop = value
608
+
609
+ @property
610
+ def server(self) -> 'Pyjo.UserAgent.Server.object':
611
+ """::
612
+
613
+ server = ua.server
614
+ ua.server = Pyjo.UserAgent.Server.new()
615
+
616
+ Application server relative URLs will be processed with, defaults to a
617
+ :mod:`Pyjo.UserAgent.Server` object. ::
618
+
619
+ # Mock web service
620
+ ua.server.app = Pyjoyment.new()
621
+ ua.server.app.get('/time', lambda c: c.render(json={'now': time.time()}))
622
+ now = ua.get('/time').result.json()['now']
623
+
624
+ # Change log level
625
+ ua.server.app.log.level = 'fatal'
626
+
627
+ # Port currently used for processing relative URLs blocking
628
+ print(ua.server.url().port)
629
+
630
+ # Port currently used for processing relative URLs non-blocking
631
+ print(ua.server.nb_url().port)
632
+
633
+ The server runs on :attr:`ioloop`, the event loop of the blocking
634
+ requests, not on the one of the :mod:`Pyjo.IOLoop` singleton, which an
635
+ application uses for a :mod:`Pyjo.Promise`, a coroutine or a timer of
636
+ :mod:`Pyjo.IOLoop` by default. A blocking request to an application
637
+ whose action waits for one of them gets no response until its
638
+ ``request_timeout``, or never without it, as nothing runs the event loop
639
+ of the singleton then. On :mod:`Pyjo.Reactor.Asyncio`, the reactor of the
640
+ singleton follows the event loop of asyncio that runs, unless it is given
641
+ a loop, so such a request gets its response there. The blocking requests
642
+ of such an application run on the singleton, as those of
643
+ :mod:`Pyjo.Test.Pyjo` do, so that they do not depend on the reactor. ::
644
+
645
+ # An application whose action settles a promise
646
+ app = Pyjoyment.new()
647
+ app.routes.get('/later', lambda c: Pyjo.Promise.timer(0.1, 'Later!').then(lambda text: c.render(text=text)))
648
+ ua = Pyjo.UserAgent.new(ioloop=Pyjo.IOLoop.singleton)
649
+ ua.server.app = app
650
+ text = ua.get('/later').result.text
651
+ """
652
+ if self._server is None:
653
+ self._server = Pyjo.UserAgent.Server.new(ioloop=self.ioloop)
654
+ self._own_server = True
655
+ return self._server
656
+
657
+ @server.setter
658
+ def server(self, value: 'Pyjo.UserAgent.Server.object') -> None:
659
+ self._server = value
660
+
661
+ def build_tx(self, *args: Any, **generator: Any) -> Pyjo.Transaction.HTTP.object:
662
+ """::
663
+
664
+ tx = ua.build_tx('GET', 'example.com')
665
+ tx = ua.build_tx('PUT', 'http://example.com', {'Accept': '*/*'}, b'Content!')
666
+ tx = ua.build_tx('PUT', 'http://example.com', {'Accept': '*/*'}, form={'a': 'b'})
667
+ tx = ua.build_tx('PUT', 'http://example.com', {'Accept': '*/*'}, json={'a': 'b'})
668
+
669
+ Generate :mod:`Pyjo.Transaction.HTTP` object with
670
+ :meth:`Pyjo.UserAgent.Transactor.Pyjo_UserAgent_Transactor.tx`. ::
671
+
672
+ # Request with custom cookie
673
+ tx = ua.build_tx('GET', 'https://example.com/account')
674
+ tx.req.cookies({'name': 'user', 'value': 'sri'})
675
+ tx = ua.start(tx)
676
+
677
+ # Deactivate gzip compression
678
+ tx = ua.build_tx('GET', 'example.com')
679
+ tx.req.headers.remove('Accept-Encoding')
680
+ tx = ua.start(tx)
681
+
682
+ # Interrupt response by raising an error
683
+ tx = ua.build_tx('GET', 'http://example.com')
684
+
685
+ @tx.res.on('progress')
686
+ def progress(res):
687
+ if (server := res.headers.server) and 'IIS' in server:
688
+ res.error = {'message': 'Oh noes, it is IIS!'}
689
+
690
+ tx = ua.start(tx)
691
+ """
692
+ return self.transactor.tx(*args, **generator)
693
+
694
+ def build_websocket_tx(self, *args: Any) -> Pyjo.Transaction.HTTP.object:
695
+ """::
696
+
697
+ tx = ua.build_websocket_tx('ws://example.com')
698
+ tx = ua.build_websocket_tx('ws://example.com', {'DNT': 1}, ['v1.proto'])
699
+
700
+ Generate :mod:`Pyjo.Transaction.HTTP` object with
701
+ :meth:`Pyjo.UserAgent.Transactor.Pyjo_UserAgent_Transactor.websocket`. ::
702
+
703
+ # Custom WebSocket handshake with cookie
704
+ tx = ua.build_websocket_tx('wss://example.com/echo')
705
+ tx.req.cookies({'name': 'user', 'value': 'sri'})
706
+
707
+ def echo(ua, tx):
708
+ if not tx.is_websocket:
709
+ return print('WebSocket handshake failed!')
710
+
711
+ @tx.on('message')
712
+ def message(tx, msg):
713
+ print(f'WebSocket message: {msg}')
714
+ tx.finish()
715
+
716
+ tx.send('Hi!')
717
+
718
+ ua.start(tx, echo)
719
+ if not Pyjo.IOLoop.is_running():
720
+ Pyjo.IOLoop.start()
721
+ """
722
+ return self.transactor.websocket(*args)
723
+
724
+ # Not in Mojolicious: the release of the connections, which Perl does in DESTROY, also at the end of a with block
725
+ def close(self) -> None:
726
+ """::
727
+
728
+ ua.close()
729
+
730
+ Close the connections of the user agent, and the server that it made
731
+ for relative URLs, as the end of a ``with`` block does. The user agent
732
+ makes new ones when it is used again.
733
+ """
734
+ self._release()
735
+
736
+ @overload
737
+ def start(self, tx: Pyjo.Transaction.HTTP.object) -> Any: ...
738
+
739
+ @overload
740
+ def start(self, tx: Pyjo.Transaction.HTTP.object, cb: Callback) -> str | None: ...
741
+
742
+ @overload
743
+ def start(self, tx: Pyjo.Transaction.HTTP.object, cb: None) -> Any: ...
744
+
745
+ def start(self, tx: Pyjo.Transaction.HTTP.object, cb: Callback | None = None) -> Any:
746
+ """::
747
+
748
+ tx = ua.start(Pyjo.Transaction.HTTP.new())
749
+
750
+ Perform blocking request for a custom :mod:`Pyjo.Transaction.HTTP`
751
+ object, which can be prepared manually or with :meth:`build_tx`. You
752
+ can also append a callback to perform requests non-blocking. ::
753
+
754
+ tx = ua.build_tx('GET', 'http://example.com')
755
+ ua.start(tx, lambda ua, tx: print(tx.result.body))
756
+ if not Pyjo.IOLoop.is_running():
757
+ Pyjo.IOLoop.start()
758
+
759
+ A blocking request cannot wait for its response in an event loop of
760
+ :mod:`asyncio` that runs already, as in a coroutine or in an action of
761
+ an application on :mod:`Pyjo.Reactor.Asyncio`, as asyncio does not
762
+ nest event loops: it warns with :class:`RuntimeWarning` and returns
763
+ the transaction without the response. Await the promise of
764
+ :meth:`start_p`, or of a method like :attr:`get_p`, there.
765
+ """
766
+ # Fork-safety
767
+ pid = os.getpid()
768
+ if self._pid is not None and self._pid != pid:
769
+ self._cleanup().server.restart()
770
+ if self._pid is None:
771
+ self._pid = pid
772
+
773
+ # Non-blocking
774
+ if cb is not None:
775
+ if DEBUG:
776
+ warn(f'-- Non-blocking request ({_url(tx)})\n')
777
+ return self._start(Pyjo.IOLoop.object.singleton(), tx, cb)
778
+
779
+ # Blocking
780
+ if DEBUG:
781
+ warn(f'-- Blocking request ({_url(tx)})\n')
782
+ result: list[Pyjo.Transaction.HTTP.object | None] = [None]
783
+
784
+ def blocking(ua: Pyjo_UserAgent, tx: Pyjo.Transaction.HTTP.object) -> None:
785
+ ua.ioloop.stop()
786
+ result[0] = tx
787
+
788
+ self._start(self.ioloop, tx, blocking)
789
+ self.ioloop.start()
790
+
791
+ # Not in Mojolicious: a warning of a blocking request in an event loop of asyncio that runs already, where the
792
+ # event loop returns at once, as asyncio does not nest event loops, before the response
793
+ if result[0] is None:
794
+ warnings.warn(
795
+ 'Blocking request in a running event loop of asyncio, which does not wait for the response, '
796
+ 'await a promise of a request, as of get_p, instead',
797
+ RuntimeWarning,
798
+ skip_file_prefixes=(os.path.dirname(__file__),),
799
+ )
800
+ return tx
801
+
802
+ return result[0]
803
+
804
+ def start_p(self, tx: Pyjo.Transaction.HTTP.object) -> Pyjo.Promise.object:
805
+ """::
806
+
807
+ promise = ua.start_p(Pyjo.Transaction.HTTP.new())
808
+
809
+ Same as :meth:`start`, but performs all requests non-blocking and
810
+ returns a :mod:`Pyjo.Promise` object instead of accepting a callback. ::
811
+
812
+ tx = ua.build_tx('GET', 'http://example.com')
813
+
814
+ @ua.start_p(tx).then
815
+ def ok(tx):
816
+ print(tx.result.body)
817
+
818
+ ok.catch(lambda err: warn(f'Connection error: {err}')).wait()
819
+ """
820
+ promise = Pyjo.Promise.new()
821
+ self.start(tx, lambda ua, tx: ua.transactor.promisify(promise, tx))
822
+ return promise
823
+
824
+ # The arguments of the transactor come before the callback, so the method is not typed further; unlike
825
+ # Mojolicious, the callback is also the keyword argument cb
826
+ def websocket(self, *args: Any, cb: Callback | None = None) -> Any:
827
+ """::
828
+
829
+ ua.websocket('ws://example.com', lambda ua, tx: ...)
830
+ ua.websocket('ws://example.com', {'DNT': 1}, ['v1.proto'], lambda ua, tx: ...)
831
+ ua.websocket('ws://example.com', cb=lambda ua, tx: ...)
832
+
833
+ Open a non-blocking WebSocket connection with transparent handshake,
834
+ takes the same arguments as
835
+ :meth:`Pyjo.UserAgent.Transactor.Pyjo_UserAgent_Transactor.websocket`.
836
+ The callback will receive either a :mod:`Pyjo.Transaction.WebSocket`
837
+ or :mod:`Pyjo.Transaction.HTTP` object, depending on if the handshake
838
+ was successful. Without it the method is a decorator, and the decorated
839
+ name is the id of the connection. ::
840
+
841
+ @ua.websocket('wss://example.com/echo', ['v1.proto'])
842
+ def echo(ua, tx):
843
+ if not tx.is_websocket:
844
+ return print('WebSocket handshake failed!')
845
+ if not tx.protocol:
846
+ return print('Subprotocol negotiation failed!')
847
+ tx.on('finish', lambda tx, code, reason: print(f'WebSocket closed with status {code}.'))
848
+
849
+ @tx.on('message')
850
+ def message(tx, msg):
851
+ print(f'WebSocket message: {msg}')
852
+ tx.finish()
853
+
854
+ tx.send('Hi!')
855
+
856
+ if not Pyjo.IOLoop.is_running():
857
+ Pyjo.IOLoop.start()
858
+
859
+ You can activate ``permessage-deflate`` compression by setting the
860
+ ``Sec-WebSocket-Extensions`` header, this can result in much better
861
+ performance, but also increases memory usage by up to 300KiB per
862
+ connection. ::
863
+
864
+ ua.websocket('ws://example.com/foo', {'Sec-WebSocket-Extensions': 'permessage-deflate'}, lambda ua, tx: ...)
865
+ """
866
+ args, cb = _callback(args, cb)
867
+
868
+ # Not in Mojolicious: a decorator without the callback
869
+ if cb is None:
870
+
871
+ def decorator(cb: Callback) -> str | None:
872
+ return self.start(self.build_websocket_tx(*args), cb)
873
+
874
+ return decorator
875
+
876
+ return self.start(self.build_websocket_tx(*args), cb)
877
+
878
+ def websocket_p(self, *args: Any) -> Pyjo.Promise.object:
879
+ """::
880
+
881
+ promise = ua.websocket_p('ws://example.com')
882
+
883
+ Same as :meth:`websocket`, but returns a :mod:`Pyjo.Promise` object
884
+ instead of accepting a callback. ::
885
+
886
+ @ua.websocket_p('wss://example.com/echo').then
887
+ def ok(tx):
888
+ promise = Pyjo.Promise.new()
889
+ tx.on('finish', lambda tx, code, reason: promise.resolve())
890
+
891
+ @tx.on('message')
892
+ def message(tx, msg):
893
+ print(f'WebSocket message: {msg}')
894
+ tx.finish()
895
+
896
+ tx.send('Hi!')
897
+ return promise
898
+
899
+ ok.catch(lambda err: warn(f'WebSocket error: {err}')).wait()
900
+ """
901
+ return self.start_p(self.build_websocket_tx(*args))
902
+
903
+ def _cleanup(self) -> Self:
904
+ self._pid = None
905
+ for id in list(self._connections):
906
+ self._finish(id, True)
907
+ return self
908
+
909
+ def _connect(self, loop: Pyjo.IOLoop.object, tx: Pyjo.Transaction.object, handle: Any = None) -> str:
910
+ t = self.transactor
911
+ proto, host, port = t.endpoint(tx) if handle is not None else t.peer(tx)
912
+
913
+ options: dict[str, Any] = {'timeout': self.connect_timeout}
914
+ if proto == 'http+unix':
915
+ options['path'] = host
916
+ else:
917
+ options['address'], options['port'] = host, port
918
+ # Unlike Mojolicious: socket_options of IO::Socket::IP is source_address
919
+ if self.source_address is not None:
920
+ options['source_address'] = self.source_address
921
+ if handle is not None:
922
+ options['handle'] = handle
923
+
924
+ # SOCKS
925
+ if proto == 'socks':
926
+ options['socks_address'], options['socks_port'] = options['address'], options['port']
927
+ proto, options['address'], options['port'] = t.endpoint(tx)
928
+ req = tx.req
929
+ req.via_proxy = False
930
+ if req.proxy is not None and (userinfo := req.proxy.userinfo):
931
+ user, *password = [part or '' for part in split(':', userinfo)]
932
+ options['socks_user'], options['socks_pass'] = user, password[0] if password else None
933
+
934
+ # TLS
935
+ if options.setdefault('tls', proto == 'https'):
936
+ options['tls_ca'], options['tls_cert'], options['tls_key'] = self.ca, self.cert, self.key
937
+ context = self.tls_context
938
+ # Unlike Mojolicious: no verification changes the context, where Mojo sets SSL_verify_mode of tls_options
939
+ if self.insecure:
940
+ if context is None:
941
+ context = Pyjo.IOLoop.TLS.context(**options)
942
+ context.check_hostname = False
943
+ context.verify_mode = ssl.CERT_NONE
944
+ if context is not None:
945
+ options['tls_context'] = context
946
+
947
+ ref = weakref.ref(self)
948
+ id = ''
949
+
950
+ def connected(loop: Pyjo.IOLoop.object, err: Any, stream: Pyjo.IOLoop.Stream.object) -> None:
951
+ # Connection error
952
+ if (ua := ref()) is None:
953
+ return
954
+ if err:
955
+ return ua._error(id, err)
956
+
957
+ # Connection established
958
+ def timeout(stream: Pyjo.IOLoop.Stream.object) -> None:
959
+ _call(ref, '_error', id, 'Inactivity timeout')
960
+
961
+ def close(stream: Pyjo.IOLoop.Stream.object) -> None:
962
+ _call(ref, '_finish', id, True)
963
+
964
+ def error(stream: Pyjo.IOLoop.Stream.object, err: Any) -> None:
965
+ _call(ref, '_error', id, err)
966
+
967
+ def read(stream: Pyjo.IOLoop.Stream.object, chunk: bytes) -> None:
968
+ _call(ref, '_read', id, chunk)
969
+
970
+ stream.on('timeout', timeout)
971
+ stream.on('close', close)
972
+ stream.on('error', error)
973
+ stream.on('read', read)
974
+ ua._process(id)
975
+
976
+ id = loop.client(connected, **options)
977
+ return id
978
+
979
+ def _connect_proxy(self, loop: Pyjo.IOLoop.object, old: Pyjo.Transaction.HTTP.object, cb: Callback) -> str | None:
980
+ # Start CONNECT request
981
+ if (new := self.transactor.proxy_connect(old)) is None:
982
+ return None
983
+ id: str | None = None
984
+
985
+ def connected(ua: Pyjo_UserAgent, tx: Pyjo.Transaction.HTTP.object) -> None:
986
+ nonlocal id
987
+ assert id is not None
988
+
989
+ # Real transaction
990
+ old.previous = tx
991
+ old.req.via_proxy = False
992
+ c = ua._connections[id] = {'cb': cb, 'ioloop': loop, 'tx': old}
993
+
994
+ # CONNECT failed
995
+ if tx.error or not tx.res.is_success or not tx.keep_alive:
996
+ return ua._error(id, 'Proxy connection failed')
997
+
998
+ # Start real transaction without TLS upgrade
999
+ if tx.req.url.protocol != 'https':
1000
+ return ua._process(id)
1001
+
1002
+ # TLS upgrade before starting the real transaction
1003
+ handle = loop.stream(id).steal_handle()
1004
+ ua._remove(id)
1005
+ id = ua._connect(loop, old, handle)
1006
+ ua._connections[id] = c
1007
+
1008
+ id = self._start(loop, new, connected)
1009
+ return id
1010
+
1011
+ def _connection(self, loop: Pyjo.IOLoop.object, tx: Pyjo.Transaction.HTTP.object, cb: Callback) -> str | None:
1012
+ # Reuse connection
1013
+ proto, host, port = self.transactor.endpoint(tx)
1014
+ if id := self._dequeue(loop, f'{proto}:{host}:{port}', True):
1015
+ if DEBUG:
1016
+ warn(f'-- Reusing connection {id} ({proto}://{host}:{port})\n')
1017
+ c = self._connections[id]
1018
+ c['cb'], c['tx'] = cb, tx
1019
+ if tx.connection is None:
1020
+ tx.kept_alive = True
1021
+ self._process(id)
1022
+ return id
1023
+
1024
+ # CONNECT request to proxy required
1025
+ if id := self._connect_proxy(loop, tx, cb):
1026
+ return id
1027
+
1028
+ # New connection
1029
+ if proto not in ('http', 'https', 'http+unix'):
1030
+ tx.res.error = {'message': f'Unsupported protocol: {proto}'}
1031
+ return loop.next_tick(lambda loop: cb(self, tx))
1032
+ id = self._connect(loop, tx)
1033
+ if DEBUG:
1034
+ warn(f'-- Connect {id} ({proto}://{host}:{port})\n')
1035
+ self._connections[id] = {'cb': cb, 'ioloop': loop, 'tx': tx}
1036
+
1037
+ return id
1038
+
1039
+ def _dequeue(self, loop: Pyjo.IOLoop.object, name: str, test: bool = False) -> str | None:
1040
+ old = self._queue.setdefault(loop, [])
1041
+ found: str | None = None
1042
+ new: list[tuple[str, str]] = []
1043
+ for queued in old:
1044
+ if found or name not in queued:
1045
+ new.append(queued)
1046
+ continue
1047
+
1048
+ # Search for id/name and sort out corrupted connections if necessary
1049
+ if (stream := loop.stream(queued[1])) is None:
1050
+ continue
1051
+ if test and stream.is_readable:
1052
+ stream.close()
1053
+ else:
1054
+ found = queued[1]
1055
+ old[:] = new
1056
+
1057
+ return found
1058
+
1059
+ def _error(self, id: str, err: Any) -> None:
1060
+ if (tx := self._connections.get(id, {}).get('tx')) is not None:
1061
+ tx.res.error = {'message': str(err)}
1062
+ self._finish(id, True)
1063
+
1064
+ def _finish(self, id: str, close: bool = False) -> None:
1065
+ # Remove request timeout and finish transaction
1066
+ if (c := self._connections.get(id)) is None:
1067
+ return None
1068
+ if (timeout := c.pop('timeout', None)) is not None and (loop := c.get('ioloop')) is not None:
1069
+ loop.remove(timeout)
1070
+ if (old := c.get('tx')) is None:
1071
+ return self._reuse(id, close)
1072
+
1073
+ # Premature connection close
1074
+ res = old.closed().res.finish()
1075
+ if close and not res.code and not res.error:
1076
+ res.error = {'message': 'Premature connection close'}
1077
+
1078
+ # Always remove connection for WebSockets
1079
+ if old.is_websocket:
1080
+ return self._remove(id)
1081
+ self.cookie_jar.collect(old)
1082
+
1083
+ # Upgrade connection to WebSocket
1084
+ if (new := self.transactor.upgrade(old)) is not None:
1085
+ ref = weakref.ref(self)
1086
+
1087
+ def resume(tx: Pyjo.Transaction.object) -> None:
1088
+ _call(ref, '_write', id)
1089
+
1090
+ new.on('resume', resume)
1091
+ c['tx'] = new
1092
+ c['cb'](self, new)
1093
+ new.client_read(old.res.content.leftovers())
1094
+ return None
1095
+
1096
+ # CONNECT requests always have a follow-up request
1097
+ if old.req.method.upper() != 'CONNECT':
1098
+ self._reuse(id, close)
1099
+ if res.is_error:
1100
+ res.error = {'message': res.message, 'code': res.code}
1101
+ if not self._redirect(c, old):
1102
+ c['cb'](self, old)
1103
+
1104
+ def _process(self, id: str) -> None:
1105
+ c = self._connections[id]
1106
+ stream = c['ioloop'].stream(id)
1107
+ stream.timeout = self.inactivity_timeout
1108
+ tx = c['tx']
1109
+ tx.connection = id
1110
+ handle = stream.handle
1111
+ if handle.family != getattr(socket, 'AF_UNIX', None):
1112
+ tx.local_address, tx.local_port = handle.getsockname()[:2]
1113
+ tx.remote_address, tx.remote_port = handle.getpeername()[:2]
1114
+
1115
+ ref = weakref.ref(self)
1116
+
1117
+ def resume(tx: Pyjo.Transaction.object) -> None:
1118
+ _call(ref, '_write', id)
1119
+
1120
+ tx.on('resume', resume)
1121
+ self._write(id)
1122
+
1123
+ def _read(self, id: str, chunk: bytes) -> None:
1124
+ # Corrupted connection
1125
+ if (tx := self._connections.get(id, {}).get('tx')) is None:
1126
+ return self._remove(id)
1127
+ if DEBUG:
1128
+ warn(term_escape(f'-- Client <<< Server ({_url(tx)})\n{chunk.decode("latin-1")}\n'))
1129
+ tx.client_read(chunk)
1130
+ if tx.is_finished:
1131
+ self._finish(id)
1132
+
1133
+ def _redirect(self, c: dict[str, Any], old: Pyjo.Transaction.HTTP.object) -> str | None:
1134
+ if (new := self.transactor.redirect(old)) is None:
1135
+ return None
1136
+ if not len(old.redirects()) < self.max_redirects:
1137
+ return None
1138
+ return self._start(c['ioloop'], new, c.pop('cb'))
1139
+
1140
+ def _remove(self, id: str) -> None:
1141
+ if (c := self._connections.pop(id, None)) is None or (loop := c.get('ioloop')) is None:
1142
+ return
1143
+ self._dequeue(loop, id)
1144
+ loop.remove(id)
1145
+
1146
+ def _reuse(self, id: str, close: bool) -> None:
1147
+ # Connection close
1148
+ c = self._connections.get(id, {})
1149
+ tx = c.pop('tx', None)
1150
+ max = self.max_connections
1151
+ if close or tx is None or not max or not tx.keep_alive or tx.error:
1152
+ return self._remove(id)
1153
+
1154
+ # Keep connection alive
1155
+ queue = self._queue.setdefault(c['ioloop'], [])
1156
+ while len(queue) and len(queue) >= max:
1157
+ self._remove(queue.pop(0)[1])
1158
+ queue.append((':'.join(str(part) for part in self.transactor.endpoint(tx)), id))
1159
+
1160
+ def _start(self, loop: Pyjo.IOLoop.object, tx: Pyjo.Transaction.HTTP.object, cb: Callback) -> str | None:
1161
+ # Application server
1162
+ self.emit('prepare', tx)
1163
+ url = tx.req.url
1164
+ if not url.is_abs:
1165
+ server = self.server
1166
+ base = server.url() if loop is self.ioloop else server.nb_url()
1167
+ url.scheme, url.host, url.port = base.scheme, base.host, base.port
1168
+
1169
+ for helper in (self.proxy, self.cookie_jar):
1170
+ helper.prepare(tx)
1171
+ if (max := self.max_response_size) is not None:
1172
+ tx.res.max_message_size = max
1173
+ self.emit('start', tx)
1174
+
1175
+ # Allow test servers sharing the same event loop to clean up connections
1176
+ if not loop.is_running:
1177
+ loop.next_tick(lambda loop: None)
1178
+ loop.one_tick()
1179
+ if not (id := self._connection(loop, tx, cb)):
1180
+ return None
1181
+
1182
+ if t := self.request_timeout:
1183
+ ref = weakref.ref(self)
1184
+ c = self._connections.setdefault(id, {})
1185
+ if not c.get('timeout'):
1186
+ c['timeout'] = loop.timer(t, lambda loop: _call(ref, '_error', id, 'Request timeout'))
1187
+
1188
+ return id
1189
+
1190
+ def _write(self, id: str) -> None:
1191
+ # Protect from resume event recursion
1192
+ c = self._connections.get(id)
1193
+ if c is None or (tx := c.get('tx')) is None or c.get('writing'):
1194
+ return
1195
+ writing = c.get('writing')
1196
+ c['writing'] = True
1197
+ try:
1198
+ chunk = tx.client_write()
1199
+ if DEBUG:
1200
+ warn(term_escape(f'-- Client >>> Server ({_url(tx)})\n{chunk.decode("latin-1")}\n'))
1201
+ if not len(chunk):
1202
+ return
1203
+
1204
+ ref = weakref.ref(self)
1205
+
1206
+ def written(stream: Pyjo.IOLoop.Stream.object) -> None:
1207
+ _call(ref, '_write', id)
1208
+
1209
+ c['ioloop'].stream(id).write(chunk, written)
1210
+ finally:
1211
+ c['writing'] = writing
1212
+
1213
+
1214
+ # The callback, the last of the arguments or the keyword argument cb, and the arguments before it
1215
+ def _callback(args: tuple[Any, ...], cb: Callback | None) -> tuple[tuple[Any, ...], Callback | None]:
1216
+ if args and callable(args[-1]):
1217
+ if cb is not None:
1218
+ raise TypeError('callback given twice, as the last argument and as cb')
1219
+ return args[:-1], args[-1]
1220
+ return args, cb
1221
+
1222
+
1223
+ # A method of the user agent, as long as it exists, as Mojo weakens the user agent in its callbacks
1224
+ def _call(ref: 'weakref.ref[Any]', method: str, *args: Any) -> None:
1225
+ if (ua := ref()) is not None:
1226
+ getattr(ua, method)(*args)
1227
+
1228
+
1229
+ # A number of the environment, where a false value is the default with "||" and only a missing one with "//"
1230
+ def _number(value: str | None, or_: bool, default: float = 0) -> float:
1231
+ if value is None or (or_ and value in ('', '0')):
1232
+ return default
1233
+ number = float(value)
1234
+ return int(number) if number.is_integer() else number
1235
+
1236
+
1237
+ def _url(tx: Pyjo.Transaction.object) -> str:
1238
+ return str(tx.req.url.to_abs())
1239
+
1240
+
1241
+ new = Pyjo_UserAgent.new
1242
+ object = Pyjo_UserAgent
1243
+ UserAgent = Pyjo_UserAgent