@vercel/python 5.0.5 → 5.0.7

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.7",
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
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  import sys
2
3
  import os
3
4
  import site
@@ -5,9 +6,19 @@ import importlib
5
6
  import base64
6
7
  import json
7
8
  import inspect
9
+ import threading
10
+ import asyncio
11
+ import http
12
+ import time
8
13
  from importlib import util
9
- from http.server import BaseHTTPRequestHandler
14
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
10
15
  import socket
16
+ import functools
17
+ import logging
18
+ import builtins
19
+ from typing import Callable, Literal
20
+ import contextvars
21
+ import io
11
22
 
12
23
  _here = os.path.dirname(__file__)
13
24
  _vendor_rel = '__VC_HANDLER_VENDOR_DIR'
@@ -51,72 +62,69 @@ def format_headers(headers, decode=False):
51
62
  keyToList[key].append(value)
52
63
  return keyToList
53
64
 
54
- if 'VERCEL_IPC_PATH' in os.environ:
55
- from http.server import ThreadingHTTPServer
56
- import http
57
- import time
58
- import contextvars
59
- import functools
60
- import builtins
61
- import logging
65
+ # Custom logging handler so logs are properly categorized
66
+ class VCLogHandler(logging.Handler):
67
+ def __init__(self, send_message: Callable[[dict], None], context_getter: Callable[[], dict] | None = None):
68
+ super().__init__()
69
+ self._send_message = send_message
70
+ self._context_getter = context_getter
62
71
 
63
- start_time = time.time()
64
- sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
65
- sock.connect(os.getenv("VERCEL_IPC_PATH", ""))
72
+ def emit(self, record):
73
+ try:
74
+ message = record.getMessage()
75
+ except Exception:
76
+ try:
77
+ message = f"{record.msg}"
78
+ except Exception:
79
+ message = ""
80
+
81
+ if record.levelno >= logging.CRITICAL:
82
+ level = "fatal"
83
+ elif record.levelno >= logging.ERROR:
84
+ level = "error"
85
+ elif record.levelno >= logging.WARNING:
86
+ level = "warn"
87
+ elif record.levelno >= logging.INFO:
88
+ level = "info"
89
+ else:
90
+ level = "debug"
66
91
 
67
- send_message = lambda message: sock.sendall((json.dumps(message) + '\0').encode())
68
- storage = contextvars.ContextVar('storage', default=None)
92
+ ctx = None
93
+ try:
94
+ ctx = self._context_getter() if self._context_getter is not None else None
95
+ except Exception:
96
+ ctx = None
69
97
 
70
- # Override urlopen from urllib3 (& requests) to send Request Metrics
71
- try:
72
- import urllib3
73
- from urllib.parse import urlparse
98
+ if ctx is not None:
99
+ try:
100
+ self._send_message({
101
+ "type": "log",
102
+ "payload": {
103
+ "context": {
104
+ "invocationId": ctx['invocationId'],
105
+ "requestId": ctx['requestId'],
106
+ },
107
+ "message": base64.b64encode(message.encode()).decode(),
108
+ "level": level,
109
+ }
110
+ })
111
+ except Exception:
112
+ pass
113
+ else:
114
+ try:
115
+ sys.stdout.write(message + "\n")
116
+ except Exception:
117
+ pass
74
118
 
75
- def timed_request(func):
76
- fetchId = 0
77
- @functools.wraps(func)
78
- def wrapper(self, method, url, *args, **kwargs):
79
- nonlocal fetchId
80
- fetchId += 1
81
- start_time = int(time.time() * 1000)
82
- result = func(self, method, url, *args, **kwargs)
83
- elapsed_time = int(time.time() * 1000) - start_time
84
- parsed_url = urlparse(url)
85
- context = storage.get()
86
- if context is not None:
87
- send_message({
88
- "type": "metric",
89
- "payload": {
90
- "context": {
91
- "invocationId": context['invocationId'],
92
- "requestId": context['requestId'],
93
- },
94
- "type": "fetch-metric",
95
- "payload": {
96
- "pathname": parsed_url.path,
97
- "search": parsed_url.query,
98
- "start": start_time,
99
- "duration": elapsed_time,
100
- "host": parsed_url.hostname or self.host,
101
- "statusCode": result.status,
102
- "method": method,
103
- "id": fetchId
104
- }
105
- }
106
- })
107
- return result
108
- return wrapper
109
- urllib3.connectionpool.HTTPConnectionPool.urlopen = timed_request(urllib3.connectionpool.HTTPConnectionPool.urlopen)
110
- except:
111
- pass
112
119
 
120
+ def setup_logging(send_message: Callable[[dict], None], storage: contextvars.ContextVar[dict | None]):
113
121
  # Override sys.stdout and sys.stderr to map logs to the correct request
114
122
  class StreamWrapper:
