@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/dist/index.js +742 -217
- package/package.json +3 -2
- package/vc_init.py +89 -35
- package/vc_init_dev_asgi.py +58 -0
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vercel/python",
|
|
3
|
-
"version": "5.0.
|
|
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
|
-
|
|
365
|
-
|
|
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
|
-
#
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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)
|