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
Pyjo/Asset/File.py ADDED
@@ -0,0 +1,377 @@
1
+ """
2
+ Pyjo.Asset.File - File storage for HTTP content
3
+ ===============================================
4
+ ::
5
+
6
+ from Pyjo.Asset.File import File
7
+
8
+ # Temporary file
9
+ file = File()
10
+ file.add_chunk(b'foo bar baz')
11
+ if file.contains(b'bar') >= 0:
12
+ print('File contains "bar"')
13
+ print(file.slurp())
14
+
15
+ # Or import the module, as in Google Python Style Guide
16
+
17
+ import Pyjo.Asset.File
18
+
19
+ # Temporary file
20
+ file = Pyjo.Asset.File.new()
21
+ file.add_chunk(b'foo bar baz')
22
+ if file.contains(b'bar') >= 0:
23
+ print('File contains "bar"')
24
+ print(file.slurp())
25
+
26
+ # Existing file
27
+ file = Pyjo.Asset.File.new(path='/home/sri/foo.txt')
28
+ file.move_to('/yada.txt')
29
+ print(file.slurp())
30
+
31
+ :mod:`Pyjo.Asset.File` is a file storage backend for HTTP content.
32
+
33
+ The end of a ``with`` block closes the file, and deletes it with
34
+ :attr:`~Pyjo_Asset_File.cleanup`.
35
+
36
+ Events
37
+ ------
38
+
39
+ :mod:`Pyjo.Asset.File` inherits all events from :mod:`Pyjo.Asset`.
40
+
41
+ See also
42
+ --------
43
+
44
+ :mod:`Pyjoyment`, https://mojolicious.org.
45
+
46
+ Ported from `Mojo::Asset::File <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Asset/File.pm>`_
47
+ of Mojolicious 9.49.
48
+ """
49
+
50
+ import Pyjo.Asset
51
+ import Pyjo.Exception
52
+ import Pyjo.File
53
+
54
+ import atexit
55
+ import builtins
56
+ import os
57
+ import sys
58
+ import tempfile
59
+ import weakref
60
+
61
+ from types import TracebackType
62
+ from typing import IO, Self
63
+
64
+
65
+ type Anything = builtins.object
66
+
67
+
68
+ class Pyjo_Asset_File(Pyjo.Asset.object):
69
+ """
70
+ :mod:`Pyjo.Asset.File` inherits all methods from :mod:`Pyjo.Asset` and
71
+ implements the following new ones.
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ *,
77
+ cleanup: bool | None = None,
78
+ end_range: int | None = None,
79
+ handle: IO[bytes] | None = None,
80
+ path: str | os.PathLike[str] | None = None,
81
+ start_range: int = 0,
82
+ tmpdir: str | os.PathLike[str] | None = None,
83
+ ) -> None:
84
+ """::
85
+
86
+ file = Pyjo.Asset.File.new()
87
+ file = Pyjo.Asset.File.new(path='/home/sri/test.txt')
88
+
89
+ Construct a new :mod:`Pyjo.Asset.File` object.
90
+ """
91
+ super().__init__(end_range=end_range, start_range=start_range)
92
+
93
+ self.cleanup = cleanup
94
+ """::
95
+
96
+ boolean = file.cleanup
97
+ file.cleanup = boolean
98
+
99
+ Delete :attr:`path` automatically once the file is not used anymore.
100
+ """
101
+
102
+ self.path = None if path is None else os.fspath(path)
103
+ """::
104
+
105
+ path = file.path
106
+ file.path = '/home/sri/foo.txt'
107
+
108
+ File path used to create :attr:`handle`.
109
+ """
110
+
111
+ self._handle = handle
112
+ self._pid = os.getpid()
113
+ self._tmpdir = None if tmpdir is None else os.fspath(tmpdir)
114
+ _FILES.add(self)
115
+
116
+ def __del__(self) -> None:
117
+ # Module globals may be gone at the end of the interpreter
118
+ try:
119
+ if not sys.is_finalizing():
120
+ self.close()
121
+ except Exception:
122
+ pass
123
+
124
+ # Not in Mojolicious: a context manager closes the file where it ends, as PyPy frees the object only later
125
+ def __enter__(self) -> Self:
126
+ return self
127
+
128
+ def __exit__(
129
+ self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
130
+ ) -> None:
131
+ self.close()
132
+
133
+ @property
134
+ def handle(self) -> IO[bytes]:
135
+ """::
136
+
137
+ handle = file.handle
138
+ file.handle = open('/home/sri/foo.txt', 'rb')
139
+
140
+ Filehandle, created on demand for :attr:`path`, which can be
141
+ generated automatically and safely based on :attr:`tmpdir`.
142
+ """
143
+ if self._handle is not None:
144
+ return self._handle
145
+
146
+ # Open existing file
147
+ path = self.path
148
+ if path is not None and os.path.exists(path):
149
+ self._handle = Pyjo.File.new(path).open('rb', buffering=0)
150
+ return self._handle
151
+
152
+ if self.cleanup is None:
153
+ self.cleanup = True
154
+
155
+ # Create a temporary file
156
+ if path is None:
157
+ fd, path = tempfile.mkstemp(prefix='pyjo.tmp.', dir=self.tmpdir)
158
+ os.close(fd)
159
+ self.path = path
160
+
161
+ # Create a specific file, appended to as Perl's +>> does
162
+ self._handle = Pyjo.File.new(path).open('a+b', buffering=0)
163
+ return self._handle
164
+
165
+ @handle.setter
166
+ def handle(self, value: IO[bytes]) -> None:
167
+ self._handle = value
168
+
169
+ @property
170
+ def tmpdir(self) -> str:
171
+ """::
172
+
173
+ tmpdir = file.tmpdir
174
+ file.tmpdir = '/tmp'
175
+
176
+ Temporary directory used to generate :attr:`path`, defaults to the
177
+ value of the ``PYJO_TMPDIR`` environment variable or auto-detection.
178
+ """
179
+ if self._tmpdir is None:
180
+ self._tmpdir = os.environ.get('PYJO_TMPDIR') or tempfile.gettempdir()
181
+ return self._tmpdir
182
+
183
+ @tmpdir.setter
184
+ def tmpdir(self, value: str | os.PathLike[str]) -> None:
185
+ self._tmpdir = os.fspath(value)
186
+
187
+ def add_chunk(self, chunk: bytes) -> Self:
188
+ """::
189
+
190
+ file = file.add_chunk(b'foo bar baz')
191
+
192
+ Add chunk of data.
193
+ """
194
+ try:
195
+ written = self.handle.write(chunk)
196
+ except OSError as e:
197
+ error = type(e)(f"Can't write to asset: {e.strerror}" if e.strerror else "Can't write to asset")
198
+ error.errno = e.errno
199
+ raise error from e
200
+ if written != len(chunk):
201
+ raise Pyjo.Exception.new("Can't write to asset: incomplete write")
202
+ return self
203
+
204
+ # Not in Mojolicious: what DESTROY does, which Python does when it frees the file, PyPy only later
205
+ def close(self) -> None:
206
+ """::
207
+
208
+ file.close()
209
+
210
+ Close :attr:`handle` and delete :attr:`path` with :attr:`cleanup`, as
211
+ it happens when the file is freed.
212
+ """
213
+ if (handle := self._handle) is not None:
214
+ handle.close()
215
+ if not self.cleanup or (path := self.path) is None:
216
+ return
217
+
218
+ # Only the process that created the file is allowed to remove it
219
+ if os.access(path, os.W_OK) and self._pid == os.getpid():
220
+ Pyjo.File.new(path).remove()
221
+
222
+ def contains(self, string: bytes) -> int:
223
+ """::
224
+
225
+ position = file.contains(b'bar')
226
+
227
+ Check if asset contains a specific string.
228
+ """
229
+ handle = self.handle
230
+ handle.seek(self.start_range, os.SEEK_SET)
231
+
232
+ # Calculate window size
233
+ end = self.end_range if self.end_range is not None else self.size
234
+ length = len(string)
235
+ size = length if length > 131072 else 131072
236
+ if size > end - self.start_range:
237
+ size = end - self.start_range
238
+
239
+ # Sliding window search
240
+ offset = 0
241
+ window = handle.read(length) or b''
242
+ start = len(window)
243
+ while offset < end:
244
+ # Read as much as possible
245
+ diff = end - (start + offset)
246
+ buffer = handle.read(diff if diff < size else size) or b''
247
+ read = len(buffer)
248
+ window += buffer
249
+
250
+ # Search window
251
+ if (pos := window.find(string)) >= 0:
252
+ return offset + pos
253
+ offset += read
254
+ if read == 0 or offset == end:
255
+ return -1
256
+
257
+ # Resize window
258
+ window = window[read:]
259
+
260
+ return -1
261
+
262
+ def get_chunk(self, offset: int, max: int | None = None) -> bytes:
263
+ """::
264
+
265
+ chunk = file.get_chunk(offset)
266
+ chunk = file.get_chunk(offset, max)
267
+
268
+ Get chunk of data starting from a specific position, defaults to a
269
+ maximum chunk size of ``131072`` bytes (128KiB).
270
+ """
271
+ if max is None:
272
+ max = 131072
273
+
274
+ offset += self.start_range
275
+ handle = self.handle
276
+ handle.seek(offset, os.SEEK_SET)
277
+
278
+ if (end := self.end_range) is not None:
279
+ if (chunk := end + 1 - offset) <= 0:
280
+ return b''
281
+ return handle.read(chunk if chunk < max else max) or b''
282
+ return handle.read(max) or b''
283
+
284
+ @property
285
+ def is_file(self) -> bool:
286
+ """::
287
+
288
+ boolean = file.is_file
289
+
290
+ True, this is a :mod:`Pyjo.Asset.File` object.
291
+ """
292
+ return True
293
+
294
+ def move_to(self, to: Anything) -> Self:
295
+ """::
296
+
297
+ file = file.move_to('/home/sri/bar.txt')
298
+
299
+ Move asset data into a specific file and disable :attr:`cleanup`.
300
+ """
301
+ # Windows requires that the handle is closed
302
+ self.handle.close()
303
+ self._handle = None
304
+
305
+ # Move file and prevent clean up
306
+ Pyjo.File.new(self.path).move_to(to)
307
+ self.path = str(to)
308
+ self.cleanup = False
309
+ return self
310
+
311
+ @property
312
+ def mtime(self) -> float:
313
+ """::
314
+
315
+ mtime = file.mtime
316
+
317
+ Modification time of asset.
318
+ """
319
+ return os.fstat(self.handle.fileno()).st_mtime
320
+
321
+ @property
322
+ def size(self) -> int:
323
+ """::
324
+
325
+ size = file.size
326
+
327
+ Size of asset data in bytes.
328
+ """
329
+ return os.fstat(self.handle.fileno()).st_size
330
+
331
+ def slurp(self) -> bytes:
332
+ """::
333
+
334
+ content = file.slurp()
335
+
336
+ Read all asset data at once.
337
+ """
338
+ handle = self.handle
339
+ handle.seek(0, os.SEEK_SET)
340
+ content = b''
341
+ try:
342
+ while buffer := handle.read(131072):
343
+ content += buffer
344
+ except OSError as e:
345
+ error = type(e)(f"Can't read from asset: {e.strerror}" if e.strerror else "Can't read from asset")
346
+ error.errno = e.errno
347
+ raise error from e
348
+ return content
349
+
350
+ def to_file(self) -> Self:
351
+ """::
352
+
353
+ file = file.to_file()
354
+
355
+ Does nothing but return the invocant, since we already have a
356
+ :mod:`Pyjo.Asset.File` object.
357
+ """
358
+ return self
359
+
360
+
361
+ # Not in Mojolicious: the files that exist, which are closed, and deleted with cleanup, at the end of the interpreter, as
362
+ # Perl does it then, while __del__ does nothing
363
+ _FILES: 'weakref.WeakSet[Pyjo_Asset_File]' = weakref.WeakSet()
364
+
365
+
366
+ @atexit.register
367
+ def _close() -> None:
368
+ for file in list(_FILES):
369
+ try:
370
+ file.close()
371
+ except Exception:
372
+ pass
373
+
374
+
375
+ new = Pyjo_Asset_File.new
376
+ object = Pyjo_Asset_File
377
+ File = Pyjo_Asset_File
Pyjo/Asset/Memory.py ADDED
@@ -0,0 +1,235 @@
1
+ """
2
+ Pyjo.Asset.Memory - In-memory storage for HTTP content
3
+ ======================================================
4
+ ::
5
+
6
+ from Pyjo.Asset.Memory import Memory
7
+
8
+ mem = Memory()
9
+ mem.add_chunk(b'foo bar baz')
10
+ print(mem.slurp())
11
+
12
+ # Or import the module, as in Google Python Style Guide
13
+
14
+ import Pyjo.Asset.Memory
15
+
16
+ mem = Pyjo.Asset.Memory.new()
17
+ mem.add_chunk(b'foo bar baz')
18
+ print(mem.slurp())
19
+
20
+ :mod:`Pyjo.Asset.Memory` is an in-memory storage backend for HTTP content.
21
+
22
+ Events
23
+ ------
24
+
25
+ :mod:`Pyjo.Asset.Memory` inherits all events from :mod:`Pyjo.Asset` and can
26
+ emit the following new ones.
27
+
28
+ upgrade
29
+ ~~~~~~~
30
+ ::
31
+
32
+ mem.on('upgrade', lambda mem, file: ...)
33
+
34
+ Emitted when asset gets upgraded to a :mod:`Pyjo.Asset.File` object. ::
35
+
36
+ mem.on('upgrade', lambda mem, file: file.set(tmpdir='/tmp'))
37
+
38
+ See also
39
+ --------
40
+
41
+ :mod:`Pyjoyment`, https://mojolicious.org.
42
+
43
+ Ported from `Mojo::Asset::Memory <https://github.com/mojolicious/mojo/blob/v9.51/lib/Mojo/Asset/Memory.pm>`_
44
+ of Mojolicious 9.51.
45
+ """
46
+
47
+ import Pyjo.Asset
48
+ import Pyjo.Asset.File
49
+ import Pyjo.Exception
50
+ import Pyjo.File
51
+
52
+ import builtins
53
+ import os
54
+ import time
55
+
56
+ from typing import Self
57
+
58
+
59
+ type Anything = builtins.object
60
+
61
+ # The time the program started, as Perl's $^T: the time this module is loaded
62
+ MTIME = time.time()
63
+
64
+
65
+ class Pyjo_Asset_Memory(Pyjo.Asset.object):
66
+ """
67
+ :mod:`Pyjo.Asset.Memory` inherits all methods from :mod:`Pyjo.Asset`
68
+ and implements the following new ones.
69
+ """
70
+
71
+ def __init__(
72
+ self,
73
+ *,
74
+ auto_upgrade: bool | None = None,
75
+ end_range: int | None = None,
76
+ max_memory_size: int | None = None,
77
+ mtime: float | None = None,
78
+ start_range: int = 0,
79
+ ) -> None:
80
+ """::
81
+
82
+ mem = Pyjo.Asset.Memory.new()
83
+ mem = Pyjo.Asset.Memory.new(max_memory_size=1024, auto_upgrade=True)
84
+
85
+ Construct a new :mod:`Pyjo.Asset.Memory` object.
86
+ """
87
+ super().__init__(end_range=end_range, start_range=start_range)
88
+
89
+ self.auto_upgrade = auto_upgrade
90
+ """::
91
+
92
+ boolean = mem.auto_upgrade
93
+ mem.auto_upgrade = boolean
94
+
95
+ Try to detect if content size exceeds :attr:`max_memory_size` limit
96
+ and automatically upgrade to a :mod:`Pyjo.Asset.File` object.
97
+ """
98
+
99
+ self._max_memory_size: int | None = max_memory_size
100
+
101
+ self._content = b''
102
+ self._frozen = False
103
+ self._mtime = mtime if mtime is not None else MTIME
104
+
105
+ @property
106
+ def max_memory_size(self) -> int:
107
+ """::
108
+
109
+ size = mem.max_memory_size
110
+ mem.max_memory_size = 1024
111
+
112
+ Maximum size in bytes of data to keep in memory before automatically
113
+ upgrading to a :mod:`Pyjo.Asset.File` object, defaults to the value
114
+ of the ``PYJO_MAX_MEMORY_SIZE`` environment variable or ``262144``
115
+ (256KiB).
116
+ """
117
+ if self._max_memory_size is None:
118
+ self._max_memory_size = int(os.environ.get('PYJO_MAX_MEMORY_SIZE') or 262144)
119
+ return self._max_memory_size
120
+
121
+ @max_memory_size.setter
122
+ def max_memory_size(self, value: int) -> None:
123
+ self._max_memory_size = value
124
+
125
+ @property
126
+ def mtime(self) -> float:
127
+ """::
128
+
129
+ mtime = mem.mtime
130
+ mem.mtime = 1408567500
131
+
132
+ Modification time of asset, defaults to the time the program started,
133
+ as Perl's ``$^T``.
134
+ """
135
+ return self._mtime
136
+
137
+ @mtime.setter
138
+ def mtime(self, value: float) -> None:
139
+ self._mtime = value
140
+
141
+ def add_chunk(self, chunk: bytes) -> Pyjo.Asset.object:
142
+ """::
143
+
144
+ mem = mem.add_chunk(b'foo bar baz')
145
+ file = mem.add_chunk(b'abc' * 262144)
146
+
147
+ Add chunk of data and upgrade to :mod:`Pyjo.Asset.File` object if
148
+ necessary.
149
+ """
150
+ if self._frozen:
151
+ raise Pyjo.Exception.new('Asset has been upgraded and is now frozen')
152
+ if self.auto_upgrade and (self.size + len(chunk)) > self.max_memory_size:
153
+ file = Pyjo.Asset.File.new()
154
+ self.emit('upgrade', file)
155
+ self._frozen = True
156
+ return file.add_chunk(self.slurp() + chunk)
157
+
158
+ self._content += chunk
159
+ return self
160
+
161
+ def contains(self, string: bytes) -> int:
162
+ """::
163
+
164
+ position = mem.contains(b'bar')
165
+
166
+ Check if asset contains a specific string.
167
+ """
168
+ start = self.start_range
169
+ pos = self._content.find(string, start)
170
+ if start and pos >= 0:
171
+ pos -= start
172
+ end = self.end_range
173
+
174
+ return -1 if end and (pos + len(string)) >= end else pos
175
+
176
+ def get_chunk(self, offset: int, max: int | None = None) -> bytes:
177
+ """::
178
+
179
+ chunk = mem.get_chunk(offset)
180
+ chunk = mem.get_chunk(offset, max)
181
+
182
+ Get chunk of data starting from a specific position, defaults to a
183
+ maximum chunk size of ``131072`` bytes (128KiB).
184
+ """
185
+ if max is None:
186
+ max = 131072
187
+
188
+ offset += self.start_range
189
+ if (end := self.end_range) is not None and (offset + max) > end:
190
+ max = end + 1 - offset
191
+
192
+ return self._content[offset : offset + max] if max > 0 else b''
193
+
194
+ def move_to(self, to: Anything) -> Self:
195
+ """::
196
+
197
+ mem = mem.move_to('/home/sri/foo.txt')
198
+
199
+ Move asset data into a specific file.
200
+ """
201
+ Pyjo.File.new(to).spew(self._content)
202
+ return self
203
+
204
+ @property
205
+ def size(self) -> int:
206
+ """::
207
+
208
+ size = mem.size
209
+
210
+ Size of asset data in bytes.
211
+ """
212
+ return len(self._content)
213
+
214
+ def slurp(self) -> bytes:
215
+ """::
216
+
217
+ content = mem.slurp()
218
+
219
+ Read all asset data at once.
220
+ """
221
+ return self._content
222
+
223
+ def to_file(self) -> Pyjo.Asset.File.object:
224
+ """::
225
+
226
+ file = mem.to_file()
227
+
228
+ Convert asset to :mod:`Pyjo.Asset.File` object.
229
+ """
230
+ return Pyjo.Asset.File.new().add_chunk(self.slurp())
231
+
232
+
233
+ new = Pyjo_Asset_Memory.new
234
+ object = Pyjo_Asset_Memory
235
+ Memory = Pyjo_Asset_Memory