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/File.py ADDED
@@ -0,0 +1,959 @@
1
+ r"""
2
+ Pyjo.File - File system paths
3
+ =============================
4
+ ::
5
+
6
+ from Pyjo.File import File
7
+
8
+ # Portably deal with file system paths
9
+ path = File('/home/sri/.vimrc')
10
+ print(path.slurp())
11
+ print(path.dirname())
12
+ print(path.basename())
13
+ print(path.extname)
14
+ print(path.sibling('.bashrc'))
15
+
16
+ # Or import the module, as in Google Python Style Guide
17
+
18
+ import Pyjo.File
19
+
20
+ # Portably deal with file system paths
21
+ path = Pyjo.File.new('/home/sri/.vimrc')
22
+ print(path.slurp())
23
+ print(path.dirname())
24
+ print(path.basename())
25
+ print(path.extname)
26
+ print(path.sibling('.bashrc'))
27
+
28
+ # Use the alternative constructor
29
+ from Pyjo.File import path
30
+ path = path('/tmp/foo/bar').make_path()
31
+ path.child('test.txt').spew('Hello Pyjo!')
32
+
33
+ :mod:`Pyjo.File` is a container for file system paths that provides a
34
+ friendly API for dealing with different operating systems. It keeps the path
35
+ as it is given, and it is an :class:`os.PathLike`, so the standard library
36
+ takes it where it takes a path. ::
37
+
38
+ # Use the path with the standard library
39
+ path = Pyjo.File.new('/home/sri/test')
40
+ with open(path) as f:
41
+ print(f.read())
42
+
43
+ See also
44
+ --------
45
+
46
+ :mod:`Pyjoyment`, https://mojolicious.org.
47
+
48
+ Ported from `Mojo::File <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/File.pm>`_
49
+ of Mojolicious 9.49.
50
+
51
+ Functions
52
+ ---------
53
+ """
54
+
55
+ import Pyjo.Base
56
+ import Pyjo.Collection
57
+ import Pyjo.Exception
58
+ import Pyjo.String.Mixin
59
+
60
+ from Pyjo.Util import decode, encode
61
+
62
+ import builtins
63
+ import inspect
64
+ import os
65
+ import pathlib
66
+ import random
67
+ import re
68
+ import shutil
69
+ import string
70
+ import sys
71
+ import tempfile as _tempfile
72
+ import weakref
73
+
74
+ from collections.abc import Callable, Iterator
75
+ from types import TracebackType
76
+ from typing import IO, TYPE_CHECKING, Any, NoReturn, Self, cast
77
+
78
+
79
+ if TYPE_CHECKING:
80
+ import Pyjo.Transaction.HTTP
81
+ import Pyjo.UserAgent
82
+
83
+
84
+ _random = random.SystemRandom()
85
+
86
+ # File::Spec and File::Basename of Perl follow the system: on Windows a path may have a volume, and both / and \
87
+ # separate its levels
88
+ _WIN32 = sys.platform == 'win32'
89
+
90
+ # The bytes of a file as they are, as sysread and syswrite of Perl pass them, where the text mode of the C runtime of
91
+ # Windows, the default of os.open there, writes \n as CRLF and reads CRLF as \n
92
+ _BINARY: int = getattr(os, 'O_BINARY', 0)
93
+
94
+
95
+ type Anything = builtins.object
96
+
97
+
98
+ class Pyjo_File(Pyjo.Base.object, Pyjo.String.Mixin.object):
99
+ """
100
+ :mod:`Pyjo.File` inherits all methods from :mod:`Pyjo.Base` and
101
+ :mod:`Pyjo.String.Mixin` and implements the following new ones.
102
+ """
103
+
104
+ def __init__(self, *parts: Anything) -> None:
105
+ """::
106
+
107
+ path = Pyjo.File.new()
108
+ path = Pyjo.File.new('/home/sri/.vimrc')
109
+ path = Pyjo.File.new('/home', 'sri', '.vimrc')
110
+ path = Pyjo.File.new(pathlib.Path('/home/sri/.vimrc'))
111
+ path = Pyjo.File.new(tempfile.NamedTemporaryFile())
112
+ path = Pyjo.File.new(tempfile.TemporaryDirectory())
113
+
114
+ Construct a new :mod:`Pyjo.File` object, defaults to using the current
115
+ working directory. A temporary file or directory is kept as long as the
116
+ object is, or until the end of a ``with`` block. ::
117
+
118
+ # "foo/bar/baz.txt" (on UNIX)
119
+ Pyjo.File.new('foo', 'bar', 'baz.txt')
120
+ """
121
+ if any(p is None for p in parts):
122
+ raise ValueError('Invalid path')
123
+
124
+ # Not in Mojolicious: a temporary file or directory of the standard
125
+ # library, kept as Mojo keeps the object of File::Temp
126
+ self._temp = parts[0] if len(parts) == 1 and _is_temp(parts[0]) else None
127
+ self._path = (
128
+ _str(parts[0])
129
+ if len(parts) == 1
130
+ else _catfile(*map(_str, parts))
131
+ if len(parts)
132
+ else _canonpath(os.getcwd())
133
+ )
134
+
135
+ # Not in Mojolicious: a context manager, which removes the temporary file or directory that the object keeps where the
136
+ # block ends, as PyPy frees the object only when its garbage collector runs
137
+ def __enter__(self) -> Self:
138
+ return self
139
+
140
+ def __exit__(
141
+ self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
142
+ ) -> None:
143
+ if (temp := self._temp) is not None:
144
+ _release(temp)
145
+
146
+ def __bool__(self) -> bool:
147
+ """::
148
+
149
+ boolean = bool(path)
150
+
151
+ Always true.
152
+ """
153
+ return True
154
+
155
+ def __fspath__(self) -> str:
156
+ """::
157
+
158
+ string = os.fspath(path)
159
+
160
+ The path for the standard library, as :meth:`to_str` gives it.
161
+ """
162
+ return self._path
163
+
164
+ def __getitem__(self, index: int) -> str:
165
+ """::
166
+
167
+ part = path[0]
168
+
169
+ A part of the path, as :meth:`to_list` gives it.
170
+ """
171
+ return self.to_list()[index]
172
+
173
+ def __iter__(self) -> Iterator[str]:
174
+ """::
175
+
176
+ for part in path:
177
+ ...
178
+
179
+ Iterate the parts of the path, as :meth:`to_list` gives them.
180
+ """
181
+ return iter(self.to_list())
182
+
183
+ def __len__(self) -> int:
184
+ """::
185
+
186
+ size = len(path)
187
+
188
+ Number of the parts of the path.
189
+ """
190
+ return len(self.to_list())
191
+
192
+ def basename(self, *suffixes: str) -> str:
193
+ """::
194
+
195
+ name = path.basename()
196
+ name = path.basename('.txt')
197
+
198
+ Return the last level of the path, as ``File::Basename`` of Perl does. ::
199
+
200
+ # ".vimrc" (on UNIX)
201
+ path('/home/sri/.vimrc').basename()
202
+
203
+ # "test" (on UNIX)
204
+ path('/home/sri/test.txt').basename('.txt')
205
+ """
206
+ return _basename(self._path, *suffixes)
207
+
208
+ def child(self, *parts: Anything) -> Self:
209
+ """::
210
+
211
+ child = path.child('.vimrc')
212
+
213
+ Return a new :mod:`Pyjo.File` object relative to the path. A child of
214
+ a temporary directory keeps the directory as long as it is kept. ::
215
+
216
+ # "/home/sri/.vimrc" (on UNIX)
217
+ path('/home').child('sri', '.vimrc')
218
+ """
219
+ child = self.new(self._path, *parts)
220
+ # Unlike Mojolicious: Perl keeps the temporary directory of
221
+ # tempdir()->child(...) until the end of the statement, Python frees it
222
+ # as soon as child() returns, so the child keeps it
223
+ child._temp = self._temp
224
+ return child
225
+
226
+ def chmod(self, mode: int) -> Self:
227
+ """::
228
+
229
+ path = path.chmod(0o644)
230
+
231
+ Change file permissions.
232
+ """
233
+ try:
234
+ os.chmod(self._path, mode)
235
+ except OSError as e:
236
+ _croak(e, f'Can\'t chmod file "{self._path}"')
237
+ return self
238
+
239
+ def copy_to(self, to: Anything) -> Self:
240
+ """::
241
+
242
+ destination = path.copy_to('/home/sri')
243
+ destination = path.copy_to('/home/sri/.vimrc.backup')
244
+
245
+ Copy file with :mod:`shutil` and return the destination as a
246
+ :mod:`Pyjo.File` object.
247
+ """
248
+ destination = self._destination(to)
249
+ try:
250
+ shutil.copyfile(self._path, destination)
251
+ except OSError as e:
252
+ _croak(e, f'Can\'t copy file "{self._path}" to "{_str(to)}"')
253
+ return self.new(destination)
254
+
255
+ def dirname(self) -> Self:
256
+ """::
257
+
258
+ name = path.dirname()
259
+
260
+ Return all but the last level of the path, as ``File::Basename`` of Perl
261
+ does, as a :mod:`Pyjo.File` object. ::
262
+
263
+ # "/home/sri" (on UNIX)
264
+ path('/home/sri/.vimrc').dirname()
265
+ """
266
+ return self.new(_dirname(self._path))
267
+
268
+ def download(
269
+ self, url: Anything, *, headers: dict[str, Any] | None = None, ua: 'Pyjo.UserAgent.object | None' = None
270
+ ) -> bool:
271
+ """::
272
+
273
+ boolean = path.download('https://example.com/test.tar.gz')
274
+ boolean = path.download('https://example.com/test.tar.gz', headers={'Accept': '*/*'})
275
+ boolean = path.download('https://example.com/test.tar.gz', ua=Pyjo.UserAgent.new())
276
+
277
+ Download file from URL, returns true once the file has been downloaded
278
+ completely. Incomplete downloads are resumed. Follows ``10`` redirects
279
+ by default and does not limit the size of the response, which will be
280
+ streamed memory efficiently. Note that this method is **EXPERIMENTAL**
281
+ and might change without warning!
282
+ """
283
+ import Pyjo.UserAgent
284
+
285
+ if ua is None:
286
+ ua = Pyjo.UserAgent.new(max_redirects=10, max_response_size=0)
287
+ tx = _download_error(ua.transactor.download(ua.head(url, headers or {}), self._path))
288
+ return bool(_download_error(ua.start(tx))) if tx is not None else True
289
+
290
+ @property
291
+ def extname(self) -> str:
292
+ """::
293
+
294
+ ext = path.extname
295
+
296
+ Return file extension of the path. ::
297
+
298
+ # "js"
299
+ path('/home/sri/test.js').extname
300
+ """
301
+ return m[1] if (m := re.search(r'.+\.([^.]+)$', self.basename())) else ''
302
+
303
+ @property
304
+ def is_abs(self) -> bool:
305
+ """::
306
+
307
+ boolean = path.is_abs
308
+
309
+ Check if the path is absolute. ::
310
+
311
+ # True (on UNIX)
312
+ path('/home/sri/.vimrc').is_abs
313
+
314
+ # False (on UNIX)
315
+ path('.vimrc').is_abs
316
+ """
317
+ return _is_abs(self._path) > 0
318
+
319
+ def list(self, *, dir: bool = False, hidden: bool = False) -> 'Pyjo.Collection.object[Self]':
320
+ """::
321
+
322
+ collection = path.list()
323
+ collection = path.list(hidden=True)
324
+
325
+ List all files in the directory and return a :mod:`Pyjo.Collection`
326
+ object containing the results as :mod:`Pyjo.File` objects. The list does
327
+ not include ``.`` and ``..``. ::
328
+
329
+ # List files
330
+ for file in path('/home/sri/myapp').list().each():
331
+ print(file)
332
+
333
+ These options are currently available:
334
+
335
+ ``dir``
336
+ Include directories.
337
+
338
+ ``hidden``
339
+ Include hidden files.
340
+ """
341
+ if not os.path.isdir(self._path):
342
+ return Pyjo.Collection.new()
343
+ try:
344
+ names = os.listdir(self._path)
345
+ except OSError as e:
346
+ _croak(e, f'Can\'t open directory "{self._path}"')
347
+ if not hidden:
348
+ names = [n for n in names if not n.startswith('.')]
349
+ files = [_catfile(self._path, n) for n in names]
350
+ if not dir:
351
+ files = [f for f in files if not os.path.isdir(f)]
352
+
353
+ return Pyjo.Collection.object(self.new(f) for f in sorted(files))
354
+
355
+ def list_tree(
356
+ self, *, dir: bool = False, hidden: bool = False, max_depth: int | None = None
357
+ ) -> 'Pyjo.Collection.object[Self]':
358
+ """::
359
+
360
+ collection = path.list_tree()
361
+ collection = path.list_tree(hidden=True)
362
+
363
+ List all files recursively in the directory and return a
364
+ :mod:`Pyjo.Collection` object containing the results as
365
+ :mod:`Pyjo.File` objects. The list does not include ``.`` and ``..``,
366
+ and symbolic links to directories are not followed. ::
367
+
368
+ # List all templates
369
+ for file in path('/home/sri/myapp/templates').list_tree().each():
370
+ print(file)
371
+
372
+ These options are currently available:
373
+
374
+ ``dir``
375
+ Include directories.
376
+
377
+ ``hidden``
378
+ Include hidden files and directories.
379
+
380
+ ``max_depth``
381
+ Maximum number of levels to descend when searching for files.
382
+ """
383
+ if not os.path.isdir(self._path):
384
+ return Pyjo.Collection.new()
385
+
386
+ results: builtins.list[Self] = []
387
+
388
+ def walk(path: str, depth: int) -> None:
389
+ try:
390
+ names = sorted(os.listdir(path))
391
+ except OSError:
392
+ return
393
+ if not hidden:
394
+ names = [n for n in names if not n.startswith('.')]
395
+ for name in names:
396
+ child = self.new(path, name)
397
+ is_dir = os.path.isdir(child)
398
+ if dir or not is_dir:
399
+ results.append(child)
400
+ if is_dir and not os.path.islink(child) and (not max_depth or depth + 1 < max_depth):
401
+ walk(child._path, depth + 1)
402
+
403
+ walk(self._path, 0)
404
+
405
+ return Pyjo.Collection.object(results)
406
+
407
+ def lstat(self) -> os.stat_result:
408
+ """::
409
+
410
+ stat = path.lstat()
411
+
412
+ Return a :class:`os.stat_result` object for the symlink. ::
413
+
414
+ # Get symlink size
415
+ print(path('/usr/sbin/sendmail').lstat().st_size)
416
+
417
+ # Get symlink modification time
418
+ print(path('/usr/sbin/sendmail').lstat().st_mtime)
419
+ """
420
+ return os.lstat(self._path)
421
+
422
+ def make_path(self, mode: int = 0o777) -> Self:
423
+ """::
424
+
425
+ path = path.make_path()
426
+ path = path.make_path(mode=0o711)
427
+
428
+ Create the directories if they don't already exist, with
429
+ :func:`os.makedirs`.
430
+ """
431
+ os.makedirs(self._path, mode, exist_ok=True)
432
+ return self
433
+
434
+ def move_to(self, to: Anything) -> Self:
435
+ """::
436
+
437
+ destination = path.move_to('/home/sri')
438
+ destination = path.move_to('/home/sri/.vimrc.backup')
439
+
440
+ Move file with :mod:`shutil` and return the destination as a
441
+ :mod:`Pyjo.File` object.
442
+ """
443
+ destination = self._destination(to)
444
+ try:
445
+ shutil.move(self._path, destination)
446
+ except OSError as e:
447
+ _croak(e, f'Can\'t move file "{self._path}" to "{_str(to)}"')
448
+ return self.new(destination)
449
+
450
+ def open(self, mode: str = 'r', *args: Any, **kwargs: Any) -> IO[Any]:
451
+ """::
452
+
453
+ handle = path.open('r+')
454
+ handle = path.open('rb')
455
+ handle = path.open('r', encoding='UTF-8')
456
+
457
+ Open file with :func:`open`.
458
+ """
459
+ try:
460
+ return builtins.open(self._path, mode, *args, **kwargs)
461
+ except OSError as e:
462
+ _croak(e, f'Can\'t open file "{self._path}"')
463
+
464
+ def realpath(self) -> Self:
465
+ """::
466
+
467
+ realpath = path.realpath()
468
+
469
+ Resolve the path with :func:`os.path.realpath` and return the result as
470
+ a :mod:`Pyjo.File` object.
471
+ """
472
+ return self.new(os.path.realpath(self._path))
473
+
474
+ def remove(self) -> Self:
475
+ """::
476
+
477
+ path = path.remove()
478
+
479
+ Delete file.
480
+ """
481
+ if os.path.exists(self._path):
482
+ try:
483
+ os.unlink(self._path)
484
+ except OSError as e:
485
+ _croak(e, f'Can\'t remove file "{self._path}"')
486
+ return self
487
+
488
+ def remove_tree(self, *, keep_root: bool = False) -> Self:
489
+ """::
490
+
491
+ path = path.remove_tree()
492
+ path = path.remove_tree(keep_root=True)
493
+
494
+ Delete this directory and any files and subdirectories it may contain.
495
+ """
496
+ if not os.path.lexists(self._path):
497
+ return self
498
+ if not keep_root:
499
+ shutil.rmtree(self._path)
500
+ return self
501
+ for name in os.listdir(self._path):
502
+ child = os.path.join(self._path, name)
503
+ if os.path.isdir(child) and not os.path.islink(child):
504
+ shutil.rmtree(child)
505
+ else:
506
+ os.unlink(child)
507
+ return self
508
+
509
+ def sibling(self, *parts: Anything) -> Self:
510
+ """::
511
+
512
+ sibling = path.sibling('.vimrc')
513
+
514
+ Return a new :mod:`Pyjo.File` object relative to the directory part of
515
+ the path. ::
516
+
517
+ # "/home/sri/.vimrc" (on UNIX)
518
+ path('/home/sri/.bashrc').sibling('.vimrc')
519
+
520
+ # "/home/sri/.ssh/known_hosts" (on UNIX)
521
+ path('/home/sri/.bashrc').sibling('.ssh', 'known_hosts')
522
+ """
523
+ return self.new(_dirname(self._path), *parts)
524
+
525
+ def slurp(self, encoding: str | None = None) -> bytes | str | None:
526
+ """::
527
+
528
+ data = path.slurp()
529
+ chars = path.slurp('UTF-8')
530
+
531
+ Read all data at once from the file. If an encoding is provided, an
532
+ attempt will be made to decode the content, and ``None`` is returned if
533
+ it fails.
534
+ """
535
+ try:
536
+ fd = os.open(self._path, os.O_RDONLY | _BINARY)
537
+ except OSError as e:
538
+ _croak(e, f'Can\'t open file "{self._path}"')
539
+ try:
540
+ content = b''
541
+ while buffer := os.read(fd, 131072):
542
+ content += buffer
543
+ except OSError as e:
544
+ _croak(e, f'Can\'t read from file "{self._path}"')
545
+ finally:
546
+ os.close(fd)
547
+
548
+ return decode(encoding, content) if encoding else content
549
+
550
+ def spew(self, content: bytes | str, encoding: str | None = None) -> Self:
551
+ """::
552
+
553
+ path = path.spew(data)
554
+ path = path.spew(chars, 'UTF-8')
555
+
556
+ Write all data at once to the file. If an encoding is provided, an
557
+ attempt to encode the content will be made prior to writing, and
558
+ characters without one are encoded with ``UTF-8``.
559
+ """
560
+ if isinstance(content, str):
561
+ # Unlike Mojolicious: characters without an encoding are UTF-8, where
562
+ # Perl writes the characters 0-255 as bytes and dies of a wider one
563
+ content = encode(encoding, content) if encoding else content.encode('UTF-8')
564
+ try:
565
+ fd = os.open(self._path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC | _BINARY, 0o666)
566
+ except OSError as e:
567
+ _croak(e, f'Can\'t open file "{self._path}"')
568
+ try:
569
+ if os.write(fd, content) != len(content):
570
+ raise OSError(f'Can\'t write to file "{self._path}"')
571
+ except OSError as e:
572
+ _croak(e, f'Can\'t write to file "{self._path}"')
573
+ finally:
574
+ os.close(fd)
575
+ return self
576
+
577
+ def spurt(self, *chunks: bytes | str) -> Self:
578
+ """::
579
+
580
+ path = path.spurt(*chunks)
581
+
582
+ Alias for :meth:`spew` that writes multiple chunks.
583
+ """
584
+ if all(isinstance(c, str) for c in chunks):
585
+ return self.spew(''.join(str(c) for c in chunks))
586
+ return self.spew(b''.join(c.encode('UTF-8') if isinstance(c, str) else c for c in chunks))
587
+
588
+ def stat(self) -> os.stat_result:
589
+ """::
590
+
591
+ stat = path.stat()
592
+
593
+ Return a :class:`os.stat_result` object for the path. ::
594
+
595
+ # Get file size
596
+ print(path('/home/sri/.bashrc').stat().st_size)
597
+
598
+ # Get file modification time
599
+ print(path('/home/sri/.bashrc').stat().st_mtime)
600
+ """
601
+ return os.stat(self._path)
602
+
603
+ def to_abs(self) -> Self:
604
+ """::
605
+
606
+ absolute = path.to_abs()
607
+
608
+ Return absolute path as a :mod:`Pyjo.File` object, the path does not
609
+ need to exist on the file system.
610
+ """
611
+ path = self._path
612
+ if (level := _is_abs(path)) == 2 or (level and not _WIN32):
613
+ return self.new(_canonpath(path))
614
+
615
+ # File::Spec::Win32's rel2abs: a path from the root takes the volume of the working directory, and one with a
616
+ # drive and no root the working directory of that drive
617
+ if _WIN32:
618
+ if level:
619
+ volume = re.match(r'(?:[a-zA-Z]:|(?:\\\\|//)[^\\/]+[\\/][^\\/]+)?', os.getcwd())
620
+ assert volume is not None
621
+ return self.new(_canonpath(volume[0] + path))
622
+ if drive := re.match(r'[a-zA-Z]:', path):
623
+ return self.new(_catfile(os.path.abspath(drive[0]), path[2:]))
624
+ return self.new(_catfile(os.getcwd(), path))
625
+
626
+ def to_list(self) -> 'builtins.list[str]':
627
+ """::
628
+
629
+ parts = path.to_list()
630
+
631
+ Split the path on directory separators. ::
632
+
633
+ # "home:sri:.vimrc" (on UNIX)
634
+ ':'.join(path('/home/sri/.vimrc').to_list())
635
+ """
636
+ return _splitdir(self._path)
637
+
638
+ # Not in Mojolicious: the conversion to pathlib
639
+ def to_path(self) -> pathlib.Path:
640
+ """::
641
+
642
+ p = path.to_path()
643
+
644
+ Turn the path into :class:`pathlib.Path`.
645
+ """
646
+ return pathlib.Path(self._path)
647
+
648
+ def to_rel(self, base: Anything) -> Self:
649
+ """::
650
+
651
+ relative = path.to_rel('/some/base/path')
652
+
653
+ Return a relative path from the original path to the destination path as
654
+ a :mod:`Pyjo.File` object. ::
655
+
656
+ # "sri/.vimrc" (on UNIX)
657
+ path('/home/sri/.vimrc').to_rel('/home')
658
+ """
659
+ return self.new(os.path.relpath(self._path, _str(base)))
660
+
661
+ # Renamed from Mojolicious: to_string
662
+ def to_str(self) -> str:
663
+ """::
664
+
665
+ string = path.to_str()
666
+
667
+ Stringify the path.
668
+ """
669
+ return self._path
670
+
671
+ def touch(self) -> Self:
672
+ """::
673
+
674
+ path = path.touch()
675
+
676
+ Create file if it does not exist or change the modification and access
677
+ time to the current time. ::
678
+
679
+ # Safely read file
680
+ print(path('.bashrc').touch().slurp())
681
+ """
682
+ if not os.path.exists(self._path):
683
+ self.open('w').close()
684
+ try:
685
+ os.utime(self._path)
686
+ except OSError as e:
687
+ _croak(e, f'Can\'t touch file "{self._path}"')
688
+ return self
689
+
690
+ # Left out from Mojolicious: with_roles, as Pyjoyment has no roles
691
+
692
+ def _destination(self, to: Anything) -> str:
693
+ to = _str(to)
694
+ return _catfile(to, _basename(self._path)) if os.path.isdir(to) else to
695
+
696
+
697
+ def curfile() -> Pyjo_File:
698
+ """::
699
+
700
+ path = curfile()
701
+
702
+ Construct a new :mod:`Pyjo.File` object for the absolute path to the
703
+ current source file.
704
+ """
705
+ frame = inspect.currentframe()
706
+ caller = frame.f_back if frame is not None else None
707
+ return Pyjo_File(os.path.realpath(caller.f_code.co_filename if caller is not None else ''))
708
+
709
+
710
+ def path(*parts: Anything) -> Pyjo_File:
711
+ """::
712
+
713
+ path = path()
714
+ path = path('/home/sri/.vimrc')
715
+ path = path('/home', 'sri', '.vimrc')
716
+ path = path(tempfile.TemporaryDirectory())
717
+
718
+ Construct a new :mod:`Pyjo.File` object, defaults to using the current
719
+ working directory. ::
720
+
721
+ # "foo/bar/baz.txt" (on UNIX)
722
+ path('foo', 'bar', 'baz.txt')
723
+ """
724
+ return Pyjo_File(*parts)
725
+
726
+
727
+ def tempdir(template: str | None = None, **kwargs: Any) -> Pyjo_File:
728
+ """::
729
+
730
+ path = tempdir()
731
+ path = tempdir('tempXXXXX')
732
+
733
+ Construct a new :mod:`Pyjo.File` object for a temporary directory, made
734
+ with :func:`tempfile.mkdtemp` and its keyword arguments, which is removed
735
+ with the object or at the end of a ``with`` block, as Perl's
736
+ ``File::Temp`` removes it, without the :exc:`ResourceWarning` that
737
+ :class:`tempfile.TemporaryDirectory` gives when it is not cleaned up
738
+ explicitly. The ``X`` characters at the end of a
739
+ template are random, and a directory with a template is made in the
740
+ current working directory, as ``File::Temp`` makes it. ::
741
+
742
+ # Longer version, which warns when the directory is removed with the object
743
+ path = path(tempfile.TemporaryDirectory())
744
+ """
745
+ if template is not None:
746
+ return Pyjo_File(_Temp(_mkdir(template, **kwargs), _rmtree))
747
+ return Pyjo_File(_Temp(cast(str, _tempfile.mkdtemp(**kwargs)), _rmtree))
748
+
749
+
750
+ def tempfile(**kwargs: Any) -> Pyjo_File:
751
+ """::
752
+
753
+ path = tempfile()
754
+ path = tempfile(dir='/tmp')
755
+
756
+ Construct a new :mod:`Pyjo.File` object for a temporary file, made with
757
+ :func:`tempfile.mkstemp` and its keyword arguments, which is removed with
758
+ the object or at the end of a ``with`` block, as Perl's ``File::Temp``
759
+ removes it, without the :exc:`ResourceWarning` that
760
+ :func:`tempfile.NamedTemporaryFile` gives when it is not closed
761
+ explicitly. ::
762
+
763
+ # Longer version, which warns when the file is removed with the object
764
+ path = path(tempfile.NamedTemporaryFile(dir='/tmp'))
765
+ """
766
+ handle, name = cast(tuple[int, str], _tempfile.mkstemp(**kwargs))
767
+ os.close(handle)
768
+ return Pyjo_File(_Temp(name, _unlink))
769
+
770
+
771
+ # A temporary file or directory of File::Temp, removed with the object without a ResourceWarning
772
+ class _Temp:
773
+ def __init__(self, name: str, remove: Callable[[str], Anything]) -> None:
774
+ self.name = name
775
+ self.remove = weakref.finalize(self, remove, name)
776
+
777
+
778
+ # Remove a temporary file or directory now, once, where the object would remove it when it goes
779
+ def _release(temp: Anything) -> None:
780
+ if isinstance(temp, _Temp):
781
+ temp.remove()
782
+ elif isinstance(temp, _tempfile.TemporaryDirectory):
783
+ cast(_tempfile.TemporaryDirectory[str], temp).cleanup()
784
+ else:
785
+ cast(Any, temp).close()
786
+
787
+
788
+ # File::Temp's newdir with a template
789
+ def _mkdir(template: str, dir: str = '.') -> str:
790
+ prefix = template.rstrip('X')
791
+ size = len(template) - len(prefix)
792
+ chars = string.ascii_letters + string.digits
793
+ while True:
794
+ name = os.path.abspath(os.path.join(dir, prefix + ''.join(_random.choice(chars) for _ in range(size))))
795
+ try:
796
+ os.mkdir(name, 0o700)
797
+ return name
798
+ except FileExistsError:
799
+ continue
800
+
801
+
802
+ def _rmtree(name: str) -> None:
803
+ shutil.rmtree(name, ignore_errors=True)
804
+
805
+
806
+ def _unlink(name: str) -> None:
807
+ try:
808
+ os.unlink(name)
809
+ except OSError:
810
+ pass
811
+
812
+
813
+ # File::Basename's basename
814
+ def _basename(path: str, *suffixes: str) -> str:
815
+ path = _strip_trailing_sep(path)
816
+ basename, dirname = _fileparse(path)
817
+ tail = ''
818
+ for suffix in suffixes:
819
+ if basename.endswith(suffix):
820
+ basename, tail = basename[: -len(suffix)], suffix + tail
821
+ if len(tail) and not len(basename):
822
+ basename = tail
823
+ return basename if len(basename) else dirname
824
+
825
+
826
+ # File::Spec's canonpath
827
+ def _canonpath(path: str) -> str:
828
+ if _WIN32:
829
+ return _canon_cat(path) if len(path) else path
830
+ path = re.sub(r'/{2,}', '/', path)
831
+ path = re.sub(r'(?:/\.)+(?:/|\Z)', '/', path)
832
+ if path != './':
833
+ path = re.sub(r'^(?:\./)+', '', path, flags=re.S)
834
+ path = re.sub(r'^/(?:\.\./)+', '/', path)
835
+ path = re.sub(r'^/\.\.$', '/', path)
836
+ if path != '/':
837
+ path = re.sub(r'/\Z', '', path)
838
+ return path
839
+
840
+
841
+ # File::Spec's catfile
842
+ def _catfile(*parts: str) -> str:
843
+ if _WIN32:
844
+ if not len(parts) or parts[0] == '':
845
+ return _canon_cat('/', *parts[1:])
846
+ if re.match(r'[a-zA-Z]:\Z', parts[0]):
847
+ return _canon_cat(parts[0] + '\\', *parts[1:])
848
+ return _canon_cat(*parts)
849
+ return _canonpath('/'.join(parts))
850
+
851
+
852
+ # File::Spec::Win32's _canon_cat
853
+ def _canon_cat(first: str, *rest: str) -> str:
854
+ volume = ''
855
+ if m := re.match(r'([A-Za-z]:)([\\/]?)', first):
856
+ volume = m[1].upper() + ('\\' if m[2] else '')
857
+ elif m := re.match(r'(?:\\\\|//)([^\\/]+)(?:[\\/]([^\\/]+))?[\\/]?', first, re.S):
858
+ volume = f'\\\\{m[1]}' + (f'\\{m[2]}' if m[2] is not None else '') + '\\'
859
+ elif m := re.match(r'[\\/]', first):
860
+ volume = '\\'
861
+ if m:
862
+ first = first[m.end() :]
863
+ path = '\\'.join((first, *rest))
864
+
865
+ path = re.sub(r'[\\/]+', r'\\', path)
866
+ path = re.sub(r'(?:(?:\A|\\)\.(?:\\\.)*(?:\\|\Z))+', r'\\', path)
867
+ while True:
868
+ path, n = re.subn(
869
+ r'(?:(?:\A|\\)[^\\]+\\\.\.(?<!\A\.\.\\\.\.)(?<!\\\.\.\\\.\.)(?:\\|\Z))+', r'\\', path, count=1, flags=re.S
870
+ )
871
+ if not n:
872
+ break
873
+ path = re.sub(r'\A\\', '', path)
874
+ path = re.sub(r'\\\Z', '', path)
875
+
876
+ if volume.endswith('\\'):
877
+ path = re.sub(r'\A\.\.(?:\\\.\.)*(?:\\|\Z)', '', path)
878
+ if path == '' and (m := re.match(r'(\\\\.*)\\\Z', volume, re.S)):
879
+ return m[1]
880
+ return volume + path if len(path) or len(volume) else '.'
881
+
882
+
883
+ def _download_error(tx: 'Pyjo.Transaction.HTTP.object') -> 'Pyjo.Transaction.HTTP.object | None':
884
+ if not (err := tx.error):
885
+ return tx
886
+ if err['message'] in ('Download complete', 'Download incomplete'):
887
+ return None
888
+ if code := err.get('code'):
889
+ raise Pyjo.Exception.new(f'{code} response: {err["message"]}')
890
+ raise Pyjo.Exception.new(f'Download error: {err["message"]}')
891
+
892
+
893
+ def _croak(e: OSError, message: str) -> NoReturn:
894
+ error = type(e)(f'{message}: {e.strerror}' if e.strerror else message)
895
+ error.errno = e.errno
896
+ raise error from e
897
+
898
+
899
+ # File::Basename's dirname
900
+ def _dirname(path: str) -> str:
901
+ basename, dirname = _fileparse(path)
902
+ dirname = _strip_trailing_sep(dirname)
903
+ if not len(basename):
904
+ basename, dirname = _fileparse(dirname)
905
+ dirname = _strip_trailing_sep(dirname)
906
+ return dirname
907
+
908
+
909
+ # File::Basename's fileparse
910
+ def _fileparse(path: str) -> tuple[str, str]:
911
+ if _WIN32:
912
+ m = re.match(r'((?:.*[:\\/])?)(.*)', path, re.S)
913
+ assert m is not None
914
+ return m[2], m[1] if re.search(r'[\\/]\Z', m[1]) else m[1] + '.\\'
915
+ m = re.match(r'^(.*/)?(.*)$', path, re.S)
916
+ assert m is not None
917
+ return m[2], m[1] or './'
918
+
919
+
920
+ # File::Spec's file_name_is_absolute, 2 for a path with a volume on Windows
921
+ def _is_abs(path: str) -> int:
922
+ if not _WIN32:
923
+ return 1 if path.startswith('/') else 0
924
+ if m := re.match(r'[a-zA-Z]:|(?:\\\\|//)[^\\/]+[\\/][^\\/]+', path):
925
+ return 2 if m[0][1] != ':' or re.match(r'[a-zA-Z]:[\\/]', path) else 0
926
+ return 1 if re.match(r'[\\/]', path) else 0
927
+
928
+
929
+ def _is_temp(value: Anything) -> bool:
930
+ return isinstance(value, (_Temp, _tempfile.TemporaryDirectory)) or type(value).__name__ == '_TemporaryFileWrapper'
931
+
932
+
933
+ # File::Spec's splitdir
934
+ def _splitdir(path: str) -> 'builtins.list[str]':
935
+ if not _WIN32:
936
+ return path.split('/')
937
+ if not re.search(r'[\\/]\Z', path):
938
+ return re.split(r'[\\/]', path) if len(path) else []
939
+ return [*re.split(r'[\\/]', path)[:-1], '']
940
+
941
+
942
+ # File::Basename's _strip_trailing_sep
943
+ def _strip_trailing_sep(path: str) -> str:
944
+ if _WIN32:
945
+ return re.sub(r'([^:])[\\/]*\Z', r'\1', path)
946
+ return re.sub(r'(.)/*\Z', r'\1', path, flags=re.S)
947
+
948
+
949
+ def _str(value: Anything) -> str:
950
+ if _is_temp(value):
951
+ return str(cast(Any, value).name)
952
+ if isinstance(value, os.PathLike):
953
+ return str(os.fspath(value)) # pyright: ignore[reportUnknownArgumentType]
954
+ return str(value)
955
+
956
+
957
+ new = Pyjo_File.new
958
+ object = Pyjo_File
959
+ File = Pyjo_File