vercel-runtime 0.1.0__tar.gz

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.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: vercel-runtime
3
+ Version: 0.1.0
4
+ License-Expression: Apache-2.0
5
+ Classifier: Development Status :: 4 - Beta
6
+ Classifier: Intended Audience :: Developers
7
+ Classifier: Intended Audience :: Information Technology
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Programming Language :: Python :: Implementation :: CPython
14
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Python: >=3.12
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "vercel-runtime"
3
+ version = "0.1.0"
4
+ license = "Apache-2.0"
5
+ requires-python = ">=3.12"
6
+ classifiers = [
7
+ "Development Status :: 4 - Beta",
8
+ "Intended Audience :: Developers",
9
+ "Intended Audience :: Information Technology",
10
+ "Operating System :: OS Independent",
11
+ "Programming Language :: Python :: 3 :: Only",
12
+ "Programming Language :: Python :: 3.12",
13
+ "Programming Language :: Python :: 3.13",
14
+ "Programming Language :: Python :: 3.14",
15
+ "Programming Language :: Python :: Implementation :: CPython",
16
+ "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
17
+ "Topic :: Software Development :: Libraries :: Python Modules",
18
+ ]
19
+
20
+ [build-system]
21
+ requires = ["uv_build>=0.9.22,<0.10.0"]
22
+ build-backend = "uv_build"
File without changes
@@ -0,0 +1,913 @@
1
+ from __future__ import annotations
2
+ import sys
3
+ import os
4
+ import site
5
+ import importlib
6
+ import base64
7
+ import json
8
+ import inspect
9
+ import asyncio
10
+ import http
11
+ import time
12
+ import traceback
13
+ from importlib import util
14
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
15
+ import socket
16
+ import functools
17
+ import logging
18
+ import builtins
19
+ from typing import Callable, Literal, TextIO, Never
20
+ import contextvars
21
+ import contextlib
22
+ import atexit
23
+
24
+
25
+ _original_stderr = sys.stderr
26
+
27
+
28
+ def _stderr(message: str):
29
+ with contextlib.suppress(Exception):
30
+ _original_stderr.write(message + "\n")
31
+ _original_stderr.flush()
32
+
33
+
34
+ def _fatal(message: str) -> Never:
35
+ _stderr(message)
36
+ sys.exit(1)
37
+
38
+
39
+ def _must_getenv(varname: str) -> str:
40
+ value = os.environ.get(varname)
41
+ if not value:
42
+ _fatal(f"{varname} is not set")
43
+ return value
44
+
45
+
46
+ _here = os.path.dirname(__file__)
47
+ _entrypoint_rel = _must_getenv("__VC_HANDLER_ENTRYPOINT")
48
+ _entrypoint_abs = _must_getenv("__VC_HANDLER_ENTRYPOINT_ABS")
49
+ _entrypoint_modname = _must_getenv("__VC_HANDLER_MODULE_NAME")
50
+
51
+
52
+ def setup_logging(send_message: Callable[[dict], None], storage: contextvars.ContextVar[dict | None]):
53
+ # Override logging.Handler to send logs to the platform when a request context is available.
54
+ class VCLogHandler(logging.Handler):
55
+ def emit(self, record: logging.LogRecord):
56
+ try:
57
+ message = record.getMessage()
58
+ except Exception:
59
+ message = repr(getattr(record, "msg", ""))
60
+
61
+ with contextlib.suppress(Exception):
62
+ if record.exc_info:
63
+ # logging allows exc_info=True or a (type, value, tb) tuple
64
+ exc_info = record.exc_info
65
+ if exc_info is True:
66
+ exc_info = sys.exc_info()
67
+ if isinstance(exc_info, tuple):
68
+ tb = ''.join(traceback.format_exception(*exc_info))
69
+ if tb:
70
+ if message:
71
+ message = f"{message}\n{tb}"
72
+ else:
73
+ message = tb
74
+
75
+ if record.levelno >= logging.CRITICAL:
76
+ level = "fatal"
77
+ elif record.levelno >= logging.ERROR:
78
+ level = "error"
79
+ elif record.levelno >= logging.WARNING:
80
+ level = "warn"
81
+ elif record.levelno >= logging.INFO:
82
+ level = "info"
83
+ else:
84
+ level = "debug"
85
+
86
+ context = storage.get()
87
+ if context is not None:
88
+ send_message({
89
+ "type": "log",
90
+ "payload": {
91
+ "context": {
92
+ "invocationId": context['invocationId'],
93
+ "requestId": context['requestId'],
94
+ },
95
+ "message": base64.b64encode(message.encode()).decode(),
96
+ "level": level,
97
+ }
98
+ })
99
+ else:
100
+ # If IPC is not ready, enqueue the message to be sent later.
101
+ enqueue_or_send_message({
102
+ "type": "log",
103
+ "payload": {
104
+ "context": {"invocationId": "0", "requestId": 0},
105
+ "message": base64.b64encode(message.encode()).decode(),
106
+ "level": level,
107
+ }
108
+ })
109
+
110
+ # Override sys.stdout and sys.stderr to map logs to the correct request
111
+ class StreamWrapper:
112
+ def __init__(self, stream: TextIO, stream_name: Literal["stdout", "stderr"]):
113
+ self.stream = stream
114
+ self.stream_name = stream_name
115
+
116
+ def write(self, message: str):
117
+ context = storage.get()
118
+ if context is not None:
119
+ send_message({
120
+ "type": "log",
121
+ "payload": {
122
+ "context": {
123
+ "invocationId": context['invocationId'],
124
+ "requestId": context['requestId'],
125
+ },
126
+ "message": base64.b64encode(message.encode()).decode(),
127
+ "stream": self.stream_name,
128
+ }
129
+ })
130
+ else:
131
+ enqueue_or_send_message({
132
+ "type": "log",
133
+ "payload": {
134
+ "context": {"invocationId": "0", "requestId": 0},
135
+ "message": base64.b64encode(message.encode()).decode(),
136
+ "stream": self.stream_name,
137
+ }
138
+ })
139
+
140
+ def __getattr__(self, name):
141
+ return getattr(self.stream, name)
142
+
143
+ sys.stdout = StreamWrapper(sys.stdout, "stdout")
144
+ sys.stderr = StreamWrapper(sys.stderr, "stderr")
145
+
146
+ logging.basicConfig(level=logging.INFO, handlers=[VCLogHandler()], force=True)
147
+
148
+ # Ensure built-in print funnels through stdout wrapper so prints are
149
+ # attributed to the current request context.
150
+ def print_wrapper(func: Callable[..., None]) -> Callable[..., None]:
151
+ @functools.wraps(func)
152
+ def wrapper(*args, sep=' ', end='\n', file=None, flush=False):
153
+ if file is None:
154
+ file = sys.stdout
155
+ if file in (sys.stdout, sys.stderr):
156
+ file.write(sep.join(map(str, args)) + end)
157
+ if flush:
158
+ file.flush()
159
+ else:
160
+ # User specified a different file, use original print behavior
161
+ func(*args, sep=sep, end=end, file=file, flush=flush)
162
+ return wrapper
163
+
164
+ builtins.print = print_wrapper(builtins.print)
165
+
166
+
167
+ # If running in the platform (IPC present), logging must be setup before importing user code so that
168
+ # logs happening outside the request context are emitted correctly.
169
+ ipc_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
170
+ storage: contextvars.ContextVar[dict | None] = contextvars.ContextVar('storage', default=None)
171
+ send_message = lambda m: None
172
+
173
+
174
+ # Buffer for pre-handshake logs (to avoid blocking IPC on startup)
175
+ _ipc_ready = False
176
+ _init_log_buf: list[dict] = []
177
+ _INIT_LOG_BUF_MAX_BYTES = 1_000_000
178
+ _init_log_buf_bytes = 0
179
+
180
+
181
+ def enqueue_or_send_message(msg: dict):
182
+ global _init_log_buf_bytes
183
+ if _ipc_ready:
184
+ send_message(msg)
185
+ return
186
+
187
+ enc_len = len(json.dumps(msg))
188
+
189
+ if _init_log_buf_bytes + enc_len <= _INIT_LOG_BUF_MAX_BYTES:
190
+ _init_log_buf.append(msg)
191
+ _init_log_buf_bytes += enc_len
192
+ else:
193
+ # Fallback so message is not lost if buffer is full
194
+ with contextlib.suppress(Exception):
195
+ payload = msg.get("payload", {})
196
+ decoded = base64.b64decode(payload.get("message", "")).decode(errors="ignore")
197
+ _original_stderr.write(decoded + "\n")
198
+
199
+
200
+ def flush_init_log_buf_to_stderr():
201
+ global _init_log_buf, _init_log_buf_bytes
202
+ try:
203
+ combined: list[str] = []
204
+ for m in _init_log_buf:
205
+ payload = m.get("payload", {})
206
+ msg = payload.get("message")
207
+ if not msg:
208
+ continue
209
+ with contextlib.suppress(Exception):
210
+ decoded = base64.b64decode(msg).decode(errors="ignore")
211
+ combined.append(decoded)
212
+ if combined:
213
+ _stderr("".join(combined))
214
+ except Exception:
215
+ pass
216
+ finally:
217
+ _init_log_buf.clear()
218
+ _init_log_buf_bytes = 0
219
+
220
+
221
+ atexit.register(flush_init_log_buf_to_stderr)
222
+
223
+
224
+ if 'VERCEL_IPC_PATH' in os.environ:
225
+ with contextlib.suppress(Exception):
226
+ ipc_sock.connect(os.getenv("VERCEL_IPC_PATH", ""))
227
+
228
+ def send_message(message: dict):
229
+ with contextlib.suppress(Exception):
230
+ ipc_sock.sendall((json.dumps(message) + '\0').encode())
231
+
232
+ setup_logging(send_message, storage)
233
+
234
+
235
+ # Import relative path https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
236
+ try:
237
+ __vc_spec = util.spec_from_file_location(_entrypoint_modname, _entrypoint_abs)
238
+ __vc_module = util.module_from_spec(__vc_spec)
239
+ sys.modules[_entrypoint_modname] = __vc_module
240
+ __vc_spec.loader.exec_module(__vc_module)
241
+ __vc_variables = dir(__vc_module)
242
+ except Exception:
243
+ _stderr(f'Error importing {_entrypoint_rel}:')
244
+ _stderr(traceback.format_exc())
245
+ exit(1)
246
+
247
+ _use_legacy_asyncio = sys.version_info < (3, 10)
248
+
249
+ def format_headers(headers, decode=False):
250
+ keyToList = {}
251
+ for key, value in headers.items():
252
+ if decode and 'decode' in dir(key) and 'decode' in dir(value):
253
+ key = key.decode()
254
+ value = value.decode()
255
+ if key not in keyToList:
256
+ keyToList[key] = []
257
+ keyToList[key].append(value)
258
+ return keyToList
259
+
260
+
261
+ class ASGIMiddleware:
262
+ """
263
+ ASGI middleware that preserves Vercel IPC semantics for request lifecycle:
264
+ - Handles /_vercel/ping
265
+ - Extracts x-vercel-internal-* headers and removes them from downstream app
266
+ - Sets request context into `storage` for logging/metrics
267
+ - Emits handler-started and end IPC messages
268
+ """
269
+ def __init__(self, app):
270
+ self.app = app
271
+
272
+ async def __call__(self, scope, receive, send):
273
+ if scope.get('type') != 'http':
274
+ # Non-HTTP traffic is forwarded verbatim
275
+ await self.app(scope, receive, send)
276
+ return
277
+
278
+ if scope.get('path') == '/_vercel/ping':
279
+ await send({
280
+ 'type': 'http.response.start',
281
+ 'status': 200,
282
+ 'headers': [],
283
+ })
284
+ await send({
285
+ 'type': 'http.response.body',
286
+ 'body': b'',
287
+ 'more_body': False,
288
+ })
289
+ return
290
+
291
+ # Extract internal headers and set per-request context
292
+ headers_list = scope.get('headers', []) or []
293
+ new_headers = []
294
+ invocation_id = "0"
295
+ request_id = 0
296
+
297
+ def _b2s(b: bytes) -> str:
298
+ try:
299
+ return b.decode()
300
+ except Exception:
301
+ return ''
302
+
303
+ for k, v in headers_list:
304
+ key = _b2s(k).lower()
305
+ val = _b2s(v)
306
+ if key == 'x-vercel-internal-invocation-id':
307
+ invocation_id = val
308
+ continue
309
+ if key == 'x-vercel-internal-request-id':
310
+ request_id = int(val) if val.isdigit() else 0
311
+ continue
312
+ if key in ('x-vercel-internal-span-id', 'x-vercel-internal-trace-id'):
313
+ continue
314
+ new_headers.append((k, v))
315
+
316
+ new_scope = dict(scope)
317
+ new_scope['headers'] = new_headers
318
+
319
+ # Announce handler start and set context for logging/metrics
320
+ send_message({
321
+ "type": "handler-started",
322
+ "payload": {
323
+ "handlerStartedAt": int(time.time() * 1000),
324
+ "context": {
325
+ "invocationId": invocation_id,
326
+ "requestId": request_id,
327
+ }
328
+ }
329
+ })
330
+
331
+ token = storage.set({
332
+ "invocationId": invocation_id,
333
+ "requestId": request_id,
334
+ })
335
+
336
+ try:
337
+ await self.app(new_scope, receive, send)
338
+ finally:
339
+ storage.reset(token)
340
+ send_message({
341
+ "type": "end",
342
+ "payload": {
343
+ "context": {
344
+ "invocationId": invocation_id,
345
+ "requestId": request_id,
346
+ }
347
+ }
348
+ })
349
+
350
+ if 'VERCEL_IPC_PATH' in os.environ:
351
+ start_time = time.time()
352
+
353
+ # Override urlopen from urllib3 (& requests) to send Request Metrics
354
+ try:
355
+ import urllib3
356
+ from urllib.parse import urlparse
357
+
358
+ def timed_request(func):
359
+ fetchId = 0
360
+ @functools.wraps(func)
361
+ def wrapper(self, method, url, *args, **kwargs):
362
+ nonlocal fetchId
363
+ fetchId += 1
364
+ start_time = int(time.time() * 1000)
365
+ result = func(self, method, url, *args, **kwargs)
366
+ elapsed_time = int(time.time() * 1000) - start_time
367
+ parsed_url = urlparse(url)
368
+ context = storage.get()
369
+ if context is not None:
370
+ send_message({
371
+ "type": "metric",
372
+ "payload": {
373
+ "context": {
374
+ "invocationId": context['invocationId'],
375
+ "requestId": context['requestId'],
376
+ },
377
+ "type": "fetch-metric",
378
+ "payload": {
379
+ "pathname": parsed_url.path,
380
+ "search": parsed_url.query,
381
+ "start": start_time,
382
+ "duration": elapsed_time,
383
+ "host": parsed_url.hostname or self.host,
384
+ "statusCode": result.status,
385
+ "method": method,
386
+ "id": fetchId
387
+ }
388
+ }
389
+ })
390
+ return result
391
+ return wrapper
392
+ urllib3.connectionpool.HTTPConnectionPool.urlopen = timed_request(urllib3.connectionpool.HTTPConnectionPool.urlopen)
393
+ except:
394
+ pass
395
+
396
+ class BaseHandler(BaseHTTPRequestHandler):
397
+ # Re-implementation of BaseHTTPRequestHandler's log_message method to
398
+ # log to stdout instead of stderr.
399
+ def log_message(self, format, *args):
400
+ message = format % args
401
+ sys.stdout.write("%s - - [%s] %s\n" %
402
+ (self.address_string(),
403
+ self.log_date_time_string(),
404
+ message.translate(self._control_char_table)))
405
+
406
+ # Re-implementation of BaseHTTPRequestHandler's handle_one_request method
407
+ # to send the end message after the response is fully sent.
408
+ def handle_one_request(self):
409
+ self.raw_requestline = self.rfile.readline(65537)
410
+ if not self.raw_requestline:
411
+ self.close_connection = True
412
+ return
413
+ if not self.parse_request():
414
+ return
415
+
416
+ if self.path == '/_vercel/ping':
417
+ self.send_response(200)
418
+ self.end_headers()
419
+ return
420
+
421
+ invocationId = self.headers.get('x-vercel-internal-invocation-id')
422
+ requestId = int(self.headers.get('x-vercel-internal-request-id'))
423
+ del self.headers['x-vercel-internal-invocation-id']
424
+ del self.headers['x-vercel-internal-request-id']
425
+ del self.headers['x-vercel-internal-span-id']
426
+ del self.headers['x-vercel-internal-trace-id']
427
+
428
+ send_message({
429
+ "type": "handler-started",
430
+ "payload": {
431
+ "handlerStartedAt": int(time.time() * 1000),
432
+ "context": {
433
+ "invocationId": invocationId,
434
+ "requestId": requestId,
435
+ }
436
+ }
437
+ })
438
+
439
+ token = storage.set({
440
+ "invocationId": invocationId,
441
+ "requestId": requestId,
442
+ })
443
+
444
+ try:
445
+ self.handle_request()
446
+ finally:
447
+ storage.reset(token)
448
+ send_message({
449
+ "type": "end",
450
+ "payload": {
451
+ "context": {
452
+ "invocationId": invocationId,
453
+ "requestId": requestId,
454
+ }
455
+ }
456
+ })
457
+
458
+ if 'handler' in __vc_variables or 'Handler' in __vc_variables:
459
+ base = __vc_module.handler if ('handler' in __vc_variables) else __vc_module.Handler
460
+ if not issubclass(base, BaseHTTPRequestHandler):
461
+ _stderr('Handler must inherit from BaseHTTPRequestHandler')
462
+ _stderr('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
463
+ exit(1)
464
+
465
+ class Handler(BaseHandler, base):
466
+ def handle_request(self):
467
+ mname = 'do_' + self.command
468
+ if not hasattr(self, mname):
469
+ self.send_error(
470
+ http.HTTPStatus.NOT_IMPLEMENTED,
471
+ "Unsupported method (%r)" % self.command)
472
+ return
473
+ method = getattr(self, mname)
474
+ method()
475
+ self.wfile.flush()
476
+ elif 'app' in __vc_variables:
477
+ if (
478
+ not inspect.iscoroutinefunction(__vc_module.app) and
479
+ not inspect.iscoroutinefunction(__vc_module.app.__call__)
480
+ ):
481
+ from io import BytesIO
482
+
483
+ string_types = (str,)
484
+ app = __vc_module.app
485
+
486
+ def wsgi_encoding_dance(s, charset="utf-8", errors="replace"):
487
+ if isinstance(s, str):
488
+ s = s.encode(charset)
489
+ return s.decode("latin1", errors)
490
+
491
+ class Handler(BaseHandler):
492
+ def handle_request(self):
493
+ # Prepare WSGI environment
494
+ if '?' in self.path:
495
+ path, query = self.path.split('?', 1)
496
+ else:
497
+ path, query = self.path, ''
498
+ content_length = int(self.headers.get('Content-Length', 0))
499
+ env = {
500
+ 'CONTENT_LENGTH': str(content_length),
501
+ 'CONTENT_TYPE': self.headers.get('content-type', ''),
502
+ 'PATH_INFO': path,
503
+ 'QUERY_STRING': query,
504
+ 'REMOTE_ADDR': self.headers.get(
505
+ 'x-forwarded-for', self.headers.get(
506
+ 'x-real-ip')),
507
+ 'REQUEST_METHOD': self.command,
508
+ 'SERVER_NAME': self.headers.get('host', 'lambda'),
509
+ 'SERVER_PORT': self.headers.get('x-forwarded-port', '80'),
510
+ 'SERVER_PROTOCOL': 'HTTP/1.1',
511
+ 'wsgi.errors': sys.stderr,
512
+ 'wsgi.input': BytesIO(self.rfile.read(content_length)),
513
+ 'wsgi.multiprocess': False,
514
+ 'wsgi.multithread': False,
515
+ 'wsgi.run_once': False,
516
+ 'wsgi.url_scheme': self.headers.get('x-forwarded-proto', 'http'),
517
+ 'wsgi.version': (1, 0),
518
+ }
519
+ for key, value in env.items():
520
+ if isinstance(value, string_types):
521
+ env[key] = wsgi_encoding_dance(value)
522
+ for k, v in self.headers.items():
523
+ env['HTTP_' + k.replace('-', '_').upper()] = v
524
+
525
+ def start_response(status, headers, exc_info=None):
526
+ self.send_response(int(status.split(' ')[0]))
527
+ for name, value in headers:
528
+ self.send_header(name, value)
529
+ self.end_headers()
530
+ return self.wfile.write
531
+
532
+ # Call the application
533
+ response = app(env, start_response)
534
+ try:
535
+ for data in response:
536
+ if data:
537
+ self.wfile.write(data)
538
+ self.wfile.flush()
539
+ finally:
540
+ if hasattr(response, 'close'):
541
+ response.close()
542
+ else:
543
+ # ASGI: Run with Uvicorn so we get proper lifespan and protocol handling
544
+ try:
545
+ import uvicorn
546
+ except Exception:
547
+ _stderr('Uvicorn is required to run ASGI apps. Please ensure it is installed.')
548
+ exit(1)
549
+
550
+ # Prefer a callable app.asgi when available; some frameworks expose a boolean here
551
+ user_app_candidate = getattr(__vc_module.app, 'asgi', None)
552
+ user_app = user_app_candidate if callable(user_app_candidate) else __vc_module.app
553
+ asgi_app = ASGIMiddleware(user_app)
554
+
555
+ # Pre-bind a socket to obtain an ephemeral port for IPC announcement
556
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
557
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
558
+ sock.bind(('127.0.0.1', 0))
559
+ sock.listen(2048)
560
+ http_port = sock.getsockname()[1]
561
+
562
+ config = uvicorn.Config(
563
+ app=asgi_app,
564
+ fd=sock.fileno(),
565
+ lifespan='auto',
566
+ access_log=False,
567
+ log_config=None,
568
+ log_level='warning',
569
+ )
570
+ server = uvicorn.Server(config)
571
+
572
+ send_message({
573
+ "type": "server-started",
574
+ "payload": {
575
+ "initDuration": int((time.time() - start_time) * 1000),
576
+ "httpPort": http_port,
577
+ }
578
+ })
579
+
580
+ # Mark IPC as ready and flush any buffered init logs
581
+ _ipc_ready = True
582
+ for m in _init_log_buf:
583
+ send_message(m)
584
+ _init_log_buf.clear()
585
+
586
+ # Run the server (blocking)
587
+ server.run()
588
+ # If the server ever returns, exit
589
+ sys.exit(0)
590
+
591
+ if 'Handler' in locals():
592
+ server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
593
+ send_message({
594
+ "type": "server-started",
595
+ "payload": {
596
+ "initDuration": int((time.time() - start_time) * 1000),
597
+ "httpPort": server.server_address[1],
598
+ }
599
+ })
600
+ # Mark IPC as ready and flush any buffered init logs
601
+ _ipc_ready = True
602
+ for m in _init_log_buf:
603
+ send_message(m)
604
+ _init_log_buf.clear()
605
+ server.serve_forever()
606
+
607
+ _stderr(f'Missing variable `handler` or `app` in file "{_entrypoint_rel}".')
608
+ _stderr('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
609
+ exit(1)
610
+
611
+ if 'handler' in __vc_variables or 'Handler' in __vc_variables:
612
+ base = __vc_module.handler if ('handler' in __vc_variables) else __vc_module.Handler
613
+ if not issubclass(base, BaseHTTPRequestHandler):
614
+ print('Handler must inherit from BaseHTTPRequestHandler')
615
+ print('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
616
+ exit(1)
617
+
618
+ print('using HTTP Handler')
619
+ from http.server import HTTPServer
620
+ import http
621
+ import _thread
622
+
623
+ server = HTTPServer(('127.0.0.1', 0), base)
624
+ port = server.server_address[1]
625
+
626
+ def vc_handler(event, context):
627
+ _thread.start_new_thread(server.handle_request, ())
628
+
629
+ payload = json.loads(event['body'])
630
+ path = payload['path']
631
+ headers = payload['headers']
632
+ method = payload['method']
633
+ encoding = payload.get('encoding')
634
+ body = payload.get('body')
635
+
636
+ if (
637
+ (body is not None and len(body) > 0) and
638
+ (encoding is not None and encoding == 'base64')
639
+ ):
640
+ body = base64.b64decode(body)
641
+
642
+ request_body = body.encode('utf-8') if isinstance(body, str) else body
643
+ conn = http.client.HTTPConnection('127.0.0.1', port)
644
+ try:
645
+ conn.request(method, path, headers=headers, body=request_body)
646
+ except (http.client.HTTPException, socket.error) as ex:
647
+ print ("Request Error: %s" % ex)
648
+ res = conn.getresponse()
649
+
650
+ return_dict = {
651
+ 'statusCode': res.status,
652
+ 'headers': format_headers(res.headers),
653
+ }
654
+
655
+ data = res.read()
656
+
657
+ try:
658
+ return_dict['body'] = data.decode('utf-8')
659
+ except UnicodeDecodeError:
660
+ return_dict['body'] = base64.b64encode(data).decode('utf-8')
661
+ return_dict['encoding'] = 'base64'
662
+
663
+ return return_dict
664
+
665
+ elif 'app' in __vc_variables:
666
+ if (
667
+ not inspect.iscoroutinefunction(__vc_module.app) and
668
+ not inspect.iscoroutinefunction(__vc_module.app.__call__)
669
+ ):
670
+ print('using Web Server Gateway Interface (WSGI)')
671
+ from io import BytesIO
672
+ from urllib.parse import urlparse
673
+ from werkzeug.datastructures import Headers
674
+ from werkzeug.wrappers import Response
675
+
676
+ string_types = (str,)
677
+
678
+ def to_bytes(x, charset=sys.getdefaultencoding(), errors="strict"):
679
+ if x is None:
680
+ return None
681
+ if isinstance(x, (bytes, bytearray, memoryview)):
682
+ return bytes(x)
683
+ if isinstance(x, str):
684
+ return x.encode(charset, errors)
685
+ raise TypeError("Expected bytes")
686
+
687
+ def wsgi_encoding_dance(s, charset="utf-8", errors="replace"):
688
+ if isinstance(s, str):
689
+ s = s.encode(charset)
690
+ return s.decode("latin1", errors)
691
+
692
+ def vc_handler(event, context):
693
+ payload = json.loads(event['body'])
694
+
695
+ headers = Headers(payload.get('headers', {}))
696
+
697
+ body = payload.get('body', '')
698
+ if body != '':
699
+ if payload.get('encoding') == 'base64':
700
+ body = base64.b64decode(body)
701
+ if isinstance(body, string_types):
702
+ body = to_bytes(body, charset='utf-8')
703
+
704
+ url = urlparse(payload['path'])
705
+ query = url.query
706
+ path = url.path
707
+
708
+ environ = {
709
+ 'CONTENT_LENGTH': str(len(body)),
710
+ 'CONTENT_TYPE': headers.get('content-type', ''),
711
+ 'PATH_INFO': path,
712
+ 'QUERY_STRING': query,
713
+ 'REMOTE_ADDR': headers.get(
714
+ 'x-forwarded-for', headers.get(
715
+ 'x-real-ip', payload.get(
716
+ 'true-client-ip', ''))),
717
+ 'REQUEST_METHOD': payload['method'],
718
+ 'SERVER_NAME': headers.get('host', 'lambda'),
719
+ 'SERVER_PORT': headers.get('x-forwarded-port', '80'),
720
+ 'SERVER_PROTOCOL': 'HTTP/1.1',
721
+ 'event': event,
722
+ 'context': context,
723
+ 'wsgi.errors': sys.stderr,
724
+ 'wsgi.input': BytesIO(body),
725
+ 'wsgi.multiprocess': False,
726
+ 'wsgi.multithread': False,
727
+ 'wsgi.run_once': False,
728
+ 'wsgi.url_scheme': headers.get('x-forwarded-proto', 'http'),
729
+ 'wsgi.version': (1, 0),
730
+ }
731
+
732
+ for key, value in environ.items():
733
+ if isinstance(value, string_types):
734
+ environ[key] = wsgi_encoding_dance(value)
735
+
736
+ for key, value in headers.items():
737
+ key = 'HTTP_' + key.upper().replace('-', '_')
738
+ if key not in ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'):
739
+ environ[key] = value
740
+
741
+ response = Response.from_app(__vc_module.app, environ)
742
+
743
+ return_dict = {
744
+ 'statusCode': response.status_code,
745
+ 'headers': format_headers(response.headers)
746
+ }
747
+
748
+ if response.data:
749
+ return_dict['body'] = base64.b64encode(response.data).decode('utf-8')
750
+ return_dict['encoding'] = 'base64'
751
+
752
+ return return_dict
753
+ else:
754
+ print('using Asynchronous Server Gateway Interface (ASGI)')
755
+ # Originally authored by Jordan Eremieff and included under MIT license:
756
+ # https://github.com/erm/mangum/blob/b4d21c8f5e304a3e17b88bc9fa345106acc50ad7/mangum/__init__.py
757
+ # https://github.com/erm/mangum/blob/b4d21c8f5e304a3e17b88bc9fa345106acc50ad7/LICENSE
758
+ import asyncio
759
+ import enum
760
+ from urllib.parse import urlparse
761
+ from werkzeug.datastructures import Headers
762
+
763
+
764
+ class ASGICycleState(enum.Enum):
765
+ REQUEST = enum.auto()
766
+ RESPONSE = enum.auto()
767
+
768
+
769
+ class ASGICycle:
770
+ def __init__(self, scope):
771
+ self.scope = scope
772
+ self.body = b''
773
+ self.state = ASGICycleState.REQUEST
774
+ self.app_queue = None
775
+ self.response = {}
776
+
777
+ def __call__(self, app, body):
778
+ """
779
+ Receives the application and any body included in the request, then builds the
780
+ ASGI instance using the connection scope.
781
+ Runs until the response is completely read from the application.
782
+ """
783
+ if _use_legacy_asyncio:
784
+ loop = asyncio.new_event_loop()
785
+ self.app_queue = asyncio.Queue(loop=loop)
786
+ else:
787
+ self.app_queue = asyncio.Queue()
788
+ self.put_message({'type': 'http.request', 'body': body, 'more_body': False})
789
+
790
+ asgi_instance = app(self.scope, self.receive, self.send)
791
+
792
+ if _use_legacy_asyncio:
793
+ asgi_task = loop.create_task(asgi_instance)
794
+ loop.run_until_complete(asgi_task)
795
+ else:
796
+ asyncio.run(self.run_asgi_instance(asgi_instance))
797
+ return self.response
798
+
799
+ async def run_asgi_instance(self, asgi_instance):
800
+ await asgi_instance
801
+
802
+ def put_message(self, message):
803
+ self.app_queue.put_nowait(message)
804
+
805
+ async def receive(self):
806
+ """
807
+ Awaited by the application to receive messages in the queue.
808
+ """
809
+ message = await self.app_queue.get()
810
+ return message
811
+
812
+ async def send(self, message):
813
+ """
814
+ Awaited by the application to send messages to the current cycle instance.
815
+ """
816
+ message_type = message['type']
817
+
818
+ if self.state is ASGICycleState.REQUEST:
819
+ if message_type != 'http.response.start':
820
+ raise RuntimeError(
821
+ f"Expected 'http.response.start', received: {message_type}"
822
+ )
823
+
824
+ status_code = message['status']
825
+ raw_headers = message.get('headers', [])
826
+
827
+ # Headers from werkzeug transform bytes header value
828
+ # from b'value' to "b'value'" so we need to process
829
+ # ASGI headers manually
830
+ decoded_headers = []
831
+ for key, value in raw_headers:
832
+ decoded_key = key.decode() if isinstance(key, bytes) else key
833
+ decoded_value = value.decode() if isinstance(value, bytes) else value
834
+ decoded_headers.append((decoded_key, decoded_value))
835
+
836
+ headers = Headers(decoded_headers)
837
+
838
+ self.on_request(headers, status_code)
839
+ self.state = ASGICycleState.RESPONSE
840
+
841
+ elif self.state is ASGICycleState.RESPONSE:
842
+ if message_type != 'http.response.body':
843
+ raise RuntimeError(
844
+ f"Expected 'http.response.body', received: {message_type}"
845
+ )
846
+
847
+ body = message.get('body', b'')
848
+ more_body = message.get('more_body', False)
849
+
850
+ # The body must be completely read before returning the response.
851
+ self.body += body
852
+
853
+ if not more_body:
854
+ self.on_response()
855
+ self.put_message({'type': 'http.disconnect'})
856
+
857
+ def on_request(self, headers, status_code):
858
+ self.response['statusCode'] = status_code
859
+ self.response['headers'] = format_headers(headers, decode=True)
860
+
861
+ def on_response(self):
862
+ if self.body:
863
+ self.response['body'] = base64.b64encode(self.body).decode('utf-8')
864
+ self.response['encoding'] = 'base64'
865
+
866
+ def vc_handler(event, context):
867
+ payload = json.loads(event['body'])
868
+
869
+ headers = payload.get('headers', {})
870
+
871
+ body = payload.get('body', b'')
872
+ if payload.get('encoding') == 'base64':
873
+ body = base64.b64decode(body)
874
+ elif not isinstance(body, bytes):
875
+ body = body.encode()
876
+
877
+ url = urlparse(payload['path'])
878
+ query = url.query.encode()
879
+ path = url.path
880
+
881
+ headers_encoded = []
882
+ for k, v in headers.items():
883
+ # Cope with repeated headers in the encoding.
884
+ if isinstance(v, list):
885
+ headers_encoded.append([k.lower().encode(), [i.encode() for i in v]])
886
+ else:
887
+ headers_encoded.append([k.lower().encode(), v.encode()])
888
+
889
+ scope = {
890
+ 'server': (headers.get('host', 'lambda'), headers.get('x-forwarded-port', 80)),
891
+ 'client': (headers.get(
892
+ 'x-forwarded-for', headers.get(
893
+ 'x-real-ip', payload.get(
894
+ 'true-client-ip', ''))), 0),
895
+ 'scheme': headers.get('x-forwarded-proto', 'http'),
896
+ 'root_path': '',
897
+ 'query_string': query,
898
+ 'headers': headers_encoded,
899
+ 'type': 'http',
900
+ 'http_version': '1.1',
901
+ 'method': payload['method'],
902
+ 'path': path,
903
+ 'raw_path': path.encode(),
904
+ }
905
+
906
+ asgi_cycle = ASGICycle(scope)
907
+ response = asgi_cycle(__vc_module.app, body)
908
+ return response
909
+
910
+ else:
911
+ print(f'Missing variable `handler` or `app` in file "{_entrypoint_rel}".')
912
+ print('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
913
+ exit(1)