@vercel/python 6.0.2 → 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.2",
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",
package/vc_init.py CHANGED
@@ -210,12 +210,17 @@ if 'VERCEL_IPC_PATH' in os.environ:
210
210
 
211
211
 
212
212
  # Import relative path https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
213
- user_mod_path = os.path.join(_here, "__VC_HANDLER_ENTRYPOINT") # absolute
214
- __vc_spec = util.spec_from_file_location("__VC_HANDLER_MODULE_NAME", user_mod_path)
215
- __vc_module = util.module_from_spec(__vc_spec)
216
- sys.modules["__VC_HANDLER_MODULE_NAME"] = __vc_module
217
- __vc_spec.loader.exec_module(__vc_module)
218
- __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)
219
224
 
220
225
  _use_legacy_asyncio = sys.version_info < (3, 10)
221
226
 
@@ -231,6 +236,95 @@ def format_headers(headers, decode=False):
231
236
  return keyToList
232
237
 
233
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
+
234
328
  if 'VERCEL_IPC_PATH' in os.environ:
235
329
  start_time = time.time()
236
330
 
@@ -424,80 +518,53 @@ if 'VERCEL_IPC_PATH' in os.environ:
424
518
  if hasattr(response, 'close'):
425
519
  response.close()
426
520
  else:
427
- from urllib.parse import urlparse
428
- from io import BytesIO
429
- import asyncio
430
-
431
- app = __vc_module.app
432
-
433
- class Handler(BaseHandler):
434
- def handle_request(self):
435
- # Prepare ASGI scope
436
- url = urlparse(self.path)
437
- headers_encoded = []
438
- for k, v in self.headers.items():
439
- # Cope with repeated headers in the encoding.
440
- if isinstance(v, list):
441
- headers_encoded.append([k.lower().encode(), [i.encode() for i in v]])
442
- else:
443
- headers_encoded.append([k.lower().encode(), v.encode()])
444
- scope = {
445
- 'server': (self.headers.get('host', 'lambda'), self.headers.get('x-forwarded-port', 80)),
446
- 'client': (self.headers.get(
447
- 'x-forwarded-for', self.headers.get(
448
- 'x-real-ip')), 0),
449
- 'scheme': self.headers.get('x-forwarded-proto', 'http'),
450
- 'root_path': '',
451
- 'query_string': url.query.encode(),
452
- 'headers': headers_encoded,
453
- 'type': 'http',
454
- 'http_version': '1.1',
455
- 'method': self.command,
456
- 'path': url.path,
457
- 'raw_path': url.path.encode(),
458
- }
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)
459
549
 
460
- if 'content-length' in self.headers:
461
- content_length = int(self.headers['content-length'])
462
- body = self.rfile.read(content_length)
463
- else:
464
- 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
+ })
465
557
 
466
- if _use_legacy_asyncio:
467
- loop = asyncio.new_event_loop()
468
- app_queue = asyncio.Queue(loop=loop)
469
- else:
470
- app_queue = asyncio.Queue()
471
- app_queue.put_nowait({'type': 'http.request', 'body': body, 'more_body': False})
472
-
473
- # Prepare ASGI receive function
474
- async def receive():
475
- message = await app_queue.get()
476
- return message
477
-
478
- # Prepare ASGI send function
479
- response_started = False
480
- async def send(event):
481
- nonlocal response_started
482
- if event['type'] == 'http.response.start':
483
- self.send_response(event['status'])
484
- if 'headers' in event:
485
- for name, value in event['headers']:
486
- self.send_header(name.decode(), value.decode())
487
- self.end_headers()
488
- response_started = True
489
- elif event['type'] == 'http.response.body':
490
- self.wfile.write(event['body'])
491
- if not event.get('more_body', False):
492
- 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()
493
563
 
494
- # Run the ASGI application
495
- asgi_instance = app(scope, receive, send)
496
- if _use_legacy_asyncio:
497
- asgi_task = loop.create_task(asgi_instance)
498
- loop.run_until_complete(asgi_task)
499
- else:
500
- asyncio.run(asgi_instance)
564
+ # Run the server (blocking)
565
+ server.run()
566
+ # If the server ever returns, exit
567
+ sys.exit(0)
501
568
 
502
569
  if 'Handler' in locals():
503
570
  server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
@@ -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