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,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