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