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.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- 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
|
+
'''
|