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,766 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Pyjo.UserAgent.Transactor - User agent transactor
|
|
3
|
+
=================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.UserAgent.Transactor import Transactor
|
|
7
|
+
|
|
8
|
+
# GET request with Accept header
|
|
9
|
+
t = Transactor()
|
|
10
|
+
print(t.tx('GET', 'http://example.com', {'Accept': '*/*'}).req.to_bytes())
|
|
11
|
+
|
|
12
|
+
# Or import the module, as in Google Python Style Guide
|
|
13
|
+
|
|
14
|
+
import Pyjo.UserAgent.Transactor
|
|
15
|
+
|
|
16
|
+
# GET request with Accept header
|
|
17
|
+
t = Pyjo.UserAgent.Transactor.new()
|
|
18
|
+
print(t.tx('GET', 'http://example.com', {'Accept': '*/*'}).req.to_bytes())
|
|
19
|
+
|
|
20
|
+
# POST request with form-data
|
|
21
|
+
print(t.tx('POST', 'example.com', form={'a': 'b'}).req.to_bytes())
|
|
22
|
+
|
|
23
|
+
# PUT request with JSON data
|
|
24
|
+
print(t.tx('PUT', 'example.com', json={'a': 'b'}).req.to_bytes())
|
|
25
|
+
|
|
26
|
+
:mod:`Pyjo.UserAgent.Transactor` is the transaction building and manipulation
|
|
27
|
+
framework used by :mod:`Pyjo.UserAgent`.
|
|
28
|
+
|
|
29
|
+
Generators
|
|
30
|
+
----------
|
|
31
|
+
|
|
32
|
+
These content generators are available by default.
|
|
33
|
+
|
|
34
|
+
form
|
|
35
|
+
~~~~
|
|
36
|
+
::
|
|
37
|
+
|
|
38
|
+
t.tx('POST', 'http://example.com', form={'a': 'b'})
|
|
39
|
+
|
|
40
|
+
Generate query string, ``application/x-www-form-urlencoded`` or
|
|
41
|
+
``multipart/form-data`` content. See :meth:`~Pyjo_UserAgent_Transactor.tx`
|
|
42
|
+
for more.
|
|
43
|
+
|
|
44
|
+
json
|
|
45
|
+
~~~~
|
|
46
|
+
::
|
|
47
|
+
|
|
48
|
+
t.tx('PATCH', 'http://example.com', json={'a': 'b'})
|
|
49
|
+
|
|
50
|
+
Generate JSON content with :mod:`Pyjo.JSON`. See
|
|
51
|
+
:meth:`~Pyjo_UserAgent_Transactor.tx` for more.
|
|
52
|
+
|
|
53
|
+
multipart
|
|
54
|
+
~~~~~~~~~
|
|
55
|
+
::
|
|
56
|
+
|
|
57
|
+
t.tx('PUT', 'http://example.com', multipart=['Hello', 'World!'])
|
|
58
|
+
|
|
59
|
+
Generate multipart content. See :meth:`~Pyjo_UserAgent_Transactor.tx` for
|
|
60
|
+
more.
|
|
61
|
+
|
|
62
|
+
See also
|
|
63
|
+
--------
|
|
64
|
+
|
|
65
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
66
|
+
|
|
67
|
+
Ported from `Mojo::UserAgent::Transactor <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/UserAgent/Transactor.pm>`_
|
|
68
|
+
of Mojolicious 9.49.
|
|
69
|
+
|
|
70
|
+
Classes
|
|
71
|
+
-------
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
import Pyjo.Asset
|
|
75
|
+
import Pyjo.Asset.File
|
|
76
|
+
import Pyjo.Asset.Memory
|
|
77
|
+
import Pyjo.Base
|
|
78
|
+
import Pyjo.Content
|
|
79
|
+
import Pyjo.Content.MultiPart
|
|
80
|
+
import Pyjo.Content.Single
|
|
81
|
+
import Pyjo.File
|
|
82
|
+
import Pyjo.Headers
|
|
83
|
+
import Pyjo.Message.Response
|
|
84
|
+
import Pyjo.Parameters
|
|
85
|
+
import Pyjo.Promise
|
|
86
|
+
import Pyjo.Transaction
|
|
87
|
+
import Pyjo.Transaction.HTTP
|
|
88
|
+
import Pyjo.Transaction.WebSocket
|
|
89
|
+
import Pyjo.URL
|
|
90
|
+
|
|
91
|
+
from Pyjo.JSON import encode_json
|
|
92
|
+
from Pyjo.Util import encode, url_escape
|
|
93
|
+
from Pyjo.WebSocket import challenge, client_handshake
|
|
94
|
+
|
|
95
|
+
import builtins
|
|
96
|
+
import os
|
|
97
|
+
import re
|
|
98
|
+
import weakref
|
|
99
|
+
|
|
100
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
101
|
+
from typing import Any, Self, cast
|
|
102
|
+
from urllib.parse import SplitResult
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
type Anything = builtins.object
|
|
106
|
+
|
|
107
|
+
# A generator is called with the transactor, the transaction, its data and its options
|
|
108
|
+
type Generator = Callable[..., Anything]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Pyjo_UserAgent_Transactor(Pyjo.Base.object):
|
|
112
|
+
"""
|
|
113
|
+
:mod:`Pyjo.UserAgent.Transactor` inherits all methods from
|
|
114
|
+
:mod:`Pyjo.Base` and implements the following new ones.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
*,
|
|
120
|
+
compressed: bool | None = None,
|
|
121
|
+
generators: dict[str, Generator] | None = None,
|
|
122
|
+
# Unlike Mojolicious: Pyjoyment names itself
|
|
123
|
+
name: str = 'Pyjoyment (Python)',
|
|
124
|
+
) -> None:
|
|
125
|
+
"""::
|
|
126
|
+
|
|
127
|
+
t = Pyjo.UserAgent.Transactor.new()
|
|
128
|
+
t = Pyjo.UserAgent.Transactor.new(compressed=False)
|
|
129
|
+
|
|
130
|
+
Construct a new :mod:`Pyjo.UserAgent.Transactor` object.
|
|
131
|
+
"""
|
|
132
|
+
self._compressed: bool | None = compressed
|
|
133
|
+
|
|
134
|
+
self.generators: dict[str, Generator] = (
|
|
135
|
+
generators if generators is not None else {'form': _form, 'json': _json, 'multipart': _multipart}
|
|
136
|
+
)
|
|
137
|
+
"""::
|
|
138
|
+
|
|
139
|
+
generators = t.generators
|
|
140
|
+
t.generators = {'foo': lambda t, tx, *args, **options: ...}
|
|
141
|
+
|
|
142
|
+
Registered content generators, by default only ``form``, ``json`` and
|
|
143
|
+
``multipart`` are already defined.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
self.name = name
|
|
147
|
+
"""::
|
|
148
|
+
|
|
149
|
+
name = t.name
|
|
150
|
+
t.name = 'Pyjoyment'
|
|
151
|
+
|
|
152
|
+
Value for ``User-Agent`` request header of generated transactions,
|
|
153
|
+
defaults to ``Pyjoyment (Python)``.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def compressed(self) -> bool:
|
|
158
|
+
"""::
|
|
159
|
+
|
|
160
|
+
boolean = t.compressed
|
|
161
|
+
t.compressed = boolean
|
|
162
|
+
|
|
163
|
+
Try to negotiate compression for the response content and decompress it
|
|
164
|
+
automatically, defaults to the value of the ``PYJO_GZIP`` environment
|
|
165
|
+
variable or true.
|
|
166
|
+
"""
|
|
167
|
+
if self._compressed is None:
|
|
168
|
+
self._compressed = os.environ.get('PYJO_GZIP', '1') not in ('', '0')
|
|
169
|
+
return self._compressed
|
|
170
|
+
|
|
171
|
+
@compressed.setter
|
|
172
|
+
def compressed(self, value: bool) -> None:
|
|
173
|
+
self._compressed = value
|
|
174
|
+
|
|
175
|
+
def add_generator(self, name: str, cb: Generator) -> Self:
|
|
176
|
+
"""::
|
|
177
|
+
|
|
178
|
+
t = t.add_generator('foo', lambda t, tx, *args, **options: ...)
|
|
179
|
+
|
|
180
|
+
Register a content generator. ::
|
|
181
|
+
|
|
182
|
+
def foo(t, tx, data, **options): ...
|
|
183
|
+
|
|
184
|
+
t.add_generator('foo', foo)
|
|
185
|
+
"""
|
|
186
|
+
self.generators[name] = cb
|
|
187
|
+
return self
|
|
188
|
+
|
|
189
|
+
def download(
|
|
190
|
+
self, head: Pyjo.Transaction.HTTP.object, path: str | os.PathLike[str]
|
|
191
|
+
) -> Pyjo.Transaction.HTTP.object:
|
|
192
|
+
"""::
|
|
193
|
+
|
|
194
|
+
tx = t.download(Pyjo.Transaction.HTTP.new(), '/home/sri/test.tar.gz')
|
|
195
|
+
|
|
196
|
+
Build :mod:`Pyjo.Transaction.HTTP` resumable file download request as
|
|
197
|
+
follow-up to a ``HEAD`` request. Note that this method is
|
|
198
|
+
**EXPERIMENTAL** and might change without warning!
|
|
199
|
+
"""
|
|
200
|
+
req = head.req
|
|
201
|
+
tx = self.tx('GET', req.url.clone(), req.headers.to_dict())
|
|
202
|
+
res = tx.res
|
|
203
|
+
if error := head.error:
|
|
204
|
+
res.error = error
|
|
205
|
+
return tx
|
|
206
|
+
|
|
207
|
+
headers = head.res.headers
|
|
208
|
+
accept_ranges = 'bytes' in (headers.accept_ranges or '')
|
|
209
|
+
size = int(headers.content_length or 0)
|
|
210
|
+
|
|
211
|
+
current_size = 0
|
|
212
|
+
file = Pyjo.File.path(path)
|
|
213
|
+
if os.path.isfile(file):
|
|
214
|
+
current_size = os.path.getsize(file)
|
|
215
|
+
if not size:
|
|
216
|
+
res.error = {'message': 'Unknown file size'}
|
|
217
|
+
return tx
|
|
218
|
+
if current_size > size:
|
|
219
|
+
res.error = {'message': 'File size mismatch'}
|
|
220
|
+
return tx
|
|
221
|
+
if current_size == size:
|
|
222
|
+
res.error = {'message': 'Download complete'}
|
|
223
|
+
return tx
|
|
224
|
+
if not accept_ranges:
|
|
225
|
+
res.error = {'message': 'Server does not support partial requests'}
|
|
226
|
+
return tx
|
|
227
|
+
tx.req.headers.range = f'bytes={current_size}-{size}'
|
|
228
|
+
|
|
229
|
+
fh = file.open('ab', buffering=0)
|
|
230
|
+
|
|
231
|
+
# Not in Mojolicious: the file is closed with the transaction that is not finished, as Perl closes it with the
|
|
232
|
+
# last reference
|
|
233
|
+
weakref.finalize(tx, fh.close)
|
|
234
|
+
|
|
235
|
+
@res.content.unsubscribe('read').on('read')
|
|
236
|
+
def read(content: Pyjo.Content.object, chunk: bytes) -> None:
|
|
237
|
+
nonlocal current_size
|
|
238
|
+
current_size += len(chunk)
|
|
239
|
+
try:
|
|
240
|
+
if fh.write(chunk) != len(chunk):
|
|
241
|
+
raise OSError('Short write')
|
|
242
|
+
except OSError as e:
|
|
243
|
+
res.error = {'message': f'Can\'t write to file "{os.fspath(path)}": {e.strerror or e}'}
|
|
244
|
+
|
|
245
|
+
@res.on('finish')
|
|
246
|
+
def finish(res: Pyjo.Message.Response.object) -> None:
|
|
247
|
+
# Not in Mojolicious: the file is closed at the end, as PyPy frees it only when its garbage collector runs
|
|
248
|
+
fh.close()
|
|
249
|
+
if current_size < size:
|
|
250
|
+
res.error = {'message': 'Download incomplete'}
|
|
251
|
+
|
|
252
|
+
return tx
|
|
253
|
+
|
|
254
|
+
def endpoint(self, tx: Pyjo.Transaction.object) -> tuple[str, str | None, int]:
|
|
255
|
+
"""::
|
|
256
|
+
|
|
257
|
+
proto, host, port = t.endpoint(Pyjo.Transaction.HTTP.new())
|
|
258
|
+
|
|
259
|
+
Actual endpoint for transaction.
|
|
260
|
+
"""
|
|
261
|
+
# Basic endpoint
|
|
262
|
+
req = tx.req
|
|
263
|
+
url = req.url
|
|
264
|
+
proto = url.protocol or 'http'
|
|
265
|
+
host = url.ihost
|
|
266
|
+
port = url.port if url.port is not None else (443 if proto == 'https' else 80)
|
|
267
|
+
|
|
268
|
+
# Proxy for normal HTTP requests
|
|
269
|
+
socks = False
|
|
270
|
+
if (proxy := req.proxy) is not None:
|
|
271
|
+
socks = proxy.protocol == 'socks'
|
|
272
|
+
if proto == 'http' and not req.is_handshake and not socks:
|
|
273
|
+
return _proxy(tx, proto, host, port)
|
|
274
|
+
|
|
275
|
+
return proto, host, port
|
|
276
|
+
|
|
277
|
+
def peer(self, tx: Pyjo.Transaction.object) -> tuple[str, str | None, int]:
|
|
278
|
+
"""::
|
|
279
|
+
|
|
280
|
+
proto, host, port = t.peer(Pyjo.Transaction.HTTP.new())
|
|
281
|
+
|
|
282
|
+
Actual peer for transaction.
|
|
283
|
+
"""
|
|
284
|
+
return _proxy(tx, *self.endpoint(tx))
|
|
285
|
+
|
|
286
|
+
def promisify(self, promise: Pyjo.Promise.object, tx: Pyjo.Transaction.object) -> Pyjo.Promise.object:
|
|
287
|
+
"""::
|
|
288
|
+
|
|
289
|
+
t.promisify(Pyjo.Promise.new(), Pyjo.Transaction.HTTP.new())
|
|
290
|
+
|
|
291
|
+
Resolve or reject :mod:`Pyjo.Promise` object with
|
|
292
|
+
:mod:`Pyjo.Transaction.HTTP` object.
|
|
293
|
+
"""
|
|
294
|
+
err = tx.error
|
|
295
|
+
if err and not err.get('code'):
|
|
296
|
+
return promise.reject(err['message'])
|
|
297
|
+
if tx.req.is_handshake and not tx.is_websocket:
|
|
298
|
+
return promise.reject('WebSocket handshake failed')
|
|
299
|
+
return promise.resolve(tx)
|
|
300
|
+
|
|
301
|
+
def proxy_connect(self, old: Pyjo.Transaction.object) -> Pyjo.Transaction.HTTP.object | None:
|
|
302
|
+
"""::
|
|
303
|
+
|
|
304
|
+
tx = t.proxy_connect(Pyjo.Transaction.HTTP.new())
|
|
305
|
+
|
|
306
|
+
Build :mod:`Pyjo.Transaction.HTTP` proxy ``CONNECT`` request for
|
|
307
|
+
transaction if possible.
|
|
308
|
+
"""
|
|
309
|
+
# Already a CONNECT request
|
|
310
|
+
req = old.req
|
|
311
|
+
if req.method.upper() == 'CONNECT':
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
# No proxy
|
|
315
|
+
if (proxy := req.proxy) is None or not req.via_proxy:
|
|
316
|
+
return None
|
|
317
|
+
if proxy.protocol == 'socks':
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
# WebSocket and/or HTTPS
|
|
321
|
+
url = req.url
|
|
322
|
+
if not req.is_handshake and url.protocol != 'https':
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
# CONNECT request (expect a bad response)
|
|
326
|
+
target = url.clone()
|
|
327
|
+
target.userinfo = None
|
|
328
|
+
new = self.tx('CONNECT', target)
|
|
329
|
+
new.req.proxy = proxy
|
|
330
|
+
content = new.res.content
|
|
331
|
+
content.auto_relax = False
|
|
332
|
+
content.headers.connection = 'keep-alive'
|
|
333
|
+
|
|
334
|
+
return new
|
|
335
|
+
|
|
336
|
+
def redirect(self, old: Pyjo.Transaction.HTTP.object) -> Pyjo.Transaction.HTTP.object | None:
|
|
337
|
+
"""::
|
|
338
|
+
|
|
339
|
+
tx = t.redirect(Pyjo.Transaction.HTTP.new())
|
|
340
|
+
|
|
341
|
+
Build :mod:`Pyjo.Transaction.HTTP` follow-up request for ``301``,
|
|
342
|
+
``302``, ``303``, ``307`` or ``308`` redirect response if possible.
|
|
343
|
+
Since ``QUERY`` requests are safe and idempotent, they are never
|
|
344
|
+
redirected as ``GET`` requests and keep their content, except for
|
|
345
|
+
``303`` responses.
|
|
346
|
+
"""
|
|
347
|
+
# Commonly used codes
|
|
348
|
+
res = old.res
|
|
349
|
+
code = res.code or 0
|
|
350
|
+
if code not in (301, 302, 303, 307, 308):
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
# CONNECT requests cannot be redirected
|
|
354
|
+
req = old.req
|
|
355
|
+
if req.method.upper() == 'CONNECT':
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
# Fix location without authority and/or scheme
|
|
359
|
+
if not (locations := res.headers.every_header('Location')) or not locations[0]:
|
|
360
|
+
return None
|
|
361
|
+
location = Pyjo.URL.new(locations[0])
|
|
362
|
+
if not location.is_abs:
|
|
363
|
+
location.base = req.url
|
|
364
|
+
location = location.to_abs()
|
|
365
|
+
proto = location.protocol
|
|
366
|
+
if (proto != 'http' and proto != 'https') or not location.host:
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
# Clone request if necessary (QUERY is safe and idempotent, so it keeps its content like 307 and 308)
|
|
370
|
+
new = Pyjo.Transaction.HTTP.new()
|
|
371
|
+
method = req.method.upper()
|
|
372
|
+
if code == 307 or code == 308 or (method == 'QUERY' and code != 303):
|
|
373
|
+
if (clone := req.clone()) is None:
|
|
374
|
+
return None
|
|
375
|
+
new.req = clone
|
|
376
|
+
else:
|
|
377
|
+
new.req.method = 'GET' if code == 303 or method == 'POST' else method
|
|
378
|
+
new.req.content.headers = headers = req.headers.clone()
|
|
379
|
+
for name in [name for name in headers.names() if re.match(r'content-', name, re.I)]:
|
|
380
|
+
headers.remove(name)
|
|
381
|
+
|
|
382
|
+
content = new.res.content
|
|
383
|
+
if not self.compressed:
|
|
384
|
+
content.auto_decompress = False
|
|
385
|
+
new.req.url = location
|
|
386
|
+
headers = new.req.headers
|
|
387
|
+
for name in ('Authorization', 'Cookie', 'Host', 'Referer'):
|
|
388
|
+
headers.remove(name)
|
|
389
|
+
if res.content.has_subscribers('sse'):
|
|
390
|
+
for cb in res.content.subscribers('sse'):
|
|
391
|
+
content.on('sse', cb)
|
|
392
|
+
|
|
393
|
+
new.previous = old
|
|
394
|
+
return new
|
|
395
|
+
|
|
396
|
+
def tx(
|
|
397
|
+
self,
|
|
398
|
+
method: str,
|
|
399
|
+
url: str | Pyjo.URL.object | SplitResult,
|
|
400
|
+
*args: Mapping[str, Any] | bytes | str,
|
|
401
|
+
**generator: Any,
|
|
402
|
+
) -> Pyjo.Transaction.HTTP.object:
|
|
403
|
+
r"""::
|
|
404
|
+
|
|
405
|
+
tx = t.tx('GET', 'example.com')
|
|
406
|
+
tx = t.tx('POST', 'http://example.com')
|
|
407
|
+
tx = t.tx('GET', 'http://example.com', {'Accept': '*/*'})
|
|
408
|
+
tx = t.tx('PUT', 'http://example.com', b'Content!')
|
|
409
|
+
tx = t.tx('PUT', 'http://example.com', form={'a': 'b'})
|
|
410
|
+
tx = t.tx('PUT', 'http://example.com', json={'a': 'b'})
|
|
411
|
+
tx = t.tx('PUT', 'https://example.com', multipart=['a', 'b'])
|
|
412
|
+
tx = t.tx('POST', 'example.com', {'Accept': '*/*'}, b'Content!')
|
|
413
|
+
tx = t.tx('PUT', 'example.com', {'Accept': '*/*'}, form={'a': 'b'})
|
|
414
|
+
tx = t.tx('PUT', 'example.com', {'Accept': '*/*'}, json={'a': 'b'})
|
|
415
|
+
tx = t.tx('PUT', 'example.com', {'Accept': '*/*'}, multipart=['a', 'b'])
|
|
416
|
+
|
|
417
|
+
Versatile general purpose :mod:`Pyjo.Transaction.HTTP` transaction
|
|
418
|
+
builder for requests, with support for generators. The URL is also a
|
|
419
|
+
:class:`urllib.parse.SplitResult` of the standard library, and the
|
|
420
|
+
content a ``str``, which is encoded as UTF-8. ::
|
|
421
|
+
|
|
422
|
+
# Generate and inspect custom GET request with DNT header and content
|
|
423
|
+
print(t.tx('GET', 'example.com', {'DNT': 1}, b'Bye!').req.to_bytes())
|
|
424
|
+
|
|
425
|
+
# Stream response content to STDOUT
|
|
426
|
+
tx = t.tx('GET', 'http://example.com')
|
|
427
|
+
tx.res.content.unsubscribe('read').on('read', lambda content, chunk: print(chunk))
|
|
428
|
+
|
|
429
|
+
# PUT request with content streamed from file
|
|
430
|
+
tx = t.tx('PUT', 'http://example.com')
|
|
431
|
+
tx.req.content.asset = Pyjo.Asset.File.new(path='/foo.txt')
|
|
432
|
+
|
|
433
|
+
The ``json`` content generator uses :mod:`Pyjo.JSON` for encoding and
|
|
434
|
+
sets the content type to ``application/json``. ::
|
|
435
|
+
|
|
436
|
+
# POST request with "application/json" content
|
|
437
|
+
tx = t.tx('POST', 'http://example.com', json={'a': 'b', 'c': [1, 2, 3]})
|
|
438
|
+
|
|
439
|
+
The ``form`` content generator will automatically use query parameters
|
|
440
|
+
for ``GET`` and ``HEAD`` requests. ::
|
|
441
|
+
|
|
442
|
+
# GET request with query parameters
|
|
443
|
+
tx = t.tx('GET', 'http://example.com', form={'a': 'b'})
|
|
444
|
+
|
|
445
|
+
For all other request methods the ``application/x-www-form-urlencoded``
|
|
446
|
+
content type is used. ::
|
|
447
|
+
|
|
448
|
+
# POST request with "application/x-www-form-urlencoded" content
|
|
449
|
+
tx = t.tx('POST', 'http://example.com', form={'a': 'b', 'c': 'd'})
|
|
450
|
+
|
|
451
|
+
Parameters may be encoded with the ``charset`` option. ::
|
|
452
|
+
|
|
453
|
+
# PUT request with Shift_JIS encoded form values
|
|
454
|
+
tx = t.tx('PUT', 'example.com', form={'a': 'b'}, charset='Shift_JIS')
|
|
455
|
+
|
|
456
|
+
A list can be used for multiple form values sharing the same name. ::
|
|
457
|
+
|
|
458
|
+
# POST request with form values sharing the same name
|
|
459
|
+
tx = t.tx('POST', 'http://example.com', form={'a': ['b', 'c', 'd']})
|
|
460
|
+
|
|
461
|
+
A dict with a ``content`` or ``file`` value can be used to switch to
|
|
462
|
+
the ``multipart/form-data`` content type for file uploads. ::
|
|
463
|
+
|
|
464
|
+
# POST request with "multipart/form-data" content
|
|
465
|
+
tx = t.tx('POST', 'http://example.com', form={'mytext': {'content': 'lala'}})
|
|
466
|
+
|
|
467
|
+
# POST request with multiple files sharing the same name
|
|
468
|
+
tx = t.tx('POST', 'http://example.com', form={'mytext': [{'content': 'first'}, {'content': 'second'}]})
|
|
469
|
+
|
|
470
|
+
The ``file`` value should contain the path to the file you want to
|
|
471
|
+
upload or an asset object, like :mod:`Pyjo.Asset.File` or
|
|
472
|
+
:mod:`Pyjo.Asset.Memory`. ::
|
|
473
|
+
|
|
474
|
+
# POST request with upload streamed from file
|
|
475
|
+
tx = t.tx('POST', 'http://example.com', form={'mytext': {'file': '/foo.txt'}})
|
|
476
|
+
|
|
477
|
+
# POST request with upload streamed from asset
|
|
478
|
+
asset = Pyjo.Asset.Memory.new().add_chunk(b'lalala')
|
|
479
|
+
tx = t.tx('POST', 'http://example.com', form={'mytext': {'file': asset}})
|
|
480
|
+
|
|
481
|
+
A ``filename`` value will be generated automatically, but can also be
|
|
482
|
+
set manually if necessary. All remaining values in the dict get merged
|
|
483
|
+
into the ``multipart/form-data`` content as headers. ::
|
|
484
|
+
|
|
485
|
+
# POST request with form values and customized upload (filename and header)
|
|
486
|
+
tx = t.tx(
|
|
487
|
+
'POST',
|
|
488
|
+
'http://example.com',
|
|
489
|
+
form={
|
|
490
|
+
'a': 'b',
|
|
491
|
+
'c': 'd',
|
|
492
|
+
'mytext': {'content': 'lalala', 'filename': 'foo.txt', 'Content-Type': 'text/plain'},
|
|
493
|
+
},
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
The ``multipart/form-data`` content type can also be enforced by
|
|
497
|
+
setting the ``Content-Type`` header manually. ::
|
|
498
|
+
|
|
499
|
+
# Force "multipart/form-data"
|
|
500
|
+
headers = {'Content-Type': 'multipart/form-data'}
|
|
501
|
+
tx = t.tx('POST', 'example.com', headers, form={'a': 'b'})
|
|
502
|
+
|
|
503
|
+
The ``multipart`` content generator can be used to build custom
|
|
504
|
+
multipart requests and does not set a content type. ::
|
|
505
|
+
|
|
506
|
+
# POST request with multipart content ("foo" and "bar")
|
|
507
|
+
tx = t.tx('POST', 'http://example.com', multipart=['foo', 'bar'])
|
|
508
|
+
|
|
509
|
+
Similar to the ``form`` content generator you can also pass dicts with
|
|
510
|
+
``content`` or ``file`` values, as well as headers. ::
|
|
511
|
+
|
|
512
|
+
# POST request with multipart content streamed from file
|
|
513
|
+
tx = t.tx('POST', 'http://example.com', multipart=[{'file': '/foo.txt'}])
|
|
514
|
+
|
|
515
|
+
# PUT request with multipart content streamed from asset
|
|
516
|
+
headers = {'Content-Type': 'multipart/custom'}
|
|
517
|
+
asset = Pyjo.Asset.Memory.new().add_chunk(b'lalala')
|
|
518
|
+
tx = t.tx('PUT', 'http://example.com', headers, multipart=[{'file': asset}])
|
|
519
|
+
|
|
520
|
+
# POST request with multipart content and custom headers
|
|
521
|
+
tx = t.tx(
|
|
522
|
+
'POST',
|
|
523
|
+
'http://example.com',
|
|
524
|
+
multipart=[
|
|
525
|
+
{'content': 'Hello', 'Content-Type': 'text/plain', 'Content-Language': 'en-US'},
|
|
526
|
+
{'content': 'World!', 'Content-Type': 'text/plain', 'Content-Language': 'en-US'},
|
|
527
|
+
],
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
Values of the form and of the multipart content that are ``bytes`` are
|
|
531
|
+
sent as they are, and a ``str`` is encoded with the charset, as UTF-8
|
|
532
|
+
without one.
|
|
533
|
+
"""
|
|
534
|
+
# Method and URL
|
|
535
|
+
tx = Pyjo.Transaction.HTTP.new()
|
|
536
|
+
req = tx.req
|
|
537
|
+
req.method = method
|
|
538
|
+
if isinstance(url, Pyjo.URL.object):
|
|
539
|
+
req.url = url
|
|
540
|
+
# Not in Mojolicious: a URL of urllib.parse
|
|
541
|
+
elif isinstance(url, SplitResult):
|
|
542
|
+
req.url = Pyjo.URL.new(url)
|
|
543
|
+
else:
|
|
544
|
+
req.url.parse(url if re.search(r'^/|://', url) else f'http://{url}')
|
|
545
|
+
|
|
546
|
+
# Headers (we identify ourselves and accept gzip compression)
|
|
547
|
+
headers = req.headers
|
|
548
|
+
if args and isinstance(args[0], Mapping):
|
|
549
|
+
headers.from_dict(dict(args[0]))
|
|
550
|
+
args = args[1:]
|
|
551
|
+
if not headers.user_agent:
|
|
552
|
+
headers.user_agent = self.name
|
|
553
|
+
if not self.compressed:
|
|
554
|
+
tx.res.content.auto_decompress = False
|
|
555
|
+
elif not headers.accept_encoding:
|
|
556
|
+
headers.accept_encoding = 'gzip'
|
|
557
|
+
|
|
558
|
+
# Generator
|
|
559
|
+
if generator:
|
|
560
|
+
if not (names := [name for name in generator if name in self.generators]):
|
|
561
|
+
raise TypeError(f'Unknown generator: {", ".join(generator)}')
|
|
562
|
+
data = generator.pop(names[0])
|
|
563
|
+
self.generators[names[0]](self, tx, data, **generator)
|
|
564
|
+
|
|
565
|
+
# Body
|
|
566
|
+
elif args:
|
|
567
|
+
body = args[0]
|
|
568
|
+
# Not in Mojolicious: a str is encoded as UTF-8
|
|
569
|
+
req.body = body.encode() if isinstance(body, str) else bytes(cast(bytes, body))
|
|
570
|
+
|
|
571
|
+
return tx
|
|
572
|
+
|
|
573
|
+
def upgrade(self, tx: Pyjo.Transaction.HTTP.object) -> Pyjo.Transaction.WebSocket.object | None:
|
|
574
|
+
"""::
|
|
575
|
+
|
|
576
|
+
tx = t.upgrade(Pyjo.Transaction.HTTP.new())
|
|
577
|
+
|
|
578
|
+
Build :mod:`Pyjo.Transaction.WebSocket` follow-up transaction for
|
|
579
|
+
WebSocket handshake if possible.
|
|
580
|
+
"""
|
|
581
|
+
code = tx.res.code or 0
|
|
582
|
+
if not tx.req.is_handshake or code != 101:
|
|
583
|
+
return None
|
|
584
|
+
ws = Pyjo.Transaction.WebSocket.new(handshake=tx, masked=True)
|
|
585
|
+
if not challenge(ws):
|
|
586
|
+
return None
|
|
587
|
+
ws.established = True
|
|
588
|
+
return ws
|
|
589
|
+
|
|
590
|
+
def websocket(
|
|
591
|
+
self, url: str | Pyjo.URL.object | SplitResult, *args: Mapping[str, Any] | Sequence[str]
|
|
592
|
+
) -> Pyjo.Transaction.HTTP.object:
|
|
593
|
+
"""::
|
|
594
|
+
|
|
595
|
+
tx = t.websocket('ws://example.com')
|
|
596
|
+
tx = t.websocket('ws://example.com', {'DNT': 1}, ['v1.proto'])
|
|
597
|
+
|
|
598
|
+
Versatile :mod:`Pyjo.Transaction.HTTP` transaction builder for
|
|
599
|
+
WebSocket handshake requests.
|
|
600
|
+
"""
|
|
601
|
+
# New WebSocket transaction
|
|
602
|
+
sub: list[str] = []
|
|
603
|
+
if args and isinstance(args[-1], list):
|
|
604
|
+
sub, args = args[-1], args[:-1]
|
|
605
|
+
tx = self.tx('GET', url, *cast(tuple[Mapping[str, Any], ...], args))
|
|
606
|
+
req = tx.req
|
|
607
|
+
if sub:
|
|
608
|
+
req.headers.sec_websocket_protocol = ', '.join(sub)
|
|
609
|
+
|
|
610
|
+
# Handshake protocol
|
|
611
|
+
url = req.url
|
|
612
|
+
proto = url.protocol or ''
|
|
613
|
+
if proto == 'ws':
|
|
614
|
+
url.scheme = 'http'
|
|
615
|
+
elif proto == 'wss':
|
|
616
|
+
url.scheme = 'https'
|
|
617
|
+
elif proto == 'ws+unix':
|
|
618
|
+
url.scheme = 'http+unix'
|
|
619
|
+
|
|
620
|
+
return client_handshake(tx)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _content(headers: Pyjo.Headers.object, parts: list[Pyjo.Content.object]) -> Pyjo.Content.MultiPart.object:
|
|
624
|
+
return Pyjo.Content.MultiPart.new(headers=headers, parts=parts)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _form(
|
|
628
|
+
t: Pyjo_UserAgent_Transactor,
|
|
629
|
+
tx: Pyjo.Transaction.HTTP.object,
|
|
630
|
+
form: Mapping[str, Any],
|
|
631
|
+
*,
|
|
632
|
+
charset: str | None = 'UTF-8',
|
|
633
|
+
) -> Pyjo.Transaction.HTTP.object:
|
|
634
|
+
# Check for uploads and force multipart if necessary
|
|
635
|
+
req = tx.req
|
|
636
|
+
headers = req.headers
|
|
637
|
+
multipart = bool(re.search(r'multipart/form-data', headers.content_type or '', re.I))
|
|
638
|
+
for value in (v for values in form.values() for v in _list(values)):
|
|
639
|
+
if isinstance(value, Mapping):
|
|
640
|
+
multipart = True
|
|
641
|
+
break
|
|
642
|
+
|
|
643
|
+
# Multipart
|
|
644
|
+
if multipart:
|
|
645
|
+
req.content = _content(headers, _form_parts(charset, form))
|
|
646
|
+
_type(headers, 'multipart/form-data')
|
|
647
|
+
return tx
|
|
648
|
+
|
|
649
|
+
# Query parameters or urlencoded
|
|
650
|
+
method = req.method.upper()
|
|
651
|
+
pairs = [item for name in sorted(form) for item in (name, form[name])]
|
|
652
|
+
if method == 'GET' or method == 'HEAD':
|
|
653
|
+
req.url.query.merge(*pairs)
|
|
654
|
+
else:
|
|
655
|
+
params = Pyjo.Parameters.new(*pairs)
|
|
656
|
+
params.charset = charset
|
|
657
|
+
req.body = params.to_str().encode()
|
|
658
|
+
_type(headers, 'application/x-www-form-urlencoded')
|
|
659
|
+
|
|
660
|
+
return tx
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _form_parts(charset: str | None, form: Mapping[str, Any]) -> list[Pyjo.Content.object]:
|
|
664
|
+
parts: list[Pyjo.Content.object] = []
|
|
665
|
+
for name in sorted(form):
|
|
666
|
+
if (values := form[name]) is None:
|
|
667
|
+
continue
|
|
668
|
+
parts.extend(_parts(charset, name, _list(values)))
|
|
669
|
+
|
|
670
|
+
return parts
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _json(
|
|
674
|
+
t: Pyjo_UserAgent_Transactor, tx: Pyjo.Transaction.HTTP.object, data: Anything
|
|
675
|
+
) -> Pyjo.Transaction.HTTP.object:
|
|
676
|
+
tx.req.body = encode_json(data)
|
|
677
|
+
_type(tx.req.headers, 'application/json')
|
|
678
|
+
return tx
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _list(values: Anything) -> list[Any]:
|
|
682
|
+
return list(cast(list[Any], values)) if isinstance(values, (list, tuple)) else [values]
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _multipart(
|
|
686
|
+
t: Pyjo_UserAgent_Transactor, tx: Pyjo.Transaction.HTTP.object, parts: list[Any]
|
|
687
|
+
) -> Pyjo.Transaction.HTTP.object:
|
|
688
|
+
req = tx.req
|
|
689
|
+
req.content = _content(req.headers, _parts(None, None, parts))
|
|
690
|
+
return tx
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _parts(charset: str | None, name: str | None, values: list[Any]) -> list[Pyjo.Content.object]:
|
|
694
|
+
parts: list[Pyjo.Content.object] = []
|
|
695
|
+
for value in values:
|
|
696
|
+
part = Pyjo.Content.Single.new()
|
|
697
|
+
parts.append(part)
|
|
698
|
+
|
|
699
|
+
filename: str | None = None
|
|
700
|
+
headers = part.headers
|
|
701
|
+
if isinstance(value, Mapping):
|
|
702
|
+
# Unlike Mojolicious: the values are taken from a copy, where Mojo deletes them from the hash of the caller
|
|
703
|
+
value = dict(cast(Mapping[str, Any], value))
|
|
704
|
+
|
|
705
|
+
# File
|
|
706
|
+
if file := value.pop('file', None):
|
|
707
|
+
if not isinstance(file, Pyjo.Asset.object):
|
|
708
|
+
file = Pyjo.Asset.File.new(path=os.fspath(cast(str, file)))
|
|
709
|
+
part.asset = file
|
|
710
|
+
if isinstance(file, Pyjo.Asset.File.object) and value.get('filename') is None:
|
|
711
|
+
value['filename'] = Pyjo.File.path(file.path).basename()
|
|
712
|
+
|
|
713
|
+
# Memory
|
|
714
|
+
elif (content := value.pop('content', None)) is not None:
|
|
715
|
+
part.asset = Pyjo.Asset.Memory.new().add_chunk(_bytes(content, None))
|
|
716
|
+
|
|
717
|
+
# Filename and headers
|
|
718
|
+
filename = value.pop('filename', None)
|
|
719
|
+
headers.from_dict(value)
|
|
720
|
+
if name is None:
|
|
721
|
+
continue
|
|
722
|
+
filename = url_escape(filename if filename is not None else name, '"')
|
|
723
|
+
if charset:
|
|
724
|
+
filename = encode(charset, filename).decode('latin-1')
|
|
725
|
+
|
|
726
|
+
# Field
|
|
727
|
+
else:
|
|
728
|
+
part.asset = Pyjo.Asset.Memory.new().add_chunk(_bytes(value, charset))
|
|
729
|
+
|
|
730
|
+
# Content-Disposition
|
|
731
|
+
if name is None or headers.content_disposition is not None:
|
|
732
|
+
continue
|
|
733
|
+
name = url_escape(name, '"')
|
|
734
|
+
if charset:
|
|
735
|
+
name = encode(charset, name).decode('latin-1')
|
|
736
|
+
disposition = f'form-data; name="{name}"'
|
|
737
|
+
if filename is not None:
|
|
738
|
+
disposition += f'; filename="{filename}"'
|
|
739
|
+
headers.content_disposition = disposition
|
|
740
|
+
|
|
741
|
+
return parts
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
# Not in Mojolicious: bytes are taken as they are, and other values are encoded with the charset, as UTF-8 without one
|
|
745
|
+
def _bytes(value: Anything, charset: str | None) -> bytes:
|
|
746
|
+
if isinstance(value, (bytes, bytearray)):
|
|
747
|
+
return bytes(value)
|
|
748
|
+
return encode(charset or 'UTF-8', str(value))
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def _proxy(tx: Pyjo.Transaction.object, proto: str, host: str | None, port: int) -> tuple[str, str | None, int]:
|
|
752
|
+
req = tx.req
|
|
753
|
+
if req.via_proxy and (proxy := req.proxy) is not None:
|
|
754
|
+
return proxy.protocol, proxy.ihost, proxy.port if proxy.port is not None else (443 if proto == 'https' else 80)
|
|
755
|
+
|
|
756
|
+
return proto, host, port
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def _type(headers: Pyjo.Headers.object, content_type: str) -> None:
|
|
760
|
+
if not headers.content_type:
|
|
761
|
+
headers.content_type = content_type
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
new = Pyjo_UserAgent_Transactor.new
|
|
765
|
+
object = Pyjo_UserAgent_Transactor
|
|
766
|
+
Transactor = Pyjo_UserAgent_Transactor
|