Update app and tooling
This commit is contained in:
parent
3046531bdd
commit
e620ec7349
4950 changed files with 2975120 additions and 10 deletions
202
node_modules/@vercel/python/LICENSE
generated
vendored
Normal file
202
node_modules/@vercel/python/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2017 Vercel, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
4730
node_modules/@vercel/python/dist/index.js
generated
vendored
Normal file
4730
node_modules/@vercel/python/dist/index.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
43
node_modules/@vercel/python/package.json
generated
vendored
Normal file
43
node_modules/@vercel/python/package.json
generated
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "@vercel/python",
|
||||
"version": "6.4.1",
|
||||
"main": "./dist/index.js",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
|
||||
"files": [
|
||||
"dist",
|
||||
"vc_init.py",
|
||||
"vc_init_dev_asgi.py",
|
||||
"vc_init_dev_wsgi.py"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vercel/vercel.git",
|
||||
"directory": "packages/python"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@renovatebot/pep440": "4.2.1",
|
||||
"@types/execa": "^0.9.0",
|
||||
"@types/fs-extra": "11.0.2",
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/node": "20.11.0",
|
||||
"@types/which": "3.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"execa": "^1.0.0",
|
||||
"fs-extra": "11.1.1",
|
||||
"jest-junit": "16.0.0",
|
||||
"minimatch": "10.1.1",
|
||||
"pip-requirements-js": "1.0.2",
|
||||
"smol-toml": "1.5.2",
|
||||
"which": "3.0.0",
|
||||
"@vercel/error-utils": "2.0.3",
|
||||
"@vercel/build-utils": "13.2.16"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node ../../utils/build-builder.mjs",
|
||||
"test": "cross-env VERCEL_FORCE_PYTHON_STREAMING=1 jest --reporters=default --reporters=jest-junit --env node --verbose --runInBand --bail",
|
||||
"test-unit": "pnpm test test/unit.test.ts",
|
||||
"test-e2e": "pnpm test test/integration-*",
|
||||
"type-check": "tsc --noEmit"
|
||||
}
|
||||
}
|
||||
916
node_modules/@vercel/python/vc_init.py
generated
vendored
Normal file
916
node_modules/@vercel/python/vc_init.py
generated
vendored
Normal file
|
|
@ -0,0 +1,916 @@
|
|||
from __future__ import annotations
|
||||
import sys
|
||||
import os
|
||||
import site
|
||||
import importlib
|
||||
import base64
|
||||
import json
|
||||
import inspect
|
||||
import asyncio
|
||||
import http
|
||||
import time
|
||||
import traceback
|
||||
from importlib import util
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
import socket
|
||||
import functools
|
||||
import logging
|
||||
import builtins
|
||||
from typing import Callable, Literal, TextIO
|
||||
import contextvars
|
||||
import contextlib
|
||||
import atexit
|
||||
|
||||
|
||||
_here = os.path.dirname(__file__)
|
||||
_vendor_rel = '__VC_HANDLER_VENDOR_DIR'
|
||||
_vendor = os.path.normpath(os.path.join(_here, _vendor_rel))
|
||||
|
||||
if os.path.isdir(_vendor):
|
||||
# Process .pth files like a real site-packages dir
|
||||
site.addsitedir(_vendor)
|
||||
|
||||
# Move _vendor to the front (after script dir if present)
|
||||
try:
|
||||
while _vendor in sys.path:
|
||||
sys.path.remove(_vendor)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Put vendored deps ahead of site-packages but after the script dir
|
||||
idx = 1 if (sys.path and sys.path[0] in ('', _here)) else 0
|
||||
sys.path.insert(idx, _vendor)
|
||||
|
||||
importlib.invalidate_caches()
|
||||
|
||||
|
||||
def setup_logging(send_message: Callable[[dict], None], storage: contextvars.ContextVar[dict | None]):
|
||||
# Override logging.Handler to send logs to the platform when a request context is available.
|
||||
class VCLogHandler(logging.Handler):
|
||||
def emit(self, record: logging.LogRecord):
|
||||
try:
|
||||
message = record.getMessage()
|
||||
except Exception:
|
||||
message = repr(getattr(record, "msg", ""))
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
if record.exc_info:
|
||||
# logging allows exc_info=True or a (type, value, tb) tuple
|
||||
exc_info = record.exc_info
|
||||
if exc_info is True:
|
||||
exc_info = sys.exc_info()
|
||||
if isinstance(exc_info, tuple):
|
||||
tb = ''.join(traceback.format_exception(*exc_info))
|
||||
if tb:
|
||||
if message:
|
||||
message = f"{message}\n{tb}"
|
||||
else:
|
||||
message = tb
|
||||
|
||||
if record.levelno >= logging.CRITICAL:
|
||||
level = "fatal"
|
||||
elif record.levelno >= logging.ERROR:
|
||||
level = "error"
|
||||
elif record.levelno >= logging.WARNING:
|
||||
level = "warn"
|
||||
elif record.levelno >= logging.INFO:
|
||||
level = "info"
|
||||
else:
|
||||
level = "debug"
|
||||
|
||||
context = storage.get()
|
||||
if context is not None:
|
||||
send_message({
|
||||
"type": "log",
|
||||
"payload": {
|
||||
"context": {
|
||||
"invocationId": context['invocationId'],
|
||||
"requestId": context['requestId'],
|
||||
},
|
||||
"message": base64.b64encode(message.encode()).decode(),
|
||||
"level": level,
|
||||
}
|
||||
})
|
||||
else:
|
||||
# If IPC is not ready, enqueue the message to be sent later.
|
||||
enqueue_or_send_message({
|
||||
"type": "log",
|
||||
"payload": {
|
||||
"context": {"invocationId": "0", "requestId": 0},
|
||||
"message": base64.b64encode(message.encode()).decode(),
|
||||
"level": level,
|
||||
}
|
||||
})
|
||||
|
||||
# Override sys.stdout and sys.stderr to map logs to the correct request
|
||||
class StreamWrapper:
|
||||
def __init__(self, stream: TextIO, stream_name: Literal["stdout", "stderr"]):
|
||||
self.stream = stream
|
||||
self.stream_name = stream_name
|
||||
|
||||
def write(self, message: str):
|
||||
context = storage.get()
|
||||
if context is not None:
|
||||
send_message({
|
||||
"type": "log",
|
||||
"payload": {
|
||||
"context": {
|
||||
"invocationId": context['invocationId'],
|
||||
"requestId": context['requestId'],
|
||||
},
|
||||
"message": base64.b64encode(message.encode()).decode(),
|
||||
"stream": self.stream_name,
|
||||
}
|
||||
})
|
||||
else:
|
||||
enqueue_or_send_message({
|
||||
"type": "log",
|
||||
"payload": {
|
||||
"context": {"invocationId": "0", "requestId": 0},
|
||||
"message": base64.b64encode(message.encode()).decode(),
|
||||
"stream": self.stream_name,
|
||||
}
|
||||
})
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.stream, name)
|
||||
|
||||
sys.stdout = StreamWrapper(sys.stdout, "stdout")
|
||||
sys.stderr = StreamWrapper(sys.stderr, "stderr")
|
||||
|
||||
logging.basicConfig(level=logging.INFO, handlers=[VCLogHandler()], force=True)
|
||||
|
||||
# Ensure built-in print funnels through stdout wrapper so prints are
|
||||
# attributed to the current request context.
|
||||
def print_wrapper(func: Callable[..., None]) -> Callable[..., None]:
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, sep=' ', end='\n', file=None, flush=False):
|
||||
if file is None:
|
||||
file = sys.stdout
|
||||
if file in (sys.stdout, sys.stderr):
|
||||
file.write(sep.join(map(str, args)) + end)
|
||||
if flush:
|
||||
file.flush()
|
||||
else:
|
||||
# User specified a different file, use original print behavior
|
||||
func(*args, sep=sep, end=end, file=file, flush=flush)
|
||||
return wrapper
|
||||
|
||||
builtins.print = print_wrapper(builtins.print)
|
||||
|
||||
|
||||
def _stderr(message: str):
|
||||
with contextlib.suppress(Exception):
|
||||
_original_stderr.write(message + "\n")
|
||||
_original_stderr.flush()
|
||||
|
||||
|
||||
# If running in the platform (IPC present), logging must be setup before importing user code so that
|
||||
# logs happening outside the request context are emitted correctly.
|
||||
ipc_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
storage: contextvars.ContextVar[dict | None] = contextvars.ContextVar('storage', default=None)
|
||||
send_message = lambda m: None
|
||||
_original_stderr = sys.stderr
|
||||
|
||||
|
||||
# Buffer for pre-handshake logs (to avoid blocking IPC on startup)
|
||||
_ipc_ready = False
|
||||
_init_log_buf: list[dict] = []
|
||||
_INIT_LOG_BUF_MAX_BYTES = 1_000_000
|
||||
_init_log_buf_bytes = 0
|
||||
|
||||
|
||||
def enqueue_or_send_message(msg: dict):
|
||||
global _init_log_buf_bytes
|
||||
if _ipc_ready:
|
||||
send_message(msg)
|
||||
return
|
||||
|
||||
enc_len = len(json.dumps(msg))
|
||||
|
||||
if _init_log_buf_bytes + enc_len <= _INIT_LOG_BUF_MAX_BYTES:
|
||||
_init_log_buf.append(msg)
|
||||
_init_log_buf_bytes += enc_len
|
||||
else:
|
||||
# Fallback so message is not lost if buffer is full
|
||||
with contextlib.suppress(Exception):
|
||||
payload = msg.get("payload", {})
|
||||
decoded = base64.b64decode(payload.get("message", "")).decode(errors="ignore")
|
||||
_original_stderr.write(decoded + "\n")
|
||||
|
||||
|
||||
def flush_init_log_buf_to_stderr():
|
||||
global _init_log_buf, _init_log_buf_bytes
|
||||
try:
|
||||
combined: list[str] = []
|
||||
for m in _init_log_buf:
|
||||
payload = m.get("payload", {})
|
||||
msg = payload.get("message")
|
||||
if not msg:
|
||||
continue
|
||||
with contextlib.suppress(Exception):
|
||||
decoded = base64.b64decode(msg).decode(errors="ignore")
|
||||
combined.append(decoded)
|
||||
if combined:
|
||||
_stderr("".join(combined))
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
_init_log_buf.clear()
|
||||
_init_log_buf_bytes = 0
|
||||
|
||||
|
||||
atexit.register(flush_init_log_buf_to_stderr)
|
||||
|
||||
|
||||
if 'VERCEL_IPC_PATH' in os.environ:
|
||||
with contextlib.suppress(Exception):
|
||||
ipc_sock.connect(os.getenv("VERCEL_IPC_PATH", ""))
|
||||
|
||||
def send_message(message: dict):
|
||||
with contextlib.suppress(Exception):
|
||||
ipc_sock.sendall((json.dumps(message) + '\0').encode())
|
||||
|
||||
setup_logging(send_message, storage)
|
||||
|
||||
|
||||
# Import relative path https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
|
||||
try:
|
||||
user_mod_path = os.path.join(_here, "__VC_HANDLER_ENTRYPOINT") # absolute
|
||||
__vc_spec = util.spec_from_file_location("__VC_HANDLER_MODULE_NAME", user_mod_path)
|
||||
__vc_module = util.module_from_spec(__vc_spec)
|
||||
sys.modules["__VC_HANDLER_MODULE_NAME"] = __vc_module
|
||||
__vc_spec.loader.exec_module(__vc_module)
|
||||
__vc_variables = dir(__vc_module)
|
||||
except Exception:
|
||||
_stderr(f'Error importing __VC_HANDLER_ENTRYPOINT:')
|
||||
_stderr(traceback.format_exc())
|
||||
exit(1)
|
||||
|
||||
_use_legacy_asyncio = sys.version_info < (3, 10)
|
||||
|
||||
def format_headers(headers, decode=False):
|
||||
keyToList = {}
|
||||
for key, value in headers.items():
|
||||
if decode and 'decode' in dir(key) and 'decode' in dir(value):
|
||||
key = key.decode()
|
||||
value = value.decode()
|
||||
if key not in keyToList:
|
||||
keyToList[key] = []
|
||||
keyToList[key].append(value)
|
||||
return keyToList
|
||||
|
||||
|
||||
class ASGIMiddleware:
|
||||
"""
|
||||
ASGI middleware that preserves Vercel IPC semantics for request lifecycle:
|
||||
- Handles /_vercel/ping
|
||||
- Extracts x-vercel-internal-* headers and removes them from downstream app
|
||||
- Sets request context into `storage` for logging/metrics
|
||||
- Emits handler-started and end IPC messages
|
||||
"""
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
if scope.get('type') != 'http':
|
||||
# Non-HTTP traffic is forwarded verbatim
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
if scope.get('path') == '/_vercel/ping':
|
||||
await send({
|
||||
'type': 'http.response.start',
|
||||
'status': 200,
|
||||
'headers': [],
|
||||
})
|
||||
await send({
|
||||
'type': 'http.response.body',
|
||||
'body': b'',
|
||||
'more_body': False,
|
||||
})
|
||||
return
|
||||
|
||||
# Extract internal headers and set per-request context
|
||||
headers_list = scope.get('headers', []) or []
|
||||
new_headers = []
|
||||
invocation_id = "0"
|
||||
request_id = 0
|
||||
|
||||
def _b2s(b: bytes) -> str:
|
||||
try:
|
||||
return b.decode()
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
for k, v in headers_list:
|
||||
key = _b2s(k).lower()
|
||||
val = _b2s(v)
|
||||
if key == 'x-vercel-internal-invocation-id':
|
||||
invocation_id = val
|
||||
continue
|
||||
if key == 'x-vercel-internal-request-id':
|
||||
request_id = int(val) if val.isdigit() else 0
|
||||
continue
|
||||
if key in ('x-vercel-internal-span-id', 'x-vercel-internal-trace-id'):
|
||||
continue
|
||||
new_headers.append((k, v))
|
||||
|
||||
new_scope = dict(scope)
|
||||
new_scope['headers'] = new_headers
|
||||
|
||||
# Announce handler start and set context for logging/metrics
|
||||
send_message({
|
||||
"type": "handler-started",
|
||||
"payload": {
|
||||
"handlerStartedAt": int(time.time() * 1000),
|
||||
"context": {
|
||||
"invocationId": invocation_id,
|
||||
"requestId": request_id,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
token = storage.set({
|
||||
"invocationId": invocation_id,
|
||||
"requestId": request_id,
|
||||
})
|
||||
|
||||
try:
|
||||
await self.app(new_scope, receive, send)
|
||||
finally:
|
||||
storage.reset(token)
|
||||
send_message({
|
||||
"type": "end",
|
||||
"payload": {
|
||||
"context": {
|
||||
"invocationId": invocation_id,
|
||||
"requestId": request_id,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if 'VERCEL_IPC_PATH' in os.environ:
|
||||
start_time = time.time()
|
||||
|
||||
# Override urlopen from urllib3 (& requests) to send Request Metrics
|
||||
try:
|
||||
import urllib3
|
||||
from urllib.parse import urlparse
|
||||
|
||||
def timed_request(func):
|
||||
fetchId = 0
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, method, url, *args, **kwargs):
|
||||
nonlocal fetchId
|
||||
fetchId += 1
|
||||
start_time = int(time.time() * 1000)
|
||||
result = func(self, method, url, *args, **kwargs)
|
||||
elapsed_time = int(time.time() * 1000) - start_time
|
||||
parsed_url = urlparse(url)
|
||||
context = storage.get()
|
||||
if context is not None:
|
||||
send_message({
|
||||
"type": "metric",
|
||||
"payload": {
|
||||
"context": {
|
||||
"invocationId": context['invocationId'],
|
||||
"requestId": context['requestId'],
|
||||
},
|
||||
"type": "fetch-metric",
|
||||
"payload": {
|
||||
"pathname": parsed_url.path,
|
||||
"search": parsed_url.query,
|
||||
"start": start_time,
|
||||
"duration": elapsed_time,
|
||||
"host": parsed_url.hostname or self.host,
|
||||
"statusCode": result.status,
|
||||
"method": method,
|
||||
"id": fetchId
|
||||
}
|
||||
}
|
||||
})
|
||||
return result
|
||||
return wrapper
|
||||
urllib3.connectionpool.HTTPConnectionPool.urlopen = timed_request(urllib3.connectionpool.HTTPConnectionPool.urlopen)
|
||||
except:
|
||||
pass
|
||||
|
||||
class BaseHandler(BaseHTTPRequestHandler):
|
||||
# Re-implementation of BaseHTTPRequestHandler's log_message method to
|
||||
# log to stdout instead of stderr.
|
||||
def log_message(self, format, *args):
|
||||
message = format % args
|
||||
sys.stdout.write("%s - - [%s] %s\n" %
|
||||
(self.address_string(),
|
||||
self.log_date_time_string(),
|
||||
message.translate(self._control_char_table)))
|
||||
|
||||
# Re-implementation of BaseHTTPRequestHandler's handle_one_request method
|
||||
# to send the end message after the response is fully sent.
|
||||
def handle_one_request(self):
|
||||
self.raw_requestline = self.rfile.readline(65537)
|
||||
if not self.raw_requestline:
|
||||
self.close_connection = True
|
||||
return
|
||||
if not self.parse_request():
|
||||
return
|
||||
|
||||
if self.path == '/_vercel/ping':
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
invocationId = self.headers.get('x-vercel-internal-invocation-id')
|
||||
requestId = int(self.headers.get('x-vercel-internal-request-id'))
|
||||
del self.headers['x-vercel-internal-invocation-id']
|
||||
del self.headers['x-vercel-internal-request-id']
|
||||
del self.headers['x-vercel-internal-span-id']
|
||||
del self.headers['x-vercel-internal-trace-id']
|
||||
|
||||
send_message({
|
||||
"type": "handler-started",
|
||||
"payload": {
|
||||
"handlerStartedAt": int(time.time() * 1000),
|
||||
"context": {
|
||||
"invocationId": invocationId,
|
||||
"requestId": requestId,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
token = storage.set({
|
||||
"invocationId": invocationId,
|
||||
"requestId": requestId,
|
||||
})
|
||||
|
||||
try:
|
||||
self.handle_request()
|
||||
finally:
|
||||
storage.reset(token)
|
||||
send_message({
|
||||
"type": "end",
|
||||
"payload": {
|
||||
"context": {
|
||||
"invocationId": invocationId,
|
||||
"requestId": requestId,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if 'handler' in __vc_variables or 'Handler' in __vc_variables:
|
||||
base = __vc_module.handler if ('handler' in __vc_variables) else __vc_module.Handler
|
||||
if not issubclass(base, BaseHTTPRequestHandler):
|
||||
_stderr('Handler must inherit from BaseHTTPRequestHandler')
|
||||
_stderr('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
|
||||
exit(1)
|
||||
|
||||
class Handler(BaseHandler, base):
|
||||
def handle_request(self):
|
||||
mname = 'do_' + self.command
|
||||
if not hasattr(self, mname):
|
||||
self.send_error(
|
||||
http.HTTPStatus.NOT_IMPLEMENTED,
|
||||
"Unsupported method (%r)" % self.command)
|
||||
return
|
||||
method = getattr(self, mname)
|
||||
method()
|
||||
self.wfile.flush()
|
||||
elif 'app' in __vc_variables:
|
||||
if (
|
||||
not inspect.iscoroutinefunction(__vc_module.app) and
|
||||
not inspect.iscoroutinefunction(__vc_module.app.__call__)
|
||||
):
|
||||
from io import BytesIO
|
||||
|
||||
string_types = (str,)
|
||||
app = __vc_module.app
|
||||
|
||||
def wsgi_encoding_dance(s, charset="utf-8", errors="replace"):
|
||||
if isinstance(s, str):
|
||||
s = s.encode(charset)
|
||||
return s.decode("latin1", errors)
|
||||
|
||||
class Handler(BaseHandler):
|
||||
def handle_request(self):
|
||||
# Prepare WSGI environment
|
||||
if '?' in self.path:
|
||||
path, query = self.path.split('?', 1)
|
||||
else:
|
||||
path, query = self.path, ''
|
||||
content_length = int(self.headers.get('Content-Length', 0))
|
||||
env = {
|
||||
'CONTENT_LENGTH': str(content_length),
|
||||
'CONTENT_TYPE': self.headers.get('content-type', ''),
|
||||
'PATH_INFO': path,
|
||||
'QUERY_STRING': query,
|
||||
'REMOTE_ADDR': self.headers.get(
|
||||
'x-forwarded-for', self.headers.get(
|
||||
'x-real-ip')),
|
||||
'REQUEST_METHOD': self.command,
|
||||
'SERVER_NAME': self.headers.get('host', 'lambda'),
|
||||
'SERVER_PORT': self.headers.get('x-forwarded-port', '80'),
|
||||
'SERVER_PROTOCOL': 'HTTP/1.1',
|
||||
'wsgi.errors': sys.stderr,
|
||||
'wsgi.input': BytesIO(self.rfile.read(content_length)),
|
||||
'wsgi.multiprocess': False,
|
||||
'wsgi.multithread': False,
|
||||
'wsgi.run_once': False,
|
||||
'wsgi.url_scheme': self.headers.get('x-forwarded-proto', 'http'),
|
||||
'wsgi.version': (1, 0),
|
||||
}
|
||||
for key, value in env.items():
|
||||
if isinstance(value, string_types):
|
||||
env[key] = wsgi_encoding_dance(value)
|
||||
for k, v in self.headers.items():
|
||||
env['HTTP_' + k.replace('-', '_').upper()] = v
|
||||
|
||||
def start_response(status, headers, exc_info=None):
|
||||
self.send_response(int(status.split(' ')[0]))
|
||||
for name, value in headers:
|
||||
self.send_header(name, value)
|
||||
self.end_headers()
|
||||
return self.wfile.write
|
||||
|
||||
# Call the application
|
||||
response = app(env, start_response)
|
||||
try:
|
||||
for data in response:
|
||||
if data:
|
||||
self.wfile.write(data)
|
||||
self.wfile.flush()
|
||||
finally:
|
||||
if hasattr(response, 'close'):
|
||||
response.close()
|
||||
else:
|
||||
# ASGI: Run with Uvicorn so we get proper lifespan and protocol handling
|
||||
try:
|
||||
import uvicorn
|
||||
except Exception:
|
||||
_stderr('Uvicorn is required to run ASGI apps. Please ensure it is installed.')
|
||||
exit(1)
|
||||
|
||||
# Prefer a callable app.asgi when available; some frameworks expose a boolean here
|
||||
user_app_candidate = getattr(__vc_module.app, 'asgi', None)
|
||||
user_app = user_app_candidate if callable(user_app_candidate) else __vc_module.app
|
||||
asgi_app = ASGIMiddleware(user_app)
|
||||
|
||||
# Pre-bind a socket to obtain an ephemeral port for IPC announcement
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(('127.0.0.1', 0))
|
||||
sock.listen(2048)
|
||||
http_port = sock.getsockname()[1]
|
||||
|
||||
config = uvicorn.Config(
|
||||
app=asgi_app,
|
||||
fd=sock.fileno(),
|
||||
lifespan='auto',
|
||||
access_log=False,
|
||||
log_config=None,
|
||||
log_level='warning',
|
||||
)
|
||||
server = uvicorn.Server(config)
|
||||
|
||||
send_message({
|
||||
"type": "server-started",
|
||||
"payload": {
|
||||
"initDuration": int((time.time() - start_time) * 1000),
|
||||
"httpPort": http_port,
|
||||
}
|
||||
})
|
||||
|
||||
# Mark IPC as ready and flush any buffered init logs
|
||||
_ipc_ready = True
|
||||
for m in _init_log_buf:
|
||||
send_message(m)
|
||||
_init_log_buf.clear()
|
||||
|
||||
# Run the server (blocking)
|
||||
server.run()
|
||||
# If the server ever returns, exit
|
||||
sys.exit(0)
|
||||
|
||||
if 'Handler' in locals():
|
||||
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
|
||||
send_message({
|
||||
"type": "server-started",
|
||||
"payload": {
|
||||
"initDuration": int((time.time() - start_time) * 1000),
|
||||
"httpPort": server.server_address[1],
|
||||
}
|
||||
})
|
||||
# Mark IPC as ready and flush any buffered init logs
|
||||
_ipc_ready = True
|
||||
for m in _init_log_buf:
|
||||
send_message(m)
|
||||
_init_log_buf.clear()
|
||||
server.serve_forever()
|
||||
|
||||
_stderr('Missing variable `handler` or `app` in file "__VC_HANDLER_ENTRYPOINT".')
|
||||
_stderr('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
|
||||
exit(1)
|
||||
|
||||
if 'handler' in __vc_variables or 'Handler' in __vc_variables:
|
||||
base = __vc_module.handler if ('handler' in __vc_variables) else __vc_module.Handler
|
||||
if not issubclass(base, BaseHTTPRequestHandler):
|
||||
print('Handler must inherit from BaseHTTPRequestHandler')
|
||||
print('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
|
||||
exit(1)
|
||||
|
||||
print('using HTTP Handler')
|
||||
from http.server import HTTPServer
|
||||
import http
|
||||
import _thread
|
||||
|
||||
server = HTTPServer(('127.0.0.1', 0), base)
|
||||
port = server.server_address[1]
|
||||
|
||||
def vc_handler(event, context):
|
||||
_thread.start_new_thread(server.handle_request, ())
|
||||
|
||||
payload = json.loads(event['body'])
|
||||
path = payload['path']
|
||||
headers = payload['headers']
|
||||
method = payload['method']
|
||||
encoding = payload.get('encoding')
|
||||
body = payload.get('body')
|
||||
|
||||
if (
|
||||
(body is not None and len(body) > 0) and
|
||||
(encoding is not None and encoding == 'base64')
|
||||
):
|
||||
body = base64.b64decode(body)
|
||||
|
||||
request_body = body.encode('utf-8') if isinstance(body, str) else body
|
||||
conn = http.client.HTTPConnection('127.0.0.1', port)
|
||||
try:
|
||||
conn.request(method, path, headers=headers, body=request_body)
|
||||
except (http.client.HTTPException, socket.error) as ex:
|
||||
print ("Request Error: %s" % ex)
|
||||
res = conn.getresponse()
|
||||
|
||||
return_dict = {
|
||||
'statusCode': res.status,
|
||||
'headers': format_headers(res.headers),
|
||||
}
|
||||
|
||||
data = res.read()
|
||||
|
||||
try:
|
||||
return_dict['body'] = data.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
return_dict['body'] = base64.b64encode(data).decode('utf-8')
|
||||
return_dict['encoding'] = 'base64'
|
||||
|
||||
return return_dict
|
||||
|
||||
elif 'app' in __vc_variables:
|
||||
if (
|
||||
not inspect.iscoroutinefunction(__vc_module.app) and
|
||||
not inspect.iscoroutinefunction(__vc_module.app.__call__)
|
||||
):
|
||||
print('using Web Server Gateway Interface (WSGI)')
|
||||
from io import BytesIO
|
||||
from urllib.parse import urlparse
|
||||
from werkzeug.datastructures import Headers
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
string_types = (str,)
|
||||
|
||||
def to_bytes(x, charset=sys.getdefaultencoding(), errors="strict"):
|
||||
if x is None:
|
||||
return None
|
||||
if isinstance(x, (bytes, bytearray, memoryview)):
|
||||
return bytes(x)
|
||||
if isinstance(x, str):
|
||||
return x.encode(charset, errors)
|
||||
raise TypeError("Expected bytes")
|
||||
|
||||
def wsgi_encoding_dance(s, charset="utf-8", errors="replace"):
|
||||
if isinstance(s, str):
|
||||
s = s.encode(charset)
|
||||
return s.decode("latin1", errors)
|
||||
|
||||
def vc_handler(event, context):
|
||||
payload = json.loads(event['body'])
|
||||
|
||||
headers = Headers(payload.get('headers', {}))
|
||||
|
||||
body = payload.get('body', '')
|
||||
if body != '':
|
||||
if payload.get('encoding') == 'base64':
|
||||
body = base64.b64decode(body)
|
||||
if isinstance(body, string_types):
|
||||
body = to_bytes(body, charset='utf-8')
|
||||
|
||||
url = urlparse(payload['path'])
|
||||
query = url.query
|
||||
path = url.path
|
||||
|
||||
environ = {
|
||||
'CONTENT_LENGTH': str(len(body)),
|
||||
'CONTENT_TYPE': headers.get('content-type', ''),
|
||||
'PATH_INFO': path,
|
||||
'QUERY_STRING': query,
|
||||
'REMOTE_ADDR': headers.get(
|
||||
'x-forwarded-for', headers.get(
|
||||
'x-real-ip', payload.get(
|
||||
'true-client-ip', ''))),
|
||||
'REQUEST_METHOD': payload['method'],
|
||||
'SERVER_NAME': headers.get('host', 'lambda'),
|
||||
'SERVER_PORT': headers.get('x-forwarded-port', '80'),
|
||||
'SERVER_PROTOCOL': 'HTTP/1.1',
|
||||
'event': event,
|
||||
'context': context,
|
||||
'wsgi.errors': sys.stderr,
|
||||
'wsgi.input': BytesIO(body),
|
||||
'wsgi.multiprocess': False,
|
||||
'wsgi.multithread': False,
|
||||
'wsgi.run_once': False,
|
||||
'wsgi.url_scheme': headers.get('x-forwarded-proto', 'http'),
|
||||
'wsgi.version': (1, 0),
|
||||
}
|
||||
|
||||
for key, value in environ.items():
|
||||
if isinstance(value, string_types):
|
||||
environ[key] = wsgi_encoding_dance(value)
|
||||
|
||||
for key, value in headers.items():
|
||||
key = 'HTTP_' + key.upper().replace('-', '_')
|
||||
if key not in ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'):
|
||||
environ[key] = value
|
||||
|
||||
response = Response.from_app(__vc_module.app, environ)
|
||||
|
||||
return_dict = {
|
||||
'statusCode': response.status_code,
|
||||
'headers': format_headers(response.headers)
|
||||
}
|
||||
|
||||
if response.data:
|
||||
return_dict['body'] = base64.b64encode(response.data).decode('utf-8')
|
||||
return_dict['encoding'] = 'base64'
|
||||
|
||||
return return_dict
|
||||
else:
|
||||
print('using Asynchronous Server Gateway Interface (ASGI)')
|
||||
# Originally authored by Jordan Eremieff and included under MIT license:
|
||||
# https://github.com/erm/mangum/blob/b4d21c8f5e304a3e17b88bc9fa345106acc50ad7/mangum/__init__.py
|
||||
# https://github.com/erm/mangum/blob/b4d21c8f5e304a3e17b88bc9fa345106acc50ad7/LICENSE
|
||||
import asyncio
|
||||
import enum
|
||||
from urllib.parse import urlparse
|
||||
from werkzeug.datastructures import Headers
|
||||
|
||||
|
||||
class ASGICycleState(enum.Enum):
|
||||
REQUEST = enum.auto()
|
||||
RESPONSE = enum.auto()
|
||||
|
||||
|
||||
class ASGICycle:
|
||||
def __init__(self, scope):
|
||||
self.scope = scope
|
||||
self.body = b''
|
||||
self.state = ASGICycleState.REQUEST
|
||||
self.app_queue = None
|
||||
self.response = {}
|
||||
|
||||
def __call__(self, app, body):
|
||||
"""
|
||||
Receives the application and any body included in the request, then builds the
|
||||
ASGI instance using the connection scope.
|
||||
Runs until the response is completely read from the application.
|
||||
"""
|
||||
if _use_legacy_asyncio:
|
||||
loop = asyncio.new_event_loop()
|
||||
self.app_queue = asyncio.Queue(loop=loop)
|
||||
else:
|
||||
self.app_queue = asyncio.Queue()
|
||||
self.put_message({'type': 'http.request', 'body': body, 'more_body': False})
|
||||
|
||||
asgi_instance = app(self.scope, self.receive, self.send)
|
||||
|
||||
if _use_legacy_asyncio:
|
||||
asgi_task = loop.create_task(asgi_instance)
|
||||
loop.run_until_complete(asgi_task)
|
||||
else:
|
||||
asyncio.run(self.run_asgi_instance(asgi_instance))
|
||||
return self.response
|
||||
|
||||
async def run_asgi_instance(self, asgi_instance):
|
||||
await asgi_instance
|
||||
|
||||
def put_message(self, message):
|
||||
self.app_queue.put_nowait(message)
|
||||
|
||||
async def receive(self):
|
||||
"""
|
||||
Awaited by the application to receive messages in the queue.
|
||||
"""
|
||||
message = await self.app_queue.get()
|
||||
return message
|
||||
|
||||
async def send(self, message):
|
||||
"""
|
||||
Awaited by the application to send messages to the current cycle instance.
|
||||
"""
|
||||
message_type = message['type']
|
||||
|
||||
if self.state is ASGICycleState.REQUEST:
|
||||
if message_type != 'http.response.start':
|
||||
raise RuntimeError(
|
||||
f"Expected 'http.response.start', received: {message_type}"
|
||||
)
|
||||
|
||||
status_code = message['status']
|
||||
raw_headers = message.get('headers', [])
|
||||
|
||||
# Headers from werkzeug transform bytes header value
|
||||
# from b'value' to "b'value'" so we need to process
|
||||
# ASGI headers manually
|
||||
decoded_headers = []
|
||||
for key, value in raw_headers:
|
||||
decoded_key = key.decode() if isinstance(key, bytes) else key
|
||||
decoded_value = value.decode() if isinstance(value, bytes) else value
|
||||
decoded_headers.append((decoded_key, decoded_value))
|
||||
|
||||
headers = Headers(decoded_headers)
|
||||
|
||||
self.on_request(headers, status_code)
|
||||
self.state = ASGICycleState.RESPONSE
|
||||
|
||||
elif self.state is ASGICycleState.RESPONSE:
|
||||
if message_type != 'http.response.body':
|
||||
raise RuntimeError(
|
||||
f"Expected 'http.response.body', received: {message_type}"
|
||||
)
|
||||
|
||||
body = message.get('body', b'')
|
||||
more_body = message.get('more_body', False)
|
||||
|
||||
# The body must be completely read before returning the response.
|
||||
self.body += body
|
||||
|
||||
if not more_body:
|
||||
self.on_response()
|
||||
self.put_message({'type': 'http.disconnect'})
|
||||
|
||||
def on_request(self, headers, status_code):
|
||||
self.response['statusCode'] = status_code
|
||||
self.response['headers'] = format_headers(headers, decode=True)
|
||||
|
||||
def on_response(self):
|
||||
if self.body:
|
||||
self.response['body'] = base64.b64encode(self.body).decode('utf-8')
|
||||
self.response['encoding'] = 'base64'
|
||||
|
||||
def vc_handler(event, context):
|
||||
payload = json.loads(event['body'])
|
||||
|
||||
headers = payload.get('headers', {})
|
||||
|
||||
body = payload.get('body', b'')
|
||||
if payload.get('encoding') == 'base64':
|
||||
body = base64.b64decode(body)
|
||||
elif not isinstance(body, bytes):
|
||||
body = body.encode()
|
||||
|
||||
url = urlparse(payload['path'])
|
||||
query = url.query.encode()
|
||||
path = url.path
|
||||
|
||||
headers_encoded = []
|
||||
for k, v in headers.items():
|
||||
# Cope with repeated headers in the encoding.
|
||||
if isinstance(v, list):
|
||||
headers_encoded.append([k.lower().encode(), [i.encode() for i in v]])
|
||||
else:
|
||||
headers_encoded.append([k.lower().encode(), v.encode()])
|
||||
|
||||
scope = {
|
||||
'server': (headers.get('host', 'lambda'), headers.get('x-forwarded-port', 80)),
|
||||
'client': (headers.get(
|
||||
'x-forwarded-for', headers.get(
|
||||
'x-real-ip', payload.get(
|
||||
'true-client-ip', ''))), 0),
|
||||
'scheme': headers.get('x-forwarded-proto', 'http'),
|
||||
'root_path': '',
|
||||
'query_string': query,
|
||||
'headers': headers_encoded,
|
||||
'type': 'http',
|
||||
'http_version': '1.1',
|
||||
'method': payload['method'],
|
||||
'path': path,
|
||||
'raw_path': path.encode(),
|
||||
}
|
||||
|
||||
asgi_cycle = ASGICycle(scope)
|
||||
response = asgi_cycle(__vc_module.app, body)
|
||||
return response
|
||||
|
||||
else:
|
||||
print('Missing variable `handler` or `app` in file "__VC_HANDLER_ENTRYPOINT".')
|
||||
print('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
|
||||
exit(1)
|
||||
100
node_modules/@vercel/python/vc_init_dev_asgi.py
generated
vendored
Normal file
100
node_modules/@vercel/python/vc_init_dev_asgi.py
generated
vendored
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Auto-generated template used by vercel dev (Python, ASGI)
|
||||
# Serves static files from PUBLIC_DIR before delegating to the user ASGI app.
|
||||
import sys
|
||||
import os
|
||||
from os import path as _p
|
||||
from importlib import import_module
|
||||
|
||||
|
||||
# Simple ANSI coloring. Respect NO_COLOR environment variable.
|
||||
_NO_COLOR = 'NO_COLOR' in os.environ
|
||||
_RESET = "\x1b[0m"
|
||||
_YELLOW = "\x1b[33m"
|
||||
_GREEN = "\x1b[32m"
|
||||
_RED = "\x1b[31m"
|
||||
|
||||
def _color(text: str, code: str) -> str:
|
||||
if _NO_COLOR:
|
||||
return text
|
||||
return f"{code}{text}{_RESET}"
|
||||
|
||||
|
||||
# Optional StaticFiles import; tolerate missing deps
|
||||
StaticFiles = None
|
||||
try:
|
||||
from fastapi.staticfiles import StaticFiles as _SF
|
||||
StaticFiles = _SF
|
||||
except Exception:
|
||||
try:
|
||||
from starlette.staticfiles import StaticFiles as _SF
|
||||
StaticFiles = _SF
|
||||
except Exception:
|
||||
StaticFiles = None
|
||||
|
||||
USER_MODULE = "__VC_DEV_MODULE_PATH__"
|
||||
_mod = import_module(USER_MODULE)
|
||||
_app = getattr(_mod, 'app', None)
|
||||
if _app is None:
|
||||
raise RuntimeError(
|
||||
f"Missing 'app' in module '{USER_MODULE}'. Define `app = ...` (ASGI app)."
|
||||
)
|
||||
|
||||
# Prefer a callable app.asgi when available; some frameworks expose a boolean here
|
||||
_CAND = getattr(_app, 'asgi', None)
|
||||
USER_ASGI_APP = _CAND if callable(_CAND) else _app
|
||||
|
||||
PUBLIC_DIR = 'public'
|
||||
|
||||
# Prepare static files app (if starlette/fastapi installed)
|
||||
static_app = None
|
||||
if StaticFiles is not None:
|
||||
try:
|
||||
try:
|
||||
static_app = StaticFiles(directory=PUBLIC_DIR, check_dir=False)
|
||||
except TypeError:
|
||||
# Older Starlette without check_dir parameter
|
||||
static_app = StaticFiles(directory=PUBLIC_DIR)
|
||||
except Exception:
|
||||
static_app = None
|
||||
|
||||
|
||||
async def app(scope, receive, send):
|
||||
if static_app is not None and scope.get('type') == 'http':
|
||||
req_path = scope.get('path', '/') or '/'
|
||||
safe = _p.normpath(req_path).lstrip('/')
|
||||
full = _p.join(PUBLIC_DIR, safe)
|
||||
try:
|
||||
base = _p.realpath(PUBLIC_DIR)
|
||||
target = _p.realpath(full)
|
||||
if (target == base or target.startswith(base + _p.sep)) and _p.isfile(target):
|
||||
await static_app(scope, receive, send)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
await USER_ASGI_APP(scope, receive, send)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Development runner for ASGI: prefer uvicorn, then hypercorn.
|
||||
# Bind to localhost on an ephemeral port and emit a recognizable log line
|
||||
# so the caller can detect the bound port.
|
||||
host = '127.0.0.1'
|
||||
try:
|
||||
import uvicorn
|
||||
uvicorn.run('vc_init_dev_asgi:app', host=host, port=0, use_colors=True)
|
||||
except Exception:
|
||||
try:
|
||||
import asyncio
|
||||
from hypercorn.config import Config
|
||||
from hypercorn.asyncio import serve
|
||||
|
||||
config = Config()
|
||||
config.bind = [f'{host}:0']
|
||||
|
||||
async def _run():
|
||||
await serve(app, config)
|
||||
|
||||
asyncio.run(_run())
|
||||
except Exception:
|
||||
print(_color('No ASGI server found. Please install either "uvicorn" or "hypercorn" (e.g. "pip install uvicorn").', _RED), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
123
node_modules/@vercel/python/vc_init_dev_wsgi.py
generated
vendored
Normal file
123
node_modules/@vercel/python/vc_init_dev_wsgi.py
generated
vendored
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
"""
|
||||
Auto-generated template used by vercel dev (Python, WSGI)
|
||||
Serves static files from PUBLIC_DIR before delegating to the user WSGI app.
|
||||
|
||||
This file is written to the project at .vercel/python/vc_init_dev_wsgi.py
|
||||
and imported by the dev server launcher.
|
||||
"""
|
||||
from importlib import import_module
|
||||
from os import path as _p
|
||||
import os
|
||||
import mimetypes
|
||||
|
||||
# Simple ANSI coloring. Respect NO_COLOR environment variable.
|
||||
_NO_COLOR = 'NO_COLOR' in os.environ
|
||||
_RESET = "\x1b[0m"
|
||||
_YELLOW = "\x1b[33m"
|
||||
_GREEN = "\x1b[32m"
|
||||
|
||||
def _color(text: str, code: str) -> str:
|
||||
if _NO_COLOR:
|
||||
return text
|
||||
return f"{code}{text}{_RESET}"
|
||||
|
||||
USER_MODULE = "__VC_DEV_MODULE_PATH__"
|
||||
PUBLIC_DIR = "public"
|
||||
|
||||
_mod = import_module(USER_MODULE)
|
||||
_app = getattr(_mod, "app", None)
|
||||
if _app is None:
|
||||
raise RuntimeError(
|
||||
f"Missing 'app' in module '{USER_MODULE}'. Define `app = ...` (WSGI app)."
|
||||
)
|
||||
|
||||
|
||||
def _is_safe_file(base_dir: str, target: str) -> bool:
|
||||
try:
|
||||
base = _p.realpath(base_dir)
|
||||
tgt = _p.realpath(target)
|
||||
return (tgt == base or tgt.startswith(base + os.sep)) and _p.isfile(tgt)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _static_wsgi_app(environ, start_response):
|
||||
# Only handle GET/HEAD requests for static assets
|
||||
if environ.get("REQUEST_METHOD", "GET") not in ("GET", "HEAD"):
|
||||
return _not_found(start_response)
|
||||
|
||||
req_path = environ.get("PATH_INFO", "/") or "/"
|
||||
safe = _p.normpath(req_path).lstrip("/")
|
||||
full = _p.join(PUBLIC_DIR, safe)
|
||||
if not _is_safe_file(PUBLIC_DIR, full):
|
||||
return _not_found(start_response)
|
||||
|
||||
ctype, encoding = mimetypes.guess_type(full)
|
||||
headers = [("Content-Type", ctype or "application/octet-stream")]
|
||||
try:
|
||||
# For HEAD requests, send headers only
|
||||
if environ.get("REQUEST_METHOD") == "HEAD":
|
||||
start_response("200 OK", headers)
|
||||
return []
|
||||
with open(full, "rb") as f:
|
||||
data = f.read()
|
||||
headers.append(("Content-Length", str(len(data))))
|
||||
start_response("200 OK", headers)
|
||||
return [data]
|
||||
except Exception:
|
||||
return _not_found(start_response)
|
||||
|
||||
|
||||
def _not_found(start_response):
|
||||
start_response("404 Not Found", [("Content-Type", "text/plain; charset=utf-8")])
|
||||
return [b"Not Found"]
|
||||
|
||||
|
||||
def _combined_app(environ, start_response):
|
||||
# Try static first; if 404 then delegate to user app
|
||||
captured_status = ""
|
||||
captured_headers = tuple()
|
||||
body_chunks = []
|
||||
|
||||
def capture_start_response(status, headers, exc_info=None): # type: ignore[no-redef]
|
||||
nonlocal captured_status, captured_headers
|
||||
captured_status = status
|
||||
captured_headers = tuple(headers)
|
||||
# Return a writer that buffers the body
|
||||
def write(chunk: bytes):
|
||||
body_chunks.append(chunk)
|
||||
return write
|
||||
|
||||
result = _static_wsgi_app(environ, capture_start_response)
|
||||
# If static handler produced 200, forward its response
|
||||
if captured_status.startswith("200 "):
|
||||
# Send headers and any chunks collected
|
||||
writer = start_response(captured_status, list(captured_headers))
|
||||
for chunk in body_chunks:
|
||||
writer(chunk)
|
||||
return result
|
||||
|
||||
# Otherwise, delegate to user's WSGI app
|
||||
return _app(environ, start_response)
|
||||
|
||||
|
||||
# Public WSGI application consumed by the dev runner
|
||||
app = _combined_app
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Development runner: prefer Werkzeug, fall back to stdlib wsgiref.
|
||||
# Bind to localhost on an ephemeral port and emit a recognizable log line
|
||||
# so the caller can detect the bound port.
|
||||
host = "127.0.0.1"
|
||||
try:
|
||||
from werkzeug.serving import run_simple
|
||||
run_simple(host, 0, app, use_reloader=False)
|
||||
except Exception:
|
||||
import sys
|
||||
print(_color("Werkzeug not installed; falling back to wsgiref (no reloader).", _YELLOW), file=sys.stderr)
|
||||
from wsgiref.simple_server import make_server
|
||||
httpd = make_server(host, 0, app)
|
||||
port = httpd.server_port
|
||||
print(_color(f"Serving on http://{host}:{port}", _GREEN))
|
||||
httpd.serve_forever()
|
||||
Loading…
Add table
Add a link
Reference in a new issue