115
- def __init__(self, stream, stream_name):
123
+ def __init__(self, stream: io.TextIOBase, stream_name: Literal["stdout", "stderr"]):
116
124
  self.stream = stream
117
125
  self.stream_name = stream_name
118
126
 
119
- def write(self, message):
127
+ def write(self, message: str):
120
128
  context = storage.get()
121
129
  if context is not None:
122
130
  send_message({
@@ -139,19 +147,15 @@ if 'VERCEL_IPC_PATH' in os.environ:
139
147
  sys.stdout = StreamWrapper(sys.stdout, "stdout")
140
148
  sys.stderr = StreamWrapper(sys.stderr, "stderr")
141
149
 
142
- # Override the global print to log to stdout
143
- def print_wrapper(func):
144
- @functools.wraps(func)
145
- def wrapper(*args, **kwargs):
146
- sys.stdout.write(' '.join(map(str, args)) + '\n')
147
- return wrapper
148
- builtins.print = print_wrapper(builtins.print)
149
-
150
- # Override logging to maps logs to the correct request
151
- def logging_wrapper(func, level="info"):
150
+ # Wrap top-level logging helpers to emit structured logs when a request
151
+ # context is available; otherwise fall back to the original behavior.
152
+ def logging_wrapper(func: Callable[..., None], level: str = "info") -> Callable[..., None]:
152
153
  @functools.wraps(func)
153
154
  def wrapper(*args, **kwargs):
154
- context = storage.get()
155
+ try:
156
+ context = storage.get()
157
+ except Exception:
158
+ context = None
155
159
  if context is not None:
156
160
  send_message({
157
161
  "type": "log",
@@ -168,12 +172,77 @@ if 'VERCEL_IPC_PATH' in os.environ:
168
172
  func(*args, **kwargs)
169
173
  return wrapper
170
174
 
171
- logging.basicConfig(level=logging.INFO)
172
- logging.debug = logging_wrapper(logging.debug)
173
- logging.info = logging_wrapper(logging.info)
175
+ logging.basicConfig(level=logging.INFO, handlers=[VCLogHandler(send_message, storage.get)], force=True)
176
+ logging.debug = logging_wrapper(logging.debug, "debug")
177
+ logging.info = logging_wrapper(logging.info, "info")
174
178
  logging.warning = logging_wrapper(logging.warning, "warn")
175
179
  logging.error = logging_wrapper(logging.error, "error")
176
- logging.critical = logging_wrapper(logging.critical, "error")
180
+ logging.fatal = logging_wrapper(logging.fatal, "fatal")
181
+ logging.critical = logging_wrapper(logging.critical, "fatal")
182
+
183
+ # Ensure built-in print funnels through stdout wrapper so prints are
184
+ # attributed to the current request context.
185
+ def print_wrapper(func: Callable[..., None]) -> Callable[..., None]:
186
+ @functools.wraps(func)
187
+ def wrapper(*args, **kwargs):
188
+ sys.stdout.write(' '.join(map(str, args)) + '\n')
189
+ return wrapper
190
+
191
+ builtins.print = print_wrapper(builtins.print)
192
+
193
+
194
+ if 'VERCEL_IPC_PATH' in os.environ:
195
+ start_time = time.time()
196
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
197
+ sock.connect(os.getenv("VERCEL_IPC_PATH", ""))
198
+
199
+ send_message = lambda message: sock.sendall((json.dumps(message) + '\0').encode())
200
+ storage = contextvars.ContextVar('storage', default=None)
201
+
202
+ # Override urlopen from urllib3 (& requests) to send Request Metrics
203
+ try:
204
+ import urllib3
205
+ from urllib.parse import urlparse
206
+
207
+ def timed_request(func):
208
+ fetchId = 0
209
+ @functools.wraps(func)
210
+ def wrapper(self, method, url, *args, **kwargs):
211
+ nonlocal fetchId
212
+ fetchId += 1
213
+ start_time = int(time.time() * 1000)
214
+ result = func(self, method, url, *args, **kwargs)
215
+ elapsed_time = int(time.time() * 1000) - start_time
216
+ parsed_url = urlparse(url)
217
+ context = storage.get()
218
+ if context is not None:
219
+ send_message({
220
+ "type": "metric",
221
+ "payload": {
222
+ "context": {
223
+ "invocationId": context['invocationId'],
224
+ "requestId": context['requestId'],
225
+ },
226
+ "type": "fetch-metric",
227
+ "payload": {
228
+ "pathname": parsed_url.path,
229
+ "search": parsed_url.query,
230
+ "start": start_time,
231
+ "duration": elapsed_time,
232
+ "host": parsed_url.hostname or self.host,
233
+ "statusCode": result.status,
234
+ "method": method,
235
+ "id": fetchId
236
+ }
237
+ }
238
+ })
239
+ return result
240
+ return wrapper
241
+ urllib3.connectionpool.HTTPConnectionPool.urlopen = timed_request(urllib3.connectionpool.HTTPConnectionPool.urlopen)
242
+ except:
243
+ pass
244
+
245
+ setup_logging(send_message, storage)
177
246
 
178
247
  class BaseHandler(BaseHTTPRequestHandler):
179
248
  # Re-implementation of BaseHTTPRequestHandler's log_message method to
@@ -256,6 +325,7 @@ if 'VERCEL_IPC_PATH' in os.environ:
256
325
  method()
257
326
  self.wfile.flush()
258
327
  elif 'app' in __vc_variables:
328
+ # WSGI
259
329
  if (
260
330
  not inspect.iscoroutinefunction(__vc_module.app) and
261
331
  not inspect.iscoroutinefunction(__vc_module.app.__call__)
@@ -321,10 +391,10 @@ if 'VERCEL_IPC_PATH' in os.environ:
321
391
  finally:
322
392
  if hasattr(response, 'close'):
323
393
  response.close()
394
+ # ASGI
324
395
  else:
325
396
  from urllib.parse import urlparse
326
397
  from io import BytesIO
327
- import asyncio
328
398
 
329
399
  app = __vc_module.app
330
400
 
@@ -339,6 +409,7 @@ if 'VERCEL_IPC_PATH' in os.environ:
339
409
  headers_encoded.append([k.lower().encode(), [i.encode() for i in v]])
340
410
  else:
341
411
  headers_encoded.append([k.lower().encode(), v.encode()])
412
+
342
413
  scope = {
343
414
  'server': (self.headers.get('host', 'lambda'), self.headers.get('x-forwarded-port', 80)),
344
415
  'client': (self.headers.get(
@@ -361,41 +432,91 @@ if 'VERCEL_IPC_PATH' in os.environ:
361
432
  else:
362
433
  body = b''
363
434
 
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()
435
+ # Event to signal that the response has been fully sent
436
+ response_done = threading.Event()
391
437
 
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)
438
+ # Propagate request context to background thread for logging & metrics
439
+ request_context = storage.get()
440
+
441
+ def run_asgi():
442
+ # Ensure request context is available in this thread
443
+ if request_context is not None:
444
+ token = storage.set(request_context)
445
+ else:
446
+ token = None
447
+ # Track if headers were sent, so we can synthesize a 500 on early failure
448
+ response_started = False
449
+ try:
450
+ async def runner():
451
+ # Per-request app queue
452
+ if _use_legacy_asyncio:
453
+ loop = asyncio.get_running_loop()
454
+ app_queue = asyncio.Queue(loop=loop)
455
+ else:
456
+ app_queue = asyncio.Queue()
457
+
458
+ await app_queue.put({'type': 'http.request', 'body': body, 'more_body': False})
459
+
460
+ async def receive():
461
+ message = await app_queue.get()
462
+ return message
463
+
464
+ async def send(event):
465
+ nonlocal response_started
466
+ if event['type'] == 'http.response.start':
467
+ self.send_response(event['status'])
468
+ if 'headers' in event:
469
+ for name, value in event['headers']:
470
+ self.send_header(name.decode(), value.decode())
471
+ self.end_headers()
472
+ response_started = True
473
+ elif event['type'] == 'http.response.body':
474
+ # Stream body as it is produced; flush on completion
475
+ body_bytes = event.get('body', b'') or b''
476
+ if body_bytes:
477
+ self.wfile.write(body_bytes)
478
+ if not event.get('more_body', False):
479
+ try:
480
+ self.wfile.flush()
481
+ finally:
482
+ response_done.set()
483
+ try:
484
+ app_queue.put_nowait({'type': 'http.disconnect'})
485
+ except Exception:
486
+ pass
487
+
488
+ # Run ASGI app (includes background tasks)
489
+ asgi_instance = app(scope, receive, send)
490
+ await asgi_instance
491
+
492
+ asyncio.run(runner())
493
+ except Exception:
494
+ # If the app raised before starting the response, synthesize a 500
495
+ try:
496
+ if not response_started:
497
+ self.send_response(500)
498
+ self.end_headers()
499
+ try:
500
+ self.wfile.flush()
501
+ except Exception:
502
+ pass
503
+ except Exception:
504
+ pass
505
+ finally:
506
+ # Always unblock the waiting thread to avoid hangs
507
+ try:
508
+ response_done.set()
509
+ except Exception:
510
+ pass
511
+ if token is not None:
512
+ storage.reset(token)
513
+
514
+ # Run ASGI in background thread to allow returning after final flush
515
+ t = threading.Thread(target=run_asgi, daemon=True)
516
+ t.start()
517
+
518
+ # Wait until final body chunk has been flushed to client
519
+ response_done.wait()
399
520
 
400
521
  if 'Handler' in locals():
401
522
  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)