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,372 @@
1
+ """
2
+ Pyjoyment.Command.Author.generate.lambda_zip - Lambda ZIP generator command
3
+ ===========================================================================
4
+ ::
5
+
6
+ Usage: APPLICATION generate lambda-zip [OPTIONS]
7
+
8
+ ./myapp.py generate lambda-zip
9
+ python -m MyApp generate lambda-zip
10
+ python -m MyApp generate lambda-zip --arch arm64
11
+ python -m MyApp generate lambda-zip --python 3.15t
12
+ python -m MyApp generate lambda-zip --no-layer
13
+ python -m MyApp generate lambda-zip --runtime python3.14
14
+
15
+ Options:
16
+ -a, --arch <name> Architecture of the function, "arm64" or "x86_64",
17
+ defaults to "x86_64"
18
+ -h, --help Show this summary of available options
19
+ --no-layer Put Python and Pyjoyment into the ZIP of the
20
+ function, without layers
21
+ -p, --python <version> Version of Python, as "3.14" or "3.15t" for the
22
+ free-threaded build, defaults to the version that
23
+ runs the generator
24
+ -r, --runtime <name> Runtime of the function, "provided.al2023" or one
25
+ of Python, as "python3.14", defaults to
26
+ "provided.al2023"
27
+
28
+ :mod:`Pyjoyment.Command.Author.generate.lambda_zip` generates the ZIP files
29
+ of an event function of AWS Lambda on a custom runtime, as
30
+ ``provided.al2023``, in ``dist``: ``function.zip``, with the application, its
31
+ dependencies and the ``bootstrap`` that runs it, ``python.zip``, a layer with
32
+ Python, which Lambda extracts in ``/opt``, so that the function runs on any
33
+ Python, as the free-threaded build or a version that the runtimes of Python of
34
+ Lambda do not have yet, and ``pyjoyment.zip``, a layer with Pyjoyment.
35
+
36
+ The ``bootstrap`` runs the script of the application, or its package, which
37
+ is the runtime of the function itself, as :mod:`Pyjoyment.Command.lambda`
38
+ detects, with the runtime client of :mod:`Pyjo.Server.Lambda`. A function
39
+ whose Function URL streams its responses has
40
+ ``PYJO_LAMBDA_INVOKE_MODE=RESPONSE_STREAM`` in its environment, as
41
+ :attr:`Pyjo.Server.Lambda.Pyjo_Server_Lambda.invoke_mode` says.
42
+
43
+ The files are made with uv, which has to be on ``PATH``. Python is the build
44
+ of python-build-standalone that uv installs, for Linux with glibc and the
45
+ architecture of ``--arch``: the interpreter and its standard library,
46
+ without IDLE, Tkinter, turtledemo, ensurepip and pip, which a function does
47
+ not use. The dependencies are the ones of ``pyproject.toml``, which
48
+ :mod:`Pyjoyment.Command.Author.generate.pyproject` generates, locked with
49
+ ``uv.lock`` if there is one, or the ones of the inline metadata of PEP 723 of
50
+ a :mod:`Pyjoyment.Lite` script that has it and no ``pyproject.toml``, locked
51
+ with its ``.lock`` file if there is one, with the wheels of Amazon Linux 2023,
52
+ ``manylinux_2_34``. The application is the files of the current directory,
53
+ without hidden ones, ``__pycache__`` and ``dist``, and a file of it that starts
54
+ with ``#!``, as the wrapper of ``AWS_LAMBDA_EXEC_WRAPPER``, is executable.
55
+
56
+ Python cannot write its bytecode in ``/var/task`` and ``/opt``, which are
57
+ read-only, so the generator compiles it with the Python of the same version
58
+ that uv finds or installs for the machine that it runs on, with the
59
+ invalidation mode ``unchecked-hash``, which Python uses without checking the
60
+ source. The files of the same input are the same, as the times in the ZIP are
61
+ fixed, so that a deployment that compares their hashes, as the one of
62
+ OpenTofu, changes only what has changed. They are written anew each time.
63
+
64
+ The layer of Pyjoyment has the packages ``Pyjo`` and ``Pyjoyment`` that the
65
+ dependencies install, with their bytecode, in
66
+ ``python/lib/python3.X/site-packages``, which the Python of ``python.zip`` and
67
+ the runtime client of the runtime of Python of Lambda of the same version both
68
+ put in their path, the runtime client unless ``PYTHONPATH`` is set, so that one
69
+ layer serves the functions of both, and a function can take a layer of
70
+ Pyjoyment that is published already in its place. A wrapper of
71
+ ``AWS_LAMBDA_EXEC_WRAPPER`` runs in the place of that runtime client, so its
72
+ function names the directory in ``PYTHONPATH``.
73
+
74
+ With ``--runtime`` of a runtime of Python of Lambda, as ``python3.14``, the
75
+ function runs on the Python of Lambda, of the version of the runtime, without
76
+ ``python.zip`` and without ``bootstrap``, with the handler of the application,
77
+ as ``app.handler`` for a script ``app.py`` that ends with
78
+ ``handler = app.start()``.
79
+
80
+ With ``--no-layer``, Python and Pyjoyment are in ``function.zip``, Python in
81
+ the directory ``python``, and the function needs no layer, at the cost of a
82
+ larger ZIP for each deployment.
83
+
84
+ This is a core command, that means it is always enabled and its code a good
85
+ example for learning to build new commands, you're welcome to fork it.
86
+
87
+ See :mod:`Pyjoyment.Commands` for a list of commands that are available by
88
+ default.
89
+
90
+ See also
91
+ --------
92
+
93
+ :mod:`Pyjoyment`, https://mojolicious.org.
94
+
95
+ It has no counterpart in Mojolicious.
96
+
97
+ Classes
98
+ -------
99
+ """
100
+
101
+ import Pyjo.Exception
102
+ import Pyjo.File
103
+ import Pyjoyment.Command
104
+
105
+ from Pyjo.Util import extract_usage, getopt
106
+
107
+ import os
108
+ import re
109
+ import shlex
110
+ import shutil
111
+ import stat
112
+ import subprocess
113
+ import sys
114
+ import zipfile
115
+
116
+
117
+ # The architectures of Lambda and the machines of Linux
118
+ _MACHINES = {'arm64': 'aarch64', 'x86_64': 'x86_64'}
119
+
120
+ # The parts of the standard library that a function does not use
121
+ _UNUSED = ('ensurepip', 'idlelib', 'site-packages', 'tkinter', 'turtledemo')
122
+
123
+ # The packages of Pyjoyment and its metadata, which go to a layer of their own
124
+ _PYJOYMENT = r'Pyjo|Pyjoyment|(?i:pyjoyment-[^-]+\.dist-info)'
125
+
126
+ # The requirements that uv exports, for uv pip install
127
+ _FORMAT = ('--format', 'requirements.txt', '-o')
128
+
129
+ # The bytecode of the modules of a directory, with their paths on Lambda, which compileall would join with the
130
+ # separator of the machine, a backslash on Windows, and in processes of their own, as "compileall -j 0" does, which
131
+ # are not forks of this one, whose state they would inherit
132
+ _COMPILE = r"""
133
+ import concurrent.futures, itertools, multiprocessing, os, py_compile, sys
134
+ root, prefix = sys.argv[1:]
135
+ files = [os.path.join(d, f) for d, _, names in sorted(os.walk(root)) for f in sorted(names) if f.endswith('.py')]
136
+ dfiles = [f'{prefix}/' + os.path.relpath(f, root).replace(os.sep, '/') for f in files]
137
+ mode = itertools.repeat(py_compile.PycInvalidationMode.UNCHECKED_HASH)
138
+ args = (files, itertools.repeat(None), dfiles, itertools.repeat(False), itertools.repeat(-1), mode, itertools.repeat(1))
139
+ context = multiprocessing.get_context('forkserver') if multiprocessing.get_start_method() == 'fork' else None
140
+ with concurrent.futures.ProcessPoolExecutor(mp_context=context) as pool:
141
+ sys.exit(not all(pool.map(py_compile.compile, *args, chunksize=64)))
142
+ """
143
+
144
+
145
+ class Pyjoyment_Command_Author_generate_lambda_zip(Pyjoyment.Command.object):
146
+ """
147
+ :mod:`Pyjoyment.Command.Author.generate.lambda_zip` inherits all
148
+ attributes and methods from :mod:`Pyjoyment.Command` and implements the
149
+ following new ones.
150
+ """
151
+
152
+ description = 'Generate ZIP files of AWS Lambda'
153
+ """::
154
+
155
+ description = lambda_zip.description
156
+ lambda_zip.description = 'Foo'
157
+
158
+ Short description of this command, used for the command list.
159
+ """
160
+
161
+ usage = extract_usage()
162
+ """::
163
+
164
+ usage = lambda_zip.usage
165
+ lambda_zip.usage = 'Foo'
166
+
167
+ Usage information for this command, used for the help screen.
168
+ """
169
+
170
+ def run(self, *args: str) -> None:
171
+ """::
172
+
173
+ lambda_zip.run(*sys.argv[1:])
174
+
175
+ Run this command.
176
+ """
177
+ arguments = list(args)
178
+ if not (
179
+ opts := getopt(arguments, {'a|arch=s': None, 'no-layer': None, 'p|python=s': None, 'r|runtime=s': None})
180
+ ):
181
+ raise Pyjo.Exception.new(self.usage)
182
+
183
+ arch: str = opts.arch or 'x86_64'
184
+ if (machine := _MACHINES.get(arch)) is None:
185
+ raise Pyjo.Exception.new(f'Unknown architecture: {arch}')
186
+ runtime: str = opts.runtime or 'provided.al2023'
187
+ version: str = opts.python or f'{sys.version_info.major}.{sys.version_info.minor}'
188
+ if managed := re.fullmatch(r'python(3\.\d+)', runtime):
189
+ if opts.python and opts.python != managed[1]:
190
+ raise Pyjo.Exception.new(f'Option --python is not for the runtime {runtime}')
191
+ version = managed[1]
192
+ elif runtime != 'provided.al2023':
193
+ raise Pyjo.Exception.new(f'Unknown runtime: {runtime}')
194
+ if not re.fullmatch(r'3\.\d+(?:\.\d+)?t?', version):
195
+ raise Pyjo.Exception.new(f'Unknown version of Python: {version}')
196
+ layer = not opts.no_layer
197
+
198
+ # The script of Pyjoyment.Lite or the package of the application, and the dependencies of the inline metadata of
199
+ # PEP 723 of the script, if it has them and there is no pyproject.toml, as generate dockerfile has them
200
+ exe = os.environ.get('PYJO_EXE')
201
+ script = Pyjo.File.path(exe).to_rel(self.app.home).to_str() if exe else None
202
+ cmd = [script] if script else ['-m', type(self.app).__module__]
203
+ project = os.path.isfile(self.rel_file('pyproject.toml')) or not exe or not _metadata(Pyjo.File.path(exe))
204
+
205
+ # The Python of the machine that compiles the bytecode and gives the tags of the wheels, of the same version,
206
+ # with the GIL unless the version is the free-threaded one, which uv does not take otherwise
207
+ local = version if version.endswith('t') else f'{version}+gil'
208
+
209
+ with Pyjo.File.tempdir() as tmp:
210
+ # Python, the interpreter and its standard library, in the directory of its full version, which the variables
211
+ # of its build name, and not the link of its minor version, or the Python of the runtime of Lambda
212
+ python = tmp.child('layer', 'python')
213
+ prefix = '/opt/python' if layer else '/var/task/python'
214
+ if managed:
215
+ interpreter = f'python{version}'
216
+ else:
217
+ install = tmp.child('install')
218
+ self._uv(
219
+ 'python', 'install', '--no-bin', '--install-dir', install, f'cpython-{version}-linux-{machine}-gnu'
220
+ )
221
+ home = next(p for p in sorted(os.listdir(install)) if re.match(r'cpython-\d+\.\d+\.\d+', p))
222
+ # The layer, which Lambda extracts in /opt, with Python in its directory python
223
+ interpreter = _python(install.child(home), python, prefix)
224
+
225
+ # The dependencies, without the scripts of their packages, which run the Python of the machine, and the lock
226
+ # of uv
227
+ function = tmp.child('function').make_path()
228
+ requirements = 'pyproject.toml'
229
+ if project and os.path.isfile(self.rel_file('uv.lock')):
230
+ requirements = tmp.child('requirements.txt').to_str()
231
+ self._uv('export', '--frozen', '--no-dev', '--no-emit-project', '--no-hashes', *_FORMAT, requirements)
232
+ elif not project:
233
+ frozen = ['--frozen'] if os.path.isfile(self.rel_file(f'{script}.lock')) else []
234
+ requirements = tmp.child('requirements.txt').to_str()
235
+ self._uv('export', '--script', script or '', *frozen, '--no-hashes', *_FORMAT, requirements)
236
+ platform = f'{machine}-manylinux_2_34'
237
+ deps = tmp.child('deps')
238
+ self._uv(
239
+ 'pip', 'install', '--target', deps, '--python', local, '--python-platform', platform, '-r', requirements
240
+ )
241
+ # Pyjoyment, in the site-packages of the layer, which the runtimes of Python and the Python of the layer
242
+ # put in their path
243
+ pyjoyment = tmp.child('pyjoyment')
244
+ site = pyjoyment.child('python', 'lib', interpreter, 'site-packages')
245
+ if layer:
246
+ names = sorted(os.listdir(deps)) if os.path.isdir(deps) else []
247
+ if 'Pyjoyment' not in names:
248
+ raise Pyjo.Exception.new('Pyjoyment is not among the dependencies')
249
+ site.make_path()
250
+ for name in filter(lambda n: re.fullmatch(_PYJOYMENT, n), names):
251
+ shutil.move(deps.child(name).to_str(), site.child(name).to_str())
252
+ if os.path.isdir(deps):
253
+ top = os.fspath(deps)
254
+ shutil.copytree(
255
+ deps, function, dirs_exist_ok=True, ignore=lambda dir, _: ['.lock', 'bin'] if dir == top else []
256
+ )
257
+
258
+ # The application, with its scripts executable, and the bootstrap that runs it
259
+ executables = {'bootstrap', f'python/bin/{interpreter}'}
260
+ for file in Pyjo.File.path().list_tree().each():
261
+ name = file.to_rel(Pyjo.File.path()).to_str().replace(os.sep, '/')
262
+ if name.split('/')[0] != 'dist' and '__pycache__' not in name.split('/'):
263
+ destination = function.child(*name.split('/'))
264
+ destination.dirname().make_path()
265
+ shutil.copyfile(file.to_str(), destination.to_str())
266
+ with open(file, 'rb') as source:
267
+ if source.read(2) == b'#!':
268
+ executables.add(name)
269
+ if not managed:
270
+ executable = f'{prefix}/bin/{interpreter}' if layer else f'"$LAMBDA_TASK_ROOT/python/bin/{interpreter}"'
271
+ function.child('bootstrap').spew(
272
+ self.render_data('bootstrap', {'cmd': shlex.join(cmd), 'python': executable})
273
+ )
274
+
275
+ # The bytecode, as /var/task and /opt are read-only
276
+ if not managed:
277
+ self._compile(local, python, prefix)
278
+ if layer:
279
+ self._compile(local, site, f'/opt/python/lib/{interpreter}/site-packages')
280
+ self._compile(local, function, '/var/task')
281
+
282
+ # The ZIP files, without the layers of an earlier build that this one does not write
283
+ dist = self.rel_file('dist')
284
+ self.create_dir(dist)
285
+ for name in ('python.zip', 'pyjoyment.zip'):
286
+ dist.child(name).remove()
287
+ if layer and not managed:
288
+ _zip(python.dirname(), dist.child('python.zip'), {f'python/bin/{interpreter}'})
289
+ self._loud(f' [write] {dist.child("python.zip")}')
290
+ elif not managed:
291
+ shutil.copytree(python, function.child('python'))
292
+ if layer:
293
+ _zip(pyjoyment, dist.child('pyjoyment.zip'), set())
294
+ self._loud(f' [write] {dist.child("pyjoyment.zip")}')
295
+ _zip(function, dist.child('function.zip'), executables)
296
+ self._loud(f' [write] {dist.child("function.zip")}')
297
+
298
+ # The bytecode of a set of strings, as in "x in {'a', 'b'}", has the order of their hashes, which a fixed seed keeps
299
+ def _compile(self, local: str, dir: Pyjo.File.object, prefix: str) -> None:
300
+ env = {**os.environ, 'PYTHONHASHSEED': '0'}
301
+ self._uv('run', '--no-project', '--python', local, '--', 'python', '-c', _COMPILE, dir, prefix, env=env)
302
+
303
+ def _uv(self, *args: str | Pyjo.File.object, env: dict[str, str] | None = None) -> None:
304
+ command = [*_uv(), '--quiet', *map(str, args)]
305
+ if (code := subprocess.run(command, env=env).returncode) != 0:
306
+ raise Pyjo.Exception.new(f'Command "uv {" ".join(map(str, args[:2]))}" failed with exit status {code}')
307
+
308
+
309
+ # The interpreter and its standard library, without what a function does not use, with the prefix of Python on Lambda in
310
+ # the variables of the build, where uv puts the directory that it installs in, and the name of the interpreter
311
+ def _python(home: Pyjo.File.object, python: Pyjo.File.object, prefix: str) -> str:
312
+ bin = home.child('bin')
313
+ names = sorted(os.listdir(bin))
314
+ interpreter = next(n for n in names if re.fullmatch(r'python3\.\d+t?', n) and not os.path.islink(bin.child(n)))
315
+ python.child('bin').make_path()
316
+ shutil.copyfile(bin.child(interpreter).to_str(), python.child('bin', interpreter).to_str())
317
+ library = python.child('lib', interpreter)
318
+ shutil.copytree(home.child('lib', interpreter), library, symlinks=True)
319
+ for name in os.listdir(library):
320
+ if name in _UNUSED or name.startswith('config-'):
321
+ shutil.rmtree(library.child(name))
322
+ elif name.startswith('_sysconfigdata_') and name.endswith('.py'):
323
+ # The directory in the literals of Python, with the backslashes of Windows escaped
324
+ data = library.child(name)
325
+ text = str(data.slurp('UTF-8')).replace(home.to_str().replace('\\', '\\\\'), prefix)
326
+ data.spew(text.replace(home.to_str(), prefix), 'UTF-8')
327
+ for name in os.listdir(library.child('lib-dynload')):
328
+ if name.startswith('_tkinter'):
329
+ os.remove(library.child('lib-dynload', name))
330
+ return interpreter
331
+
332
+
333
+ # Whether the script has the inline metadata of PEP 723
334
+ def _metadata(path: Pyjo.File.object) -> bool:
335
+ try:
336
+ return re.search(r'(?m)^# /// script\r?$', str(path.slurp('UTF-8') or '')) is not None
337
+ except OSError:
338
+ return False
339
+
340
+
341
+ # The command of uv
342
+ def _uv() -> list[str]:
343
+ if (uv := shutil.which('uv')) is None:
344
+ raise Pyjo.Exception.new('Command "uv" not found')
345
+ return [uv]
346
+
347
+
348
+ # A ZIP file of the directory, the same for the same files, with the modes of Lambda, which the files of Windows do not
349
+ # have, and without the symbolic links of Python, which the interpreter does not need
350
+ def _zip(dir: Pyjo.File.object, path: Pyjo.File.object, executables: set[str]) -> None:
351
+ with zipfile.ZipFile(path.to_str(), 'w', zipfile.ZIP_DEFLATED) as archive:
352
+ for file in dir.list_tree(hidden=True).each():
353
+ if os.path.islink(file):
354
+ continue
355
+ name = file.to_rel(dir).to_str().replace(os.sep, '/')
356
+ info = zipfile.ZipInfo(name, (1980, 1, 1, 0, 0, 0))
357
+ info.compress_type = zipfile.ZIP_DEFLATED
358
+ info.external_attr = (stat.S_IFREG | (0o755 if name in executables else 0o644)) << 16
359
+ with open(file, 'rb') as source, archive.open(info, 'w') as destination:
360
+ shutil.copyfileobj(source, destination)
361
+
362
+
363
+ new = Pyjoyment_Command_Author_generate_lambda_zip.new
364
+ object = Pyjoyment_Command_Author_generate_lambda_zip
365
+
366
+ __DATA__ = r"""
367
+ @@ bootstrap
368
+ #!/bin/sh
369
+ # The runtime of the event function: the application, with the runtime client of Pyjo.Server.Lambda
370
+ cd "$LAMBDA_TASK_ROOT" || exit 1
371
+ exec <%= python %> <%= cmd %>
372
+ """
@@ -0,0 +1,113 @@
1
+ """
2
+ Pyjoyment.Command.Author.generate.lite_app - Lite app generator command
3
+ =======================================================================
4
+ ::
5
+
6
+ Usage: APPLICATION generate lite-app [OPTIONS] [NAME]
7
+
8
+ pyjo generate lite-app
9
+ pyjo generate lite-app foo.py
10
+
11
+ Options:
12
+ -h, --help Show this summary of available options
13
+
14
+ :mod:`Pyjoyment.Command.Author.generate.lite_app` generate fully functional
15
+ :mod:`Pyjoyment.Lite` applications.
16
+
17
+ This is a core command, that means it is always enabled and its code a good
18
+ example for learning to build new commands, you're welcome to fork it.
19
+
20
+ See :mod:`Pyjoyment.Commands` for a list of commands that are available by
21
+ default.
22
+
23
+ See also
24
+ --------
25
+
26
+ :mod:`Pyjoyment`, https://mojolicious.org.
27
+
28
+ Ported from `Mojolicious::Command::Author::generate::lite_app
29
+ <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojolicious/Command/Author/generate/lite_app.pm>`_ of
30
+ Mojolicious 9.49.
31
+
32
+ Classes
33
+ -------
34
+ """
35
+
36
+ import Pyjoyment.Command
37
+
38
+ from Pyjo.Util import extract_usage
39
+
40
+
41
+ class Pyjoyment_Command_Author_generate_lite_app(Pyjoyment.Command.object):
42
+ """
43
+ :mod:`Pyjoyment.Command.Author.generate.lite_app` inherits all attributes
44
+ and methods from :mod:`Pyjoyment.Command` and implements the following new
45
+ ones.
46
+ """
47
+
48
+ description = 'Generate Pyjoyment.Lite application'
49
+ """::
50
+
51
+ description = app.description
52
+ app.description = 'Foo'
53
+
54
+ Short description of this command, used for the command list.
55
+ """
56
+
57
+ usage = extract_usage()
58
+ """::
59
+
60
+ usage = app.usage
61
+ app.usage = 'Foo'
62
+
63
+ Usage information for this command, used for the help screen.
64
+ """
65
+
66
+ def run(self, name: str | None = None, *args: str) -> None:
67
+ """::
68
+
69
+ app.run(*sys.argv[1:])
70
+
71
+ Run this command.
72
+ """
73
+ name = name or 'myapp.py'
74
+ self.render_to_rel_file('liteapp', name)
75
+ self.chmod_rel_file(name, 0o744)
76
+
77
+
78
+ new = Pyjoyment_Command_Author_generate_lite_app.new
79
+ object = Pyjoyment_Command_Author_generate_lite_app
80
+
81
+ __DATA__ = r'''
82
+ @@ liteapp
83
+ #!/usr/bin/env python
84
+ """
85
+ <% %>__DATA__
86
+
87
+ <% %>@@ index.html.ep
88
+ %% c.layout('default')
89
+ %% c.title('Welcome')
90
+ <h1>Welcome to the Pyjoyment real-time web framework!</h1>
91
+
92
+ <% %>@@ layouts/default.html.ep
93
+ <!DOCTYPE html>
94
+ <html>
95
+ <head><title><%%= c.title() %></title></head>
96
+ <body><%%= content() %></body>
97
+ </html>
98
+ """
99
+
100
+ from Pyjoyment.Lite import *
101
+
102
+ %# Not in Mojolicious: the plugin Health, whose endpoints the probes of containers and orchestrators request
103
+ # Liveness and readiness for containers and orchestrators
104
+ plugin('Health')
105
+
106
+
107
+ @get('/')
108
+ def index(c):
109
+ c.render(template='index')
110
+
111
+
112
+ app.start()
113
+ '''