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.
Files changed (200) hide show
  1. Pyjo/App/ASGI.py +735 -0
  2. Pyjo/App/CGI.py +773 -0
  3. Pyjo/App/FastCGI.py +631 -0
  4. Pyjo/App/Lambda.py +987 -0
  5. Pyjo/App/WSGI.py +427 -0
  6. Pyjo/App/__init__.py +409 -0
  7. Pyjo/Asset/File.py +377 -0
  8. Pyjo/Asset/Memory.py +235 -0
  9. Pyjo/Asset/__init__.py +200 -0
  10. Pyjo/Base.py +215 -0
  11. Pyjo/Cache.py +119 -0
  12. Pyjo/Collection.py +570 -0
  13. Pyjo/Content/MultiPart.py +325 -0
  14. Pyjo/Content/Single.py +193 -0
  15. Pyjo/Content/__init__.py +806 -0
  16. Pyjo/Cookie/Request.py +90 -0
  17. Pyjo/Cookie/Response.py +294 -0
  18. Pyjo/Cookie/__init__.py +143 -0
  19. Pyjo/DOM/CSS.py +1019 -0
  20. Pyjo/DOM/HTML.py +550 -0
  21. Pyjo/DOM/__init__.py +1329 -0
  22. Pyjo/Date.py +294 -0
  23. Pyjo/EventEmitter.py +522 -0
  24. Pyjo/Exception.py +494 -0
  25. Pyjo/File.py +959 -0
  26. Pyjo/GetOpt.py +605 -0
  27. Pyjo/Headers.py +760 -0
  28. Pyjo/HelloWorld.py +77 -0
  29. Pyjo/Home.py +109 -0
  30. Pyjo/IOLoop/ChildProcess.py +591 -0
  31. Pyjo/IOLoop/Client.py +736 -0
  32. Pyjo/IOLoop/Datagram.py +982 -0
  33. Pyjo/IOLoop/Messages.py +664 -0
  34. Pyjo/IOLoop/Server.py +458 -0
  35. Pyjo/IOLoop/Stdio.py +446 -0
  36. Pyjo/IOLoop/Stream.py +644 -0
  37. Pyjo/IOLoop/Subprocess.py +419 -0
  38. Pyjo/IOLoop/TLS.py +360 -0
  39. Pyjo/IOLoop/__init__.py +1036 -0
  40. Pyjo/IOLoop/certs/README.md +41 -0
  41. Pyjo/IOLoop/certs/ca.crt +20 -0
  42. Pyjo/IOLoop/certs/server.crt +21 -0
  43. Pyjo/IOLoop/certs/server.key +28 -0
  44. Pyjo/JSON/Pointer.py +135 -0
  45. Pyjo/JSON/RPC.py +772 -0
  46. Pyjo/JSON/Schema.py +2506 -0
  47. Pyjo/JSON/__init__.py +573 -0
  48. Pyjo/Loader.py +342 -0
  49. Pyjo/Log.py +723 -0
  50. Pyjo/Message/Request.py +725 -0
  51. Pyjo/Message/Response.py +476 -0
  52. Pyjo/Message/__init__.py +830 -0
  53. Pyjo/Parameters.py +472 -0
  54. Pyjo/Path.py +419 -0
  55. Pyjo/Promise.py +894 -0
  56. Pyjo/Prove.py +1305 -0
  57. Pyjo/Reactor/Asyncio.py +854 -0
  58. Pyjo/Reactor/Poll.py +439 -0
  59. Pyjo/Reactor/__init__.py +525 -0
  60. Pyjo/SSE.py +119 -0
  61. Pyjo/Server/ASGI.py +486 -0
  62. Pyjo/Server/CGI.py +259 -0
  63. Pyjo/Server/Daemon.py +1007 -0
  64. Pyjo/Server/FastCGI.py +517 -0
  65. Pyjo/Server/Hot/__init__.py +687 -0
  66. Pyjo/Server/Hot/__main__.py +5 -0
  67. Pyjo/Server/Lambda.py +641 -0
  68. Pyjo/Server/Prefork.py +917 -0
  69. Pyjo/Server/Prethread.py +394 -0
  70. Pyjo/Server/Reload/Backend/Poll.py +114 -0
  71. Pyjo/Server/Reload/Backend/__init__.py +126 -0
  72. Pyjo/Server/Reload/__init__.py +482 -0
  73. Pyjo/Server/Reload/__main__.py +5 -0
  74. Pyjo/Server/Stdio.py +228 -0
  75. Pyjo/Server/WSGI.py +349 -0
  76. Pyjo/Server/__init__.py +396 -0
  77. Pyjo/String/Bytes.py +532 -0
  78. Pyjo/String/HTML.py +210 -0
  79. Pyjo/String/Mixin.py +333 -0
  80. Pyjo/String/Template.py +470 -0
  81. Pyjo/String/Unicode.py +553 -0
  82. Pyjo/String/__init__.py +0 -0
  83. Pyjo/TOML.py +232 -0
  84. Pyjo/Template.py +1290 -0
  85. Pyjo/Test/Pyjo.py +1454 -0
  86. Pyjo/Test/__init__.py +1233 -0
  87. Pyjo/Test/pytest_plugin.py +305 -0
  88. Pyjo/Transaction/HTTP.py +374 -0
  89. Pyjo/Transaction/WebSocket.py +709 -0
  90. Pyjo/Transaction/__init__.py +429 -0
  91. Pyjo/URL/Template.py +459 -0
  92. Pyjo/URL/__init__.py +651 -0
  93. Pyjo/Upload.py +139 -0
  94. Pyjo/UserAgent/CookieJar.py +391 -0
  95. Pyjo/UserAgent/Proxy.py +138 -0
  96. Pyjo/UserAgent/Server.py +239 -0
  97. Pyjo/UserAgent/Transactor.py +766 -0
  98. Pyjo/UserAgent/__init__.py +1243 -0
  99. Pyjo/Util.py +3567 -0
  100. Pyjo/WebSocket.py +332 -0
  101. Pyjo/YAML.py +1167 -0
  102. Pyjo/__init__.py +441 -0
  103. Pyjo/__main__.py +35 -0
  104. Pyjo/py.typed +0 -0
  105. Pyjoyment/Command/Author/__init__.py +20 -0
  106. Pyjoyment/Command/Author/generate/__init__.py +106 -0
  107. Pyjoyment/Command/Author/generate/app.py +299 -0
  108. Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
  109. Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
  110. Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
  111. Pyjoyment/Command/Author/generate/lite_app.py +113 -0
  112. Pyjoyment/Command/Author/generate/plugin.py +267 -0
  113. Pyjoyment/Command/Author/generate/pyproject.py +123 -0
  114. Pyjoyment/Command/Author/inflate.py +101 -0
  115. Pyjoyment/Command/__init__.py +332 -0
  116. Pyjoyment/Command/asgi.py +92 -0
  117. Pyjoyment/Command/cgi.py +90 -0
  118. Pyjoyment/Command/daemon.py +160 -0
  119. Pyjoyment/Command/eval.py +128 -0
  120. Pyjoyment/Command/fastcgi.py +99 -0
  121. Pyjoyment/Command/get.py +305 -0
  122. Pyjoyment/Command/lambda.py +117 -0
  123. Pyjoyment/Command/prefork.py +181 -0
  124. Pyjoyment/Command/prethread.py +126 -0
  125. Pyjoyment/Command/routes.py +133 -0
  126. Pyjoyment/Command/stdio.py +92 -0
  127. Pyjoyment/Command/version.py +141 -0
  128. Pyjoyment/Command/wsgi.py +93 -0
  129. Pyjoyment/Commands.py +546 -0
  130. Pyjoyment/Controller.py +1307 -0
  131. Pyjoyment/HelloWorld.py +56 -0
  132. Pyjoyment/Lite.py +494 -0
  133. Pyjoyment/Plugin/AllowedHosts.py +205 -0
  134. Pyjoyment/Plugin/Config.py +248 -0
  135. Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
  136. Pyjoyment/Plugin/EPLRenderer.py +134 -0
  137. Pyjoyment/Plugin/EPRenderer.py +212 -0
  138. Pyjoyment/Plugin/HeaderCondition.py +98 -0
  139. Pyjoyment/Plugin/Health.py +325 -0
  140. Pyjoyment/Plugin/JSONConfig.py +172 -0
  141. Pyjoyment/Plugin/Mount.py +223 -0
  142. Pyjoyment/Plugin/PyRenderer.py +209 -0
  143. Pyjoyment/Plugin/TOMLConfig.py +141 -0
  144. Pyjoyment/Plugin/TagHelpers.py +959 -0
  145. Pyjoyment/Plugin/YAMLConfig.py +178 -0
  146. Pyjoyment/Plugin/__init__.py +80 -0
  147. Pyjoyment/Plugins.py +215 -0
  148. Pyjoyment/Renderer.py +673 -0
  149. Pyjoyment/Routes/Match.py +251 -0
  150. Pyjoyment/Routes/Pattern.py +443 -0
  151. Pyjoyment/Routes/Route.py +827 -0
  152. Pyjoyment/Routes/__init__.py +419 -0
  153. Pyjoyment/Sessions.py +255 -0
  154. Pyjoyment/Static.py +421 -0
  155. Pyjoyment/Types.py +238 -0
  156. Pyjoyment/Validator/Validation.py +395 -0
  157. Pyjoyment/Validator/__init__.py +304 -0
  158. Pyjoyment/__init__.py +1083 -0
  159. Pyjoyment/__main__.py +5 -0
  160. Pyjoyment/py.typed +0 -0
  161. Pyjoyment/resources/public/favicon.ico +0 -0
  162. Pyjoyment/resources/public/favicon.svg +345 -0
  163. Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
  164. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
  165. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
  166. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
  167. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
  168. Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
  169. Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
  170. Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
  171. Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
  172. Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
  173. Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
  174. Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
  175. Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
  176. Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
  177. Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
  178. Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
  179. Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
  180. Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
  181. Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
  182. Pyjoyment/resources/public/pyjo/logo.png +0 -0
  183. Pyjoyment/resources/public/pyjo/logo.svg +346 -0
  184. Pyjoyment/resources/public/pyjo/notfound.png +0 -0
  185. Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
  186. Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
  187. Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
  188. Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
  189. Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
  190. Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
  191. Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
  192. Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
  193. Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
  194. Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
  195. Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
  196. pyjoyment-0.1.0.dist-info/METADATA +376 -0
  197. pyjoyment-0.1.0.dist-info/RECORD +200 -0
  198. pyjoyment-0.1.0.dist-info/WHEEL +4 -0
  199. pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
  200. pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -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