@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.
Files changed (2) hide show
  1. package/package.json +4 -3
  2. 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.1",
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.3.5",
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