@vercel/python 4.3.1 → 4.4.0
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.
- package/package.json +4 -3
- package/vc_init.py +299 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vercel/python",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"@types/jest": "27.4.1",
|
|
20
20
|
"@types/node": "14.18.33",
|
|
21
21
|
"@types/which": "3.0.0",
|
|
22
|
-
"@vercel/build-utils": "8.
|
|
22
|
+
"@vercel/build-utils": "8.4.12",
|
|
23
|
+
"cross-env": "7.0.3",
|
|
23
24
|
"execa": "^1.0.0",
|
|
24
25
|
"fs-extra": "11.1.1",
|
|
25
26
|
"jest-junit": "16.0.0",
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
},
|
|
28
29
|
"scripts": {
|
|
29
30
|
"build": "node ../../utils/build-builder.mjs",
|
|
30
|
-
"test": "jest --reporters=default --reporters=jest-junit --env node --verbose --runInBand --bail",
|
|
31
|
+
"test": "cross-env VERCEL_FORCE_PYTHON_STREAMING=1 jest --reporters=default --reporters=jest-junit --env node --verbose --runInBand --bail",
|
|
31
32
|
"test-unit": "pnpm test test/unit.test.ts",
|
|
32
33
|
"test-e2e": "pnpm test test/integration-*",
|
|
33
34
|
"type-check": "tsc --noEmit"
|
package/vc_init.py
CHANGED
|
@@ -5,6 +5,7 @@ import inspect
|
|
|
5
5
|
from importlib import util
|
|
6
6
|
from http.server import BaseHTTPRequestHandler
|
|
7
7
|
import socket
|
|
8
|
+
import os
|
|
8
9
|
|
|
9
10
|
# Import relative path https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
|
|
10
11
|
__vc_spec = util.spec_from_file_location("__VC_HANDLER_MODULE_NAME", "./__VC_HANDLER_ENTRYPOINT")
|
|
@@ -26,6 +27,304 @@ def format_headers(headers, decode=False):
|
|
|
26
27
|
keyToList[key].append(value)
|
|
27
28
|
return keyToList
|
|
28
29
|
|
|
30
|
+
if 'VERCEL_IPC_FD' in os.environ:
|
|
31
|
+
from http.server import HTTPServer
|
|
32
|
+
import http
|
|
33
|
+
import time
|
|
34
|
+
import contextvars
|
|
35
|
+
import functools
|
|
36
|
+
import builtins
|
|
37
|
+
import logging
|
|
38
|
+
|
|
39
|
+
ipc_fd = int(os.getenv("VERCEL_IPC_FD", ""))
|
|
40
|
+
sock = socket.socket(fileno=ipc_fd)
|
|
41
|
+
start_time = time.time()
|
|
42
|
+
|
|
43
|
+
send_message = lambda message: sock.sendall((json.dumps(message) + '\0').encode())
|
|
44
|
+
storage = contextvars.ContextVar('storage', default=None)
|
|
45
|
+
|
|
46
|
+
# Override sys.stdout and sys.stderr to map logs to the correct request
|
|
47
|
+
class StreamWrapper:
|
|
48
|
+
def __init__(self, stream, stream_name):
|
|
49
|
+
self.stream = stream
|
|
50
|
+
self.stream_name = stream_name
|
|
51
|
+
|
|
52
|
+
def write(self, message):
|
|
53
|
+
context = storage.get()
|
|
54
|
+
if context is not None:
|
|
55
|
+
send_message({
|
|
56
|
+
"type": "log",
|
|
57
|
+
"payload": {
|
|
58
|
+
"context": {
|
|
59
|
+
"invocationId": context['invocationId'],
|
|
60
|
+
"requestId": context['requestId'],
|
|
61
|
+
},
|
|
62
|
+
"message": base64.b64encode(message.encode()).decode(),
|
|
63
|
+
"stream": self.stream_name,
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
else:
|
|
67
|
+
self.stream.write(message)
|
|
68
|
+
|
|
69
|
+
def __getattr__(self, name):
|
|
70
|
+
return getattr(self.stream, name)
|
|
71
|
+
|
|
72
|
+
sys.stdout = StreamWrapper(sys.stdout, "stdout")
|
|
73
|
+
sys.stderr = StreamWrapper(sys.stderr, "stderr")
|
|
74
|
+
|
|
75
|
+
# Override the global print to log to stdout
|
|
76
|
+
def print_wrapper(func):
|
|
77
|
+
@functools.wraps(func)
|
|
78
|
+
def wrapper(*args, **kwargs):
|
|
79
|
+
sys.stdout.write(' '.join(map(str, args)) + '\n')
|
|
80
|
+
return wrapper
|
|
81
|
+
builtins.print = print_wrapper(builtins.print)
|
|
82
|
+
|
|
83
|
+
# Override logging to maps logs to the correct request
|
|
84
|
+
def logging_wrapper(func, level="info"):
|
|
85
|
+
@functools.wraps(func)
|
|
86
|
+
def wrapper(*args, **kwargs):
|
|
87
|
+
context = storage.get()
|
|
88
|
+
if context is not None:
|
|
89
|
+
send_message({
|
|
90
|
+
"type": "log",
|
|
91
|
+
"payload": {
|
|
92
|
+
"context": {
|
|
93
|
+
"invocationId": context['invocationId'],
|
|
94
|
+
"requestId": context['requestId'],
|
|
95
|
+
},
|
|
96
|
+
"message": base64.b64encode(f"{args[0]}".encode()).decode(),
|
|
97
|
+
"level": level,
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
else:
|
|
101
|
+
func(*args, **kwargs)
|
|
102
|
+
return wrapper
|
|
103
|
+
|
|
104
|
+
logging.basicConfig(level=logging.INFO)
|
|
105
|
+
logging.debug = logging_wrapper(logging.debug)
|
|
106
|
+
logging.info = logging_wrapper(logging.info)
|
|
107
|
+
logging.warning = logging_wrapper(logging.warning, "warn")
|
|
108
|
+
logging.error = logging_wrapper(logging.error, "error")
|
|
109
|
+
logging.critical = logging_wrapper(logging.critical, "error")
|
|
110
|
+
|
|
111
|
+
class BaseHandler(BaseHTTPRequestHandler):
|
|
112
|
+
# Re-implementation of BaseHTTPRequestHandler's log_message method to
|
|
113
|
+
# log to stdout instead of stderr.
|
|
114
|
+
def log_message(self, format, *args):
|
|
115
|
+
message = format % args
|
|
116
|
+
sys.stdout.write("%s - - [%s] %s\n" %
|
|
117
|
+
(self.address_string(),
|
|
118
|
+
self.log_date_time_string(),
|
|
119
|
+
message.translate(self._control_char_table)))
|
|
120
|
+
|
|
121
|
+
# Re-implementation of BaseHTTPRequestHandler's handle_one_request method
|
|
122
|
+
# to send the end message after the response is fully sent.
|
|
123
|
+
def handle_one_request(self):
|
|
124
|
+
self.raw_requestline = self.rfile.readline(65537)
|
|
125
|
+
if not self.raw_requestline:
|
|
126
|
+
self.close_connection = True
|
|
127
|
+
return
|
|
128
|
+
if not self.parse_request():
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
invocationId = self.headers.get('x-vercel-internal-invocation-id')
|
|
132
|
+
requestId = int(self.headers.get('x-vercel-internal-request-id'))
|
|
133
|
+
del self.headers['x-vercel-internal-invocation-id']
|
|
134
|
+
del self.headers['x-vercel-internal-request-id']
|
|
135
|
+
del self.headers['x-vercel-internal-span-id']
|
|
136
|
+
del self.headers['x-vercel-internal-trace-id']
|
|
137
|
+
|
|
138
|
+
token = storage.set({
|
|
139
|
+
"invocationId": invocationId,
|
|
140
|
+
"requestId": requestId,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
self.handle_request()
|
|
145
|
+
finally:
|
|
146
|
+
storage.reset(token)
|
|
147
|
+
send_message({
|
|
148
|
+
"type": "end",
|
|
149
|
+
"payload": {
|
|
150
|
+
"context": {
|
|
151
|
+
"invocationId": invocationId,
|
|
152
|
+
"requestId": requestId,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
if 'handler' in __vc_variables or 'Handler' in __vc_variables:
|
|
158
|
+
base = __vc_module.handler if ('handler' in __vc_variables) else __vc_module.Handler
|
|
159
|
+
if not issubclass(base, BaseHTTPRequestHandler):
|
|
160
|
+
print('Handler must inherit from BaseHTTPRequestHandler')
|
|
161
|
+
print('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
|
|
162
|
+
exit(1)
|
|
163
|
+
|
|
164
|
+
class Handler(BaseHandler, base):
|
|
165
|
+
def handle_request(self):
|
|
166
|
+
mname = 'do_' + self.command
|
|
167
|
+
if not hasattr(self, mname):
|
|
168
|
+
self.send_error(
|
|
169
|
+
http.HTTPStatus.NOT_IMPLEMENTED,
|
|
170
|
+
"Unsupported method (%r)" % self.command)
|
|
171
|
+
return
|
|
172
|
+
method = getattr(self, mname)
|
|
173
|
+
method()
|
|
174
|
+
self.wfile.flush()
|
|
175
|
+
elif 'app' in __vc_variables:
|
|
176
|
+
if (
|
|
177
|
+
not inspect.iscoroutinefunction(__vc_module.app) and
|
|
178
|
+
not inspect.iscoroutinefunction(__vc_module.app.__call__)
|
|
179
|
+
):
|
|
180
|
+
from io import BytesIO
|
|
181
|
+
|
|
182
|
+
string_types = (str,)
|
|
183
|
+
app = __vc_module.app
|
|
184
|
+
|
|
185
|
+
def wsgi_encoding_dance(s, charset="utf-8", errors="replace"):
|
|
186
|
+
if isinstance(s, str):
|
|
187
|
+
s = s.encode(charset)
|
|
188
|
+
return s.decode("latin1", errors)
|
|
189
|
+
|
|
190
|
+
class Handler(BaseHandler):
|
|
191
|
+
def handle_request(self):
|
|
192
|
+
# Prepare WSGI environment
|
|
193
|
+
if '?' in self.path:
|
|
194
|
+
path, query = self.path.split('?', 1)
|
|
195
|
+
else:
|
|
196
|
+
path, query = self.path, ''
|
|
197
|
+
content_length = int(self.headers.get('Content-Length', 0))
|
|
198
|
+
env = {
|
|
199
|
+
'CONTENT_LENGTH': str(content_length),
|
|
200
|
+
'CONTENT_TYPE': self.headers.get('content-type', ''),
|
|
201
|
+
'PATH_INFO': path,
|
|
202
|
+
'QUERY_STRING': query,
|
|
203
|
+
'REMOTE_ADDR': self.headers.get(
|
|
204
|
+
'x-forwarded-for', self.headers.get(
|
|
205
|
+
'x-real-ip')),
|
|
206
|
+
'REQUEST_METHOD': self.command,
|
|
207
|
+
'SERVER_NAME': self.headers.get('host', 'lambda'),
|
|
208
|
+
'SERVER_PORT': self.headers.get('x-forwarded-port', '80'),
|
|
209
|
+
'SERVER_PROTOCOL': 'HTTP/1.1',
|
|
210
|
+
'wsgi.errors': sys.stderr,
|
|
211
|
+
'wsgi.input': BytesIO(self.rfile.read(content_length)),
|
|
212
|
+
'wsgi.multiprocess': False,
|
|
213
|
+
'wsgi.multithread': False,
|
|
214
|
+
'wsgi.run_once': False,
|
|
215
|
+
'wsgi.url_scheme': self.headers.get('x-forwarded-proto', 'http'),
|
|
216
|
+
'wsgi.version': (1, 0),
|
|
217
|
+
}
|
|
218
|
+
for key, value in env.items():
|
|
219
|
+
if isinstance(value, string_types):
|
|
220
|
+
env[key] = wsgi_encoding_dance(value)
|
|
221
|
+
for k, v in self.headers.items():
|
|
222
|
+
env['HTTP_' + k.replace('-', '_').upper()] = v
|
|
223
|
+
# Response body
|
|
224
|
+
body = BytesIO()
|
|
225
|
+
|
|
226
|
+
def start_response(status, headers, exc_info=None):
|
|
227
|
+
self.send_response(int(status.split(' ')[0]))
|
|
228
|
+
for name, value in headers:
|
|
229
|
+
self.send_header(name, value)
|
|
230
|
+
self.end_headers()
|
|
231
|
+
return body.write
|
|
232
|
+
|
|
233
|
+
# Call the application
|
|
234
|
+
response = app(env, start_response)
|
|
235
|
+
try:
|
|
236
|
+
for data in response:
|
|
237
|
+
if data:
|
|
238
|
+
body.write(data)
|
|
239
|
+
finally:
|
|
240
|
+
if hasattr(response, 'close'):
|
|
241
|
+
response.close()
|
|
242
|
+
body = body.getvalue()
|
|
243
|
+
self.wfile.write(body)
|
|
244
|
+
self.wfile.flush()
|
|
245
|
+
else:
|
|
246
|
+
from urllib.parse import urlparse
|
|
247
|
+
from io import BytesIO
|
|
248
|
+
import asyncio
|
|
249
|
+
|
|
250
|
+
app = __vc_module.app
|
|
251
|
+
|
|
252
|
+
class Handler(BaseHandler):
|
|
253
|
+
def handle_request(self):
|
|
254
|
+
# Prepare ASGI scope
|
|
255
|
+
url = urlparse(self.path)
|
|
256
|
+
headers_encoded = []
|
|
257
|
+
for k, v in self.headers.items():
|
|
258
|
+
# Cope with repeated headers in the encoding.
|
|
259
|
+
if isinstance(v, list):
|
|
260
|
+
headers_encoded.append([k.lower().encode(), [i.encode() for i in v]])
|
|
261
|
+
else:
|
|
262
|
+
headers_encoded.append([k.lower().encode(), v.encode()])
|
|
263
|
+
scope = {
|
|
264
|
+
'server': (self.headers.get('host', 'lambda'), self.headers.get('x-forwarded-port', 80)),
|
|
265
|
+
'client': (self.headers.get(
|
|
266
|
+
'x-forwarded-for', self.headers.get(
|
|
267
|
+
'x-real-ip')), 0),
|
|
268
|
+
'scheme': self.headers.get('x-forwarded-proto', 'http'),
|
|
269
|
+
'root_path': '',
|
|
270
|
+
'query_string': url.query.encode(),
|
|
271
|
+
'headers': headers_encoded,
|
|
272
|
+
'type': 'http',
|
|
273
|
+
'http_version': '1.1',
|
|
274
|
+
'method': self.command,
|
|
275
|
+
'path': url.path,
|
|
276
|
+
'raw_path': url.path.encode(),
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# Prepare ASGI receive function
|
|
280
|
+
async def receive():
|
|
281
|
+
if 'content-length' in self.headers:
|
|
282
|
+
content_length = int(self.headers['content-length'])
|
|
283
|
+
body = self.rfile.read(content_length)
|
|
284
|
+
return {
|
|
285
|
+
'type': 'http.request',
|
|
286
|
+
'body': body,
|
|
287
|
+
'more_body': False,
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
'type': 'http.request',
|
|
291
|
+
'body': b'',
|
|
292
|
+
'more_body': False,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# Prepare ASGI send function
|
|
296
|
+
response_started = False
|
|
297
|
+
async def send(event):
|
|
298
|
+
nonlocal response_started
|
|
299
|
+
if event['type'] == 'http.response.start':
|
|
300
|
+
self.send_response(event['status'])
|
|
301
|
+
if 'headers' in event:
|
|
302
|
+
for name, value in event['headers']:
|
|
303
|
+
self.send_header(name.decode(), value.decode())
|
|
304
|
+
self.end_headers()
|
|
305
|
+
response_started = True
|
|
306
|
+
elif event['type'] == 'http.response.body':
|
|
307
|
+
self.wfile.write(event['body'])
|
|
308
|
+
if not event.get('more_body', False):
|
|
309
|
+
self.wfile.flush()
|
|
310
|
+
|
|
311
|
+
# Run the ASGI application
|
|
312
|
+
asyncio.run(app(scope, receive, send))
|
|
313
|
+
|
|
314
|
+
if 'Handler' in locals():
|
|
315
|
+
server = HTTPServer(('127.0.0.1', 0), Handler)
|
|
316
|
+
send_message({
|
|
317
|
+
"type": "server-started",
|
|
318
|
+
"payload": {
|
|
319
|
+
"initDuration": int((time.time() - start_time) * 1000),
|
|
320
|
+
"httpPort": server.server_address[1],
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
server.serve_forever()
|
|
324
|
+
|
|
325
|
+
print('Missing variable `handler` or `app` in file "__VC_HANDLER_ENTRYPOINT".')
|
|
326
|
+
print('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
|
|
327
|
+
exit(1)
|
|
29
328
|
|
|
30
329
|
if 'handler' in __vc_variables or 'Handler' in __vc_variables:
|
|
31
330
|
base = __vc_module.handler if ('handler' in __vc_variables) else __vc_module.Handler
|