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
Pyjo/Message/__init__.py
ADDED
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Pyjo.Message - HTTP message base class
|
|
3
|
+
======================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Message import Message
|
|
7
|
+
|
|
8
|
+
class MyMessage(Message):
|
|
9
|
+
def cookies(self, *cookies): ...
|
|
10
|
+
def extract_start_line(self, buffer): ...
|
|
11
|
+
def get_start_line_chunk(self, offset): ...
|
|
12
|
+
@property
|
|
13
|
+
def start_line_size(self): ...
|
|
14
|
+
|
|
15
|
+
# Or import the module, as in Google Python Style Guide
|
|
16
|
+
|
|
17
|
+
import Pyjo.Message
|
|
18
|
+
|
|
19
|
+
class MyMessage(Pyjo.Message.object):
|
|
20
|
+
def cookies(self, *cookies): ...
|
|
21
|
+
def extract_start_line(self, buffer): ...
|
|
22
|
+
def get_start_line_chunk(self, offset): ...
|
|
23
|
+
@property
|
|
24
|
+
def start_line_size(self): ...
|
|
25
|
+
|
|
26
|
+
:mod:`Pyjo.Message` is an abstract base class for HTTP message containers,
|
|
27
|
+
based on `RFC 7230 <https://tools.ietf.org/html/rfc7230>`_,
|
|
28
|
+
`RFC 7231 <https://tools.ietf.org/html/rfc7231>`_ and
|
|
29
|
+
`RFC 2388 <https://tools.ietf.org/html/rfc2388>`_, like
|
|
30
|
+
:mod:`Pyjo.Message.Request` and :mod:`Pyjo.Message.Response`.
|
|
31
|
+
|
|
32
|
+
Events
|
|
33
|
+
------
|
|
34
|
+
|
|
35
|
+
:mod:`Pyjo.Message` inherits all events from :mod:`Pyjo.EventEmitter` and can
|
|
36
|
+
emit the following new ones.
|
|
37
|
+
|
|
38
|
+
finish
|
|
39
|
+
~~~~~~
|
|
40
|
+
::
|
|
41
|
+
|
|
42
|
+
msg.on('finish', lambda msg: ...)
|
|
43
|
+
|
|
44
|
+
Emitted after message building or parsing is finished. ::
|
|
45
|
+
|
|
46
|
+
before = time.time()
|
|
47
|
+
msg.on('finish', lambda msg: msg.headers.header('X-Parser-Time', int(time.time() - before)))
|
|
48
|
+
|
|
49
|
+
progress
|
|
50
|
+
~~~~~~~~
|
|
51
|
+
::
|
|
52
|
+
|
|
53
|
+
msg.on('progress', lambda msg, *args: ...)
|
|
54
|
+
|
|
55
|
+
Emitted when message building or parsing makes progress. ::
|
|
56
|
+
|
|
57
|
+
# Building
|
|
58
|
+
msg.on('progress', lambda msg, state, offset: print(f'Building "{state}" at offset {offset}'))
|
|
59
|
+
|
|
60
|
+
# Parsing
|
|
61
|
+
@msg.on('progress')
|
|
62
|
+
def progress(msg):
|
|
63
|
+
if not (length := int(msg.headers.content_length or 0)):
|
|
64
|
+
return
|
|
65
|
+
size = msg.content.progress
|
|
66
|
+
print('Progress:', 100 if size == length else int(size / (length / 100)), '%')
|
|
67
|
+
|
|
68
|
+
See also
|
|
69
|
+
--------
|
|
70
|
+
|
|
71
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
72
|
+
|
|
73
|
+
Ported from `Mojo::Message <https://github.com/mojolicious/mojo/blob/v9.52/lib/Mojo/Message.pm>`_
|
|
74
|
+
of Mojolicious 9.52.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
import Pyjo.Asset.Memory
|
|
78
|
+
import Pyjo.Collection
|
|
79
|
+
import Pyjo.Content
|
|
80
|
+
import Pyjo.Content.MultiPart
|
|
81
|
+
import Pyjo.Content.Single
|
|
82
|
+
import Pyjo.Cookie
|
|
83
|
+
import Pyjo.DOM
|
|
84
|
+
import Pyjo.EventEmitter
|
|
85
|
+
import Pyjo.Exception
|
|
86
|
+
import Pyjo.Headers
|
|
87
|
+
import Pyjo.JSON.Pointer
|
|
88
|
+
import Pyjo.Parameters
|
|
89
|
+
import Pyjo.Upload
|
|
90
|
+
|
|
91
|
+
from Pyjo.JSON import j
|
|
92
|
+
from Pyjo.Util import decode
|
|
93
|
+
|
|
94
|
+
import builtins
|
|
95
|
+
import collections
|
|
96
|
+
import os
|
|
97
|
+
import re
|
|
98
|
+
|
|
99
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
100
|
+
from typing import Any, NotRequired, Self, TypedDict, cast, overload
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
type Anything = builtins.object
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Error(TypedDict):
|
|
107
|
+
"""An error of a message, with the status code of a response for a 4xx or
|
|
108
|
+
5xx response."""
|
|
109
|
+
|
|
110
|
+
message: str
|
|
111
|
+
code: NotRequired[int]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# The parameters and the uploads of multipart/form-data, parsed once, as the hash of Mojo
|
|
115
|
+
class _FormData(TypedDict):
|
|
116
|
+
params: list[tuple[str, str]]
|
|
117
|
+
uploads: list[Pyjo.Upload.object]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Pyjo_Message(Pyjo.EventEmitter.object):
|
|
121
|
+
"""
|
|
122
|
+
:mod:`Pyjo.Message` inherits all methods from :mod:`Pyjo.EventEmitter` and
|
|
123
|
+
implements the following new ones.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
# The default of max_message_size, which a response raises
|
|
127
|
+
_MAX_MESSAGE_SIZE = 16777216
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
*,
|
|
132
|
+
content: Pyjo.Content.object | None = None,
|
|
133
|
+
default_charset: str | None = 'UTF-8',
|
|
134
|
+
max_line_size: int | None = None,
|
|
135
|
+
max_message_size: int | None = None,
|
|
136
|
+
version: str = '1.1',
|
|
137
|
+
) -> None:
|
|
138
|
+
"""::
|
|
139
|
+
|
|
140
|
+
msg = Pyjo.Message.Request.new()
|
|
141
|
+
msg = Pyjo.Message.Response.new(content=Pyjo.Content.MultiPart.new())
|
|
142
|
+
|
|
143
|
+
Construct a new message object.
|
|
144
|
+
"""
|
|
145
|
+
super().__init__()
|
|
146
|
+
|
|
147
|
+
self.default_charset = default_charset
|
|
148
|
+
"""::
|
|
149
|
+
|
|
150
|
+
charset = msg.default_charset
|
|
151
|
+
msg.default_charset = 'UTF-8'
|
|
152
|
+
|
|
153
|
+
Default charset used by :attr:`text` and to extract data from
|
|
154
|
+
``application/x-www-form-urlencoded`` or ``multipart/form-data``
|
|
155
|
+
message body, defaults to ``UTF-8``.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
self._max_line_size: int | None = max_line_size
|
|
159
|
+
self._max_message_size: int | None = max_message_size
|
|
160
|
+
|
|
161
|
+
self.version = version
|
|
162
|
+
"""::
|
|
163
|
+
|
|
164
|
+
version = msg.version
|
|
165
|
+
msg.version = '1.1'
|
|
166
|
+
|
|
167
|
+
HTTP version of message, defaults to ``1.1``.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
self._content = content
|
|
171
|
+
|
|
172
|
+
# The state of the parser and of the generator, as the keys of Mojo's hash
|
|
173
|
+
self._body_params: Pyjo.Parameters.object | None = None
|
|
174
|
+
self._buffer = bytearray()
|
|
175
|
+
self._cached: dict[str, dict[str | None, list[Any]]] = {}
|
|
176
|
+
self._dom: Pyjo.DOM.object | None = None
|
|
177
|
+
self._error: Error | None = None
|
|
178
|
+
self._finished = False
|
|
179
|
+
self._fix = False
|
|
180
|
+
self._formdata: _FormData | None = None
|
|
181
|
+
self._json: Any = None
|
|
182
|
+
self._limit = False
|
|
183
|
+
self._raw_size = 0
|
|
184
|
+
self._state: str | None = None
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def max_line_size(self) -> int:
|
|
188
|
+
"""::
|
|
189
|
+
|
|
190
|
+
size = msg.max_line_size
|
|
191
|
+
msg.max_line_size = 1024
|
|
192
|
+
|
|
193
|
+
Maximum start-line size in bytes, defaults to the value of the
|
|
194
|
+
``PYJO_MAX_LINE_SIZE`` environment variable or ``65536`` (64KiB).
|
|
195
|
+
"""
|
|
196
|
+
if self._max_line_size is None:
|
|
197
|
+
# Unlike Mojolicious: 64KiB, as _MAXLINE of http.client and the lines of Pyjo.Headers, where Mojo has 8KiB
|
|
198
|
+
self._max_line_size = int(os.environ.get('PYJO_MAX_LINE_SIZE') or 65536)
|
|
199
|
+
return self._max_line_size
|
|
200
|
+
|
|
201
|
+
@max_line_size.setter
|
|
202
|
+
def max_line_size(self, value: int) -> None:
|
|
203
|
+
self._max_line_size = value
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def max_message_size(self) -> int:
|
|
207
|
+
"""::
|
|
208
|
+
|
|
209
|
+
size = msg.max_message_size
|
|
210
|
+
msg.max_message_size = 1024
|
|
211
|
+
|
|
212
|
+
Maximum message size in bytes, defaults to the value of the
|
|
213
|
+
``PYJO_MAX_MESSAGE_SIZE`` environment variable or ``16777216``
|
|
214
|
+
(16MiB). Setting the value to ``0`` will allow messages of indefinite
|
|
215
|
+
size.
|
|
216
|
+
"""
|
|
217
|
+
if self._max_message_size is None:
|
|
218
|
+
self._max_message_size = (
|
|
219
|
+
int(size) if (size := os.environ.get('PYJO_MAX_MESSAGE_SIZE')) is not None else self._MAX_MESSAGE_SIZE
|
|
220
|
+
)
|
|
221
|
+
return self._max_message_size
|
|
222
|
+
|
|
223
|
+
@max_message_size.setter
|
|
224
|
+
def max_message_size(self, value: int) -> None:
|
|
225
|
+
self._max_message_size = value
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def content(self) -> Pyjo.Content.object:
|
|
229
|
+
"""::
|
|
230
|
+
|
|
231
|
+
content = msg.content
|
|
232
|
+
msg.content = Pyjo.Content.Single.new()
|
|
233
|
+
|
|
234
|
+
Message content, defaults to a :mod:`Pyjo.Content.Single` object.
|
|
235
|
+
"""
|
|
236
|
+
if self._content is None:
|
|
237
|
+
self._content = Pyjo.Content.Single.new()
|
|
238
|
+
return self._content
|
|
239
|
+
|
|
240
|
+
@content.setter
|
|
241
|
+
def content(self, value: Pyjo.Content.object) -> None:
|
|
242
|
+
self._content = value
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def body(self) -> bytes:
|
|
246
|
+
"""::
|
|
247
|
+
|
|
248
|
+
data = msg.body
|
|
249
|
+
msg.body = b'Hello!'
|
|
250
|
+
|
|
251
|
+
Slurp or replace :attr:`content`.
|
|
252
|
+
"""
|
|
253
|
+
content = self.content
|
|
254
|
+
return b'' if content.is_multipart else _single(content).asset.slurp()
|
|
255
|
+
|
|
256
|
+
@body.setter
|
|
257
|
+
def body(self, value: bytes) -> None:
|
|
258
|
+
# Multipart content needs to be downgraded
|
|
259
|
+
content = self.content
|
|
260
|
+
if content.is_multipart:
|
|
261
|
+
content = self.content = Pyjo.Content.Single.new()
|
|
262
|
+
_single(content).asset = Pyjo.Asset.Memory.new().add_chunk(value)
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def body_params(self) -> Pyjo.Parameters.object:
|
|
266
|
+
"""::
|
|
267
|
+
|
|
268
|
+
params = msg.body_params
|
|
269
|
+
|
|
270
|
+
``POST`` parameters extracted from ``application/x-www-form-urlencoded``
|
|
271
|
+
or ``multipart/form-data`` message body, usually a
|
|
272
|
+
:mod:`Pyjo.Parameters` object. Note that this method caches all data, so
|
|
273
|
+
it should not be called before the entire message body has been
|
|
274
|
+
received. Parts of the message body need to be loaded into memory to
|
|
275
|
+
parse ``POST`` parameters, so you have to make sure it is not
|
|
276
|
+
excessively large. There's a 16MiB limit for requests and a 2GiB limit
|
|
277
|
+
for responses by default. ::
|
|
278
|
+
|
|
279
|
+
# Get POST parameter names and values
|
|
280
|
+
params = msg.body_params.to_dict()
|
|
281
|
+
"""
|
|
282
|
+
if self._body_params is not None:
|
|
283
|
+
return self._body_params
|
|
284
|
+
params = self._body_params = Pyjo.Parameters.new()
|
|
285
|
+
params.charset = self.content.charset or self.default_charset
|
|
286
|
+
|
|
287
|
+
# "application/x-www-form-urlencoded"
|
|
288
|
+
type = self.headers.content_type or ''
|
|
289
|
+
if re.search(r'application/x-www-form-urlencoded', type, re.I):
|
|
290
|
+
params.parse(_single(self.content).asset.slurp())
|
|
291
|
+
|
|
292
|
+
# "multipart/form-data"
|
|
293
|
+
elif re.search(r'multipart/form-data', type, re.I):
|
|
294
|
+
for name, value in self._parse_formdata()['params']:
|
|
295
|
+
params.append(name, value)
|
|
296
|
+
|
|
297
|
+
return params
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def body_size(self) -> int:
|
|
301
|
+
"""::
|
|
302
|
+
|
|
303
|
+
size = msg.body_size
|
|
304
|
+
|
|
305
|
+
Content size in bytes.
|
|
306
|
+
"""
|
|
307
|
+
return self.content.body_size
|
|
308
|
+
|
|
309
|
+
def build_body(self) -> bytes:
|
|
310
|
+
"""::
|
|
311
|
+
|
|
312
|
+
data = msg.build_body()
|
|
313
|
+
|
|
314
|
+
Render whole body with :meth:`get_body_chunk`.
|
|
315
|
+
"""
|
|
316
|
+
return self._build(self.get_body_chunk)
|
|
317
|
+
|
|
318
|
+
def build_headers(self) -> bytes:
|
|
319
|
+
"""::
|
|
320
|
+
|
|
321
|
+
data = msg.build_headers()
|
|
322
|
+
|
|
323
|
+
Render all headers with :meth:`get_header_chunk`.
|
|
324
|
+
"""
|
|
325
|
+
return self._build(self.get_header_chunk)
|
|
326
|
+
|
|
327
|
+
def build_start_line(self) -> bytes:
|
|
328
|
+
"""::
|
|
329
|
+
|
|
330
|
+
data = msg.build_start_line()
|
|
331
|
+
|
|
332
|
+
Render start-line with :meth:`get_start_line_chunk`.
|
|
333
|
+
"""
|
|
334
|
+
return self._build(self.get_start_line_chunk)
|
|
335
|
+
|
|
336
|
+
def cookie(self, name: str) -> Pyjo.Cookie.object | None:
|
|
337
|
+
"""::
|
|
338
|
+
|
|
339
|
+
cookie = msg.cookie('foo')
|
|
340
|
+
|
|
341
|
+
Access message cookies, usually :mod:`Pyjo.Cookie.Request` or
|
|
342
|
+
:mod:`Pyjo.Cookie.Response` objects. If there are multiple cookies
|
|
343
|
+
sharing the same name, and you want to access more than just the last
|
|
344
|
+
one, you can use :meth:`every_cookie`. Note that this method caches all
|
|
345
|
+
data, so it should not be called before all headers have been
|
|
346
|
+
received. ::
|
|
347
|
+
|
|
348
|
+
# Get cookie value
|
|
349
|
+
print(msg.cookie('foo').value)
|
|
350
|
+
"""
|
|
351
|
+
cookies = self._cache('cookies', self.cookies, name)
|
|
352
|
+
return cookies[-1] if cookies else None
|
|
353
|
+
|
|
354
|
+
@overload
|
|
355
|
+
def cookies(self) -> Sequence[Pyjo.Cookie.object]: ...
|
|
356
|
+
|
|
357
|
+
@overload
|
|
358
|
+
def cookies(self, cookie: Pyjo.Cookie.object | Mapping[str, Any], /, *cookies: Anything) -> Self: ...
|
|
359
|
+
|
|
360
|
+
def cookies(self, *cookies: Anything) -> Sequence[Pyjo.Cookie.object] | Self:
|
|
361
|
+
"""::
|
|
362
|
+
|
|
363
|
+
cookies = msg.cookies()
|
|
364
|
+
|
|
365
|
+
Access message cookies. Meant to be overloaded in a subclass.
|
|
366
|
+
"""
|
|
367
|
+
raise NotImplementedError('Method "cookies" not implemented by subclass')
|
|
368
|
+
|
|
369
|
+
@overload
|
|
370
|
+
def dom(self) -> Pyjo.DOM.object | None: ...
|
|
371
|
+
|
|
372
|
+
@overload
|
|
373
|
+
def dom(self, css: Anything, /) -> 'Pyjo.Collection.object[Pyjo.DOM.object] | None': ...
|
|
374
|
+
|
|
375
|
+
def dom(self, *args: Anything) -> 'Pyjo.DOM.object | Pyjo.Collection.object[Pyjo.DOM.object] | None':
|
|
376
|
+
r"""::
|
|
377
|
+
|
|
378
|
+
dom = msg.dom()
|
|
379
|
+
collection = msg.dom('a[href]')
|
|
380
|
+
|
|
381
|
+
Retrieve message body from :attr:`text` and turn it into a
|
|
382
|
+
:mod:`Pyjo.DOM` object, an optional selector can be used to call the
|
|
383
|
+
method :meth:`Pyjo.DOM.Pyjo_DOM.find` on it right away, which then
|
|
384
|
+
returns a :mod:`Pyjo.Collection` object. Note that this method caches
|
|
385
|
+
all data, so it should not be called before the entire message body has
|
|
386
|
+
been received. The whole message body needs to be loaded into memory to
|
|
387
|
+
parse it, so you have to make sure it is not excessively large. There's
|
|
388
|
+
a 16MiB limit for requests and a 2GiB limit for responses by default. ::
|
|
389
|
+
|
|
390
|
+
# Perform "find" right away
|
|
391
|
+
print(msg.dom('h1, h2, h3').map('text').join("\n"))
|
|
392
|
+
|
|
393
|
+
# Use everything else Pyjo.DOM has to offer
|
|
394
|
+
print(msg.dom().at('title').text)
|
|
395
|
+
print(msg.dom().at('body').children().map('tag').uniq().join("\n"))
|
|
396
|
+
"""
|
|
397
|
+
if self.content.is_multipart:
|
|
398
|
+
return None
|
|
399
|
+
if self._dom is None:
|
|
400
|
+
self._dom = Pyjo.DOM.new(self.text)
|
|
401
|
+
return self._dom.find(args[0]) if args else self._dom
|
|
402
|
+
|
|
403
|
+
@property
|
|
404
|
+
def error(self) -> Error | None:
|
|
405
|
+
"""::
|
|
406
|
+
|
|
407
|
+
err = msg.error
|
|
408
|
+
msg.error = {'message': 'Parser error'}
|
|
409
|
+
|
|
410
|
+
Get or set message error, a ``None`` return value indicates that there
|
|
411
|
+
is no error. Setting it finishes the message. ::
|
|
412
|
+
|
|
413
|
+
# Connection or parser error
|
|
414
|
+
msg.error = {'message': 'Connection refused'}
|
|
415
|
+
|
|
416
|
+
# 4xx/5xx response
|
|
417
|
+
msg.error = {'message': 'Internal Server Error', 'code': 500}
|
|
418
|
+
"""
|
|
419
|
+
return self._error
|
|
420
|
+
|
|
421
|
+
@error.setter
|
|
422
|
+
def error(self, value: Error | None) -> None:
|
|
423
|
+
self._error = value
|
|
424
|
+
self.finish()
|
|
425
|
+
|
|
426
|
+
def every_cookie(self, name: str) -> list[Pyjo.Cookie.object]:
|
|
427
|
+
"""::
|
|
428
|
+
|
|
429
|
+
cookies = msg.every_cookie('foo')
|
|
430
|
+
|
|
431
|
+
Similar to :meth:`cookie`, but returns all message cookies sharing the
|
|
432
|
+
same name as a list. ::
|
|
433
|
+
|
|
434
|
+
# Get first cookie value
|
|
435
|
+
print(msg.every_cookie('foo')[0].value)
|
|
436
|
+
"""
|
|
437
|
+
return self._cache('cookies', self.cookies, name)
|
|
438
|
+
|
|
439
|
+
def every_upload(self, name: str) -> list[Pyjo.Upload.object]:
|
|
440
|
+
"""::
|
|
441
|
+
|
|
442
|
+
uploads = msg.every_upload('foo')
|
|
443
|
+
|
|
444
|
+
Similar to :meth:`upload`, but returns all file uploads sharing the
|
|
445
|
+
same name as a list. ::
|
|
446
|
+
|
|
447
|
+
# Get content of first uploaded file
|
|
448
|
+
print(msg.every_upload('foo')[0].asset.slurp())
|
|
449
|
+
"""
|
|
450
|
+
return self._cache('uploads', self.uploads, name)
|
|
451
|
+
|
|
452
|
+
def extract_start_line(self, buffer: bytearray) -> bool | None:
|
|
453
|
+
"""::
|
|
454
|
+
|
|
455
|
+
boolean = msg.extract_start_line(buffer)
|
|
456
|
+
|
|
457
|
+
Extract start-line from a :class:`bytearray`, which loses it. Meant to
|
|
458
|
+
be overloaded in a subclass.
|
|
459
|
+
"""
|
|
460
|
+
raise NotImplementedError('Method "extract_start_line" not implemented by subclass')
|
|
461
|
+
|
|
462
|
+
def finish(self) -> Self:
|
|
463
|
+
"""::
|
|
464
|
+
|
|
465
|
+
msg = msg.finish()
|
|
466
|
+
|
|
467
|
+
Finish message parser/generator.
|
|
468
|
+
"""
|
|
469
|
+
self._state = 'finished'
|
|
470
|
+
if self._finished:
|
|
471
|
+
return self
|
|
472
|
+
self._finished = True
|
|
473
|
+
return self.emit('finish')
|
|
474
|
+
|
|
475
|
+
def fix_headers(self) -> Self:
|
|
476
|
+
"""::
|
|
477
|
+
|
|
478
|
+
msg = msg.fix_headers()
|
|
479
|
+
|
|
480
|
+
Make sure message has all required headers.
|
|
481
|
+
"""
|
|
482
|
+
if self._fix:
|
|
483
|
+
return self
|
|
484
|
+
self._fix = True
|
|
485
|
+
|
|
486
|
+
# Content-Length or Connection (unless chunked transfer encoding is used), "0" is false as in Perl
|
|
487
|
+
content = self.content
|
|
488
|
+
headers = content.headers
|
|
489
|
+
if content.is_multipart:
|
|
490
|
+
headers.remove('Content-Length')
|
|
491
|
+
elif content.is_chunked or headers.content_length not in (None, '', '0'):
|
|
492
|
+
return self
|
|
493
|
+
if content.is_dynamic:
|
|
494
|
+
headers.connection = 'close'
|
|
495
|
+
else:
|
|
496
|
+
headers.content_length = self.body_size
|
|
497
|
+
|
|
498
|
+
return self
|
|
499
|
+
|
|
500
|
+
def get_body_chunk(self, offset: int) -> bytes | None:
|
|
501
|
+
"""::
|
|
502
|
+
|
|
503
|
+
data = msg.get_body_chunk(offset)
|
|
504
|
+
|
|
505
|
+
Get a chunk of body data starting from a specific position. Note that
|
|
506
|
+
it might not be possible to get the same chunk twice if content was
|
|
507
|
+
generated dynamically.
|
|
508
|
+
"""
|
|
509
|
+
self.emit('progress', 'body', offset)
|
|
510
|
+
chunk = self.content.get_body_chunk(offset)
|
|
511
|
+
if chunk is None or len(chunk):
|
|
512
|
+
return chunk
|
|
513
|
+
self.finish()
|
|
514
|
+
|
|
515
|
+
return chunk
|
|
516
|
+
|
|
517
|
+
def get_header_chunk(self, offset: int) -> bytes:
|
|
518
|
+
"""::
|
|
519
|
+
|
|
520
|
+
data = msg.get_header_chunk(offset)
|
|
521
|
+
|
|
522
|
+
Get a chunk of header data, starting from a specific position. Note
|
|
523
|
+
that this method finalizes the message.
|
|
524
|
+
"""
|
|
525
|
+
self.emit('progress', 'headers', offset)
|
|
526
|
+
return self.fix_headers().content.get_header_chunk(offset)
|
|
527
|
+
|
|
528
|
+
def get_start_line_chunk(self, offset: int) -> bytes:
|
|
529
|
+
"""::
|
|
530
|
+
|
|
531
|
+
data = msg.get_start_line_chunk(offset)
|
|
532
|
+
|
|
533
|
+
Get a chunk of start-line data starting from a specific position. Meant
|
|
534
|
+
to be overloaded in a subclass.
|
|
535
|
+
"""
|
|
536
|
+
raise NotImplementedError('Method "get_start_line_chunk" not implemented by subclass')
|
|
537
|
+
|
|
538
|
+
@property
|
|
539
|
+
def header_size(self) -> int:
|
|
540
|
+
"""::
|
|
541
|
+
|
|
542
|
+
size = msg.header_size
|
|
543
|
+
|
|
544
|
+
Size of headers in bytes. Note that this method finalizes the message.
|
|
545
|
+
"""
|
|
546
|
+
return self.fix_headers().content.header_size
|
|
547
|
+
|
|
548
|
+
@property
|
|
549
|
+
def headers(self) -> Pyjo.Headers.object:
|
|
550
|
+
"""::
|
|
551
|
+
|
|
552
|
+
headers = msg.headers
|
|
553
|
+
|
|
554
|
+
Message headers, usually a :mod:`Pyjo.Headers` object. ::
|
|
555
|
+
|
|
556
|
+
# Longer version
|
|
557
|
+
headers = msg.content.headers
|
|
558
|
+
"""
|
|
559
|
+
return self.content.headers
|
|
560
|
+
|
|
561
|
+
@property
|
|
562
|
+
def is_finished(self) -> bool:
|
|
563
|
+
"""::
|
|
564
|
+
|
|
565
|
+
boolean = msg.is_finished
|
|
566
|
+
|
|
567
|
+
Check if message parser/generator is finished.
|
|
568
|
+
"""
|
|
569
|
+
return self._state == 'finished'
|
|
570
|
+
|
|
571
|
+
@property
|
|
572
|
+
def is_limit_exceeded(self) -> bool:
|
|
573
|
+
"""::
|
|
574
|
+
|
|
575
|
+
boolean = msg.is_limit_exceeded
|
|
576
|
+
|
|
577
|
+
Check if message has exceeded :attr:`max_line_size`,
|
|
578
|
+
:attr:`max_message_size`,
|
|
579
|
+
:attr:`Pyjo.Content.Pyjo_Content.max_buffer_size` or
|
|
580
|
+
:attr:`Pyjo.Headers.Pyjo_Headers.max_line_size`.
|
|
581
|
+
"""
|
|
582
|
+
return self._limit
|
|
583
|
+
|
|
584
|
+
def json(self, pointer: str | None = None) -> Any:
|
|
585
|
+
"""::
|
|
586
|
+
|
|
587
|
+
value = msg.json()
|
|
588
|
+
value = msg.json('/foo/bar')
|
|
589
|
+
|
|
590
|
+
Decode JSON message body directly using :mod:`Pyjo.JSON` if possible,
|
|
591
|
+
a ``None`` return value indicates a bare ``null`` or that decoding
|
|
592
|
+
failed. An optional JSON Pointer can be used to extract a specific value
|
|
593
|
+
with :mod:`Pyjo.JSON.Pointer`. Note that this method caches all data,
|
|
594
|
+
so it should not be called before the entire message body has been
|
|
595
|
+
received. The whole message body needs to be loaded into memory to
|
|
596
|
+
parse it, so you have to make sure it is not excessively large. There's
|
|
597
|
+
a 16MiB limit for requests and a 2GiB limit for responses by default. ::
|
|
598
|
+
|
|
599
|
+
# Extract JSON values
|
|
600
|
+
print(msg.json()['foo']['bar'][23])
|
|
601
|
+
print(msg.json('/foo/bar/23'))
|
|
602
|
+
"""
|
|
603
|
+
if self.content.is_multipart:
|
|
604
|
+
return None
|
|
605
|
+
if self._json is None:
|
|
606
|
+
self._json = j(self.body)
|
|
607
|
+
data = self._json
|
|
608
|
+
return Pyjo.JSON.Pointer.new(data).get(pointer) if pointer else data
|
|
609
|
+
|
|
610
|
+
def parse(self, chunk: bytes) -> Self:
|
|
611
|
+
"""::
|
|
612
|
+
|
|
613
|
+
msg = msg.parse(b'HTTP/1.1 200 OK...')
|
|
614
|
+
|
|
615
|
+
Parse message chunk.
|
|
616
|
+
"""
|
|
617
|
+
if self._error is not None:
|
|
618
|
+
return self
|
|
619
|
+
self._raw_size += len(chunk)
|
|
620
|
+
self._buffer += chunk
|
|
621
|
+
|
|
622
|
+
# Start-line
|
|
623
|
+
if not self._state:
|
|
624
|
+
# Check start-line size
|
|
625
|
+
length = self._buffer.find(b'\x0a')
|
|
626
|
+
if length < 0:
|
|
627
|
+
length = len(self._buffer)
|
|
628
|
+
if length > self.max_line_size:
|
|
629
|
+
return self._limit_exceeded('Maximum start-line size exceeded')
|
|
630
|
+
|
|
631
|
+
if self.extract_start_line(self._buffer):
|
|
632
|
+
self._state = 'content'
|
|
633
|
+
|
|
634
|
+
# Content
|
|
635
|
+
state = self._state or ''
|
|
636
|
+
if state == 'content' or state == 'finished':
|
|
637
|
+
buffer, self._buffer = bytes(self._buffer), bytearray()
|
|
638
|
+
self.content = self.content.parse(buffer)
|
|
639
|
+
|
|
640
|
+
# Check message size
|
|
641
|
+
max = self.max_message_size
|
|
642
|
+
if max and max < self._raw_size:
|
|
643
|
+
return self._limit_exceeded('Maximum message size exceeded')
|
|
644
|
+
|
|
645
|
+
# Check header size
|
|
646
|
+
if self.headers.is_limit_exceeded:
|
|
647
|
+
return self._limit_exceeded('Maximum header size exceeded')
|
|
648
|
+
|
|
649
|
+
# Check buffer size
|
|
650
|
+
if self.content.is_limit_exceeded:
|
|
651
|
+
return self._limit_exceeded('Maximum buffer size exceeded')
|
|
652
|
+
|
|
653
|
+
return self.finish() if self.emit('progress').content.is_finished else self
|
|
654
|
+
|
|
655
|
+
def save_to(self, path: Anything) -> Self:
|
|
656
|
+
"""::
|
|
657
|
+
|
|
658
|
+
msg = msg.save_to('/some/path/index.html')
|
|
659
|
+
|
|
660
|
+
Save message body to a file.
|
|
661
|
+
"""
|
|
662
|
+
content = self.content
|
|
663
|
+
if content.is_multipart:
|
|
664
|
+
raise Pyjo.Exception.new('Multipart content cannot be saved to files')
|
|
665
|
+
_single(content).asset.move_to(path)
|
|
666
|
+
return self
|
|
667
|
+
|
|
668
|
+
@property
|
|
669
|
+
def start_line_size(self) -> int:
|
|
670
|
+
"""::
|
|
671
|
+
|
|
672
|
+
size = msg.start_line_size
|
|
673
|
+
|
|
674
|
+
Size of the start-line in bytes. Meant to be overloaded in a subclass.
|
|
675
|
+
"""
|
|
676
|
+
raise NotImplementedError('Method "start_line_size" not implemented by subclass')
|
|
677
|
+
|
|
678
|
+
@property
|
|
679
|
+
def text(self) -> str:
|
|
680
|
+
"""::
|
|
681
|
+
|
|
682
|
+
string = msg.text
|
|
683
|
+
|
|
684
|
+
Retrieve :attr:`body` and try to decode it with
|
|
685
|
+
:attr:`Pyjo.Content.Pyjo_Content.charset` or :attr:`default_charset`.
|
|
686
|
+
A body that does not decode is a string of the characters 0-255, one
|
|
687
|
+
for each byte.
|
|
688
|
+
"""
|
|
689
|
+
body = self.body
|
|
690
|
+
charset = self.content.charset or self.default_charset
|
|
691
|
+
if charset and (text := decode(charset, body)) is not None:
|
|
692
|
+
return text
|
|
693
|
+
return body.decode('latin-1')
|
|
694
|
+
|
|
695
|
+
def to_bytes(self) -> bytes:
|
|
696
|
+
"""::
|
|
697
|
+
|
|
698
|
+
data = msg.to_bytes()
|
|
699
|
+
|
|
700
|
+
Render whole message. Note that this method finalizes the message, and
|
|
701
|
+
that it might not be possible to render the same message twice if
|
|
702
|
+
content was generated dynamically.
|
|
703
|
+
"""
|
|
704
|
+
# Renamed from Mojolicious: to_string, as the message is rendered as bytes
|
|
705
|
+
return self.build_start_line() + self.build_headers() + self.build_body()
|
|
706
|
+
|
|
707
|
+
def upload(self, name: str) -> Pyjo.Upload.object | None:
|
|
708
|
+
"""::
|
|
709
|
+
|
|
710
|
+
upload = msg.upload('foo')
|
|
711
|
+
|
|
712
|
+
Access ``multipart/form-data`` file uploads, usually :mod:`Pyjo.Upload`
|
|
713
|
+
objects. If there are multiple uploads sharing the same name, and you
|
|
714
|
+
want to access more than just the last one, you can use
|
|
715
|
+
:meth:`every_upload`. Note that this method caches all data, so it
|
|
716
|
+
should not be called before the entire message body has been
|
|
717
|
+
received. ::
|
|
718
|
+
|
|
719
|
+
# Get content of uploaded file
|
|
720
|
+
print(msg.upload('foo').asset.slurp())
|
|
721
|
+
"""
|
|
722
|
+
uploads = self._cache('uploads', self.uploads, name)
|
|
723
|
+
return uploads[-1] if uploads else None
|
|
724
|
+
|
|
725
|
+
def uploads(self) -> list[Pyjo.Upload.object]:
|
|
726
|
+
"""::
|
|
727
|
+
|
|
728
|
+
uploads = msg.uploads()
|
|
729
|
+
|
|
730
|
+
All ``multipart/form-data`` file uploads, usually :mod:`Pyjo.Upload`
|
|
731
|
+
objects. ::
|
|
732
|
+
|
|
733
|
+
# Names of all uploads
|
|
734
|
+
for upload in msg.uploads():
|
|
735
|
+
print(upload.name)
|
|
736
|
+
"""
|
|
737
|
+
return list(self._parse_formdata()['uploads'])
|
|
738
|
+
|
|
739
|
+
def _build(self, method: Callable[[int], bytes | None]) -> bytes:
|
|
740
|
+
buffer, offset = b'', 0
|
|
741
|
+
while True:
|
|
742
|
+
# No chunk yet, try again
|
|
743
|
+
if (chunk := method(offset)) is None:
|
|
744
|
+
continue
|
|
745
|
+
|
|
746
|
+
# End of part
|
|
747
|
+
if not (length := len(chunk)):
|
|
748
|
+
break
|
|
749
|
+
|
|
750
|
+
offset += length
|
|
751
|
+
buffer += chunk
|
|
752
|
+
|
|
753
|
+
return buffer
|
|
754
|
+
|
|
755
|
+
def _cache[T: Pyjo.Cookie.object | Pyjo.Upload.object](
|
|
756
|
+
self, method: str, objects: Callable[[], Sequence[T]], name: str
|
|
757
|
+
) -> list[T]:
|
|
758
|
+
# Cache objects by name
|
|
759
|
+
if method not in self._cached:
|
|
760
|
+
cached = self._cached[method] = {}
|
|
761
|
+
for obj in objects():
|
|
762
|
+
cached.setdefault(obj.name, []).append(obj)
|
|
763
|
+
|
|
764
|
+
return self._cached[method].get(name, [])
|
|
765
|
+
|
|
766
|
+
def _limit_exceeded(self, message: str) -> Self:
|
|
767
|
+
# Renamed from Mojolicious: _limit, the attribute _limit is the flag
|
|
768
|
+
self._limit = True
|
|
769
|
+
self.error = {'message': message}
|
|
770
|
+
return self
|
|
771
|
+
|
|
772
|
+
# Renamed from Mojolicious: _formdata, the attribute _formdata is the cache
|
|
773
|
+
def _parse_formdata(self) -> _FormData:
|
|
774
|
+
if self._formdata is not None:
|
|
775
|
+
return self._formdata
|
|
776
|
+
formdata = self._formdata = _FormData(params=[], uploads=[])
|
|
777
|
+
content = self.content
|
|
778
|
+
if not content.is_multipart:
|
|
779
|
+
return formdata
|
|
780
|
+
charset = content.charset or self.default_charset
|
|
781
|
+
|
|
782
|
+
# Check all parts recursively
|
|
783
|
+
# A deque, whose popleft and extendleft take and put back the parts as shift and unshift of Perl do, where pop(0) of
|
|
784
|
+
# a list moves all the others, which the many parts of a form of a client would make slow
|
|
785
|
+
parts: collections.deque[Pyjo.Content.object] = collections.deque([content])
|
|
786
|
+
while parts:
|
|
787
|
+
part = parts.popleft()
|
|
788
|
+
if part.is_multipart:
|
|
789
|
+
parts.extendleft(reversed(cast(Pyjo.Content.MultiPart.object, part).parts))
|
|
790
|
+
continue
|
|
791
|
+
|
|
792
|
+
if not (disposition := part.headers.content_disposition):
|
|
793
|
+
continue
|
|
794
|
+
filename = m[1] if (m := re.search(r'[; ]filename="((?:\\"|[^"])*)"', disposition)) else None
|
|
795
|
+
if (name := m[1] if (m := re.search(r'[; ]name="((?:\\"|[^;"])*)"', disposition)) else None) is None:
|
|
796
|
+
continue
|
|
797
|
+
if charset:
|
|
798
|
+
if name:
|
|
799
|
+
name = decoded if (decoded := decode(charset, name)) is not None else name
|
|
800
|
+
if filename:
|
|
801
|
+
filename = decoded if (decoded := decode(charset, filename)) is not None else filename
|
|
802
|
+
|
|
803
|
+
# Upload
|
|
804
|
+
if filename is not None:
|
|
805
|
+
formdata['uploads'].append(
|
|
806
|
+
Pyjo.Upload.new(name=name, filename=filename, asset=_single(part).asset, headers=part.headers)
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
# Parameter
|
|
810
|
+
else:
|
|
811
|
+
value: bytes | str = _single(part).asset.slurp()
|
|
812
|
+
if charset:
|
|
813
|
+
value = decoded if (decoded := decode(charset, value)) is not None else value
|
|
814
|
+
|
|
815
|
+
# A value that does not decode is a string of the characters 0-255, as a Perl byte string
|
|
816
|
+
if isinstance(value, bytes):
|
|
817
|
+
value = value.decode('latin-1')
|
|
818
|
+
formdata['params'].append((name, value))
|
|
819
|
+
|
|
820
|
+
return formdata
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
# The asset of content that is not multipart
|
|
824
|
+
def _single(content: Pyjo.Content.object) -> Pyjo.Content.Single.object:
|
|
825
|
+
return cast(Pyjo.Content.Single.object, content)
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
new = Pyjo_Message.new
|
|
829
|
+
object = Pyjo_Message
|
|
830
|
+
Message = Pyjo_Message
|