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,325 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Pyjo.Content.MultiPart - HTTP multipart content
|
|
3
|
+
===============================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Content.MultiPart import MultiPart
|
|
7
|
+
|
|
8
|
+
multi = MultiPart()
|
|
9
|
+
multi.parse(b'Content-Type: multipart/mixed; boundary=---foobar')
|
|
10
|
+
single = multi.parts[4]
|
|
11
|
+
|
|
12
|
+
# Or import the module, as in Google Python Style Guide
|
|
13
|
+
|
|
14
|
+
import Pyjo.Content.MultiPart
|
|
15
|
+
|
|
16
|
+
multi = Pyjo.Content.MultiPart.new()
|
|
17
|
+
multi.parse(b'Content-Type: multipart/mixed; boundary=---foobar')
|
|
18
|
+
single = multi.parts[4]
|
|
19
|
+
|
|
20
|
+
:mod:`Pyjo.Content.MultiPart` is a container for HTTP multipart content,
|
|
21
|
+
based on `RFC 7230 <https://tools.ietf.org/html/rfc7230>`_,
|
|
22
|
+
`RFC 7231 <https://tools.ietf.org/html/rfc7231>`_ and
|
|
23
|
+
`RFC 2388 <https://tools.ietf.org/html/rfc2388>`_.
|
|
24
|
+
|
|
25
|
+
Events
|
|
26
|
+
------
|
|
27
|
+
|
|
28
|
+
:mod:`Pyjo.Content.MultiPart` inherits all events from :mod:`Pyjo.Content`
|
|
29
|
+
and can emit the following new ones.
|
|
30
|
+
|
|
31
|
+
part
|
|
32
|
+
~~~~
|
|
33
|
+
::
|
|
34
|
+
|
|
35
|
+
multi.on('part', lambda multi, single: ...)
|
|
36
|
+
|
|
37
|
+
Emitted when a new :mod:`Pyjo.Content.Single` part starts, before its
|
|
38
|
+
headers have been parsed. ::
|
|
39
|
+
|
|
40
|
+
@multi.on('part')
|
|
41
|
+
def part(multi, single):
|
|
42
|
+
@single.on('body')
|
|
43
|
+
def body(single):
|
|
44
|
+
if m := re.search(r'name="([^"]+)"', single.headers.content_disposition or ''):
|
|
45
|
+
print(f'Field: {m[1]}')
|
|
46
|
+
|
|
47
|
+
See also
|
|
48
|
+
--------
|
|
49
|
+
|
|
50
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
51
|
+
|
|
52
|
+
Ported from `Mojo::Content::MultiPart <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Content/MultiPart.pm>`_
|
|
53
|
+
of Mojolicious 9.49.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
import Pyjo.Content
|
|
57
|
+
import Pyjo.Content.Single
|
|
58
|
+
|
|
59
|
+
from Pyjo.Util import b64_encode
|
|
60
|
+
|
|
61
|
+
import random
|
|
62
|
+
import re
|
|
63
|
+
|
|
64
|
+
from typing import Any, Self
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Pyjo_Content_MultiPart(Pyjo.Content.object):
|
|
68
|
+
"""
|
|
69
|
+
:mod:`Pyjo.Content.MultiPart` inherits all methods from
|
|
70
|
+
:mod:`Pyjo.Content` and implements the following new ones.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, *, parts: list[Pyjo.Content.object] | None = None, **kwargs: Any) -> None:
|
|
74
|
+
"""::
|
|
75
|
+
|
|
76
|
+
multi = Pyjo.Content.MultiPart.new()
|
|
77
|
+
multi = Pyjo.Content.MultiPart.new(parts=[Pyjo.Content.Single.new()])
|
|
78
|
+
|
|
79
|
+
Construct a new :mod:`Pyjo.Content.MultiPart` object and subscribe to
|
|
80
|
+
event ``read`` with default content parser.
|
|
81
|
+
"""
|
|
82
|
+
super().__init__(**kwargs)
|
|
83
|
+
|
|
84
|
+
self.parts: list[Pyjo.Content.object] = parts if parts is not None else []
|
|
85
|
+
"""::
|
|
86
|
+
|
|
87
|
+
parts = multi.parts
|
|
88
|
+
multi.parts = [Pyjo.Content.Single.new()]
|
|
89
|
+
|
|
90
|
+
Content parts embedded in this multipart content, usually
|
|
91
|
+
:mod:`Pyjo.Content.Single` objects.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
self._boundary: str | None = None
|
|
95
|
+
self._last_len = 0
|
|
96
|
+
self._last_part = 0
|
|
97
|
+
self._multi_state: str | None = None
|
|
98
|
+
self._multipart = bytearray()
|
|
99
|
+
self._offset: int | None = None
|
|
100
|
+
self.on('read', Pyjo_Content_MultiPart._read)
|
|
101
|
+
|
|
102
|
+
def body_contains(self, chunk: bytes) -> bool:
|
|
103
|
+
"""::
|
|
104
|
+
|
|
105
|
+
boolean = multi.body_contains(b'foobarbaz')
|
|
106
|
+
|
|
107
|
+
Check if content parts contain a specific string.
|
|
108
|
+
"""
|
|
109
|
+
for part in self.parts:
|
|
110
|
+
if part.headers_contain(chunk) or part.body_contains(chunk):
|
|
111
|
+
return True
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def body_size(self) -> int:
|
|
116
|
+
"""::
|
|
117
|
+
|
|
118
|
+
size = multi.body_size
|
|
119
|
+
|
|
120
|
+
Content size in bytes.
|
|
121
|
+
"""
|
|
122
|
+
# Check for existing Content-Length header
|
|
123
|
+
if length := self._number(self.headers.content_length):
|
|
124
|
+
return int(length)
|
|
125
|
+
|
|
126
|
+
# Calculate length of whole body
|
|
127
|
+
length = boundary_len = len(self.build_boundary()) + 6
|
|
128
|
+
for part in self.parts:
|
|
129
|
+
length += part.header_size + part.body_size + boundary_len
|
|
130
|
+
|
|
131
|
+
return length
|
|
132
|
+
|
|
133
|
+
def build_boundary(self) -> str:
|
|
134
|
+
"""::
|
|
135
|
+
|
|
136
|
+
boundary = multi.build_boundary()
|
|
137
|
+
|
|
138
|
+
Generate a suitable boundary for content and add it to
|
|
139
|
+
``Content-Type`` header.
|
|
140
|
+
"""
|
|
141
|
+
# Check for existing boundary
|
|
142
|
+
if (boundary := self.boundary) is not None:
|
|
143
|
+
return boundary
|
|
144
|
+
|
|
145
|
+
# Generate and check boundary
|
|
146
|
+
size = 1
|
|
147
|
+
while True:
|
|
148
|
+
boundary = re.sub(r'\W', 'X', b64_encode(random.randbytes(size * 3)))
|
|
149
|
+
size += 1
|
|
150
|
+
if not self.body_contains(boundary.encode('latin-1')):
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
# Add boundary to Content-Type header
|
|
154
|
+
headers = self.headers
|
|
155
|
+
before, after = 'multipart/mixed', ''
|
|
156
|
+
if m := re.fullmatch(r'(.*multipart/[^;]+)(.*)', headers.content_type or ''):
|
|
157
|
+
before, after = m[1], m[2]
|
|
158
|
+
headers.content_type = f'{before}; boundary={boundary}{after}'
|
|
159
|
+
|
|
160
|
+
return boundary
|
|
161
|
+
|
|
162
|
+
def clone(self) -> Self | None:
|
|
163
|
+
"""::
|
|
164
|
+
|
|
165
|
+
clone = multi.clone()
|
|
166
|
+
|
|
167
|
+
Return a new :mod:`Pyjo.Content.MultiPart` object cloned from this
|
|
168
|
+
content if possible, otherwise return ``None``.
|
|
169
|
+
"""
|
|
170
|
+
if (clone := super().clone()) is None:
|
|
171
|
+
return None
|
|
172
|
+
clone.parts = self.parts
|
|
173
|
+
return clone
|
|
174
|
+
|
|
175
|
+
def get_body_chunk(self, offset: int) -> bytes | None:
|
|
176
|
+
"""::
|
|
177
|
+
|
|
178
|
+
chunk = multi.get_body_chunk(0)
|
|
179
|
+
|
|
180
|
+
Get a chunk of content starting from a specific position. Note that it
|
|
181
|
+
might not be possible to get the same chunk twice if content was
|
|
182
|
+
generated dynamically.
|
|
183
|
+
"""
|
|
184
|
+
# Body generator
|
|
185
|
+
if self.is_dynamic:
|
|
186
|
+
return self.generate_body_chunk(offset)
|
|
187
|
+
|
|
188
|
+
# First boundary
|
|
189
|
+
if self._boundary is None:
|
|
190
|
+
self._boundary = self.build_boundary()
|
|
191
|
+
boundary = self._boundary.encode('latin-1')
|
|
192
|
+
boundary_len = len(boundary) + 6
|
|
193
|
+
length = boundary_len - 2
|
|
194
|
+
if length > offset:
|
|
195
|
+
return (b'--' + boundary + b'\x0d\x0a')[offset:]
|
|
196
|
+
|
|
197
|
+
# Skip parts that have already been processed
|
|
198
|
+
start = 0
|
|
199
|
+
if self._offset and offset > self._offset:
|
|
200
|
+
length, start = self._last_len, self._last_part + 1
|
|
201
|
+
|
|
202
|
+
# Prepare content part by part
|
|
203
|
+
parts = self.parts
|
|
204
|
+
for i in range(start, len(parts)):
|
|
205
|
+
part = parts[i]
|
|
206
|
+
|
|
207
|
+
# Headers
|
|
208
|
+
header_len = part.header_size
|
|
209
|
+
if (length + header_len) > offset:
|
|
210
|
+
return part.get_header_chunk(offset - length)
|
|
211
|
+
length += header_len
|
|
212
|
+
|
|
213
|
+
# Content
|
|
214
|
+
content_len = part.body_size
|
|
215
|
+
if (length + content_len) > offset:
|
|
216
|
+
return part.get_body_chunk(offset - length)
|
|
217
|
+
length += content_len
|
|
218
|
+
|
|
219
|
+
# Boundary
|
|
220
|
+
if len(parts) - 1 == i:
|
|
221
|
+
boundary += b'--'
|
|
222
|
+
boundary_len += 2
|
|
223
|
+
if (length + boundary_len) > offset:
|
|
224
|
+
return (b'\x0d\x0a--' + boundary + b'\x0d\x0a')[offset - length :]
|
|
225
|
+
length += boundary_len
|
|
226
|
+
|
|
227
|
+
self._last_len, self._last_part, self._offset = length, i, offset
|
|
228
|
+
|
|
229
|
+
# The end, an empty string as the value of the condition of Perl's loop, which Mojo returns
|
|
230
|
+
return b''
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def is_multipart(self) -> bool:
|
|
234
|
+
"""::
|
|
235
|
+
|
|
236
|
+
boolean = multi.is_multipart
|
|
237
|
+
|
|
238
|
+
True, this is a :mod:`Pyjo.Content.MultiPart` object.
|
|
239
|
+
"""
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
# The default content parser, without a reference to the content that would make a cycle
|
|
243
|
+
@staticmethod
|
|
244
|
+
def _read(multi: 'Pyjo_Content_MultiPart', chunk: bytes) -> None:
|
|
245
|
+
multi._multipart += chunk
|
|
246
|
+
boundary = (multi.boundary or '').encode('latin-1')
|
|
247
|
+
while (multi._multi_state or 'multipart_preamble') != 'finished':
|
|
248
|
+
state = multi._multi_state = multi._multi_state or 'multipart_preamble'
|
|
249
|
+
|
|
250
|
+
# Preamble
|
|
251
|
+
if state == 'multipart_preamble':
|
|
252
|
+
if not multi._parse_multipart_preamble(boundary):
|
|
253
|
+
break
|
|
254
|
+
|
|
255
|
+
# Boundary
|
|
256
|
+
elif state == 'multipart_boundary':
|
|
257
|
+
if not multi._parse_multipart_boundary(boundary):
|
|
258
|
+
break
|
|
259
|
+
|
|
260
|
+
# Body
|
|
261
|
+
elif state == 'multipart_body':
|
|
262
|
+
if not multi._parse_multipart_body(boundary):
|
|
263
|
+
break
|
|
264
|
+
|
|
265
|
+
# Check buffer size
|
|
266
|
+
if len(multi._multipart) > multi.max_buffer_size:
|
|
267
|
+
multi._state, multi._limit = 'finished', True
|
|
268
|
+
|
|
269
|
+
def _parse_multipart_body(self, boundary: bytes) -> bool:
|
|
270
|
+
# Whole part in buffer
|
|
271
|
+
pos = self._multipart.find(b'\x0d\x0a--' + boundary)
|
|
272
|
+
if pos < 0:
|
|
273
|
+
length = len(self._multipart) - (len(boundary) + 8)
|
|
274
|
+
if not length > 0:
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
# Store chunk
|
|
278
|
+
chunk = bytes(self._multipart[:length])
|
|
279
|
+
del self._multipart[:length]
|
|
280
|
+
self.parts[-1] = self.parts[-1].parse(chunk)
|
|
281
|
+
return False
|
|
282
|
+
|
|
283
|
+
# Store chunk
|
|
284
|
+
chunk = bytes(self._multipart[:pos])
|
|
285
|
+
del self._multipart[:pos]
|
|
286
|
+
self.parts[-1] = self.parts[-1].parse(chunk)
|
|
287
|
+
self._multi_state = 'multipart_boundary'
|
|
288
|
+
return True
|
|
289
|
+
|
|
290
|
+
def _parse_multipart_boundary(self, boundary: bytes) -> bool:
|
|
291
|
+
# Boundary begins
|
|
292
|
+
if self._multipart.startswith(b'\x0d\x0a--' + boundary + b'\x0d\x0a'):
|
|
293
|
+
del self._multipart[: len(boundary) + 6]
|
|
294
|
+
|
|
295
|
+
# New part
|
|
296
|
+
part = Pyjo.Content.Single.new(relaxed=True)
|
|
297
|
+
self.emit('part', part)
|
|
298
|
+
self.parts.append(part)
|
|
299
|
+
self._multi_state = 'multipart_body'
|
|
300
|
+
return True
|
|
301
|
+
|
|
302
|
+
# Boundary ends
|
|
303
|
+
end = b'\x0d\x0a--' + boundary + b'--'
|
|
304
|
+
if self._multipart.startswith(end):
|
|
305
|
+
del self._multipart[: len(end)]
|
|
306
|
+
self._multi_state = 'finished'
|
|
307
|
+
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
def _parse_multipart_preamble(self, boundary: bytes) -> bool:
|
|
311
|
+
# No boundary yet
|
|
312
|
+
if (pos := self._multipart.find(b'--' + boundary)) < 0:
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
# Replace preamble with carriage return and line feed
|
|
316
|
+
self._multipart[:pos] = b'\x0d\x0a'
|
|
317
|
+
|
|
318
|
+
# Parse boundary
|
|
319
|
+
self._multi_state = 'multipart_boundary'
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
new = Pyjo_Content_MultiPart.new
|
|
324
|
+
object = Pyjo_Content_MultiPart
|
|
325
|
+
MultiPart = Pyjo_Content_MultiPart
|
Pyjo/Content/Single.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Pyjo.Content.Single - HTTP content
|
|
3
|
+
==================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Content.Single import Single
|
|
7
|
+
|
|
8
|
+
single = Single()
|
|
9
|
+
single.parse(b'Content-Length: 12\x0d\x0a\x0d\x0aHello World!')
|
|
10
|
+
print(single.headers.content_length)
|
|
11
|
+
|
|
12
|
+
# Or import the module, as in Google Python Style Guide
|
|
13
|
+
|
|
14
|
+
import Pyjo.Content.Single
|
|
15
|
+
|
|
16
|
+
single = Pyjo.Content.Single.new()
|
|
17
|
+
single.parse(b'Content-Length: 12\x0d\x0a\x0d\x0aHello World!')
|
|
18
|
+
print(single.headers.content_length)
|
|
19
|
+
|
|
20
|
+
:mod:`Pyjo.Content.Single` is a container for HTTP content, based on
|
|
21
|
+
`RFC 7230 <https://tools.ietf.org/html/rfc7230>`_ and
|
|
22
|
+
`RFC 7231 <https://tools.ietf.org/html/rfc7231>`_.
|
|
23
|
+
|
|
24
|
+
Events
|
|
25
|
+
------
|
|
26
|
+
|
|
27
|
+
:mod:`Pyjo.Content.Single` inherits all events from :mod:`Pyjo.Content` and
|
|
28
|
+
can emit the following new ones.
|
|
29
|
+
|
|
30
|
+
upgrade
|
|
31
|
+
~~~~~~~
|
|
32
|
+
::
|
|
33
|
+
|
|
34
|
+
single.on('upgrade', lambda single, multi: ...)
|
|
35
|
+
|
|
36
|
+
Emitted when content gets upgraded to a :mod:`Pyjo.Content.MultiPart`
|
|
37
|
+
object. ::
|
|
38
|
+
|
|
39
|
+
@single.on('upgrade')
|
|
40
|
+
def upgrade(single, multi):
|
|
41
|
+
if m := re.search(r'multipart/([^;]+)', multi.headers.content_type, re.I):
|
|
42
|
+
print(f'Multipart: {m[1]}')
|
|
43
|
+
|
|
44
|
+
See also
|
|
45
|
+
--------
|
|
46
|
+
|
|
47
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
48
|
+
|
|
49
|
+
Ported from `Mojo::Content::Single <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Content/Single.pm>`_
|
|
50
|
+
of Mojolicious 9.49.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
import Pyjo.Asset
|
|
54
|
+
import Pyjo.Asset.Memory
|
|
55
|
+
import Pyjo.Content
|
|
56
|
+
import Pyjo.Content.MultiPart
|
|
57
|
+
|
|
58
|
+
from typing import Any, Self
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Pyjo_Content_Single(Pyjo.Content.object):
|
|
62
|
+
"""
|
|
63
|
+
:mod:`Pyjo.Content.Single` inherits all methods from :mod:`Pyjo.Content`
|
|
64
|
+
and implements the following new ones.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, *, asset: Pyjo.Asset.object | None = None, auto_upgrade: bool = True, **kwargs: Any) -> None:
|
|
68
|
+
"""::
|
|
69
|
+
|
|
70
|
+
single = Pyjo.Content.Single.new()
|
|
71
|
+
single = Pyjo.Content.Single.new(asset=Pyjo.Asset.File.new())
|
|
72
|
+
|
|
73
|
+
Construct a new :mod:`Pyjo.Content.Single` object and subscribe to
|
|
74
|
+
event ``read`` with default content parser.
|
|
75
|
+
"""
|
|
76
|
+
super().__init__(**kwargs)
|
|
77
|
+
|
|
78
|
+
self.auto_upgrade = auto_upgrade
|
|
79
|
+
"""::
|
|
80
|
+
|
|
81
|
+
boolean = single.auto_upgrade
|
|
82
|
+
single.auto_upgrade = boolean
|
|
83
|
+
|
|
84
|
+
Try to detect multipart content and automatically upgrade to a
|
|
85
|
+
:mod:`Pyjo.Content.MultiPart` object, defaults to a true value.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
self._asset = asset
|
|
89
|
+
self._body_size: int | None = None
|
|
90
|
+
self._read = self.on('read', _read)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def asset(self) -> Pyjo.Asset.object:
|
|
94
|
+
"""::
|
|
95
|
+
|
|
96
|
+
asset = single.asset
|
|
97
|
+
single.asset = Pyjo.Asset.Memory.new()
|
|
98
|
+
|
|
99
|
+
The actual content, defaults to a :mod:`Pyjo.Asset.Memory` object
|
|
100
|
+
with :attr:`Pyjo.Asset.Memory.Pyjo_Asset_Memory.auto_upgrade` enabled.
|
|
101
|
+
"""
|
|
102
|
+
if self._asset is None:
|
|
103
|
+
self._asset = Pyjo.Asset.Memory.new(auto_upgrade=True)
|
|
104
|
+
return self._asset
|
|
105
|
+
|
|
106
|
+
@asset.setter
|
|
107
|
+
def asset(self, value: Pyjo.Asset.object) -> None:
|
|
108
|
+
self._asset = value
|
|
109
|
+
|
|
110
|
+
def body_contains(self, chunk: bytes) -> bool:
|
|
111
|
+
"""::
|
|
112
|
+
|
|
113
|
+
boolean = single.body_contains(b'1234567')
|
|
114
|
+
|
|
115
|
+
Check if content contains a specific string.
|
|
116
|
+
"""
|
|
117
|
+
return self.asset.contains(chunk) >= 0
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def body_size(self) -> int:
|
|
121
|
+
"""::
|
|
122
|
+
|
|
123
|
+
size = single.body_size
|
|
124
|
+
|
|
125
|
+
Content size in bytes.
|
|
126
|
+
"""
|
|
127
|
+
if self.is_dynamic:
|
|
128
|
+
return int(self._number(self.headers.content_length) or 0)
|
|
129
|
+
if self._body_size is None:
|
|
130
|
+
self._body_size = self.asset.size
|
|
131
|
+
return self._body_size
|
|
132
|
+
|
|
133
|
+
def clone(self) -> Self | None:
|
|
134
|
+
"""::
|
|
135
|
+
|
|
136
|
+
clone = single.clone()
|
|
137
|
+
|
|
138
|
+
Return a new :mod:`Pyjo.Content.Single` object cloned from this
|
|
139
|
+
content if possible, otherwise return ``None``.
|
|
140
|
+
"""
|
|
141
|
+
if (clone := super().clone()) is None:
|
|
142
|
+
return None
|
|
143
|
+
clone.asset = self.asset
|
|
144
|
+
return clone
|
|
145
|
+
|
|
146
|
+
def get_body_chunk(self, offset: int) -> bytes | None:
|
|
147
|
+
"""::
|
|
148
|
+
|
|
149
|
+
chunk = single.get_body_chunk(0)
|
|
150
|
+
|
|
151
|
+
Get a chunk of content starting from a specific position. Note that it
|
|
152
|
+
might not be possible to get the same chunk twice if content was
|
|
153
|
+
generated dynamically.
|
|
154
|
+
"""
|
|
155
|
+
if self.is_dynamic:
|
|
156
|
+
return self.generate_body_chunk(offset)
|
|
157
|
+
return self.asset.get_chunk(offset)
|
|
158
|
+
|
|
159
|
+
def parse(self, chunk: bytes | None = None) -> Pyjo.Content.object:
|
|
160
|
+
r"""::
|
|
161
|
+
|
|
162
|
+
single = single.parse(b'Content-Length: 12\x0d\x0a\x0d\x0aHello World!')
|
|
163
|
+
multi = single.parse(b'Content-Type: multipart/form-data\x0d\x0a\x0d\x0a')
|
|
164
|
+
|
|
165
|
+
Parse content chunk and upgrade to :mod:`Pyjo.Content.MultiPart`
|
|
166
|
+
object if necessary.
|
|
167
|
+
"""
|
|
168
|
+
# Parse headers
|
|
169
|
+
self._parse_until_body(chunk)
|
|
170
|
+
|
|
171
|
+
# Parse body
|
|
172
|
+
if not (self.auto_upgrade and self.boundary is not None):
|
|
173
|
+
return super().parse()
|
|
174
|
+
|
|
175
|
+
# Content needs to be upgraded to multipart, with the state and the subscribers of this content, as %$self
|
|
176
|
+
self.unsubscribe('read', self._read)
|
|
177
|
+
multi = Pyjo.Content.MultiPart.new()
|
|
178
|
+
read = multi.subscribers('read')[:]
|
|
179
|
+
multi.__dict__.update(self.__dict__)
|
|
180
|
+
for cb in read:
|
|
181
|
+
multi.on('read', cb)
|
|
182
|
+
self.emit('upgrade', multi)
|
|
183
|
+
return multi.parse()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# The default content parser
|
|
187
|
+
def _read(single: Pyjo_Content_Single, chunk: bytes) -> None:
|
|
188
|
+
single.asset = single.asset.add_chunk(chunk)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
new = Pyjo_Content_Single.new
|
|
192
|
+
object = Pyjo_Content_Single
|
|
193
|
+
Single = Pyjo_Content_Single
|