@vercel/python 6.0.1 → 6.0.3

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/dist/index.js CHANGED
@@ -3004,7 +3004,12 @@ async function exportRequirementsFromUv(projectDir, uvPath, options = {}) {
3004
3004
  if (!uvPath) {
3005
3005
  throw new Error("uv is not available to export requirements");
3006
3006
  }
3007
- const args = ["export"];
3007
+ const args = [
3008
+ "export",
3009
+ "--no-default-groups",
3010
+ "--no-emit-workspace",
3011
+ "--no-editable"
3012
+ ];
3008
3013
  if (locked) {
3009
3014
  args.push("--frozen");
3010
3015
  }
@@ -3915,6 +3920,18 @@ var build = async ({
3915
3920
  targetDir: vendorBaseDir,
3916
3921
  meta
3917
3922
  });
3923
+ if (framework !== "flask") {
3924
+ await installRequirement({
3925
+ pythonPath: pythonVersion.pythonPath,
3926
+ pipPath: pythonVersion.pipPath,
3927
+ uvPath,
3928
+ dependency: "uvicorn",
3929
+ version: "0.38.0",
3930
+ workPath,
3931
+ targetDir: vendorBaseDir,
3932
+ meta
3933
+ });
3934
+ }
3918
3935
  let installedFromProjectFiles = false;
3919
3936
  if (uvLockDir) {
3920
3937
  (0, import_build_utils6.debug)('Found "uv.lock"');
@@ -4026,7 +4043,10 @@ var build = async ({
4026
4043
  "**/__pycache__/**",
4027
4044
  "**/.mypy_cache/**",
4028
4045
  "**/.ruff_cache/**",
4029
- "**/public/**"
4046
+ "**/public/**",
4047
+ "**/pnpm-lock.yaml",
4048
+ "**/yarn.lock",
4049
+ "**/package-lock.json"
4030
4050
  ];
4031
4051
  const lambdaEnv = {};
4032
4052
  lambdaEnv.PYTHONPATH = vendorDir;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/python",
3
- "version": "6.0.1",
3
+ "version": "6.0.3",
4
4
  "main": "./dist/index.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
@@ -21,7 +21,7 @@
21
21
  "@types/jest": "27.4.1",
22
22
  "@types/node": "14.18.33",
23
23
  "@types/which": "3.0.0",
24
- "@vercel/build-utils": "12.2.4",
24
+ "@vercel/build-utils": "13.0.0",
25
25
  "cross-env": "7.0.3",
26
26
  "execa": "^1.0.0",
27
27
  "fs-extra": "11.1.1",
package/vc_init.py CHANGED
@@ -9,6 +9,7 @@ import inspect
9
9
  import asyncio
10
10
  import http
11
11
  import time
12
+ import traceback
12
13
  from importlib import util
13
14
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
14
15
  import socket
@@ -51,6 +52,20 @@ def setup_logging(send_message: Callable[[dict], None], storage: contextvars.Con
51
52
  except Exception:
52
53
  message = repr(getattr(record, "msg", ""))
53
54
 
55
+ with contextlib.suppress(Exception):
56
+ if record.exc_info:
57
+ # logging allows exc_info=True or a (type, value, tb) tuple
58
+ exc_info = record.exc_info
59
+ if exc_info is True:
60
+ exc_info = sys.exc_info()
61
+ if isinstance(exc_info, tuple):
62
+ tb = ''.join(traceback.format_exception(*exc_info))
63
+ if tb:
64
+ if message:
65
+ message = f"{message}\n{tb}"
66
+ else:
67
+ message = tb
68
+
54
69
  if record.levelno >= logging.CRITICAL:
55
70
  level = "fatal"
56
71
  elif record.levelno >= logging.ERROR:
@@ -143,6 +158,12 @@ def setup_logging(send_message: Callable[[dict], None], storage: contextvars.Con
143
158
  builtins.print = print_wrapper(builtins.print)
144
159
 
145
160
 
161
+ def _stderr(message: str):
162
+ with contextlib.suppress(Exception):
163
+ _original_stderr.write(message + "\n")
164
+ _original_stderr.flush()
165
+
166
+
146
167
  # If running in the platform (IPC present), logging must be setup before importing user code so that
147
168
  # logs happening outside the request context are emitted correctly.
148
169
  ipc_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
@@ -189,12 +210,17 @@ if 'VERCEL_IPC_PATH' in os.environ:
189
210
 
190
211
 
191
212
  # Import relative path https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
192
- user_mod_path = os.path.join(_here, "__VC_HANDLER_ENTRYPOINT") # absolute
193
- __vc_spec = util.spec_from_file_location("__VC_HANDLER_MODULE_NAME", user_mod_path)
194
- __vc_module = util.module_from_spec(__vc_spec)
195
- sys.modules["__VC_HANDLER_MODULE_NAME"] = __vc_module
196
- __vc_spec.loader.exec_module(__vc_module)
197
- __vc_variables = dir(__vc_module)
213
+ try:
214
+ user_mod_path = os.path.join(_here, "__VC_HANDLER_ENTRYPOINT") # absolute
215
+ __vc_spec = util.spec_from_file_location("__VC_HANDLER_MODULE_NAME", user_mod_path)
216
+ __vc_module = util.module_from_spec(__vc_spec)
217
+ sys.modules["__VC_HANDLER_MODULE_NAME"] = __vc_module
218
+ __vc_spec.loader.exec_module(__vc_module)
219
+ __vc_variables = dir(__vc_module)
220
+ except Exception:
221
+ _stderr(f'Error importing __VC_HANDLER_ENTRYPOINT:')
222
+ _stderr(traceback.format_exc())
223
+ exit(1)
198
224
 
199
225
  _use_legacy_asyncio = sys.version_info < (3, 10)
200
226
 
@@ -210,6 +236,95 @@ def format_headers(headers, decode=False):
210
236
  return keyToList
211
237
 
212
238
 
239
+ class ASGIMiddleware:
240
+ """
241
+ ASGI middleware that preserves Vercel IPC semantics for request lifecycle:
242
+ - Handles /_vercel/ping
243
+ - Extracts x-vercel-internal-* headers and removes them from downstream app
244
+ - Sets request context into `storage` for logging/metrics
245
+ - Emits handler-started and end IPC messages
246
+ """
247
+ def __init__(self, app):
248
+ self.app = app
249
+
250
+ async def __call__(self, scope, receive, send):
251
+ if scope.get('type') != 'http':
252
+ # Non-HTTP traffic is forwarded verbatim
253
+ await self.app(scope, receive, send)
254
+ return
255
+
256
+ if scope.get('path') == '/_vercel/ping':
257
+ await send({
258
+ 'type': 'http.response.start',
259
+ 'status': 200,
260
+ 'headers': [],
261
+ })
262
+ await send({
263
+ 'type': 'http.response.body',
264
+ 'body': b'',
265
+ 'more_body': False,
266
+ })
267
+ return
268
+
269
+ # Extract internal headers and set per-request context
270
+ headers_list = scope.get('headers', []) or []
271
+ new_headers = []
272
+ invocation_id = "0"
273
+ request_id = 0
274
+
275
+ def _b2s(b: bytes) -> str:
276
+ try:
277
+ return b.decode()
278
+ except Exception:
279
+ return ''
280
+
281
+ for k, v in headers_list:
282
+ key = _b2s(k).lower()
283
+ val = _b2s(v)
284
+ if key == 'x-vercel-internal-invocation-id':
285
+ invocation_id = val
286
+ continue
287
+ if key == 'x-vercel-internal-request-id':
288
+ request_id = int(val) if val.isdigit() else 0
289
+ continue
290
+ if key in ('x-vercel-internal-span-id', 'x-vercel-internal-trace-id'):
291
+ continue
292
+ new_headers.append((k, v))
293
+
294
+ new_scope = dict(scope)
295
+ new_scope['headers'] = new_headers
296
+
297
+ # Announce handler start and set context for logging/metrics
298
+ send_message({
299
+ "type": "handler-started",
300
+ "payload": {
301
+ "handlerStartedAt": int(time.time() * 1000),
302
+ "context": {
303
+ "invocationId": invocation_id,
304
+ "requestId": request_id,
305
+ }
306
+ }
307
+ })
308
+
309
+ token = storage.set({
310
+ "invocationId": invocation_id,
311
+ "requestId": request_id,
312
+ })
313
+
314
+ try:
315
+ await self.app(new_scope, receive, send)
316
+ finally:
317
+ storage.reset(token)
318
+ send_message({
319
+ "type": "end",
320
+ "payload": {
321
+ "context": {
322
+ "invocationId": invocation_id,
323
+ "requestId": request_id,
324
+ }
325
+ }
326
+ })
327
+
213
328
  if 'VERCEL_IPC_PATH' in os.environ:
214
329
  start_time = time.time()
215
330
 
@@ -321,8 +436,8 @@ if 'VERCEL_IPC_PATH' in os.environ:
321
436
  if 'handler' in __vc_variables or 'Handler' in __vc_variables:
322
437
  base = __vc_module.handler if ('handler' in __vc_variables) else __vc_module.Handler
323
438
  if not issubclass(base, BaseHTTPRequestHandler):
324
- print('Handler must inherit from BaseHTTPRequestHandler')
325
- print('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
439
+ _stderr('Handler must inherit from BaseHTTPRequestHandler')
440
+ _stderr('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
326
441
  exit(1)
327
442
 
328
443
  class Handler(BaseHandler, base):
@@ -403,80 +518,53 @@ if 'VERCEL_IPC_PATH' in os.environ:
403
518
  if hasattr(response, 'close'):
404
519
  response.close()
405
520
  else:
406
- from urllib.parse import urlparse
407
- from io import BytesIO
408
- import asyncio
409
-
410
- app = __vc_module.app
411
-
412
- class Handler(BaseHandler):
413
- def handle_request(self):
414
- # Prepare ASGI scope
415
- url = urlparse(self.path)
416
- headers_encoded = []
417
- for k, v in self.headers.items():
418
- # Cope with repeated headers in the encoding.
419
- if isinstance(v, list):
420
- headers_encoded.append([k.lower().encode(), [i.encode() for i in v]])
421
- else:
422
- headers_encoded.append([k.lower().encode(), v.encode()])
423
- scope = {
424
- 'server': (self.headers.get('host', 'lambda'), self.headers.get('x-forwarded-port', 80)),
425
- 'client': (self.headers.get(
426
- 'x-forwarded-for', self.headers.get(
427
- 'x-real-ip')), 0),
428
- 'scheme': self.headers.get('x-forwarded-proto', 'http'),
429
- 'root_path': '',
430
- 'query_string': url.query.encode(),
431
- 'headers': headers_encoded,
432
- 'type': 'http',
433
- 'http_version': '1.1',
434
- 'method': self.command,
435
- 'path': url.path,
436
- 'raw_path': url.path.encode(),
437
- }
521
+ # ASGI: Run with Uvicorn so we get proper lifespan and protocol handling
522
+ try:
523
+ import uvicorn
524
+ except Exception:
525
+ _stderr('Uvicorn is required to run ASGI apps. Please ensure it is installed.')
526
+ exit(1)
527
+
528
+ # Prefer a callable app.asgi when available; some frameworks expose a boolean here
529
+ user_app_candidate = getattr(__vc_module.app, 'asgi', None)
530
+ user_app = user_app_candidate if callable(user_app_candidate) else __vc_module.app
531
+ asgi_app = ASGIMiddleware(user_app)
532
+
533
+ # Pre-bind a socket to obtain an ephemeral port for IPC announcement
534
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
535
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
536
+ sock.bind(('127.0.0.1', 0))
537
+ sock.listen(2048)
538
+ http_port = sock.getsockname()[1]
539
+
540
+ config = uvicorn.Config(
541
+ app=asgi_app,
542
+ fd=sock.fileno(),
543
+ lifespan='auto',
544
+ access_log=False,
545
+ log_config=None,
546
+ log_level='warning',
547
+ )
548
+ server = uvicorn.Server(config)
438
549
 
439
- if 'content-length' in self.headers:
440
- content_length = int(self.headers['content-length'])
441
- body = self.rfile.read(content_length)
442
- else:
443
- body = b''
550
+ send_message({
551
+ "type": "server-started",
552
+ "payload": {
553
+ "initDuration": int((time.time() - start_time) * 1000),
554
+ "httpPort": http_port,
555
+ }
556
+ })
444
557
 
445
- if _use_legacy_asyncio:
446
- loop = asyncio.new_event_loop()
447
- app_queue = asyncio.Queue(loop=loop)
448
- else:
449
- app_queue = asyncio.Queue()
450
- app_queue.put_nowait({'type': 'http.request', 'body': body, 'more_body': False})
451
-
452
- # Prepare ASGI receive function
453
- async def receive():
454
- message = await app_queue.get()
455
- return message
456
-
457
- # Prepare ASGI send function
458
- response_started = False
459
- async def send(event):
460
- nonlocal response_started
461
- if event['type'] == 'http.response.start':
462
- self.send_response(event['status'])
463
- if 'headers' in event:
464
- for name, value in event['headers']:
465
- self.send_header(name.decode(), value.decode())
466
- self.end_headers()
467
- response_started = True
468
- elif event['type'] == 'http.response.body':
469
- self.wfile.write(event['body'])
470
- if not event.get('more_body', False):
471
- self.wfile.flush()
558
+ # Mark IPC as ready and flush any buffered init logs
559
+ _ipc_ready = True
560
+ for m in _init_log_buf:
561
+ send_message(m)
562
+ _init_log_buf.clear()
472
563
 
473
- # Run the ASGI application
474
- asgi_instance = app(scope, receive, send)
475
- if _use_legacy_asyncio:
476
- asgi_task = loop.create_task(asgi_instance)
477
- loop.run_until_complete(asgi_task)
478
- else:
479
- asyncio.run(asgi_instance)
564
+ # Run the server (blocking)
565
+ server.run()
566
+ # If the server ever returns, exit
567
+ sys.exit(0)
480
568
 
481
569
  if 'Handler' in locals():
482
570
  server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
@@ -494,8 +582,8 @@ if 'VERCEL_IPC_PATH' in os.environ:
494
582
  _init_log_buf.clear()
495
583
  server.serve_forever()
496
584
 
497
- print('Missing variable `handler` or `app` in file "__VC_HANDLER_ENTRYPOINT".')
498
- print('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
585
+ _stderr('Missing variable `handler` or `app` in file "__VC_HANDLER_ENTRYPOINT".')
586
+ _stderr('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
499
587
  exit(1)
500
588
 
501
589
  if 'handler' in __vc_variables or 'Handler' in __vc_variables:
@@ -39,8 +39,9 @@ if _app is None:
39
39
  f"Missing 'app' in module '{USER_MODULE}'. Define `app = ...` (ASGI app)."
40
40
  )
41
41
 
42
- # Sanic compatibility: prefer `app.asgi` when available
43
- USER_ASGI_APP = getattr(_app, 'asgi', _app)
42
+ # Prefer a callable app.asgi when available; some frameworks expose a boolean here
43
+ _CAND = getattr(_app, 'asgi', None)
44
+ USER_ASGI_APP = _CAND if callable(_CAND) else _app
44
45
 
45
46
  PUBLIC_DIR = 'public'
46
47