Pyjoyment 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. Pyjo/App/ASGI.py +735 -0
  2. Pyjo/App/CGI.py +773 -0
  3. Pyjo/App/FastCGI.py +631 -0
  4. Pyjo/App/Lambda.py +987 -0
  5. Pyjo/App/WSGI.py +427 -0
  6. Pyjo/App/__init__.py +409 -0
  7. Pyjo/Asset/File.py +377 -0
  8. Pyjo/Asset/Memory.py +235 -0
  9. Pyjo/Asset/__init__.py +200 -0
  10. Pyjo/Base.py +215 -0
  11. Pyjo/Cache.py +119 -0
  12. Pyjo/Collection.py +570 -0
  13. Pyjo/Content/MultiPart.py +325 -0
  14. Pyjo/Content/Single.py +193 -0
  15. Pyjo/Content/__init__.py +806 -0
  16. Pyjo/Cookie/Request.py +90 -0
  17. Pyjo/Cookie/Response.py +294 -0
  18. Pyjo/Cookie/__init__.py +143 -0
  19. Pyjo/DOM/CSS.py +1019 -0
  20. Pyjo/DOM/HTML.py +550 -0
  21. Pyjo/DOM/__init__.py +1329 -0
  22. Pyjo/Date.py +294 -0
  23. Pyjo/EventEmitter.py +522 -0
  24. Pyjo/Exception.py +494 -0
  25. Pyjo/File.py +959 -0
  26. Pyjo/GetOpt.py +605 -0
  27. Pyjo/Headers.py +760 -0
  28. Pyjo/HelloWorld.py +77 -0
  29. Pyjo/Home.py +109 -0
  30. Pyjo/IOLoop/ChildProcess.py +591 -0
  31. Pyjo/IOLoop/Client.py +736 -0
  32. Pyjo/IOLoop/Datagram.py +982 -0
  33. Pyjo/IOLoop/Messages.py +664 -0
  34. Pyjo/IOLoop/Server.py +458 -0
  35. Pyjo/IOLoop/Stdio.py +446 -0
  36. Pyjo/IOLoop/Stream.py +644 -0
  37. Pyjo/IOLoop/Subprocess.py +419 -0
  38. Pyjo/IOLoop/TLS.py +360 -0
  39. Pyjo/IOLoop/__init__.py +1036 -0
  40. Pyjo/IOLoop/certs/README.md +41 -0
  41. Pyjo/IOLoop/certs/ca.crt +20 -0
  42. Pyjo/IOLoop/certs/server.crt +21 -0
  43. Pyjo/IOLoop/certs/server.key +28 -0
  44. Pyjo/JSON/Pointer.py +135 -0
  45. Pyjo/JSON/RPC.py +772 -0
  46. Pyjo/JSON/Schema.py +2506 -0
  47. Pyjo/JSON/__init__.py +573 -0
  48. Pyjo/Loader.py +342 -0
  49. Pyjo/Log.py +723 -0
  50. Pyjo/Message/Request.py +725 -0
  51. Pyjo/Message/Response.py +476 -0
  52. Pyjo/Message/__init__.py +830 -0
  53. Pyjo/Parameters.py +472 -0
  54. Pyjo/Path.py +419 -0
  55. Pyjo/Promise.py +894 -0
  56. Pyjo/Prove.py +1305 -0
  57. Pyjo/Reactor/Asyncio.py +854 -0
  58. Pyjo/Reactor/Poll.py +439 -0
  59. Pyjo/Reactor/__init__.py +525 -0
  60. Pyjo/SSE.py +119 -0
  61. Pyjo/Server/ASGI.py +486 -0
  62. Pyjo/Server/CGI.py +259 -0
  63. Pyjo/Server/Daemon.py +1007 -0
  64. Pyjo/Server/FastCGI.py +517 -0
  65. Pyjo/Server/Hot/__init__.py +687 -0
  66. Pyjo/Server/Hot/__main__.py +5 -0
  67. Pyjo/Server/Lambda.py +641 -0
  68. Pyjo/Server/Prefork.py +917 -0
  69. Pyjo/Server/Prethread.py +394 -0
  70. Pyjo/Server/Reload/Backend/Poll.py +114 -0
  71. Pyjo/Server/Reload/Backend/__init__.py +126 -0
  72. Pyjo/Server/Reload/__init__.py +482 -0
  73. Pyjo/Server/Reload/__main__.py +5 -0
  74. Pyjo/Server/Stdio.py +228 -0
  75. Pyjo/Server/WSGI.py +349 -0
  76. Pyjo/Server/__init__.py +396 -0
  77. Pyjo/String/Bytes.py +532 -0
  78. Pyjo/String/HTML.py +210 -0
  79. Pyjo/String/Mixin.py +333 -0
  80. Pyjo/String/Template.py +470 -0
  81. Pyjo/String/Unicode.py +553 -0
  82. Pyjo/String/__init__.py +0 -0
  83. Pyjo/TOML.py +232 -0
  84. Pyjo/Template.py +1290 -0
  85. Pyjo/Test/Pyjo.py +1454 -0
  86. Pyjo/Test/__init__.py +1233 -0
  87. Pyjo/Test/pytest_plugin.py +305 -0
  88. Pyjo/Transaction/HTTP.py +374 -0
  89. Pyjo/Transaction/WebSocket.py +709 -0
  90. Pyjo/Transaction/__init__.py +429 -0
  91. Pyjo/URL/Template.py +459 -0
  92. Pyjo/URL/__init__.py +651 -0
  93. Pyjo/Upload.py +139 -0
  94. Pyjo/UserAgent/CookieJar.py +391 -0
  95. Pyjo/UserAgent/Proxy.py +138 -0
  96. Pyjo/UserAgent/Server.py +239 -0
  97. Pyjo/UserAgent/Transactor.py +766 -0
  98. Pyjo/UserAgent/__init__.py +1243 -0
  99. Pyjo/Util.py +3567 -0
  100. Pyjo/WebSocket.py +332 -0
  101. Pyjo/YAML.py +1167 -0
  102. Pyjo/__init__.py +441 -0
  103. Pyjo/__main__.py +35 -0
  104. Pyjo/py.typed +0 -0
  105. Pyjoyment/Command/Author/__init__.py +20 -0
  106. Pyjoyment/Command/Author/generate/__init__.py +106 -0
  107. Pyjoyment/Command/Author/generate/app.py +299 -0
  108. Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
  109. Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
  110. Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
  111. Pyjoyment/Command/Author/generate/lite_app.py +113 -0
  112. Pyjoyment/Command/Author/generate/plugin.py +267 -0
  113. Pyjoyment/Command/Author/generate/pyproject.py +123 -0
  114. Pyjoyment/Command/Author/inflate.py +101 -0
  115. Pyjoyment/Command/__init__.py +332 -0
  116. Pyjoyment/Command/asgi.py +92 -0
  117. Pyjoyment/Command/cgi.py +90 -0
  118. Pyjoyment/Command/daemon.py +160 -0
  119. Pyjoyment/Command/eval.py +128 -0
  120. Pyjoyment/Command/fastcgi.py +99 -0
  121. Pyjoyment/Command/get.py +305 -0
  122. Pyjoyment/Command/lambda.py +117 -0
  123. Pyjoyment/Command/prefork.py +181 -0
  124. Pyjoyment/Command/prethread.py +126 -0
  125. Pyjoyment/Command/routes.py +133 -0
  126. Pyjoyment/Command/stdio.py +92 -0
  127. Pyjoyment/Command/version.py +141 -0
  128. Pyjoyment/Command/wsgi.py +93 -0
  129. Pyjoyment/Commands.py +546 -0
  130. Pyjoyment/Controller.py +1307 -0
  131. Pyjoyment/HelloWorld.py +56 -0
  132. Pyjoyment/Lite.py +494 -0
  133. Pyjoyment/Plugin/AllowedHosts.py +205 -0
  134. Pyjoyment/Plugin/Config.py +248 -0
  135. Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
  136. Pyjoyment/Plugin/EPLRenderer.py +134 -0
  137. Pyjoyment/Plugin/EPRenderer.py +212 -0
  138. Pyjoyment/Plugin/HeaderCondition.py +98 -0
  139. Pyjoyment/Plugin/Health.py +325 -0
  140. Pyjoyment/Plugin/JSONConfig.py +172 -0
  141. Pyjoyment/Plugin/Mount.py +223 -0
  142. Pyjoyment/Plugin/PyRenderer.py +209 -0
  143. Pyjoyment/Plugin/TOMLConfig.py +141 -0
  144. Pyjoyment/Plugin/TagHelpers.py +959 -0
  145. Pyjoyment/Plugin/YAMLConfig.py +178 -0
  146. Pyjoyment/Plugin/__init__.py +80 -0
  147. Pyjoyment/Plugins.py +215 -0
  148. Pyjoyment/Renderer.py +673 -0
  149. Pyjoyment/Routes/Match.py +251 -0
  150. Pyjoyment/Routes/Pattern.py +443 -0
  151. Pyjoyment/Routes/Route.py +827 -0
  152. Pyjoyment/Routes/__init__.py +419 -0
  153. Pyjoyment/Sessions.py +255 -0
  154. Pyjoyment/Static.py +421 -0
  155. Pyjoyment/Types.py +238 -0
  156. Pyjoyment/Validator/Validation.py +395 -0
  157. Pyjoyment/Validator/__init__.py +304 -0
  158. Pyjoyment/__init__.py +1083 -0
  159. Pyjoyment/__main__.py +5 -0
  160. Pyjoyment/py.typed +0 -0
  161. Pyjoyment/resources/public/favicon.ico +0 -0
  162. Pyjoyment/resources/public/favicon.svg +345 -0
  163. Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
  164. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
  165. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
  166. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
  167. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
  168. Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
  169. Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
  170. Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
  171. Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
  172. Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
  173. Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
  174. Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
  175. Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
  176. Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
  177. Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
  178. Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
  179. Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
  180. Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
  181. Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
  182. Pyjoyment/resources/public/pyjo/logo.png +0 -0
  183. Pyjoyment/resources/public/pyjo/logo.svg +346 -0
  184. Pyjoyment/resources/public/pyjo/notfound.png +0 -0
  185. Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
  186. Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
  187. Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
  188. Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
  189. Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
  190. Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
  191. Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
  192. Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
  193. Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
  194. Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
  195. Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
  196. pyjoyment-0.1.0.dist-info/METADATA +376 -0
  197. pyjoyment-0.1.0.dist-info/RECORD +200 -0
  198. pyjoyment-0.1.0.dist-info/WHEEL +4 -0
  199. pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
  200. pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjo/App/Lambda.py ADDED
