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.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- 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
|