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/Content/__init__.py
ADDED
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Pyjo.Content - HTTP content base class
|
|
3
|
+
======================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Content import Content
|
|
7
|
+
|
|
8
|
+
class MyContent(Content):
|
|
9
|
+
def body_contains(self, chunk): ...
|
|
10
|
+
@property
|
|
11
|
+
def body_size(self): ...
|
|
12
|
+
def get_body_chunk(self, offset): ...
|
|
13
|
+
|
|
14
|
+
# Or import the module, as in Google Python Style Guide
|
|
15
|
+
|
|
16
|
+
import Pyjo.Content
|
|
17
|
+
|
|
18
|
+
class MyContent(Pyjo.Content.object):
|
|
19
|
+
def body_contains(self, chunk): ...
|
|
20
|
+
@property
|
|
21
|
+
def body_size(self): ...
|
|
22
|
+
def get_body_chunk(self, offset): ...
|
|
23
|
+
|
|
24
|
+
:mod:`Pyjo.Content` is an abstract base class for HTTP content containers,
|
|
25
|
+
based on `RFC 7230 <https://tools.ietf.org/html/rfc7230>`_ and
|
|
26
|
+
`RFC 7231 <https://tools.ietf.org/html/rfc7231>`_, like
|
|
27
|
+
:mod:`Pyjo.Content.MultiPart` and :mod:`Pyjo.Content.Single`.
|
|
28
|
+
|
|
29
|
+
Events
|
|
30
|
+
------
|
|
31
|
+
|
|
32
|
+
:mod:`Pyjo.Content` inherits all events from :mod:`Pyjo.EventEmitter` and can
|
|
33
|
+
emit the following new ones.
|
|
34
|
+
|
|
35
|
+
body
|
|
36
|
+
~~~~
|
|
37
|
+
::
|
|
38
|
+
|
|
39
|
+
content.on('body', lambda content: ...)
|
|
40
|
+
|
|
41
|
+
Emitted once all headers have been parsed and the body starts. ::
|
|
42
|
+
|
|
43
|
+
@content.on('body')
|
|
44
|
+
def body(content):
|
|
45
|
+
if content.headers.header('X-No-MultiPart'):
|
|
46
|
+
content.auto_upgrade = False
|
|
47
|
+
|
|
48
|
+
drain
|
|
49
|
+
~~~~~
|
|
50
|
+
::
|
|
51
|
+
|
|
52
|
+
content.on('drain', lambda content, offset: ...)
|
|
53
|
+
|
|
54
|
+
Emitted once all data has been written. ::
|
|
55
|
+
|
|
56
|
+
content.on('drain', lambda content, offset: content.write_chunk(str(time.time()).encode()))
|
|
57
|
+
|
|
58
|
+
read
|
|
59
|
+
~~~~
|
|
60
|
+
::
|
|
61
|
+
|
|
62
|
+
content.on('read', lambda content, chunk: ...)
|
|
63
|
+
|
|
64
|
+
Emitted when a new chunk of content arrives. ::
|
|
65
|
+
|
|
66
|
+
content.on('read', lambda content, chunk: print(f'Streaming: {chunk}'))
|
|
67
|
+
|
|
68
|
+
sse
|
|
69
|
+
~~~
|
|
70
|
+
::
|
|
71
|
+
|
|
72
|
+
content.on('sse', lambda content, event: ...)
|
|
73
|
+
|
|
74
|
+
Emitted when a new Server-Sent Event (SSE) connection has been established,
|
|
75
|
+
with ``None`` for the event, and for each new event that arrives. Note that
|
|
76
|
+
this event is **EXPERIMENTAL** and may change without warning! ::
|
|
77
|
+
|
|
78
|
+
@content.on('sse')
|
|
79
|
+
def sse(content, event):
|
|
80
|
+
if event:
|
|
81
|
+
print(f'Type: {event["type"]}, Data: {event["text"]}')
|
|
82
|
+
else:
|
|
83
|
+
print('SSE connection established')
|
|
84
|
+
|
|
85
|
+
See also
|
|
86
|
+
--------
|
|
87
|
+
|
|
88
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
89
|
+
|
|
90
|
+
Ported from `Mojo::Content <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Content.pm>`_
|
|
91
|
+
of Mojolicious 9.49.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
import Pyjo.EventEmitter
|
|
95
|
+
import Pyjo.Headers
|
|
96
|
+
|
|
97
|
+
from Pyjo.SSE import build_event, parse_event
|
|
98
|
+
|
|
99
|
+
import builtins
|
|
100
|
+
import os
|
|
101
|
+
import re
|
|
102
|
+
import zlib
|
|
103
|
+
|
|
104
|
+
from collections.abc import Callable, Mapping
|
|
105
|
+
from typing import Any, Self
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
type Anything = builtins.object
|
|
109
|
+
|
|
110
|
+
# A callback of a write gets the content once all data has been written
|
|
111
|
+
type DrainCallback = Callable[..., Anything]
|
|
112
|
+
|
|
113
|
+
BOUNDARY_RE = re.compile(r'multipart.*boundary\s*=\s*(?:"([^"]+)"|([\w\'(),.:?\-+/]+))', re.I)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Pyjo_Content(Pyjo.EventEmitter.object):
|
|
117
|
+
"""
|
|
118
|
+
:mod:`Pyjo.Content` inherits all methods from :mod:`Pyjo.EventEmitter` and
|
|
119
|
+
implements the following new ones.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
*,
|
|
125
|
+
auto_decompress: bool | None = None,
|
|
126
|
+
auto_relax: bool | None = None,
|
|
127
|
+
headers: Pyjo.Headers.object | None = None,
|
|
128
|
+
max_buffer_size: int | None = None,
|
|
129
|
+
max_leftover_size: int | None = None,
|
|
130
|
+
relaxed: bool | None = None,
|
|
131
|
+
skip_body: bool | None = None,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""::
|
|
134
|
+
|
|
135
|
+
content = Pyjo.Content.Single.new()
|
|
136
|
+
content = Pyjo.Content.Single.new(headers=Pyjo.Headers.new())
|
|
137
|
+
|
|
138
|
+
Construct a new content object.
|
|
139
|
+
"""
|
|
140
|
+
super().__init__()
|
|
141
|
+
|
|
142
|
+
self.auto_decompress = auto_decompress
|
|
143
|
+
"""::
|
|
144
|
+
|
|
145
|
+
boolean = content.auto_decompress
|
|
146
|
+
content.auto_decompress = boolean
|
|
147
|
+
|
|
148
|
+
Decompress content automatically if :attr:`is_compressed` is true.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
self.auto_relax = auto_relax
|
|
152
|
+
"""::
|
|
153
|
+
|
|
154
|
+
boolean = content.auto_relax
|
|
155
|
+
content.auto_relax = boolean
|
|
156
|
+
|
|
157
|
+
Try to detect when relaxed parsing is necessary.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
self._max_buffer_size: int | None = max_buffer_size
|
|
161
|
+
self._max_leftover_size: int | None = max_leftover_size
|
|
162
|
+
|
|
163
|
+
self.relaxed = relaxed
|
|
164
|
+
"""::
|
|
165
|
+
|
|
166
|
+
boolean = content.relaxed
|
|
167
|
+
content.relaxed = boolean
|
|
168
|
+
|
|
169
|
+
Activate relaxed parsing for responses that are terminated with a
|
|
170
|
+
connection close.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
self.skip_body = skip_body
|
|
174
|
+
"""::
|
|
175
|
+
|
|
176
|
+
boolean = content.skip_body
|
|
177
|
+
content.skip_body = boolean
|
|
178
|
+
|
|
179
|
+
Skip body parsing and finish after headers.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
self._headers = headers
|
|
183
|
+
|
|
184
|
+
# The state of the parser and of the generator, as the keys of Mojo's hash
|
|
185
|
+
self._body = False
|
|
186
|
+
self._body_buffer: bytes | None = None
|
|
187
|
+
self._buffer = bytearray()
|
|
188
|
+
self._chunk_len = 0
|
|
189
|
+
self._chunk_state: str | None = None
|
|
190
|
+
self._chunked = False
|
|
191
|
+
self._chunks = 0
|
|
192
|
+
self._dynamic = False
|
|
193
|
+
self._eof = False
|
|
194
|
+
self._gz: Any = None
|
|
195
|
+
self._gz_total = 0
|
|
196
|
+
self._header_buffer: bytes | None = None
|
|
197
|
+
self._header_size: int | None = None
|
|
198
|
+
self._limit = False
|
|
199
|
+
self._post_buffer = b''
|
|
200
|
+
self._pre_buffer = bytearray()
|
|
201
|
+
self._raw_size = 0
|
|
202
|
+
self._real_size: int | None = None
|
|
203
|
+
self._size = 0
|
|
204
|
+
self._sse = False
|
|
205
|
+
self._sse_buffer = bytearray()
|
|
206
|
+
self._state: str | None = None
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def max_buffer_size(self) -> int:
|
|
210
|
+
"""::
|
|
211
|
+
|
|
212
|
+
size = content.max_buffer_size
|
|
213
|
+
content.max_buffer_size = 1024
|
|
214
|
+
|
|
215
|
+
Maximum size in bytes of buffer for content parser, defaults to the
|
|
216
|
+
value of the ``PYJO_MAX_BUFFER_SIZE`` environment variable or
|
|
217
|
+
``262144`` (256KiB).
|
|
218
|
+
"""
|
|
219
|
+
if self._max_buffer_size is None:
|
|
220
|
+
self._max_buffer_size = int(os.environ.get('PYJO_MAX_BUFFER_SIZE') or 262144)
|
|
221
|
+
return self._max_buffer_size
|
|
222
|
+
|
|
223
|
+
@max_buffer_size.setter
|
|
224
|
+
def max_buffer_size(self, value: int) -> None:
|
|
225
|
+
self._max_buffer_size = value
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def max_leftover_size(self) -> int:
|
|
229
|
+
"""::
|
|
230
|
+
|
|
231
|
+
size = content.max_leftover_size
|
|
232
|
+
content.max_leftover_size = 1024
|
|
233
|
+
|
|
234
|
+
Maximum size in bytes of buffer for pipelined HTTP requests, defaults
|
|
235
|
+
to the value of the ``PYJO_MAX_LEFTOVER_SIZE`` environment variable or
|
|
236
|
+
``262144`` (256KiB).
|
|
237
|
+
"""
|
|
238
|
+
if self._max_leftover_size is None:
|
|
239
|
+
self._max_leftover_size = int(os.environ.get('PYJO_MAX_LEFTOVER_SIZE') or 262144)
|
|
240
|
+
return self._max_leftover_size
|
|
241
|
+
|
|
242
|
+
@max_leftover_size.setter
|
|
243
|
+
def max_leftover_size(self, value: int) -> None:
|
|
244
|
+
self._max_leftover_size = value
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def headers(self) -> Pyjo.Headers.object:
|
|
248
|
+
"""::
|
|
249
|
+
|
|
250
|
+
headers = content.headers
|
|
251
|
+
content.headers = Pyjo.Headers.new()
|
|
252
|
+
|
|
253
|
+
Content headers, defaults to a :mod:`Pyjo.Headers` object.
|
|
254
|
+
"""
|
|
255
|
+
if self._headers is None:
|
|
256
|
+
self._headers = Pyjo.Headers.new()
|
|
257
|
+
return self._headers
|
|
258
|
+
|
|
259
|
+
@headers.setter
|
|
260
|
+
def headers(self, value: Pyjo.Headers.object) -> None:
|
|
261
|
+
self._headers = value
|
|
262
|
+
|
|
263
|
+
def body_contains(self, chunk: bytes) -> bool:
|
|
264
|
+
"""::
|
|
265
|
+
|
|
266
|
+
boolean = content.body_contains(b'foo bar baz')
|
|
267
|
+
|
|
268
|
+
Check if content contains a specific string. Meant to be overloaded in
|
|
269
|
+
a subclass.
|
|
270
|
+
"""
|
|
271
|
+
raise NotImplementedError('Method "body_contains" not implemented by subclass')
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def body_size(self) -> int:
|
|
275
|
+
"""::
|
|
276
|
+
|
|
277
|
+
size = content.body_size
|
|
278
|
+
|
|
279
|
+
Content size in bytes. Meant to be overloaded in a subclass.
|
|
280
|
+
"""
|
|
281
|
+
raise NotImplementedError('Method "body_size" not implemented by subclass')
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def boundary(self) -> str | None:
|
|
285
|
+
"""::
|
|
286
|
+
|
|
287
|
+
boundary = content.boundary
|
|
288
|
+
|
|
289
|
+
Extract multipart boundary from ``Content-Type`` header.
|
|
290
|
+
"""
|
|
291
|
+
if m := BOUNDARY_RE.search(self.headers.content_type or ''):
|
|
292
|
+
return m[1] if m[1] is not None else m[2]
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def charset(self) -> str | None:
|
|
297
|
+
"""::
|
|
298
|
+
|
|
299
|
+
charset = content.charset
|
|
300
|
+
|
|
301
|
+
Extract charset from ``Content-Type`` header.
|
|
302
|
+
"""
|
|
303
|
+
if m := re.search(r'charset\s*=\s*"?([^"\s;]+)"?', self.headers.content_type or '', re.I):
|
|
304
|
+
return m[1]
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
def clone(self) -> Self | None:
|
|
308
|
+
"""::
|
|
309
|
+
|
|
310
|
+
clone = content.clone()
|
|
311
|
+
|
|
312
|
+
Return a new content object cloned from this content if possible,
|
|
313
|
+
otherwise return ``None``.
|
|
314
|
+
"""
|
|
315
|
+
if self.is_dynamic:
|
|
316
|
+
return None
|
|
317
|
+
return type(self).new(headers=self.headers.clone())
|
|
318
|
+
|
|
319
|
+
def generate_body_chunk(self, offset: int) -> bytes | None:
|
|
320
|
+
"""::
|
|
321
|
+
|
|
322
|
+
chunk = content.generate_body_chunk(0)
|
|
323
|
+
|
|
324
|
+
Generate dynamic content.
|
|
325
|
+
"""
|
|
326
|
+
if self._body_buffer is None:
|
|
327
|
+
self._body_buffer = b''
|
|
328
|
+
if not len(self._body_buffer):
|
|
329
|
+
self.emit('drain', offset)
|
|
330
|
+
if len(self._body_buffer):
|
|
331
|
+
chunk, self._body_buffer = self._body_buffer, None
|
|
332
|
+
return chunk
|
|
333
|
+
if self._eof:
|
|
334
|
+
return b''
|
|
335
|
+
|
|
336
|
+
length = self._number(self.headers.content_length)
|
|
337
|
+
return b'' if length is not None and length == offset else None
|
|
338
|
+
|
|
339
|
+
def get_body_chunk(self, offset: int) -> bytes | None:
|
|
340
|
+
"""::
|
|
341
|
+
|
|
342
|
+
chunk = content.get_body_chunk(0)
|
|
343
|
+
|
|
344
|
+
Get a chunk of content starting from a specific position. Meant to be
|
|
345
|
+
overloaded in a subclass.
|
|
346
|
+
"""
|
|
347
|
+
raise NotImplementedError('Method "get_body_chunk" not implemented by subclass')
|
|
348
|
+
|
|
349
|
+
def get_header_chunk(self, offset: int) -> bytes:
|
|
350
|
+
"""::
|
|
351
|
+
|
|
352
|
+
chunk = content.get_header_chunk(13)
|
|
353
|
+
|
|
354
|
+
Get a chunk of the headers starting from a specific position. Note that
|
|
355
|
+
this method finalizes the content.
|
|
356
|
+
"""
|
|
357
|
+
return self._headers_buffer()[offset : offset + 131072]
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def header_size(self) -> int:
|
|
361
|
+
"""::
|
|
362
|
+
|
|
363
|
+
size = content.header_size
|
|
364
|
+
|
|
365
|
+
Size of headers in bytes. Note that this method finalizes the content.
|
|
366
|
+
"""
|
|
367
|
+
return len(self._headers_buffer())
|
|
368
|
+
|
|
369
|
+
def headers_contain(self, chunk: bytes) -> bool:
|
|
370
|
+
"""::
|
|
371
|
+
|
|
372
|
+
boolean = content.headers_contain(b'foo bar baz')
|
|
373
|
+
|
|
374
|
+
Check if headers contain a specific string. Note that this method
|
|
375
|
+
finalizes the content.
|
|
376
|
+
"""
|
|
377
|
+
return self._headers_buffer().find(chunk) >= 0
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def is_chunked(self) -> bool:
|
|
381
|
+
"""::
|
|
382
|
+
|
|
383
|
+
boolean = content.is_chunked
|
|
384
|
+
|
|
385
|
+
Check if ``Transfer-Encoding`` header indicates chunked transfer
|
|
386
|
+
encoding.
|
|
387
|
+
"""
|
|
388
|
+
return bool(self.headers.transfer_encoding)
|
|
389
|
+
|
|
390
|
+
@property
|
|
391
|
+
def is_compressed(self) -> bool:
|
|
392
|
+
"""::
|
|
393
|
+
|
|
394
|
+
boolean = content.is_compressed
|
|
395
|
+
|
|
396
|
+
Check ``Content-Encoding`` header for ``gzip`` value.
|
|
397
|
+
"""
|
|
398
|
+
return (self.headers.content_encoding or '').lower() == 'gzip'
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
def is_dynamic(self) -> bool:
|
|
402
|
+
"""::
|
|
403
|
+
|
|
404
|
+
boolean = content.is_dynamic
|
|
405
|
+
|
|
406
|
+
Check if content will be dynamically generated, which prevents
|
|
407
|
+
:meth:`clone` from working.
|
|
408
|
+
"""
|
|
409
|
+
return self._dynamic
|
|
410
|
+
|
|
411
|
+
@property
|
|
412
|
+
def is_finished(self) -> bool:
|
|
413
|
+
"""::
|
|
414
|
+
|
|
415
|
+
boolean = content.is_finished
|
|
416
|
+
|
|
417
|
+
Check if parser is finished.
|
|
418
|
+
"""
|
|
419
|
+
return self._state == 'finished'
|
|
420
|
+
|
|
421
|
+
@property
|
|
422
|
+
def is_limit_exceeded(self) -> bool:
|
|
423
|
+
"""::
|
|
424
|
+
|
|
425
|
+
boolean = content.is_limit_exceeded
|
|
426
|
+
|
|
427
|
+
Check if buffer has exceeded :attr:`max_buffer_size`.
|
|
428
|
+
"""
|
|
429
|
+
return self._limit
|
|
430
|
+
|
|
431
|
+
@property
|
|
432
|
+
def is_multipart(self) -> bool:
|
|
433
|
+
"""::
|
|
434
|
+
|
|
435
|
+
boolean = content.is_multipart
|
|
436
|
+
|
|
437
|
+
False, this is not a :mod:`Pyjo.Content.MultiPart` object.
|
|
438
|
+
"""
|
|
439
|
+
return False
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def is_parsing_body(self) -> bool:
|
|
443
|
+
"""::
|
|
444
|
+
|
|
445
|
+
boolean = content.is_parsing_body
|
|
446
|
+
|
|
447
|
+
Check if body parsing started yet.
|
|
448
|
+
"""
|
|
449
|
+
return self._state == 'body'
|
|
450
|
+
|
|
451
|
+
@property
|
|
452
|
+
def is_sse(self) -> bool:
|
|
453
|
+
"""::
|
|
454
|
+
|
|
455
|
+
boolean = content.is_sse
|
|
456
|
+
|
|
457
|
+
Check if ``Content-Type`` header indicates Server-Sent Events (SSE).
|
|
458
|
+
Note that this method is **EXPERIMENTAL** and may change without
|
|
459
|
+
warning!
|
|
460
|
+
"""
|
|
461
|
+
return (self.headers.content_type or '') == 'text/event-stream'
|
|
462
|
+
|
|
463
|
+
def leftovers(self) -> bytes:
|
|
464
|
+
"""::
|
|
465
|
+
|
|
466
|
+
data = content.leftovers()
|
|
467
|
+
|
|
468
|
+
Get leftover data from content parser.
|
|
469
|
+
"""
|
|
470
|
+
return bytes(self._buffer)
|
|
471
|
+
|
|
472
|
+
def parse(self, chunk: bytes | None = None) -> 'Pyjo_Content':
|
|
473
|
+
r"""::
|
|
474
|
+
|
|
475
|
+
content = content.parse(b'Content-Length: 12\x0d\x0a\x0d\x0aHello World!')
|
|
476
|
+
|
|
477
|
+
Parse content chunk.
|
|
478
|
+
"""
|
|
479
|
+
# Headers
|
|
480
|
+
self._parse_until_body(chunk)
|
|
481
|
+
if self._state == 'headers':
|
|
482
|
+
return self
|
|
483
|
+
|
|
484
|
+
# Chunked content
|
|
485
|
+
if self._real_size is None:
|
|
486
|
+
self._real_size = 0
|
|
487
|
+
if self.is_chunked:
|
|
488
|
+
self._parse_chunked()
|
|
489
|
+
if self._chunk_state == 'finished':
|
|
490
|
+
self._state = 'finished'
|
|
491
|
+
|
|
492
|
+
# SSE
|
|
493
|
+
elif self.is_sse:
|
|
494
|
+
self._parse_sse()
|
|
495
|
+
|
|
496
|
+
# Not chunked, pass through to second buffer
|
|
497
|
+
else:
|
|
498
|
+
self._real_size += len(self._pre_buffer)
|
|
499
|
+
limit = self.is_finished and len(self._buffer) > self.max_leftover_size
|
|
500
|
+
if not limit:
|
|
501
|
+
self._buffer += self._pre_buffer
|
|
502
|
+
self._pre_buffer = bytearray()
|
|
503
|
+
|
|
504
|
+
# No content
|
|
505
|
+
if self.skip_body:
|
|
506
|
+
self._state = 'finished'
|
|
507
|
+
return self
|
|
508
|
+
|
|
509
|
+
# Relaxed parsing
|
|
510
|
+
headers = self.headers
|
|
511
|
+
length = headers.content_length if headers.content_length is not None else ''
|
|
512
|
+
if self.auto_relax and not len(length):
|
|
513
|
+
connection = (headers.connection or '').lower()
|
|
514
|
+
if connection == 'close' or not connection:
|
|
515
|
+
self.relaxed = True
|
|
516
|
+
|
|
517
|
+
# Chunked or relaxed content
|
|
518
|
+
if self.is_chunked or self.relaxed:
|
|
519
|
+
self._decompress(bytes(self._buffer))
|
|
520
|
+
self._size += len(self._buffer)
|
|
521
|
+
self._buffer = bytearray()
|
|
522
|
+
return self
|
|
523
|
+
|
|
524
|
+
# Normal content
|
|
525
|
+
size = self._number(length) or 0
|
|
526
|
+
if (need := size - self._size) > 0:
|
|
527
|
+
have = len(self._buffer)
|
|
528
|
+
take = int(need) if need < have else have
|
|
529
|
+
chunk = bytes(self._buffer[:take])
|
|
530
|
+
del self._buffer[:take]
|
|
531
|
+
self._decompress(chunk)
|
|
532
|
+
self._size += len(chunk)
|
|
533
|
+
if size <= self.progress:
|
|
534
|
+
self._state = 'finished'
|
|
535
|
+
|
|
536
|
+
return self
|
|
537
|
+
|
|
538
|
+
def parse_body(self, chunk: bytes | None = None) -> 'Pyjo_Content':
|
|
539
|
+
"""::
|
|
540
|
+
|
|
541
|
+
content = content.parse_body(b'Hi!')
|
|
542
|
+
|
|
543
|
+
Parse body chunk and skip headers.
|
|
544
|
+
"""
|
|
545
|
+
self._state = 'body'
|
|
546
|
+
return self.parse(chunk)
|
|
547
|
+
|
|
548
|
+
@property
|
|
549
|
+
def progress(self) -> int:
|
|
550
|
+
"""::
|
|
551
|
+
|
|
552
|
+
size = content.progress
|
|
553
|
+
|
|
554
|
+
Size of content already received from message in bytes.
|
|
555
|
+
"""
|
|
556
|
+
if self._state not in ('body', 'finished'):
|
|
557
|
+
return 0
|
|
558
|
+
return self._raw_size - (self._header_size or 0)
|
|
559
|
+
|
|
560
|
+
def write(self, chunk: bytes | None = None, cb: DrainCallback | None = None) -> Self:
|
|
561
|
+
"""::
|
|
562
|
+
|
|
563
|
+
content = content.write()
|
|
564
|
+
content = content.write(b'')
|
|
565
|
+
content = content.write(chunk)
|
|
566
|
+
content = content.write(chunk, lambda content, offset: ...)
|
|
567
|
+
|
|
568
|
+
Write dynamic content non-blocking, the optional drain callback will
|
|
569
|
+
be executed once all data has been written. Calling this method
|
|
570
|
+
without a chunk of data will finalize the :attr:`headers` and allow
|
|
571
|
+
for dynamic content to be written later. You can write an empty chunk
|
|
572
|
+
of data at any time to end the stream. ::
|
|
573
|
+
|
|
574
|
+
# Make sure previous chunk of data has been written before continuing
|
|
575
|
+
content.write(
|
|
576
|
+
b'He',
|
|
577
|
+
lambda content, offset: content.write(b'llo!', lambda content, offset: content.write(b'')),
|
|
578
|
+
)
|
|
579
|
+
"""
|
|
580
|
+
self._dynamic = True
|
|
581
|
+
if chunk is not None:
|
|
582
|
+
self._body_buffer = (self._body_buffer or b'') + chunk
|
|
583
|
+
if cb is not None:
|
|
584
|
+
self.once('drain', cb)
|
|
585
|
+
if chunk is not None and not len(chunk):
|
|
586
|
+
self._eof = True
|
|
587
|
+
|
|
588
|
+
return self
|
|
589
|
+
|
|
590
|
+
def write_chunk(self, chunk: bytes | None = None, cb: DrainCallback | None = None) -> Self:
|
|
591
|
+
"""::
|
|
592
|
+
|
|
593
|
+
content = content.write_chunk()
|
|
594
|
+
content = content.write_chunk(b'')
|
|
595
|
+
content = content.write_chunk(chunk)
|
|
596
|
+
content = content.write_chunk(chunk, lambda content, offset: ...)
|
|
597
|
+
|
|
598
|
+
Write dynamic content non-blocking with chunked transfer encoding, the
|
|
599
|
+
optional drain callback will be executed once all data has been
|
|
600
|
+
written. Calling this method without a chunk of data will finalize the
|
|
601
|
+
:attr:`headers` and allow for dynamic content to be written later. You
|
|
602
|
+
can write an empty chunk of data at any time to end the stream. ::
|
|
603
|
+
|
|
604
|
+
# Make sure previous chunk of data has been written before continuing
|
|
605
|
+
content.write_chunk(
|
|
606
|
+
b'He',
|
|
607
|
+
lambda content, offset: content.write_chunk(b'llo!', lambda content, offset: content.write_chunk(b'')),
|
|
608
|
+
)
|
|
609
|
+
"""
|
|
610
|
+
if not self._chunked:
|
|
611
|
+
self.headers.transfer_encoding = 'chunked'
|
|
612
|
+
self._chunked = self._dynamic = True
|
|
613
|
+
|
|
614
|
+
if chunk is not None:
|
|
615
|
+
self._body_buffer = (self._body_buffer or b'') + self._build_chunk(chunk)
|
|
616
|
+
if cb is not None:
|
|
617
|
+
self.once('drain', cb)
|
|
618
|
+
if chunk is not None and not len(chunk):
|
|
619
|
+
self._eof = True
|
|
620
|
+
|
|
621
|
+
return self
|
|
622
|
+
|
|
623
|
+
def write_sse(self, event: Mapping[str, Any] | None = None, cb: DrainCallback | None = None) -> Self:
|
|
624
|
+
"""::
|
|
625
|
+
|
|
626
|
+
content = content.write_sse()
|
|
627
|
+
content = content.write_sse(event)
|
|
628
|
+
content = content.write_sse(event, lambda content, offset: ...)
|
|
629
|
+
|
|
630
|
+
Write Server-Sent Event (SSE) non-blocking, the optional drain callback
|
|
631
|
+
will be executed once all data has been written. Calling this method
|
|
632
|
+
without an event will finalize the response headers and allow for
|
|
633
|
+
events to be written later. Note that this method is **EXPERIMENTAL**
|
|
634
|
+
and may change without warning!
|
|
635
|
+
"""
|
|
636
|
+
if not self._sse:
|
|
637
|
+
self.headers.content_type = 'text/event-stream'
|
|
638
|
+
self._sse = True
|
|
639
|
+
|
|
640
|
+
if event is None:
|
|
641
|
+
return self.write()
|
|
642
|
+
return self.write(build_event(event), cb)
|
|
643
|
+
|
|
644
|
+
def _build_chunk(self, chunk: bytes) -> bytes:
|
|
645
|
+
# End
|
|
646
|
+
if not len(chunk):
|
|
647
|
+
return b'\x0d\x0a0\x0d\x0a\x0d\x0a'
|
|
648
|
+
|
|
649
|
+
# First chunk has no leading CRLF
|
|
650
|
+
crlf = b'\x0d\x0a' if self._chunks else b''
|
|
651
|
+
self._chunks += 1
|
|
652
|
+
return crlf + f'{len(chunk):x}'.encode() + b'\x0d\x0a' + chunk
|
|
653
|
+
|
|
654
|
+
def _decompress(self, chunk: bytes) -> None:
|
|
655
|
+
# No compression
|
|
656
|
+
if not (self.auto_decompress and self.is_compressed):
|
|
657
|
+
self.emit('read', chunk)
|
|
658
|
+
return
|
|
659
|
+
|
|
660
|
+
# Decompress
|
|
661
|
+
out = self._inflate(chunk)
|
|
662
|
+
self.emit('read', out)
|
|
663
|
+
|
|
664
|
+
# Replace Content-Encoding with Content-Length
|
|
665
|
+
if self._gz.eof:
|
|
666
|
+
self.headers.content_length = self._gz_total
|
|
667
|
+
self.headers.remove('Content-Encoding')
|
|
668
|
+
|
|
669
|
+
# Check buffer size
|
|
670
|
+
if len(self._post_buffer) > self.max_buffer_size:
|
|
671
|
+
self._state, self._limit = 'finished', True
|
|
672
|
+
|
|
673
|
+
# Renamed from Mojolicious: _headers, the attribute _headers is the headers
|
|
674
|
+
def _headers_buffer(self) -> bytes:
|
|
675
|
+
if self._header_buffer is None:
|
|
676
|
+
headers = self.headers.to_bytes()
|
|
677
|
+
self._header_buffer = headers + b'\x0d\x0a\x0d\x0a' if headers else b'\x0d\x0a'
|
|
678
|
+
return self._header_buffer
|
|
679
|
+
|
|
680
|
+
def _parse_chunked(self) -> None:
|
|
681
|
+
# Trailing headers
|
|
682
|
+
if self._chunk_state == 'trailing_headers':
|
|
683
|
+
return self._parse_chunked_trailing_headers()
|
|
684
|
+
|
|
685
|
+
while length := len(self._pre_buffer):
|
|
686
|
+
# Start new chunk (ignore the chunk extension)
|
|
687
|
+
if not self._chunk_len:
|
|
688
|
+
if not (m := re.match(rb'(?:\x0d?\x0a)?([0-9a-fA-F]+).*\x0a', self._pre_buffer)):
|
|
689
|
+
break
|
|
690
|
+
end, self._chunk_len = m.end(), int(m[1], 16)
|
|
691
|
+
del self._pre_buffer[:end]
|
|
692
|
+
if self._chunk_len:
|
|
693
|
+
continue
|
|
694
|
+
|
|
695
|
+
# Last chunk
|
|
696
|
+
self._chunk_state = 'trailing_headers'
|
|
697
|
+
break
|
|
698
|
+
|
|
699
|
+
# Remove as much as possible from payload
|
|
700
|
+
if self._chunk_len < length:
|
|
701
|
+
length = self._chunk_len
|
|
702
|
+
self._buffer += self._pre_buffer[:length]
|
|
703
|
+
del self._pre_buffer[:length]
|
|
704
|
+
self._real_size = (self._real_size or 0) + length
|
|
705
|
+
self._chunk_len -= length
|
|
706
|
+
|
|
707
|
+
# Trailing headers
|
|
708
|
+
if self._chunk_state == 'trailing_headers':
|
|
709
|
+
self._parse_chunked_trailing_headers()
|
|
710
|
+
|
|
711
|
+
# Check buffer size
|
|
712
|
+
if len(self._pre_buffer) > self.max_buffer_size:
|
|
713
|
+
self._state, self._limit = 'finished', True
|
|
714
|
+
|
|
715
|
+
def _parse_chunked_trailing_headers(self) -> None:
|
|
716
|
+
headers = self.headers.parse(self._pre_buffer)
|
|
717
|
+
self._pre_buffer = bytearray()
|
|
718
|
+
if not headers.is_finished:
|
|
719
|
+
return
|
|
720
|
+
self._chunk_state = 'finished'
|
|
721
|
+
|
|
722
|
+
# Take care of leftover and replace Transfer-Encoding with Content-Length
|
|
723
|
+
self._buffer += headers.leftovers()
|
|
724
|
+
headers.remove('Transfer-Encoding')
|
|
725
|
+
if not headers.content_length:
|
|
726
|
+
headers.content_length = self._real_size
|
|
727
|
+
|
|
728
|
+
def _parse_headers(self) -> None:
|
|
729
|
+
headers = self.headers.parse(self._pre_buffer)
|
|
730
|
+
self._pre_buffer = bytearray()
|
|
731
|
+
if not headers.is_finished:
|
|
732
|
+
return
|
|
733
|
+
self._state = 'body'
|
|
734
|
+
|
|
735
|
+
# Take care of leftovers
|
|
736
|
+
leftovers = headers.leftovers()
|
|
737
|
+
self._pre_buffer = bytearray(leftovers)
|
|
738
|
+
self._header_size = self._raw_size - len(leftovers)
|
|
739
|
+
|
|
740
|
+
def _inflate(self, chunk: bytes) -> bytes:
|
|
741
|
+
# Decompress, as Compress::Raw::Zlib does with WANT_GZIP
|
|
742
|
+
self._post_buffer += chunk
|
|
743
|
+
if self._gz is None:
|
|
744
|
+
self._gz = zlib.decompressobj(wbits=16 + zlib.MAX_WBITS)
|
|
745
|
+
gz = self._gz
|
|
746
|
+
try:
|
|
747
|
+
out = gz.decompress(self._post_buffer)
|
|
748
|
+
self._post_buffer = gz.unused_data
|
|
749
|
+
except zlib.error:
|
|
750
|
+
out = b''
|
|
751
|
+
self._gz_total += len(out)
|
|
752
|
+
return out
|
|
753
|
+
|
|
754
|
+
def _parse_sse(self) -> None:
|
|
755
|
+
# Connection established, with None for the event that Mojo leaves out
|
|
756
|
+
if not self._sse:
|
|
757
|
+
self.emit('sse', None)
|
|
758
|
+
self._sse = True
|
|
759
|
+
|
|
760
|
+
# Unlike Mojolicious: the events of a compressed stream are parsed from the decompressed bytes, where Mojo parses
|
|
761
|
+
# the compressed ones and finds no event
|
|
762
|
+
buffer = self._pre_buffer
|
|
763
|
+
if self.auto_decompress and self.is_compressed:
|
|
764
|
+
self._sse_buffer += self._inflate(bytes(self._pre_buffer))
|
|
765
|
+
self._pre_buffer = bytearray()
|
|
766
|
+
buffer = self._sse_buffer
|
|
767
|
+
|
|
768
|
+
# Parse SSE
|
|
769
|
+
while event := parse_event(buffer):
|
|
770
|
+
self.emit('sse', event)
|
|
771
|
+
|
|
772
|
+
# Check buffer size
|
|
773
|
+
if len(buffer) > self.max_buffer_size or len(self._post_buffer) > self.max_buffer_size:
|
|
774
|
+
self._state, self._limit = 'finished', True
|
|
775
|
+
|
|
776
|
+
# Perl's looks_like_number and the number of a string
|
|
777
|
+
@staticmethod
|
|
778
|
+
def _number(value: str | int | float | None) -> int | float | None:
|
|
779
|
+
if value is None:
|
|
780
|
+
return None
|
|
781
|
+
if isinstance(value, int | float):
|
|
782
|
+
return value
|
|
783
|
+
try:
|
|
784
|
+
return int(value.strip())
|
|
785
|
+
except ValueError:
|
|
786
|
+
try:
|
|
787
|
+
return float(value)
|
|
788
|
+
except ValueError:
|
|
789
|
+
return None
|
|
790
|
+
|
|
791
|
+
def _parse_until_body(self, chunk: bytes | None) -> None:
|
|
792
|
+
chunk = chunk if chunk is not None else b''
|
|
793
|
+
self._raw_size += len(chunk)
|
|
794
|
+
self._pre_buffer += chunk
|
|
795
|
+
if not self._state:
|
|
796
|
+
self._state = 'headers'
|
|
797
|
+
if self._state == 'headers':
|
|
798
|
+
self._parse_headers()
|
|
799
|
+
if self._state != 'headers' and not self._body:
|
|
800
|
+
self._body = True
|
|
801
|
+
self.emit('body')
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
new = Pyjo_Content.new
|
|
805
|
+
object = Pyjo_Content
|
|
806
|
+
Content = Pyjo_Content
|