@@ -0,0 +1,987 @@
1
+ """
2
+ Pyjo.App.Lambda - AWS Lambda event function
3
+ ===========================================
4
+ ::
5
+
6
+ from Pyjo.App.Lambda import Lambda
7
+
8
+ app = Lambda(function='myfunction.handler')
9
+
10
+ # Or import the module, as in Google Python Style Guide
11
+
12
+ import Pyjo.App.Lambda
13
+
14
+ app = Pyjo.App.Lambda.new(function='myfunction.handler')
15
+
16
+ # Another runtime, with the events of a REST API
17
+ app = Pyjo.App.Lambda.new(command=['node', 'runtime.js'], payload='1.0')
18
+
19
+ # Mount it in an application of Pyjoyment
20
+ import Pyjoyment
21
+
22
+ pyjoyment = Pyjoyment.new()
23
+ pyjoyment.plugin('Mount', {'/function': app})
24
+
25
+ :mod:`Pyjo.App.Lambda` is an application of :mod:`Pyjo` that runs an event
26
+ function of `AWS Lambda <https://aws.amazon.com/lambda/>`_, so that a server
27
+ of :mod:`Pyjo` serves it, a route of :mod:`Pyjoyment` embeds it, as
28
+ :mod:`Pyjoyment.Plugin.Mount` does, and :mod:`Pyjo.Test.Pyjo` tests it, as
29
+ API Gateway, a Function URL or an Application Load Balancer runs it on AWS.
30
+
31
+ The function runs in a process of its own, as an execution environment of
32
+ Lambda does, which :mod:`Pyjo.IOLoop.ChildProcess` starts on the first
33
+ request and keeps for the next ones, which it gets one after another. Each
34
+ event loop has a process of its own, and the child of a fork, as a worker of
35
+ :mod:`Pyjo.Server.Prefork`, leaves the processes of its parent to it and
36
+ starts its own. The process gets its invocations from the `Runtime API
37
+ <https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html>`_ of
38
+ ``AWS_LAMBDA_RUNTIME_API``, which a :mod:`Pyjo.Server.Daemon` on a port of
39
+ the loopback serves on the same event loop, so that a handler of Python runs
40
+ with the runtime client of :mod:`Pyjo.Server.Lambda`, also one that runs an
41
+ event loop of its own, and a function of another runtime with its own runtime
42
+ client, without the event loop waiting for it.
43
+
44
+ The request is the event of the payload format of :attr:`payload`, as
45
+ :mod:`Pyjo.Server.Lambda` takes it, and the path that a route embeds the
46
+ application under is the stage of API Gateway, in front of the path, which
47
+ :mod:`Pyjo.Server.Lambda` takes for the base path of the URL. A body that is
48
+ not text, JSON, XML, JavaScript or YAML in UTF-8 is in Base64. The response is
49
+ the one of the payload format, with its ``statusCode``, its headers, its
50
+ cookies and its body, which can be in Base64, and in the format ``2.0`` a
51
+ value without ``statusCode`` is a response ``200`` of the value as JSON, as API
52
+ Gateway takes it.
53
+
54
+ A streaming response of the runtime client, as the one of
55
+ :mod:`Pyjo.Server.Lambda` with the invoke mode ``RESPONSE_STREAM``, is
56
+ written as it comes, as a Function URL with the invoke mode
57
+ ``RESPONSE_STREAM`` writes it: the status and the headers of its prelude, in
58
+ the content type ``application/vnd.awslambda.http-integration-response``,
59
+ before 8 null bytes, or ``200`` and the content type of the response, and its
60
+ body in chunks. The timeout of the function goes on until its end, and an
61
+ error in its trailers, or the timeout, in the middle of it closes the
62
+ connection, as the response cannot change anymore. ::
63
+
64
+ # The streaming responses of an application on Pyjo.Server.Lambda
65
+ app = Pyjo.App.Lambda.new(function='myapp.handler', env={'PYJO_LAMBDA_INVOKE_MODE': 'RESPONSE_STREAM'})
66
+
67
+ An error of the function or of its initialization, a response that is not
68
+ valid, a process that ends before its response, and an invocation that takes
69
+ longer than :attr:`timeout`, whose process is killed, as Lambda does, are a
70
+ response ``502``, logged, and the next request starts a new process. The
71
+ output of the process is logged, as Lambda sends it to CloudWatch. The event
72
+ loop does not watch pipes on Windows, so the response is ``502`` there.
73
+
74
+ Attributes
75
+ ----------
76
+
77
+ :mod:`Pyjo.App.Lambda` inherits all attributes from :mod:`Pyjo.App` and
78
+ implements the following new ones.
79
+
80
+ See also
81
+ --------
82
+
83
+ :mod:`Pyjoyment`, :mod:`Pyjoyment.Plugin.Mount`, :mod:`Pyjo.Server.Lambda`,
84
+ :mod:`Pyjo.App.CGI`, https://mojolicious.org.
85
+
86
+ It has no counterpart in Mojolicious; it is the reverse of
87
+ :mod:`Pyjo.Server.Lambda`.
88
+
89
+ Classes
90
+ -------
91
+ """
92
+
93
+ import Pyjo.App
94
+ import Pyjo.Exception
95
+ import Pyjo.IOLoop
96
+ import Pyjo.Server.Daemon
97
+ import Pyjo.Transaction
98
+ import Pyjo.Transaction.HTTP
99
+
100
+ from Pyjo.Server.Lambda import _RE_TEXT, _is_utf8 # pyright: ignore[reportPrivateUsage]
101
+
102
+ import base64
103
+ import json
104
+ import os
105
+ import re
106
+ import signal
107
+ import sys
108
+ import time
109
+ import uuid
110
+ import weakref
111
+ import wsgiref.util
112
+
113
+ from collections import deque
114
+ from typing import Any, cast
115
+
116
+
117
+ # The account, the region and the API of the identifiers of the events, which no AWS has
118
+ ACCOUNT = '123456789012'
119
+ REGION = 'us-east-1'
120
+ API = 'pyjo'
121
+
122
+ PREFIX = '/2018-06-01/runtime'
123
+
124
+ # The streaming response of an HTTP integration: the status and the headers as JSON, in the first 16 KB, before 8 null
125
+ # bytes and the body
126
+ INTEGRATION = 'application/vnd.awslambda.http-integration-response'
127
+ DELIMITER = b'\x00' * 8
128
+ MAX_PRELUDE = 16384
129
+
130
+
131
+ class Pyjo_App_Lambda(Pyjo.App.object):
132
+ """
133
+ :mod:`Pyjo.App.Lambda` inherits all methods from :mod:`Pyjo.App` and
134
+ implements the following new ones.
135
+ """
136
+
137
+ def __init__(self, attrs: dict[str, Any] | None = None, /, **kwargs: Any) -> None:
138
+ """::
139
+
140
+ app = Pyjo.App.Lambda.new(function='myfunction.handler')
141
+
142
+ Construct a new :mod:`Pyjo.App.Lambda` object.
143
+ """
144
+ self.command: list[str] | None = None
145
+ """::
146
+
147
+ command = app.command
148
+ app.command = ['python', '-m', 'awslambdaric', 'myfunction.handler']
149
+ app.command = ['./bootstrap']
150
+
151
+ Program of the function, with its arguments, as the runtime client of
152
+ another runtime or the ``bootstrap`` of a custom runtime, in
153
+ :attr:`task_root`, or the runtime client of :mod:`Pyjo.Server.Lambda`
154
+ with :attr:`function` and the interpreter of this process.
155
+ """
156
+
157
+ self.env: dict[str, str] = {}
158
+ """::
159
+
160
+ env = app.env
161
+ app.env = {'TABLE_NAME': 'users'}
162
+
163
+ Variables of the environment of the function, which are added to the
164
+ ones of this process and of Lambda, and replace them.
165
+ """
166
+
167
+ self.function: str | None = None
168
+ """::
169
+
170
+ handler = app.function
171
+ app.function = 'myfunction.handler'
172
+
173
+ Handler of the function, ``module.function``, in ``_HANDLER``, which
174
+ the runtime client of :mod:`Pyjo.Server.Lambda` runs without
175
+ :attr:`command`.
176
+ """
177
+
178
+ self.function_name = 'pyjo'
179
+ """::
180
+
181
+ name = app.function_name
182
+ app.function_name = 'myfunction'
183
+
184
+ Name of the function, in ``AWS_LAMBDA_FUNCTION_NAME`` and in its ARN,
185
+ defaults to ``pyjo``.
186
+ """
187
+
188
+ self.memory_size = 128
189
+ """::
190
+
191
+ size = app.memory_size
192
+ app.memory_size = 512
193
+
194
+ Memory of the function in megabytes, in
195
+ ``AWS_LAMBDA_FUNCTION_MEMORY_SIZE``, defaults to ``128``.
196
+ """
197
+
198
+ self.multi_value_headers = False
199
+ """::
200
+
201
+ boolean = app.multi_value_headers
202
+ app.multi_value_headers = True
203
+
204
+ The headers and the parameters of the query of the events of an
205
+ Application Load Balancer with multiple values, as a target group with
206
+ them has them, false by default.
207
+ """
208
+
209
+ self.payload = '2.0'
210
+ """::
211
+
212
+ payload = app.payload
213
+ app.payload = '1.0'
214
+ app.payload = 'alb'
215
+
216
+ Payload format of the events, ``2.0`` of HTTP APIs and Function URLs,
217
+ the default, ``1.0`` of REST APIs, or ``alb`` of Application Load
218
+ Balancers.
219
+ """
220
+
221
+ self.task_root: str | os.PathLike[str] | None = None
222
+ """::
223
+
224
+ root = app.task_root
225
+ app.task_root = '/var/task'
226
+
227
+ Directory of the function, in ``LAMBDA_TASK_ROOT``, which its process
228
+ runs in, and which the runtime client of :mod:`Pyjo.Server.Lambda`
229
+ imports the handler from, the current one by default.
230
+ """
231
+
232
+ self.timeout: float = 30
233
+ """::
234
+
235
+ timeout = app.timeout
236
+ app.timeout = 3
237
+
238
+ Maximum amount of time in seconds an invocation can take before its
239
+ process is killed, defaults to ``30``, as an integration of API
240
+ Gateway has it.
241
+ """
242
+
243
+ # The execution environments of each event loop, of the process that made them
244
+ self._pid = os.getpid()
245
+ self._environments: weakref.WeakKeyDictionary[Any, _Environment] = weakref.WeakKeyDictionary()
246
+ _apps.add(self)
247
+
248
+ super().__init__(attrs, **kwargs)
249
+
250
+ def close(self) -> None:
251
+ """::
252
+
253
+ app.close()
254
+
255
+ End the processes of the function, and close the connections and the
256
+ servers of :attr:`ua`, as the end of a ``with`` block does.
257
+ """
258
+ if self._pid == os.getpid():
259
+ for environment in list(self._environments.values()):
260
+ environment.close()
261
+ self._environments.clear()
262
+ super().close()
263
+
264
+ def handler(self, tx: Any) -> None:
265
+ """::
266
+
267
+ app.handler(Pyjo.Transaction.HTTP.new())
268
+ app.handler(Pyjoyment.Controller.new())
269
+
270
+ Invoke the function with the event of the request of the transaction,
271
+ or of the controller of a route that embeds the application, and write
272
+ its response.
273
+ """
274
+ if self.function is None and self.command is None:
275
+ raise Pyjo.Exception.new('No Lambda function')
276
+ if self.payload not in ('2.0', '1.0', 'alb'):
277
+ raise Pyjo.Exception.new(f'Invalid Lambda payload format "{self.payload}"')
278
+ c = None
279
+ if not isinstance(tx, Pyjo.Transaction.object):
280
+ c = tx
281
+ tx = c.tx
282
+ if not Pyjo.App._within(c): # pyright: ignore[reportPrivateUsage]
283
+ return c.helpers.reply.not_found()
284
+ c.render_later()
285
+ _Invocation(self, tx, c).start()
286
+
287
+ def _environment(self) -> '_Environment':
288
+ loop = Pyjo.IOLoop.singleton
289
+
290
+ self._leave()
291
+ environment = self._environments.get(loop)
292
+ if environment is None or environment.ended:
293
+ _register()
294
+ self._environments[loop] = environment = _Environment(self)
295
+ return environment
296
+
297
+ # The child of a fork leaves the processes of its parent to it, and closes the copies of their servers, at the fork,
298
+ # before its event loop reads the connections of the runtime clients of the parent, or of the processes that the
299
+ # child starts later
300
+ def _leave(self) -> None:
301
+ if self._pid != (pid := os.getpid()):
302
+ for environment in list(self._environments.values()):
303
+ environment.leave()
304
+ self._pid, self._environments = pid, weakref.WeakKeyDictionary()
305
+
306
+
307
+ # An invocation of the function for a request, and its response
308
+ class _Invocation:
309
+ def __init__(self, app: Pyjo_App_Lambda, tx: Any, c: Any) -> None:
310
+ self.app = app
311
+ self.tx = tx
312
+ self.c = c
313
+ self.id = str(uuid.uuid4())
314
+ self.done = False
315
+ self.environment: _Environment | None = None
316
+ self.event: dict[str, Any] = {}
317
+ self.waiting: tuple[str, float] | None = None
318
+
319
+ # A streaming response: the request of the runtime client that sends it, the prelude that comes before its body,
320
+ # its status and its headers, which go with its first chunk, whether they have gone, with the body chunked, and
321
+ # the chunks that wait to be written, after which the connection closes when it is closing
322
+ self.api: str | None = None
323
+ self.prelude: bytearray | None = None
324
+ self.head: tuple[int, list[tuple[str, str]]] | None = None
325
+ self.started = False
326
+ self.chunked = False
327
+ self.pending = 0
328
+ self.closing = False
329
+
330
+ def start(self) -> None:
331
+ self.tx.on('finish', self._finish)
332
+ self.event = _event(self.app, self.tx, self.c, self.id)
333
+
334
+ # The connection of the client waits for the response without its inactivity timeout, which the timeout of the
335
+ # function takes the place of
336
+ if (connection := self.tx.connection) is not None and (stream := Pyjo.IOLoop.stream(connection)) is not None:
337
+ self.waiting = (connection, stream.timeout)
338
+ stream.timeout = 0
339
+
340
+ self.app._environment().invoke(self) # pyright: ignore[reportPrivateUsage]
341
+
342
+ def fail(self, message: str) -> None:
343
+ if self.done:
344
+ return
345
+ self.app.log.error(message)
346
+
347
+ # A streaming response that has started cannot change, so the connection is closed once the chunks of it that
348
+ # wait have been written
349
+ if self.started:
350
+ self.done = True
351
+ self._waited()
352
+ if self.pending:
353
+ self.closing = True
354
+ else:
355
+ self._close()
356
+ return
357
+ self._write(502, [('Content-Type', 'text/plain')], b'Bad Gateway')
358
+
359
+ # The response of the payload format, or a value as JSON of the format 2.0
360
+ def respond(self, body: bytes) -> None:
361
+ if self.done:
362
+ return
363
+ try:
364
+ value = json.loads(body or b'null')
365
+ except ValueError:
366
+ return self.fail('Lambda function returned invalid JSON')
367
+ if not isinstance(value, dict) or 'statusCode' not in value:
368
+ if self.app.payload != '2.0':
369
+ return self.fail('Lambda function returned no statusCode')
370
+ text = value if isinstance(value, str) else json.dumps(value)
371
+ return self._write(200, [('Content-Type', 'application/json')], text.encode())
372
+
373
+ try:
374
+ response = cast(dict[str, Any], value)
375
+ code, headers = _status(response)
376
+ content = response.get('body')
377
+ if content is None:
378
+ data = b''
379
+ elif response.get('isBase64Encoded'):
380
+ data = base64.b64decode(content)
381
+ else:
382
+ data = str(content).encode()
383
+ except (AttributeError, TypeError, ValueError) as e:
384
+ return self.fail(f'Lambda function returned an invalid response: {e}')
385
+ self._write(code, headers, data)
386
+
387
+ # The start of a streaming response, once the headers of its request have come: with the status and the headers of
388
+ # the prelude of an HTTP integration, or of the content type of the request
389
+ def stream(self, api: str | None, content_type: str) -> None:
390
+ self.api = api
391
+ if content_type.split(';')[0].strip().lower() == INTEGRATION:
392
+ self.prelude = bytearray()
393
+ else:
394
+ self._start(200, [('Content-Type', content_type or 'application/octet-stream')])
395
+
396
+ # A chunk of a streaming response, after its prelude
397
+ def stream_chunk(self, chunk: bytes) -> None:
398
+ if self.done:
399
+ return
400
+ if (prelude := self.prelude) is not None:
401
+ prelude += chunk
402
+ if (end := prelude.find(DELIMITER)) < 0:
403
+ if len(prelude) > MAX_PRELUDE:
404
+ self.fail('Lambda function returned no prelude in the first 16 KB of its streaming response')
405
+ return
406
+ self.prelude = None
407
+ try:
408
+ value = json.loads(bytes(prelude[:end]))
409
+ code, headers = _status(cast(dict[str, Any], value) if isinstance(value, dict) else {})
410
+ except (AttributeError, KeyError, TypeError, ValueError) as e:
411
+ return self.fail(f'Lambda function returned an invalid prelude: {e}')
412
+ self._start(code, headers)
413
+ chunk = bytes(prelude[end + len(DELIMITER) :])
414
+ if not len(chunk):
415
+ return
416
+
417
+ # The next chunks of the runtime client once the chunk has been written to the client
418
+ connection = self.api
419
+
420
+ def drained(*args: Any) -> None:
421
+ self.pending -= 1
422
+ if self.closing and not self.pending:
423
+ return self._close()
424
+ if connection is not None and (stream := Pyjo.IOLoop.stream(connection)) is not None:
425
+ stream.start()
426
+
427
+ if connection is not None and (stream := Pyjo.IOLoop.stream(connection)) is not None:
428
+ stream.stop()
429
+ self._begin()
430
+ self.pending += 1
431
+ content = self.tx.res.content
432
+ (content.write_chunk if self.chunked else content.write)(chunk, drained)
433
+ self._rendered()
434
+
435
+ # The end of a streaming response, with the error of its trailers
436
+ def stream_end(self, error: str | None, body: str | None) -> None:
437
+ if error is not None:
438
+ message = _message(_json(base64.b64decode(body or ''))) if body else error
439
+ return self.fail(f'Lambda function error: {message}')
440
+ if self.done:
441
+ return
442
+ if self.prelude is not None:
443
+ return self.fail('Lambda function returned no prelude in its streaming response')
444
+ self.done = True
445
+ self._waited()
446
+ self._begin()
447
+ content = self.tx.res.content
448
+ (content.write_chunk if self.chunked else content.write)(b'')
449
+ self._rendered()
450
+
451
+ # The status and the headers of a streaming response, with its first chunk
452
+ def _begin(self) -> None:
453
+ if not self.started and (head := self.head) is not None:
454
+ self.started = True
455
+ self._headers(*head)
456
+
457
+ # The connection of the client, once what has been written to it is
458
+ def _close(self) -> None:
459
+ if (connection := self.tx.connection) is not None and (stream := Pyjo.IOLoop.stream(connection)) is not None:
460
+ stream.close_gracefully()
461
+
462
+ # The client is gone, or has its response
463
+ def _finish(self, *args: Any) -> None:
464
+ if self.done:
465
+ return
466
+ self.done = True
467
+ if (environment := self.environment) is not None:
468
+ environment.cancel(self)
469
+
470
+ def _headers(self, code: int, headers: list[tuple[str, str]]) -> None:
471
+ res = self.tx.res
472
+ res.code = code
473
+ for name, value in headers:
474
+ if not wsgiref.util.is_hop_by_hop(name) and name.lower() != 'content-length':
475
+ res.headers.add(name, value)
476
+
477
+ def _rendered(self) -> None:
478
+ if (c := self.c) is not None:
479
+ c.rendered()
480
+ else:
481
+ self.tx.resume()
482
+
483
+ # The status and the headers of a streaming response, which go with its first chunk, as its body is chunked, or ends
484
+ # with the connection of HTTP/1.0
485
+ def _start(self, code: int, headers: list[tuple[str, str]]) -> None:
486
+ self.head = (code, headers)
487
+ self.chunked = self.tx.req.version == '1.1'
488
+ if self.tx.is_empty:
489
+ self.done = True
490
+ self._waited()
491
+ self._begin()
492
+ self.tx.res.content.write(b'')
493
+ self._rendered()
494
+
495
+ # The inactivity timeout of the connection of the client again
496
+ def _waited(self) -> None:
497
+ if (waiting := self.waiting) is not None:
498
+ self.waiting = None
499
+ connection, timeout = waiting
500
+ if (stream := Pyjo.IOLoop.stream(connection)) is not None:
501
+ stream.timeout = timeout
502
+
503
+ def _write(self, code: int, headers: list[tuple[str, str]], body: bytes) -> None:
504
+ self.done = True
505
+ self._waited()
506
+ self._headers(code, headers)
507
+ self.tx.res.body = body
508
+ self._rendered()
509
+
510
+
511
+ # An execution environment of the function: its process, and the Runtime API that it gets its invocations from, one
512
+ # after another
513
+ class _Environment:
514
+ def __init__(self, app: Pyjo_App_Lambda) -> None:
515
+ self.app = app
516
+ self.current: _Invocation | None = None
517
+ self.ended = False
518
+ self.failed = False
519
+ self.next: Any = None
520
+ self.started = False
521
+ self.queue: deque[_Invocation] = deque()
522
+ self.timer: str | None = None
523
+
524
+ api = _RuntimeAPI()
525
+ api.environment = weakref.ref(self)
526
+ api.log = app.log
527
+ self.daemon = Pyjo.Server.Daemon.new(
528
+ app=api, listen=['http://127.0.0.1'], silent=True, inactivity_timeout=0
529
+ ).start()
530
+
531
+ # The variables of Lambda, of the runtime client of Pyjo.Server.Lambda without a command, and of the attribute
532
+ root: str = os.path.abspath(os.fspath(app.task_root)) if app.task_root is not None else os.getcwd()
533
+ name = app.function_name
534
+ env: dict[str, str] = {
535
+ **os.environ,
536
+ 'AWS_LAMBDA_RUNTIME_API': f'127.0.0.1:{self.daemon.ports()[0]}',
537
+ 'AWS_LAMBDA_FUNCTION_NAME': name,
538
+ 'AWS_LAMBDA_FUNCTION_VERSION': '$LATEST',
539
+ 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE': str(app.memory_size),
540
+ 'AWS_LAMBDA_LOG_GROUP_NAME': f'/aws/lambda/{name}',
541
+ 'AWS_LAMBDA_LOG_STREAM_NAME': f'{time.strftime("%Y/%m/%d", time.gmtime())}/[$LATEST]{uuid.uuid4().hex}',
542
+ 'LAMBDA_TASK_ROOT': root,
543
+ '_HANDLER': app.function or '',
544
+ 'PYTHONUNBUFFERED': '1',
545
+ }
546
+ env.setdefault('AWS_REGION', REGION)
547
+ command = app.command
548
+ if command is None:
549
+ command = [sys.executable, '-m', 'Pyjo.Server.Lambda']
550
+ checkout = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(Pyjo.App.__file__))))
551
+ env['PYTHONPATH'] = os.pathsep.join([checkout, *filter(None, [env.get('PYTHONPATH')])])
552
+ env.update(app.env)
553
+
554
+ ref = weakref.ref(self)
555
+ self.child = child = Pyjo.IOLoop.spawn(command, cwd=root, detached=True, env=env)
556
+ for stream, log in ((child.stdout, app.log.info), (child.stderr, app.log.warn)):
557
+ if stream is not None:
558
+ lines = _Lines(log)
559
+ stream.on('read', lines.read)
560
+ stream.on('close', lines.closed)
561
+
562
+ def error(child: Any, err: Any) -> None:
563
+ if (environment := ref()) is not None:
564
+ environment.end(f'Lambda function failed: {err}', True)
565
+
566
+ def close(child: Any, code: int | None, signal: int | None) -> None:
567
+ if (environment := ref()) is not None:
568
+ # A process that ends before it asks for an invocation fails the ones that wait for it too
569
+ status = f'signal {signal}' if signal else f'status {code}'
570
+ failed = environment.failed or not environment.started
571
+ environment.end(f'Lambda function ended with {status}', failed)
572
+
573
+ child.on('error', error)
574
+ child.on('close', close)
575
+
576
+ def cancel(self, invocation: _Invocation) -> None:
577
+ if invocation in self.queue:
578
+ self.queue.remove(invocation)
579
+
580
+ def close(self) -> None:
581
+ self.end('Lambda function closed', True)
582
+
583
+ # The invocation for the runtime client that waits for it
584
+ def dispatch(self) -> None:
585
+ if self.current is not None or not self.queue or (tx := self.next) is None:
586
+ return
587
+ self.next = None
588
+ self.current = invocation = self.queue.popleft()
589
+ app = self.app
590
+ deadline = int((time.time() + app.timeout) * 1000)
591
+ res = tx.res
592
+ res.code = 200
593
+ headers = res.headers
594
+ headers.content_type = 'application/json'
595
+ headers.header('Lambda-Runtime-Aws-Request-Id', invocation.id)
596
+ headers.header('Lambda-Runtime-Deadline-Ms', str(deadline))
597
+ headers.header('Lambda-Runtime-Invoked-Function-Arn', _arn(app))
598
+ headers.header('Lambda-Runtime-Trace-Id', f'Root=1-{int(time.time()):08x}-{uuid.uuid4().hex[:24]}')
599
+ res.body = json.dumps(invocation.event).encode()
600
+ tx.resume()
601
+
602
+ ref = weakref.ref(self)
603
+
604
+ def timeout(loop: Any) -> None:
605
+ if (environment := ref()) is not None:
606
+ environment.timer = None
607
+ environment.timed_out()
608
+
609
+ self.timer = Pyjo.IOLoop.timer(app.timeout, timeout)
610
+
611
+ # The end of the process, of a failure or its own, which fails the invocation that it had, and the ones that wait
612
+ # for it when the function failed, where they go to a new process otherwise
613
+ def end(self, message: str, failed: bool) -> None:
614
+ first = not self.ended
615
+ self.ended = True
616
+ self._stop()
617
+ if (invocation := self.current) is not None:
618
+ self.current = None
619
+ invocation.fail(message)
620
+ queue, self.queue = list(self.queue), deque()
621
+ if first:
622
+ self.child.kill(signal.SIGKILL)
623
+ self.daemon.close()
624
+ if failed:
625
+ for invocation in queue:
626
+ invocation.fail(message)
627
+ elif queue:
628
+ environment = self.app._environment() # pyright: ignore[reportPrivateUsage]
629
+ for invocation in queue:
630
+ environment.invoke(invocation)
631
+
632
+ def error(self, id: str, error: dict[str, Any]) -> None:
633
+ if (invocation := self._take(id)) is not None:
634
+ invocation.fail(f'Lambda function error: {_message(error)}')
635
+ self.dispatch()
636
+
637
+ def init_error(self, error: dict[str, Any]) -> None:
638
+ self.failed = True
639
+ self.app.log.error(f'Lambda function initialization error: {_message(error)}')
640
+
641
+ def invoke(self, invocation: _Invocation) -> None:
642
+ invocation.environment = self
643
+ self.queue.append(invocation)
644
+ self.dispatch()
645
+
646
+ # The child of a fork leaves the process to its parent, and closes its copy of the server
647
+ def leave(self) -> None:
648
+ self.ended = True
649
+ self._stop()
650
+ self.daemon.close()
651
+
652
+ def response(self, id: str, body: bytes) -> None:
653
+ if (invocation := self._take(id)) is not None:
654
+ invocation.respond(body)
655
+ self.dispatch()
656
+
657
+ # The start of the streaming response of the current invocation, which stays current until its end, as its timeout
658
+ # goes on
659
+ def streaming(self, id: str, api: str | None, content_type: str) -> _Invocation | None:
660
+ if (invocation := self.current) is None or invocation.id != id:
661
+ return None
662
+ invocation.stream(api, content_type)
663
+ return invocation
664
+
665
+ def streamed(self, id: str, error: str | None, body: str | None) -> None:
666
+ if (invocation := self._take(id)) is not None:
667
+ invocation.stream_end(error, body)
668
+ self.dispatch()
669
+
670
+ def timed_out(self) -> None:
671
+ app = self.app
672
+ self.end(f'Lambda function timed out after {app.timeout} seconds', False)
673
+
674
+ def wait(self, tx: Any) -> None:
675
+ self.next = tx
676
+ self.started = True
677
+ ref = weakref.ref(self)
678
+
679
+ def finish(tx: Any) -> None:
680
+ if (environment := ref()) is not None and environment.next is tx:
681
+ environment.next = None
682
+
683
+ tx.on('finish', finish)
684
+ self.dispatch()
685
+
686
+ def _stop(self) -> None:
687
+ if (timer := self.timer) is not None:
688
+ self.timer = None
689
+ Pyjo.IOLoop.remove(timer)
690
+
691
+ def _take(self, id: str) -> _Invocation | None:
692
+ if (invocation := self.current) is None or invocation.id != id:
693
+ return None
694
+ self._stop()
695
+ self.current = None
696
+ return invocation
697
+
698
+
699
+ # The lines of an output of the process, which are logged as they come
700
+ class _Lines:
701
+ def __init__(self, log: Any) -> None:
702
+ self.log = log
703
+ self.buffer = bytearray()
704
+
705
+ def closed(self, stream: Any) -> None:
706
+ if len(self.buffer):
707
+ self.line(bytes(self.buffer))
708
+ self.buffer.clear()
709
+
710
+ def line(self, line: bytes) -> None:
711
+ self.log(f'Lambda function: {line.decode("utf-8", "replace").rstrip()}')
712
+
713
+ def read(self, stream: Any, chunk: bytes) -> None:
714
+ self.buffer += chunk
715
+ while (end := self.buffer.find(b'\n')) >= 0:
716
+ self.line(bytes(self.buffer[:end]))
717
+ del self.buffer[: end + 1]
718
+
719
+
720
+ # The Runtime API of an execution environment, for its process
721
+ class _RuntimeAPI(Pyjo.App.object):
722
+ environment: 'weakref.ref[_Environment] | None' = None
723
+
724
+ # A streaming response goes to the invocation as it comes, once the headers of its request are there, and its body is
725
+ # not kept
726
+ def build_tx(self) -> Pyjo.Transaction.HTTP.object:
727
+ tx = super().build_tx()
728
+ req = weakref.ref(tx.req)
729
+ ref = weakref.ref(tx)
730
+ environment = self.environment
731
+
732
+ def body(content: Any) -> None:
733
+ if (r := req()) is None or (t := ref()) is None or not _is_streaming(r):
734
+ return
735
+ m = re.fullmatch(rf'{PREFIX}/invocation/([^/]+)/response', r.url.path.to_str())
736
+ env = environment() if environment is not None else None
737
+ if m is None or env is None or env.ended:
738
+ return
739
+ invocation = env.streaming(m.group(1), t.connection, r.headers.content_type or '')
740
+ if invocation is None:
741
+ return
742
+
743
+ def read(content: Any, chunk: bytes) -> None:
744
+ invocation.stream_chunk(bytes(chunk))
745
+
746
+ content.unsubscribe('read').on('read', read)
747
+
748
+ tx.req.content.on('body', body)
749
+ return tx
750
+
751
+ def handler(self, tx: Any) -> None:
752
+ environment = self.environment() if self.environment is not None else None
753
+ req = tx.req
754
+ method, path = req.method, req.url.path.to_str()
755
+ if environment is not None and not environment.ended:
756
+ if method == 'GET' and path == f'{PREFIX}/invocation/next':
757
+ return environment.wait(tx)
758
+ if method == 'POST' and (m := re.fullmatch(rf'{PREFIX}/invocation/([^/]+)/(response|error)', path)):
759
+ id, kind = m.group(1), m.group(2)
760
+ _reply(tx, 202)
761
+ if kind == 'response' and _is_streaming(req):
762
+ headers = req.headers
763
+ return environment.streamed(
764
+ id,
765
+ headers.header('Lambda-Runtime-Function-Error-Type'),
766
+ headers.header('Lambda-Runtime-Function-Error-Body'),
767
+ )
768
+ if kind == 'response':
769
+ return environment.response(id, req.body)
770
+ return environment.error(id, _json(req.body))
771
+ if method == 'POST' and path == f'{PREFIX}/init/error':
772
+ _reply(tx, 202)
773
+ return environment.init_error(_json(req.body))
774
+ _reply(tx, 404)
775
+
776
+
777
+ # The ARN of the function
778
+ def _arn(app: Pyjo_App_Lambda) -> str:
779
+ return f'arn:aws:lambda:{REGION}:{ACCOUNT}:function:{app.function_name}'
780
+
781
+
782
+ # The body of the request, as it is in text, or in Base64
783
+ def _body(req: Any) -> tuple[str | None, bool]:
784
+ body: bytes = req.body
785
+ if not len(body):
786
+ return None, False
787
+ if _RE_TEXT.match(req.headers.content_type or '') and _is_utf8(body):
788
+ return body.decode(), False
789
+ return base64.b64encode(body).decode('ascii'), True
790
+
791
+
792
+ # The event of the request in the payload format of the application
793
+ def _event(app: Pyjo_App_Lambda, tx: Any, c: Any, id: str) -> dict[str, Any]:
794
+ req = tx.req
795
+ url = req.url
796
+ script, path = Pyjo.App._mount(tx, c) # pyright: ignore[reportPrivateUsage]
797
+ script = script.rstrip('/')
798
+ full = script + path or '/'
799
+ path = path or '/'
800
+ stage = script.strip('/') or '$default'
801
+ pairs = [(name, value) for name in req.headers.names() for value in req.headers.every_header(name)]
802
+ body, encoded = _body(req)
803
+ method = req.method
804
+ address = tx.remote_address or '127.0.0.1'
805
+ agent = req.headers.user_agent or ''
806
+ host = req.headers.host or url.base.host or 'localhost'
807
+ now = time.time()
808
+
809
+ # The format 2.0 of HTTP APIs and Function URLs, with the path of the mount as its stage
810
+ if app.payload == '2.0':
811
+ headers: dict[str, str] = {}
812
+ cookies: list[str] = []
813
+ for name, value in pairs:
814
+ if (name := name.lower()) == 'cookie':
815
+ cookies += [cookie for cookie in value.split('; ') if cookie]
816
+ else:
817
+ headers[name] = f'{headers[name]},{value}' if name in headers else value
818
+ query: dict[str, str] = {}
819
+ for name, value in url.query.pairs:
820
+ query[name] = f'{query[name]},{value}' if name in query else value
821
+ event: dict[str, Any] = {
822
+ 'version': '2.0',
823
+ 'routeKey': '$default',
824
+ 'rawPath': full,
825
+ 'rawQueryString': url.query.to_str(),
826
+ 'headers': headers,
827
+ 'requestContext': {
828
+ 'accountId': ACCOUNT,
829
+ 'apiId': API,
830
+ 'domainName': host,
831
+ 'domainPrefix': host.split('.')[0],
832
+ 'http': {
833
+ 'method': method,
834
+ 'path': full,
835
+ 'protocol': f'HTTP/{req.version}',
836
+ 'sourceIp': address,
837
+ 'userAgent': agent,
838
+ },
839
+ 'requestId': id,
840
+ 'routeKey': '$default',
841
+ 'stage': stage,
842
+ 'time': time.strftime('%d/%b/%Y:%H:%M:%S +0000', time.gmtime(now)),
843
+ 'timeEpoch': int(now * 1000),
844
+ },
845
+ 'isBase64Encoded': encoded,
846
+ }
847
+ if cookies:
848
+ event['cookies'] = cookies
849
+ if query:
850
+ event['queryStringParameters'] = query
851
+ if body is not None:
852
+ event['body'] = body
853
+ return event
854
+
855
+ # The format 1.0 of REST APIs, with the path of the mount as its stage, in front of the path of the context
856
+ if app.payload == '1.0':
857
+ single: dict[str, str] = {}
858
+ multi: dict[str, list[str]] = {}
859
+ for name, value in pairs:
860
+ single[name] = value
861
+ multi.setdefault(name, []).append(value)
862
+ params: dict[str, str] = {}
863
+ multi_params: dict[str, list[str]] = {}
864
+ for name, value in url.query.pairs:
865
+ params[name] = value
866
+ multi_params.setdefault(name, []).append(value)
867
+ return {
868
+ 'version': '1.0',
869
+ 'resource': '/{proxy+}',
870
+ 'path': path,
871
+ 'httpMethod': method,
872
+ 'headers': single,
873
+ 'multiValueHeaders': multi,
874
+ 'queryStringParameters': params or None,
875
+ 'multiValueQueryStringParameters': multi_params or None,
876
+ 'pathParameters': {'proxy': path.lstrip('/')},
877
+ 'stageVariables': None,
878
+ 'requestContext': {
879
+ 'accountId': ACCOUNT,
880
+ 'apiId': API,
881
+ 'domainName': host,
882
+ 'httpMethod': method,
883
+ 'identity': {'sourceIp': address, 'userAgent': agent},
884
+ 'path': full,
885
+ 'protocol': f'HTTP/{req.version}',
886
+ 'requestId': id,
887
+ 'requestTimeEpoch': int(now * 1000),
888
+ 'resourcePath': '/{proxy+}',
889
+ 'stage': stage,
890
+ },
891
+ 'body': body,
892
+ 'isBase64Encoded': encoded,
893
+ }
894
+
895
+ # The events of an Application Load Balancer, with the whole path and the parameters of the query as they were
896
+ # sent, and the headers in lower case, of multiple values when the target group has them
897
+ raw = [tuple(part.split('=', 1)) if '=' in part else (part, '') for part in url.query.to_str().split('&') if part]
898
+ event = {
899
+ 'requestContext': {
900
+ 'elb': {
901
+ 'targetGroupArn': f'arn:aws:elasticloadbalancing:{REGION}:{ACCOUNT}:targetgroup/{API}/0123456789abcdef'
902
+ }
903
+ },
904
+ 'httpMethod': method,
905
+ 'path': full,
906
+ 'body': body or '',
907
+ 'isBase64Encoded': encoded,
908
+ }
909
+ if app.multi_value_headers:
910
+ multi_headers: dict[str, list[str]] = {}
911
+ for name, value in pairs:
912
+ multi_headers.setdefault(name.lower(), []).append(value)
913
+ multi_query: dict[str, list[str]] = {}
914
+ for name, value in raw:
915
+ multi_query.setdefault(name, []).append(value)
916
+ event['multiValueHeaders'] = multi_headers
917
+ event['multiValueQueryStringParameters'] = multi_query
918
+ else:
919
+ event['headers'] = {name.lower(): value for name, value in pairs}
920
+ event['queryStringParameters'] = dict(raw)
921
+ return event
922
+
923
+
924
+ def _is_streaming(req: Any) -> bool:
925
+ return req.method == 'POST' and req.headers.header('Lambda-Runtime-Function-Response-Mode') == 'streaming'
926
+
927
+
928
+ # The status and the headers of a response, or of the prelude of a streaming one, where the headers of multiple values
929
+ # take the place of the ones of the same name of one value
930
+ def _status(response: dict[str, Any]) -> tuple[int, list[tuple[str, str]]]:
931
+ code = int(response.get('statusCode', 200))
932
+ multi = cast(dict[str, list[Any]], response.get('multiValueHeaders') or {})
933
+ single = cast(dict[str, Any], response.get('headers') or {})
934
+ names = {str(name).lower() for name in multi}
935
+ headers = [(str(name), str(item)) for name, item in single.items() if str(name).lower() not in names]
936
+ headers += [(str(name), str(item)) for name, items in multi.items() for item in items]
937
+ headers += [('Set-Cookie', str(cookie)) for cookie in cast(list[Any], response.get('cookies') or [])]
938
+ return code, headers
939
+
940
+
941
+ def _json(body: bytes) -> dict[str, Any]:
942
+ try:
943
+ value = json.loads(body or b'{}')
944
+ except ValueError:
945
+ return {}
946
+ return cast(dict[str, Any], value) if isinstance(value, dict) else {}
947
+
948
+
949
+ # The message of an error of the function, with its type
950
+ def _message(error: dict[str, Any]) -> str:
951
+ kind, message = error.get('errorType'), error.get('errorMessage')
952
+ return f'{kind}: {message}' if kind else str(message)
953
+
954
+
955
+ def _reply(tx: Any, code: int) -> None:
956
+ res = tx.res
957
+ res.code = code
958
+ res.headers.content_type = 'application/json'
959
+ res.body = b'{"status":"OK"}' if code < 400 else b'{"errorMessage":"Not Found"}'
960
+ tx.resume()
961
+
962
+
963
+ # The applications whose execution environments the child of a fork leaves, at the fork. The handler is registered with the
964
+ # first environment, after the one of the reactor, which os.register_at_fork calls first, so that the reactor has moved
965
+ # to a loop of its own, as a loop of epoll shares its state with the parent
966
+ _apps: 'weakref.WeakSet[Pyjo_App_Lambda]' = weakref.WeakSet()
967
+ _registered = False
968
+
969
+
970
+ def _register() -> None:
971
+ global _registered
972
+ if not _registered and hasattr(os, 'register_at_fork'):
973
+ _registered = True
974
+ os.register_at_fork(after_in_child=_after_fork_in_child)
975
+
976
+
977
+ def _after_fork_in_child() -> None:
978
+ for app in list(_apps):
979
+ try:
980
+ app._leave() # pyright: ignore[reportPrivateUsage]
981
+ except Exception:
982
+ pass
983
+
984
+
985
+ new = Pyjo_App_Lambda.new
986
+ object = Pyjo_App_Lambda
987
+ Lambda = Pyjo_App_Lambda