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
Pyjo/Server/Lambda.py
ADDED
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.Server.Lambda - AWS Lambda server
|
|
3
|
+
======================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Server.Lambda import Lambda
|
|
7
|
+
|
|
8
|
+
server = Lambda()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@server.unsubscribe('request').on('request')
|
|
12
|
+
def request(server, tx):
|
|
13
|
+
# Request
|
|
14
|
+
method = tx.req.method
|
|
15
|
+
path = tx.req.url.path
|
|
16
|
+
|
|
17
|
+
# Response
|
|
18
|
+
tx.res.code = 200
|
|
19
|
+
tx.res.headers.content_type = 'text/plain'
|
|
20
|
+
tx.res.body = f'{method} request for {path}!'.encode()
|
|
21
|
+
|
|
22
|
+
# Resume transaction
|
|
23
|
+
tx.resume()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
handler = server.to_lambda_app()
|
|
27
|
+
|
|
28
|
+
# Or import the module, as in Google Python Style Guide
|
|
29
|
+
|
|
30
|
+
import Pyjo.Server.Lambda
|
|
31
|
+
|
|
32
|
+
server = Pyjo.Server.Lambda.new()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@server.unsubscribe('request').on('request')
|
|
36
|
+
def request(server, tx):
|
|
37
|
+
# Request
|
|
38
|
+
method = tx.req.method
|
|
39
|
+
path = tx.req.url.path
|
|
40
|
+
|
|
41
|
+
# Response
|
|
42
|
+
tx.res.code = 200
|
|
43
|
+
tx.res.headers.content_type = 'text/plain'
|
|
44
|
+
tx.res.body = f'{method} request for {path}!'.encode()
|
|
45
|
+
|
|
46
|
+
# Resume transaction
|
|
47
|
+
tx.resume()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
handler = server.to_lambda_app()
|
|
51
|
+
|
|
52
|
+
:mod:`Pyjo.Server.Lambda` allows :mod:`Pyjoyment` applications to run as
|
|
53
|
+
event functions of `AWS Lambda <https://aws.amazon.com/lambda/>`_, whose
|
|
54
|
+
handler gets the event of an HTTP request and returns its response, with the
|
|
55
|
+
payload formats of API Gateway, of Function URLs and of Application Load
|
|
56
|
+
Balancers:
|
|
57
|
+
|
|
58
|
+
* the format 2.0 of HTTP APIs and Function URLs, whose ``cookies`` are the
|
|
59
|
+
``Cookie`` header of the request and the ``Set-Cookie`` headers of the
|
|
60
|
+
response, and whose stage, other than ``$default``, is the base path of the
|
|
61
|
+
URL;
|
|
62
|
+
* the format 1.0 of REST APIs, whose stage is the base path of the URL, and
|
|
63
|
+
whose decoded parameters of the query are encoded again;
|
|
64
|
+
* the events of Application Load Balancers, whose parameters of the query are
|
|
65
|
+
as they were sent, with the headers of multiple values when the target
|
|
66
|
+
group has them, and only the last value of a header otherwise.
|
|
67
|
+
|
|
68
|
+
The event and the context of the invocation are ``lambda.event`` and
|
|
69
|
+
``lambda.context`` of :attr:`~Pyjo.Message.Request.Pyjo_Message_Request.env`
|
|
70
|
+
of the request. A body that the event has in Base64 is decoded, and the body
|
|
71
|
+
of a response is Base64 too, unless its content type is text, JSON, XML,
|
|
72
|
+
JavaScript or YAML that is UTF-8. The handler is synchronous, so the event
|
|
73
|
+
loop of :mod:`Pyjo.IOLoop` runs until the response is complete, which can
|
|
74
|
+
come later and in chunks, whose framing is left out, as the response of an
|
|
75
|
+
event function is the whole body. ::
|
|
76
|
+
|
|
77
|
+
# The handler "myapp.handler" of an event function
|
|
78
|
+
handler = Pyjo.Server.Lambda.new(app=app).to_lambda_app()
|
|
79
|
+
|
|
80
|
+
With the invoke mode ``RESPONSE_STREAM`` of
|
|
81
|
+
:attr:`~Pyjo.Server.Lambda.Pyjo_Server_Lambda.invoke_mode`, which
|
|
82
|
+
follows the one of the Function URL or the response transfer mode of the
|
|
83
|
+
REST API of API Gateway, the handler returns an iterator of the body as it is
|
|
84
|
+
written instead, which the runtime client below streams: the status and the
|
|
85
|
+
headers as JSON, 8 null bytes, and the chunks of the body as they come, with
|
|
86
|
+
the content type ``application/vnd.awslambda.http-integration-response`` of
|
|
87
|
+
the iterator. The events of Application Load Balancers, which have no
|
|
88
|
+
streaming, get the whole response. ::
|
|
89
|
+
|
|
90
|
+
# PYJO_LAMBDA_INVOKE_MODE=RESPONSE_STREAM in the environment of the function
|
|
91
|
+
handler = Pyjo.Server.Lambda.new(app=app).to_lambda_app()
|
|
92
|
+
|
|
93
|
+
Events
|
|
94
|
+
------
|
|
95
|
+
|
|
96
|
+
:mod:`Pyjo.Server.Lambda` inherits all events from :mod:`Pyjo.Server`.
|
|
97
|
+
|
|
98
|
+
Attributes
|
|
99
|
+
----------
|
|
100
|
+
|
|
101
|
+
:mod:`Pyjo.Server.Lambda` inherits all attributes from :mod:`Pyjo.Server` and
|
|
102
|
+
implements :attr:`~Pyjo.Server.Lambda.Pyjo_Server_Lambda.invoke_mode`.
|
|
103
|
+
|
|
104
|
+
Runtime client
|
|
105
|
+
--------------
|
|
106
|
+
::
|
|
107
|
+
|
|
108
|
+
Usage: python -m Pyjo.Server.Lambda [OPTIONS] [HANDLER]
|
|
109
|
+
|
|
110
|
+
python -m Pyjo.Server.Lambda myapp.handler
|
|
111
|
+
AWS_LAMBDA_RUNTIME_API=127.0.0.1:9001 python -m Pyjo.Server.Lambda myapp.handler
|
|
112
|
+
|
|
113
|
+
Options:
|
|
114
|
+
-h, --help Show this message
|
|
115
|
+
|
|
116
|
+
The client of the `Runtime API
|
|
117
|
+
<https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html>`_ of AWS
|
|
118
|
+
Lambda, which runs the handler of an event function, ``module.function`` or
|
|
119
|
+
the one of ``_HANDLER``, as a custom runtime does: it gets the next
|
|
120
|
+
invocation from the API of ``AWS_LAMBDA_RUNTIME_API``, calls the handler with
|
|
121
|
+
its event and a context, and posts the value that the handler returns as
|
|
122
|
+
JSON, or its exception, one invocation after another. An iterator of bytes
|
|
123
|
+
that the handler returns is a streaming response, chunked as it is iterated,
|
|
124
|
+
of the ``content_type`` of the iterator or ``application/octet-stream``, with
|
|
125
|
+
an exception in the middle of it in the trailers of the error, as the Runtime
|
|
126
|
+
API takes it, and an exception before its first chunk as the error of the
|
|
127
|
+
invocation. An exception of the
|
|
128
|
+
import of the handler is an error of the initialization, which ends the
|
|
129
|
+
client. The handler can be any handler of an event function, also one of
|
|
130
|
+
another framework, which runs an event loop of its own, as the client runs
|
|
131
|
+
outside of the event loop of :mod:`Pyjo.IOLoop`, and the directory of the
|
|
132
|
+
handler, ``LAMBDA_TASK_ROOT`` or the current one, is the first one of
|
|
133
|
+
:data:`sys.path`. ::
|
|
134
|
+
|
|
135
|
+
#!/bin/sh
|
|
136
|
+
# The bootstrap of a custom runtime, as provided.al2023
|
|
137
|
+
exec python3 -m Pyjo.Server.Lambda
|
|
138
|
+
|
|
139
|
+
The context has the attributes and the method of the context of the Python
|
|
140
|
+
runtime of Lambda: ``aws_request_id``, ``invoked_function_arn``,
|
|
141
|
+
``function_name``, ``function_version``, ``memory_limit_in_mb``,
|
|
142
|
+
``log_group_name``, ``log_stream_name``, ``identity``, ``client_context``
|
|
143
|
+
and ``get_remaining_time_in_millis()``, from the headers of the invocation
|
|
144
|
+
and the variables of the environment that Lambda sets.
|
|
145
|
+
|
|
146
|
+
See also
|
|
147
|
+
--------
|
|
148
|
+
|
|
149
|
+
:mod:`Pyjoyment.Command.lambda`, :mod:`Pyjo.Server.WSGI`, :mod:`Pyjoyment`,
|
|
150
|
+
https://mojolicious.org.
|
|
151
|
+
|
|
152
|
+
Classes
|
|
153
|
+
-------
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
import Pyjo.Exception
|
|
157
|
+
import Pyjo.IOLoop
|
|
158
|
+
import Pyjo.Parameters
|
|
159
|
+
import Pyjo.Server
|
|
160
|
+
import Pyjo.Transaction
|
|
161
|
+
|
|
162
|
+
from Pyjo.Server.WSGI import _IO # pyright: ignore[reportPrivateUsage]
|
|
163
|
+
from Pyjo.Util import die, extract_usage, getopt, url_escape
|
|
164
|
+
|
|
165
|
+
import base64
|
|
166
|
+
import http.client
|
|
167
|
+
import importlib
|
|
168
|
+
import json
|
|
169
|
+
import os
|
|
170
|
+
import re
|
|
171
|
+
import sys
|
|
172
|
+
import time
|
|
173
|
+
import traceback
|
|
174
|
+
import wsgiref.util
|
|
175
|
+
|
|
176
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
177
|
+
from typing import Any, NoReturn, Self, cast
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
type Event = dict[str, Any]
|
|
181
|
+
type Response = dict[str, Any]
|
|
182
|
+
type Handler = Callable[[Event, Any], Response | Iterator[bytes]]
|
|
183
|
+
|
|
184
|
+
# The content types of a body that is text, which goes as it is, where other bodies go in Base64
|
|
185
|
+
_RE_TEXT = re.compile(r'(?:text/|application/(?:[\w.-]+\+)?(?:json|xml|javascript|yaml)\b)', re.I)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class Pyjo_Server_Lambda(Pyjo.Server.object):
|
|
189
|
+
"""
|
|
190
|
+
:mod:`Pyjo.Server.Lambda` inherits all attributes and methods from
|
|
191
|
+
:mod:`Pyjo.Server` and implements the following new ones.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
def __init__(self, *, invoke_mode: str | None = None, **kwargs: Any) -> None:
|
|
195
|
+
"""::
|
|
196
|
+
|
|
197
|
+
server = Pyjo.Server.Lambda.new()
|
|
198
|
+
server = Pyjo.Server.Lambda.new(invoke_mode='RESPONSE_STREAM')
|
|
199
|
+
|
|
200
|
+
Construct a new :mod:`Pyjo.Server.Lambda` object.
|
|
201
|
+
"""
|
|
202
|
+
super().__init__(**kwargs)
|
|
203
|
+
self._invoke_mode = invoke_mode
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def invoke_mode(self) -> str:
|
|
207
|
+
"""::
|
|
208
|
+
|
|
209
|
+
mode = server.invoke_mode
|
|
210
|
+
server.invoke_mode = 'RESPONSE_STREAM'
|
|
211
|
+
|
|
212
|
+
The mode of the invocations, as the ``InvokeMode`` of the Function
|
|
213
|
+
URL, or the response transfer mode of API Gateway, of the function,
|
|
214
|
+
which the function does not learn from its invocations: ``BUFFERED``
|
|
215
|
+
or ``RESPONSE_STREAM``, defaults to the value of the
|
|
216
|
+
``PYJO_LAMBDA_INVOKE_MODE`` environment variable or ``BUFFERED``.
|
|
217
|
+
"""
|
|
218
|
+
if self._invoke_mode is None:
|
|
219
|
+
self._invoke_mode = os.environ.get('PYJO_LAMBDA_INVOKE_MODE') or 'BUFFERED'
|
|
220
|
+
return self._invoke_mode
|
|
221
|
+
|
|
222
|
+
@invoke_mode.setter
|
|
223
|
+
def invoke_mode(self, value: str) -> None:
|
|
224
|
+
self._invoke_mode = value
|
|
225
|
+
|
|
226
|
+
# reportIncompatibleMethodOverride "Method "run" overrides class "Pyjo_Server" in an incompatible manner": run takes
|
|
227
|
+
# the event and the context, as the run of Pyjo.Server.WSGI takes the environment
|
|
228
|
+
def run( # pyright: ignore[reportIncompatibleMethodOverride]
|
|
229
|
+
self, event: Event, context: Any = None
|
|
230
|
+
) -> Response | Iterator[bytes]:
|
|
231
|
+
"""::
|
|
232
|
+
|
|
233
|
+
response = server.run(event, context)
|
|
234
|
+
|
|
235
|
+
Run the event of an HTTP request and return its response, or the
|
|
236
|
+
iterator of a streaming response with the invoke mode
|
|
237
|
+
``RESPONSE_STREAM``.
|
|
238
|
+
"""
|
|
239
|
+
request = _Request(event)
|
|
240
|
+
env = request.environ()
|
|
241
|
+
env['lambda.event'] = event
|
|
242
|
+
env['lambda.context'] = context
|
|
243
|
+
|
|
244
|
+
tx = self.build_tx()
|
|
245
|
+
req = tx.req.parse(env)
|
|
246
|
+
tx.remote_address = request.remote_address
|
|
247
|
+
if request.body:
|
|
248
|
+
req.parse(request.body)
|
|
249
|
+
|
|
250
|
+
# The response, which the application can give later
|
|
251
|
+
resumed: list[bool] = []
|
|
252
|
+
|
|
253
|
+
def resume(tx: Pyjo.Transaction.object) -> None:
|
|
254
|
+
resumed.append(True)
|
|
255
|
+
|
|
256
|
+
tx.on('resume', resume)
|
|
257
|
+
self.emit('request', tx)
|
|
258
|
+
while not resumed:
|
|
259
|
+
Pyjo.IOLoop.one_tick()
|
|
260
|
+
|
|
261
|
+
# The body, which can come later and in chunks, without their framing
|
|
262
|
+
res = tx.res.fix_headers()
|
|
263
|
+
body = _IO(tx, tx.is_empty, res.content.is_chunked)
|
|
264
|
+
|
|
265
|
+
# The headers without the hop-by-hop ones, as the body is without its framing
|
|
266
|
+
headers = [(name, value) for name, value in res.headers.to_list() if not wsgiref.util.is_hop_by_hop(name)]
|
|
267
|
+
code = res.code or 404
|
|
268
|
+
|
|
269
|
+
# The body as it is written, after the status and the headers, where the events of an Application Load Balancer
|
|
270
|
+
# have no streaming
|
|
271
|
+
if self.invoke_mode == 'RESPONSE_STREAM' and not request.alb:
|
|
272
|
+
return _Stream(request.headers({'statusCode': code}, headers), body)
|
|
273
|
+
|
|
274
|
+
# The whole body
|
|
275
|
+
try:
|
|
276
|
+
content = b''.join(body)
|
|
277
|
+
finally:
|
|
278
|
+
body.close()
|
|
279
|
+
response = request.headers({'statusCode': code}, headers)
|
|
280
|
+
if request.alb:
|
|
281
|
+
response['statusDescription'] = f'{code} {res.message or res.default_message(code)}'
|
|
282
|
+
|
|
283
|
+
# Text as it is, other content in Base64
|
|
284
|
+
content_type = res.headers.content_type or ''
|
|
285
|
+
if not content or _RE_TEXT.match(content_type) and _is_utf8(content):
|
|
286
|
+
response['body'] = content.decode()
|
|
287
|
+
response['isBase64Encoded'] = False
|
|
288
|
+
else:
|
|
289
|
+
response['body'] = base64.b64encode(content).decode('ascii')
|
|
290
|
+
response['isBase64Encoded'] = True
|
|
291
|
+
return response
|
|
292
|
+
|
|
293
|
+
def run_runtime(self) -> None:
|
|
294
|
+
"""::
|
|
295
|
+
|
|
296
|
+
server.run_runtime()
|
|
297
|
+
|
|
298
|
+
Run the application for the invocations of the Runtime API of
|
|
299
|
+
``AWS_LAMBDA_RUNTIME_API``, one after another, as the runtime client
|
|
300
|
+
does with a handler, until the process ends, so that the script of
|
|
301
|
+
the application is the runtime of a custom runtime itself.
|
|
302
|
+
"""
|
|
303
|
+
if not (api := os.environ.get('AWS_LAMBDA_RUNTIME_API')):
|
|
304
|
+
raise Pyjo.Exception.new('AWS_LAMBDA_RUNTIME_API is not set')
|
|
305
|
+
|
|
306
|
+
# Preload application
|
|
307
|
+
self.app.server(self)
|
|
308
|
+
runtime = _Runtime(api)
|
|
309
|
+
try:
|
|
310
|
+
runtime.loop(self.run)
|
|
311
|
+
finally:
|
|
312
|
+
runtime.connection.close()
|
|
313
|
+
|
|
314
|
+
def to_lambda_app(self) -> Handler:
|
|
315
|
+
"""::
|
|
316
|
+
|
|
317
|
+
handler = server.to_lambda_app()
|
|
318
|
+
|
|
319
|
+
Turn :mod:`Pyjoyment` application into the handler of an event
|
|
320
|
+
function of AWS Lambda.
|
|
321
|
+
"""
|
|
322
|
+
# Preload application and wrap it
|
|
323
|
+
self.app.server(self)
|
|
324
|
+
|
|
325
|
+
def handler(event: Event, context: Any) -> Response | Iterator[bytes]:
|
|
326
|
+
return self.run(event, context)
|
|
327
|
+
|
|
328
|
+
return handler
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# The streaming response of an HTTP integration of Function URLs and API Gateway: the status and the headers as JSON, 8
|
|
332
|
+
# null bytes and the body as it is written
|
|
333
|
+
class _Stream(Iterator[bytes]):
|
|
334
|
+
content_type = 'application/vnd.awslambda.http-integration-response'
|
|
335
|
+
|
|
336
|
+
def __init__(self, response: Response, body: _IO) -> None:
|
|
337
|
+
self._prelude: bytes | None = json.dumps(response, separators=(',', ':')).encode() + b'\x00' * 8
|
|
338
|
+
self._body = body
|
|
339
|
+
|
|
340
|
+
def __iter__(self) -> Self:
|
|
341
|
+
return self
|
|
342
|
+
|
|
343
|
+
def __next__(self) -> bytes:
|
|
344
|
+
if (prelude := self._prelude) is not None:
|
|
345
|
+
self._prelude = None
|
|
346
|
+
return prelude
|
|
347
|
+
return next(self._body)
|
|
348
|
+
|
|
349
|
+
def close(self) -> None:
|
|
350
|
+
self._body.close()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# The request of an event, in one of its payload formats
|
|
354
|
+
class _Request:
|
|
355
|
+
def __init__(self, event: Event) -> None:
|
|
356
|
+
self.event = event
|
|
357
|
+
context: dict[str, Any] = event.get('requestContext') or {}
|
|
358
|
+
self.alb = 'elb' in context
|
|
359
|
+
self.v2 = event.get('version') == '2.0'
|
|
360
|
+
self.pairs: list[tuple[str, str]]
|
|
361
|
+
|
|
362
|
+
# The body, which can be in Base64
|
|
363
|
+
body: str | None = event.get('body')
|
|
364
|
+
if body is None:
|
|
365
|
+
self.body = b''
|
|
366
|
+
elif event.get('isBase64Encoded'):
|
|
367
|
+
self.body = base64.b64decode(body)
|
|
368
|
+
else:
|
|
369
|
+
self.body = body.encode()
|
|
370
|
+
|
|
371
|
+
# The format 2.0, of HTTP APIs and Function URLs
|
|
372
|
+
if self.v2:
|
|
373
|
+
http: dict[str, Any] = context.get('http') or {}
|
|
374
|
+
self.method: str = http.get('method') or 'GET'
|
|
375
|
+
self.path: str = event.get('rawPath') or '/'
|
|
376
|
+
self.query: str = event.get('rawQueryString') or ''
|
|
377
|
+
self.pairs = _pairs(event.get('headers'))
|
|
378
|
+
if cookies := event.get('cookies'):
|
|
379
|
+
self.pairs.append(('cookie', '; '.join(cookies)))
|
|
380
|
+
self.remote_address: str | None = http.get('sourceIp')
|
|
381
|
+
self.protocol: str | None = http.get('protocol')
|
|
382
|
+
self.https = True
|
|
383
|
+
|
|
384
|
+
# A stage other than the default one in front of the path
|
|
385
|
+
stage = context.get('stage')
|
|
386
|
+
prefix = f'/{stage}'
|
|
387
|
+
within = stage and stage != '$default' and (self.path == prefix or self.path.startswith(f'{prefix}/'))
|
|
388
|
+
self.base = prefix if within else None
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
# The format 1.0, of REST APIs, and Application Load Balancers
|
|
392
|
+
self.method = event.get('httpMethod') or 'GET'
|
|
393
|
+
self.path = event.get('path') or '/'
|
|
394
|
+
multi = event.get('multiValueHeaders')
|
|
395
|
+
self.pairs = _pairs(multi if multi is not None else event.get('headers'))
|
|
396
|
+
pairs = _pairs(event.get('multiValueQueryStringParameters') or event.get('queryStringParameters'))
|
|
397
|
+
|
|
398
|
+
# The parameters of an Application Load Balancer as they were sent, and the decoded ones of a REST API
|
|
399
|
+
if self.alb:
|
|
400
|
+
self.query = '&'.join(f'{name}={value}' for name, value in pairs)
|
|
401
|
+
else:
|
|
402
|
+
query = Pyjo.Parameters.new()
|
|
403
|
+
for name, value in pairs:
|
|
404
|
+
query.append(name, value)
|
|
405
|
+
self.query = query.to_str()
|
|
406
|
+
identity: dict[str, Any] = context.get('identity') or {}
|
|
407
|
+
self.remote_address = identity.get('sourceIp')
|
|
408
|
+
self.protocol = context.get('protocol')
|
|
409
|
+
self.https = any(name.lower() == 'x-forwarded-proto' and value == 'https' for name, value in self.pairs)
|
|
410
|
+
|
|
411
|
+
# The stage of a REST API in front of the path
|
|
412
|
+
path: str = context.get('path') or ''
|
|
413
|
+
self.base = path[: -len(self.path)] or None if path != self.path and path.endswith(self.path) else None
|
|
414
|
+
|
|
415
|
+
def environ(self) -> dict[str, Any]:
|
|
416
|
+
# The CGI environment that Pyjo.Message.Request parses, with a path whose bytes beyond ASCII are
|
|
417
|
+
# percent-encoded, as the path is escaped
|
|
418
|
+
env: dict[str, Any] = {
|
|
419
|
+
'REQUEST_METHOD': self.method,
|
|
420
|
+
'PATH_INFO': url_escape(self.path.encode(), rb'\x80-\xff').decode('ascii'),
|
|
421
|
+
'QUERY_STRING': self.query,
|
|
422
|
+
'SERVER_PROTOCOL': self.protocol or 'HTTP/1.1',
|
|
423
|
+
}
|
|
424
|
+
if self.base is not None:
|
|
425
|
+
env['SCRIPT_NAME'] = self.base
|
|
426
|
+
if self.https:
|
|
427
|
+
env['HTTPS'] = 'on'
|
|
428
|
+
for name, value in self.pairs:
|
|
429
|
+
key = 'HTTP_' + name.upper().replace('-', '_')
|
|
430
|
+
|
|
431
|
+
# The body is whole, without the chunked framing
|
|
432
|
+
if key == 'HTTP_TRANSFER_ENCODING':
|
|
433
|
+
continue
|
|
434
|
+
env[key] = f'{env[key]}{"; " if key == "HTTP_COOKIE" else ", "}{value}' if key in env else value
|
|
435
|
+
if self.body:
|
|
436
|
+
env['CONTENT_LENGTH'] = str(len(self.body))
|
|
437
|
+
return env
|
|
438
|
+
|
|
439
|
+
def headers(self, response: Response, pairs: list[tuple[str, str]]) -> Response:
|
|
440
|
+
# The format 2.0 with its cookies, and its other headers of multiple values with commas
|
|
441
|
+
if self.v2:
|
|
442
|
+
headers: dict[str, str] = {}
|
|
443
|
+
cookies: list[str] = []
|
|
444
|
+
for name, value in pairs:
|
|
445
|
+
if (name := name.lower()) == 'set-cookie':
|
|
446
|
+
cookies.append(value)
|
|
447
|
+
else:
|
|
448
|
+
headers[name] = f'{headers[name]},{value}' if name in headers else value
|
|
449
|
+
response['headers'] = headers
|
|
450
|
+
if cookies:
|
|
451
|
+
response['cookies'] = cookies
|
|
452
|
+
|
|
453
|
+
# The headers of multiple values of the event, or the last value of each header
|
|
454
|
+
elif 'multiValueHeaders' in self.event:
|
|
455
|
+
multi: dict[str, list[str]] = {}
|
|
456
|
+
for name, value in pairs:
|
|
457
|
+
multi.setdefault(name, []).append(value)
|
|
458
|
+
response['multiValueHeaders'] = multi
|
|
459
|
+
else:
|
|
460
|
+
response['headers'] = dict(pairs)
|
|
461
|
+
return response
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def main(argv: Sequence[str] | None = None) -> NoReturn:
|
|
465
|
+
"""::
|
|
466
|
+
|
|
467
|
+
main(['myapp.handler'])
|
|
468
|
+
|
|
469
|
+
The command line interface of the runtime client: parse the options and
|
|
470
|
+
run the handler for the invocations of the Runtime API.
|
|
471
|
+
"""
|
|
472
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
473
|
+
opts = getopt(args, {'h|help': None})
|
|
474
|
+
name = args[0] if args else os.environ.get('_HANDLER')
|
|
475
|
+
if not opts or opts.help or not name:
|
|
476
|
+
# The usage without its last newline, as SystemExit writes the message with one
|
|
477
|
+
die(extract_usage(__file__, section='Runtime client').rstrip('\n'))
|
|
478
|
+
if not (api := os.environ.get('AWS_LAMBDA_RUNTIME_API')):
|
|
479
|
+
die('AWS_LAMBDA_RUNTIME_API is not set')
|
|
480
|
+
runtime = _Runtime(api)
|
|
481
|
+
try:
|
|
482
|
+
runtime.run(name)
|
|
483
|
+
finally:
|
|
484
|
+
runtime.connection.close()
|
|
485
|
+
sys.exit(0)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
# An invocation of the Runtime API, as the context of the Python runtime of Lambda has it
|
|
489
|
+
class _Context:
|
|
490
|
+
def __init__(self, headers: http.client.HTTPMessage) -> None:
|
|
491
|
+
self.aws_request_id = headers.get('Lambda-Runtime-Aws-Request-Id', '')
|
|
492
|
+
self.invoked_function_arn = headers.get('Lambda-Runtime-Invoked-Function-Arn', '')
|
|
493
|
+
self.function_name = os.environ.get('AWS_LAMBDA_FUNCTION_NAME')
|
|
494
|
+
self.function_version = os.environ.get('AWS_LAMBDA_FUNCTION_VERSION')
|
|
495
|
+
self.memory_limit_in_mb = os.environ.get('AWS_LAMBDA_FUNCTION_MEMORY_SIZE')
|
|
496
|
+
self.log_group_name = os.environ.get('AWS_LAMBDA_LOG_GROUP_NAME')
|
|
497
|
+
self.log_stream_name = os.environ.get('AWS_LAMBDA_LOG_STREAM_NAME')
|
|
498
|
+
self.identity = _json(headers.get('Lambda-Runtime-Cognito-Identity'))
|
|
499
|
+
self.client_context = _json(headers.get('Lambda-Runtime-Client-Context'))
|
|
500
|
+
deadline = headers.get('Lambda-Runtime-Deadline-Ms')
|
|
501
|
+
self.deadline = int(deadline) if deadline and deadline.isdigit() else None
|
|
502
|
+
|
|
503
|
+
def get_remaining_time_in_millis(self) -> int:
|
|
504
|
+
if self.deadline is None:
|
|
505
|
+
return 0
|
|
506
|
+
return max(self.deadline - int(time.time() * 1000), 0)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# The client of the Runtime API, with a connection of its own, outside of the event loop
|
|
510
|
+
class _Runtime:
|
|
511
|
+
PREFIX = '/2018-06-01/runtime'
|
|
512
|
+
|
|
513
|
+
def __init__(self, api: str) -> None:
|
|
514
|
+
self.connection = http.client.HTTPConnection(api)
|
|
515
|
+
|
|
516
|
+
def run(self, name: str) -> None:
|
|
517
|
+
try:
|
|
518
|
+
handler = _handler(name)
|
|
519
|
+
except Exception as e:
|
|
520
|
+
self.post(f'{self.PREFIX}/init/error', _error(e, 'Runtime.ImportModuleError'), 'Runtime.ImportModuleError')
|
|
521
|
+
raise SystemExit(1) from None
|
|
522
|
+
self.loop(handler)
|
|
523
|
+
|
|
524
|
+
# The invocations, one after another, for the handler
|
|
525
|
+
def loop(self, handler: Callable[[Any, Any], Any]) -> None:
|
|
526
|
+
while True:
|
|
527
|
+
self.connection.request('GET', f'{self.PREFIX}/invocation/next')
|
|
528
|
+
res = self.connection.getresponse()
|
|
529
|
+
event = json.loads(res.read() or b'null')
|
|
530
|
+
context = _Context(res.headers)
|
|
531
|
+
if trace := res.headers.get('Lambda-Runtime-Trace-Id'):
|
|
532
|
+
os.environ['_X_AMZN_TRACE_ID'] = trace
|
|
533
|
+
else:
|
|
534
|
+
os.environ.pop('_X_AMZN_TRACE_ID', None)
|
|
535
|
+
path = f'{self.PREFIX}/invocation/{context.aws_request_id}'
|
|
536
|
+
try:
|
|
537
|
+
result = handler(event, context)
|
|
538
|
+
if isinstance(result, Iterator):
|
|
539
|
+
self.stream(path, cast(Iterator[bytes], result))
|
|
540
|
+
continue
|
|
541
|
+
body = json.dumps(result).encode()
|
|
542
|
+
except Exception as e:
|
|
543
|
+
self.post(f'{path}/error', _error(e), 'Unhandled')
|
|
544
|
+
continue
|
|
545
|
+
self.post(f'{path}/response', body)
|
|
546
|
+
|
|
547
|
+
def post(self, path: str, body: bytes, error: str | None = None) -> None:
|
|
548
|
+
headers = {'Content-Type': 'application/json'}
|
|
549
|
+
if error is not None:
|
|
550
|
+
headers['Lambda-Runtime-Function-Error-Type'] = error
|
|
551
|
+
self.connection.request('POST', path, body, headers)
|
|
552
|
+
self.connection.getresponse().read()
|
|
553
|
+
|
|
554
|
+
# A streaming response, chunked as it is written, with an exception in the middle of it in the trailers, and the
|
|
555
|
+
# connection closed after it, as the Runtime API asks
|
|
556
|
+
def stream(self, path: str, body: Iterator[bytes]) -> None:
|
|
557
|
+
try:
|
|
558
|
+
# The first chunk before the request, so that an exception before the response is the error of the
|
|
559
|
+
# invocation, which the Runtime API takes only before it
|
|
560
|
+
try:
|
|
561
|
+
chunk = next(body, b'')
|
|
562
|
+
except Exception as e:
|
|
563
|
+
self.post(f'{path}/error', _error(e), 'Unhandled')
|
|
564
|
+
return
|
|
565
|
+
|
|
566
|
+
connection = self.connection
|
|
567
|
+
connection.putrequest('POST', f'{path}/response', skip_accept_encoding=True)
|
|
568
|
+
connection.putheader('Content-Type', getattr(body, 'content_type', 'application/octet-stream'))
|
|
569
|
+
connection.putheader('Lambda-Runtime-Function-Response-Mode', 'streaming')
|
|
570
|
+
connection.putheader('Transfer-Encoding', 'chunked')
|
|
571
|
+
connection.putheader('Trailer', 'Lambda-Runtime-Function-Error-Type, Lambda-Runtime-Function-Error-Body')
|
|
572
|
+
connection.endheaders()
|
|
573
|
+
trailers = b''
|
|
574
|
+
try:
|
|
575
|
+
while True:
|
|
576
|
+
if chunk:
|
|
577
|
+
connection.send(b'%x\x0d\x0a%s\x0d\x0a' % (len(chunk), chunk))
|
|
578
|
+
chunk = next(body)
|
|
579
|
+
except StopIteration:
|
|
580
|
+
pass
|
|
581
|
+
except Exception as e:
|
|
582
|
+
error = base64.b64encode(_error(e)).decode('ascii')
|
|
583
|
+
trailers = (
|
|
584
|
+
f'Lambda-Runtime-Function-Error-Type: Unhandled\x0d\x0a'
|
|
585
|
+
f'Lambda-Runtime-Function-Error-Body: {error}\x0d\x0a'
|
|
586
|
+
).encode()
|
|
587
|
+
connection.send(b'0\x0d\x0a' + trailers + b'\x0d\x0a')
|
|
588
|
+
connection.getresponse().read()
|
|
589
|
+
connection.close()
|
|
590
|
+
finally:
|
|
591
|
+
if close := getattr(body, 'close', None):
|
|
592
|
+
close()
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
# The error of an exception, as the Python runtime of Lambda posts it
|
|
596
|
+
def _error(e: Exception, type: str | None = None) -> bytes:
|
|
597
|
+
stack = traceback.format_list(traceback.extract_tb(e.__traceback__))
|
|
598
|
+
return json.dumps({'errorMessage': str(e), 'errorType': type or e.__class__.__name__, 'stackTrace': stack}).encode()
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
# The handler of a name "module.function", from the directory of the task first
|
|
602
|
+
def _handler(name: str) -> Callable[[Any, Any], Any]:
|
|
603
|
+
module, _, function = name.rpartition('.')
|
|
604
|
+
if not module:
|
|
605
|
+
raise ValueError(f'Bad handler "{name}": not a module and a function')
|
|
606
|
+
root = os.environ.get('LAMBDA_TASK_ROOT') or os.getcwd()
|
|
607
|
+
if root not in sys.path:
|
|
608
|
+
sys.path.insert(0, root)
|
|
609
|
+
handler = getattr(importlib.import_module(module), function, None)
|
|
610
|
+
if not callable(handler):
|
|
611
|
+
raise AttributeError(f'Handler "{function}" missing on module "{module}"')
|
|
612
|
+
return handler
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _json(value: str | None) -> Any:
|
|
616
|
+
return json.loads(value) if value else None
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _is_utf8(body: bytes) -> bool:
|
|
620
|
+
try:
|
|
621
|
+
body.decode()
|
|
622
|
+
except UnicodeDecodeError:
|
|
623
|
+
return False
|
|
624
|
+
return True
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
# The pairs of names and values of a dict of the event, whose values can be lists of values
|
|
628
|
+
def _pairs(values: Any) -> list[tuple[str, str]]:
|
|
629
|
+
pairs: list[tuple[str, str]] = []
|
|
630
|
+
for name, value in cast(dict[str, Any], values or {}).items():
|
|
631
|
+
for item in cast(list[Any], value) if isinstance(value, list) else [value]:
|
|
632
|
+
pairs.append((name, str(item)))
|
|
633
|
+
return pairs
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
new = Pyjo_Server_Lambda.new
|
|
637
|
+
object = Pyjo_Server_Lambda
|
|
638
|
+
Lambda = Pyjo_Server_Lambda
|
|
639
|
+
|
|
640
|
+
if __name__ == '__main__':
|
|
641
|
+
main()
|