@vercel/python 5.0.5 → 5.0.6

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 CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@vercel/python",
3
- "version": "5.0.5",
3
+ "version": "5.0.6",
4
4
  "main": "./dist/index.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
7
7
  "files": [
8
8
  "dist",
9
- "vc_init.py"
9
+ "vc_init.py",
10
+ "vc_init_dev_asgi.py"
10
11
  ],
11
12
  "repository": {
12
13
  "type": "git",
package/vc_init.py CHANGED
@@ -5,6 +5,8 @@ import importlib
5
5
  import base64
6
6
  import json
7
7
  import inspect
8
+ import threading
9
+ import asyncio
8
10
  from importlib import util
9
11
  from http.server import BaseHTTPRequestHandler
10
12
  import socket
@@ -256,6 +258,7 @@ if 'VERCEL_IPC_PATH' in os.environ:
256
258
  method()
257
259
  self.wfile.flush()
258
260
  elif 'app' in __vc_variables:
261
+ # WSGI
259
262
  if (
260
263
  not inspect.iscoroutinefunction(__vc_module.app) and
261
264
  not inspect.iscoroutinefunction(__vc_module.app.__call__)
@@ -321,10 +324,10 @@ if 'VERCEL_IPC_PATH' in os.environ:
321
324
  finally:
322
325
  if hasattr(response, 'close'):
323
326
  response.close()
327
+ # ASGI
324
328
  else:
325
329
  from urllib.parse import urlparse
326
330
  from io import BytesIO
327
- import asyncio
328
331
 
329
332
  app = __vc_module.app
330
333
 
@@ -339,6 +342,7 @@ if 'VERCEL_IPC_PATH' in os.environ:
339
342
  headers_encoded.append([k.lower().encode(), [i.encode() for i in v]])
340
343
  else:
341
344
  headers_encoded.append([k.lower().encode(), v.encode()])
345
+
342
346
  scope = {
343
347
  'server': (self.headers.get('host', 'lambda'), self.headers.get('x-forwarded-port', 80)),
344
348
  'client': (self.headers.get(
@@ -361,41 +365,91 @@ if 'VERCEL_IPC_PATH' in os.environ:
361
365
  else:
362
366
  body = b''
363
367
 
364
- if _use_legacy_asyncio:
365
- loop = asyncio.new_event_loop()
366
- app_queue = asyncio.Queue(loop=loop)
367
- else:
368
- app_queue = asyncio.Queue()
369
- app_queue.put_nowait({'type': 'http.request', 'body': body, 'more_body': False})
370
-
371
- # Prepare ASGI receive function
372
- async def receive():
373
- message = await app_queue.get()
374
- return message
375
-
376
- # Prepare ASGI send function
377
- response_started = False
378
- async def send(event):
379
- nonlocal response_started
380
- if event['type'] == 'http.response.start':
381
- self.send_response(event['status'])
382
- if 'headers' in event:
383
- for name, value in event['headers']:
384
- self.send_header(name.decode(), value.decode())
385
- self.end_headers()
386
- response_started = True
387
- elif event['type'] == 'http.response.body':
388
- self.wfile.write(event['body'])
389
- if not event.get('more_body', False):
390
- self.wfile.flush()
368
+ # Event to signal that the response has been fully sent
369
+ response_done = threading.Event()
391
370
 
392
- # Run the ASGI application
393
- asgi_instance = app(scope, receive, send)
394
- if _use_legacy_asyncio:
395
- asgi_task = loop.create_task(asgi_instance)
396
- loop.run_until_complete(asgi_task)
397
- else:
398
- asyncio.run(asgi_instance)
371
+ # Propagate request context to background thread for logging & metrics
372
+ request_context = storage.get()
373
+
374
+ def run_asgi():
375
+ # Ensure request context is available in this thread
376
+ if request_context is not None:
377
+ token = storage.set(request_context)
378
+ else:
379
+ token = None
380
+ # Track if headers were sent, so we can synthesize a 500 on early failure
381
+ response_started = False
382
+ try:
383
+ async def runner():
384
+ # Per-request app queue
385
+ if _use_legacy_asyncio:
386
+ loop = asyncio.get_running_loop()
387
+ app_queue = asyncio.Queue(loop=loop)
388
+ else:
389
+ app_queue = asyncio.Queue()
390
+
391
+ await app_queue.put({'type': 'http.request', 'body': body, 'more_body': False})
392
+
393
+ async def receive():
394
+ message = await app_queue.get()
395
+ return message
396
+
397
+ async def send(event):
398
+ nonlocal response_started
399
+ if event['type'] == 'http.response.start':
400
+ self.send_response(event['status'])
401
+ if 'headers' in event:
402
+ for name, value in event['headers']:
403
+ self.send_header(name.decode(), value.decode())
404
+ self.end_headers()
405
+ response_started = True
406
+ elif event['type'] == 'http.response.body':
407
+ # Stream body as it is produced; flush on completion
408
+ body_bytes = event.get('body', b'') or b''
409
+ if body_bytes:
410
+ self.wfile.write(body_bytes)
411
+ if not event.get('more_body', False):
412
+ try:
413
+ self.wfile.flush()
414
+ finally:
415
+ response_done.set()
416
+ try:
417
+ app_queue.put_nowait({'type': 'http.disconnect'})
418
+ except Exception:
419
+ pass
420
+
421
+ # Run ASGI app (includes background tasks)
422
+ asgi_instance = app(scope, receive, send)
423
+ await asgi_instance
424
+
425
+ asyncio.run(runner())
426
+ except Exception:
427
+ # If the app raised before starting the response, synthesize a 500
428
+ try:
429
+ if not response_started:
430
+ self.send_response(500)
431
+ self.end_headers()
432
+ try:
433
+ self.wfile.flush()
434
+ except Exception:
435
+ pass
436
+ except Exception:
437
+ pass
438
+ finally:
439
+ # Always unblock the waiting thread to avoid hangs
440
+ try:
441
+ response_done.set()
442
+ except Exception:
443
+ pass
444
+ if token is not None:
445
+ storage.reset(token)
446
+
447
+ # Run ASGI in background thread to allow returning after final flush
448
+ t = threading.Thread(target=run_asgi, daemon=True)
449
+ t.start()
450
+
451
+ # Wait until final body chunk has been flushed to client
452
+ response_done.wait()
399
453
 
400
454
  if 'Handler' in locals():
401
455
  server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
@@ -0,0 +1,58 @@
1
+ # Auto-generated template used by vercel dev (Python, ASGI)
2
+ # Serves static files from PUBLIC_DIR before delegating to the user ASGI app.
3
+ from importlib import import_module
4
+ import os
5
+ from os import path as _p
6
+
7
+ # Optional StaticFiles import; tolerate missing deps
8
+ StaticFiles = None
9
+ try:
10
+ from fastapi.staticfiles import StaticFiles as _SF
11
+ StaticFiles = _SF
12
+ except Exception:
13
+ try:
14
+ from starlette.staticfiles import StaticFiles as _SF
15
+ StaticFiles = _SF
16
+ except Exception:
17
+ StaticFiles = None
18
+
19
+ USER_MODULE = "__VC_DEV_MODULE_PATH__"
20
+ _mod = import_module(USER_MODULE)
21
+ _app = getattr(_mod, 'app', None)
22
+ if _app is None:
23
+ raise RuntimeError(
24
+ f"Missing 'app' in module '{USER_MODULE}'. Define `app = ...` (ASGI app)."
25
+ )
26
+
27
+ # Sanic compatibility: prefer `app.asgi` when available
28
+ USER_ASGI_APP = getattr(_app, 'asgi', _app)
29
+
30
+ PUBLIC_DIR = 'public'
31
+
32
+ # Prepare static files app (if starlette/fastapi installed)
33
+ static_app = None
34
+ if StaticFiles is not None:
35
+ try:
36
+ try:
37
+ static_app = StaticFiles(directory=PUBLIC_DIR, check_dir=False)
38
+ except TypeError:
39
+ # Older Starlette without check_dir parameter
40
+ static_app = StaticFiles(directory=PUBLIC_DIR)
41
+ except Exception:
42
+ static_app = None
43
+
44
+
45
+ async def app(scope, receive, send):
46
+ if static_app is not None and scope.get('type') == 'http':
47
+ req_path = scope.get('path', '/') or '/'
48
+ safe = _p.normpath(req_path).lstrip('/')
49
+ full = _p.join(PUBLIC_DIR, safe)
50
+ try:
51
+ base = _p.realpath(PUBLIC_DIR)
52
+ target = _p.realpath(full)
53
+ if (target == base or target.startswith(base + _p.sep)) and _p.isfile(target):
54
+ await static_app(scope, receive, send)
55
+ return
56
+ except Exception:
57
+ pass
58
+ await USER_ASGI_APP(scope, receive, send)