@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 +22 -2
- package/package.json +2 -2
- package/vc_init.py +169 -81
- package/vc_init_dev_asgi.py +3 -2
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 = [
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
498
|
-
|
|
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:
|
package/vc_init_dev_asgi.py
CHANGED
|
@@ -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
|
-
#
|
|
43
|
-
|
|
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
|
|