@vercel/python 6.5.1 → 6.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +99 -147
- package/package.json +2 -2
- package/vc_init.py +0 -916
package/dist/index.js
CHANGED
|
@@ -2822,10 +2822,17 @@ var UvRunner = class {
|
|
|
2822
2822
|
`Failed to parse 'uv python list' output: ${err instanceof Error ? err.message : String(err)}`
|
|
2823
2823
|
);
|
|
2824
2824
|
}
|
|
2825
|
-
|
|
2826
|
-
pyList.filter(
|
|
2825
|
+
if (process.env.VERCEL_BUILD_IMAGE) {
|
|
2826
|
+
pyList = pyList.filter(
|
|
2827
2827
|
(entry) => entry.path !== null && entry.path.startsWith(UV_PYTHON_PATH_PREFIX) && entry.implementation === "cpython"
|
|
2828
|
-
)
|
|
2828
|
+
);
|
|
2829
|
+
} else {
|
|
2830
|
+
pyList = pyList.filter(
|
|
2831
|
+
(entry) => entry.path !== null && entry.implementation === "cpython"
|
|
2832
|
+
);
|
|
2833
|
+
}
|
|
2834
|
+
return new Set(
|
|
2835
|
+
pyList.map(
|
|
2829
2836
|
(entry) => `${entry.version_parts.major}.${entry.version_parts.minor}`
|
|
2830
2837
|
)
|
|
2831
2838
|
);
|
|
@@ -2869,6 +2876,14 @@ var UvRunner = class {
|
|
|
2869
2876
|
(0, import_build_utils.debug)(`Running "uv ${args.join(" ")}" in ${projectDir}...`);
|
|
2870
2877
|
await this.runUvCmd(args, projectDir, venvPath);
|
|
2871
2878
|
}
|
|
2879
|
+
/**
|
|
2880
|
+
* Run a `uv pip` command (e.g., `uv pip install`).
|
|
2881
|
+
*/
|
|
2882
|
+
async pip(options) {
|
|
2883
|
+
const { venvPath, projectDir, args } = options;
|
|
2884
|
+
const fullArgs = ["pip", ...args];
|
|
2885
|
+
await this.runUvCmd(fullArgs, projectDir, venvPath);
|
|
2886
|
+
}
|
|
2872
2887
|
async runUvCmd(args, cwd, venvPath) {
|
|
2873
2888
|
const pretty = `uv ${args.join(" ")}`;
|
|
2874
2889
|
(0, import_build_utils.debug)(`Running "${pretty}"...`);
|
|
@@ -3529,27 +3544,6 @@ async function createPyprojectToml({
|
|
|
3529
3544
|
].join("\n");
|
|
3530
3545
|
await import_fs3.default.promises.writeFile(pyprojectPath, content);
|
|
3531
3546
|
}
|
|
3532
|
-
function getDependencyName(spec) {
|
|
3533
|
-
const match = spec.match(/^[A-Za-z0-9_.-]+/);
|
|
3534
|
-
return match ? match[0].toLowerCase() : spec.toLowerCase();
|
|
3535
|
-
}
|
|
3536
|
-
async function filterMissingRuntimeDependencies({
|
|
3537
|
-
pyprojectPath,
|
|
3538
|
-
runtimeDependencies
|
|
3539
|
-
}) {
|
|
3540
|
-
let declared = [];
|
|
3541
|
-
try {
|
|
3542
|
-
const config = await (0, import_build_utils4.readConfigFile)(pyprojectPath);
|
|
3543
|
-
declared = config?.project?.dependencies || [];
|
|
3544
|
-
} catch (err) {
|
|
3545
|
-
(0, import_build_utils4.debug)("Failed to parse pyproject.toml when filtering runtime deps", err);
|
|
3546
|
-
}
|
|
3547
|
-
const declaredNames = new Set(declared.map(getDependencyName));
|
|
3548
|
-
return runtimeDependencies.filter((spec) => {
|
|
3549
|
-
const name = getDependencyName(spec);
|
|
3550
|
-
return !declaredNames.has(name);
|
|
3551
|
-
});
|
|
3552
|
-
}
|
|
3553
3547
|
function findUvLockUpwards(startDir, repoRootPath) {
|
|
3554
3548
|
const start = (0, import_path4.resolve)(startDir);
|
|
3555
3549
|
const base = repoRootPath ? (0, import_path4.resolve)(repoRootPath) : void 0;
|
|
@@ -3572,8 +3566,7 @@ async function ensureUvProject({
|
|
|
3572
3566
|
pythonVersion,
|
|
3573
3567
|
uv,
|
|
3574
3568
|
venvPath,
|
|
3575
|
-
meta
|
|
3576
|
-
runtimeDependencies
|
|
3569
|
+
meta
|
|
3577
3570
|
}) {
|
|
3578
3571
|
const uvPath = uv.getPath();
|
|
3579
3572
|
const installInfo = await detectInstallSource({
|
|
@@ -3680,19 +3673,6 @@ async function ensureUvProject({
|
|
|
3680
3673
|
});
|
|
3681
3674
|
await uv.lock(projectDir);
|
|
3682
3675
|
}
|
|
3683
|
-
if (runtimeDependencies.length) {
|
|
3684
|
-
const missingRuntimeDeps = await filterMissingRuntimeDependencies({
|
|
3685
|
-
pyprojectPath,
|
|
3686
|
-
runtimeDependencies
|
|
3687
|
-
});
|
|
3688
|
-
if (missingRuntimeDeps.length) {
|
|
3689
|
-
await uv.addDependencies({
|
|
3690
|
-
venvPath,
|
|
3691
|
-
projectDir,
|
|
3692
|
-
dependencies: missingRuntimeDeps
|
|
3693
|
-
});
|
|
3694
|
-
}
|
|
3695
|
-
}
|
|
3696
3676
|
const resolvedLockPath = lockPath && import_fs3.default.existsSync(lockPath) ? lockPath : findUvLockUpwards(projectDir, repoRootPath) || (0, import_path4.join)(projectDir, "uv.lock");
|
|
3697
3677
|
return { projectDir, pyprojectPath, lockPath: resolvedLockPath };
|
|
3698
3678
|
}
|
|
@@ -3977,6 +3957,20 @@ var ANSI_ESCAPE_RE = new RegExp(ANSI_PATTERN, "g");
|
|
|
3977
3957
|
var stripAnsi = (s) => s.replace(ANSI_ESCAPE_RE, "");
|
|
3978
3958
|
var ASGI_SHIM_MODULE = "vc_init_dev_asgi";
|
|
3979
3959
|
var WSGI_SHIM_MODULE = "vc_init_dev_wsgi";
|
|
3960
|
+
function createLogListener(callback, stream) {
|
|
3961
|
+
return (buf) => {
|
|
3962
|
+
if (callback) {
|
|
3963
|
+
callback(buf);
|
|
3964
|
+
} else {
|
|
3965
|
+
const s = buf.toString();
|
|
3966
|
+
for (const line of s.split(/\r?\n/)) {
|
|
3967
|
+
if (line) {
|
|
3968
|
+
stream.write(line.endsWith("\n") ? line : line + "\n");
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
};
|
|
3973
|
+
}
|
|
3980
3974
|
var PERSISTENT_SERVERS = /* @__PURE__ */ new Map();
|
|
3981
3975
|
var PENDING_STARTS = /* @__PURE__ */ new Map();
|
|
3982
3976
|
var restoreWarnings = null;
|
|
@@ -4053,7 +4047,14 @@ function createDevWsgiShim(workPath, modulePath) {
|
|
|
4053
4047
|
}
|
|
4054
4048
|
}
|
|
4055
4049
|
var startDevServer = async (opts) => {
|
|
4056
|
-
const {
|
|
4050
|
+
const {
|
|
4051
|
+
entrypoint: rawEntrypoint,
|
|
4052
|
+
workPath,
|
|
4053
|
+
meta = {},
|
|
4054
|
+
config,
|
|
4055
|
+
onStdout,
|
|
4056
|
+
onStderr
|
|
4057
|
+
} = opts;
|
|
4057
4058
|
const framework = config?.framework;
|
|
4058
4059
|
if (framework !== "fastapi" && framework !== "flask") {
|
|
4059
4060
|
return null;
|
|
@@ -4161,22 +4162,8 @@ If you are using a virtual environment, activate it before running "vercel dev",
|
|
|
4161
4162
|
stdio: ["inherit", "pipe", "pipe"]
|
|
4162
4163
|
});
|
|
4163
4164
|
childProcess = child;
|
|
4164
|
-
stdoutLogListener = (
|
|
4165
|
-
|
|
4166
|
-
for (const line of s.split(/\r?\n/)) {
|
|
4167
|
-
if (line) {
|
|
4168
|
-
process.stdout.write(line.endsWith("\n") ? line : line + "\n");
|
|
4169
|
-
}
|
|
4170
|
-
}
|
|
4171
|
-
};
|
|
4172
|
-
stderrLogListener = (buf) => {
|
|
4173
|
-
const s = buf.toString();
|
|
4174
|
-
for (const line of s.split(/\r?\n/)) {
|
|
4175
|
-
if (line) {
|
|
4176
|
-
process.stderr.write(line.endsWith("\n") ? line : line + "\n");
|
|
4177
|
-
}
|
|
4178
|
-
}
|
|
4179
|
-
};
|
|
4165
|
+
stdoutLogListener = createLogListener(onStdout, process.stdout);
|
|
4166
|
+
stderrLogListener = createLogListener(onStderr, process.stderr);
|
|
4180
4167
|
child.stdout?.on("data", stdoutLogListener);
|
|
4181
4168
|
child.stderr?.on("data", stderrLogListener);
|
|
4182
4169
|
const readinessRegexes = [
|
|
@@ -4239,22 +4226,8 @@ If you are using a virtual environment, activate it before running "vercel dev",
|
|
|
4239
4226
|
stdio: ["inherit", "pipe", "pipe"]
|
|
4240
4227
|
});
|
|
4241
4228
|
childProcess = child;
|
|
4242
|
-
stdoutLogListener = (
|
|
4243
|
-
|
|
4244
|
-
for (const line of s.split(/\r?\n/)) {
|
|
4245
|
-
if (line) {
|
|
4246
|
-
process.stdout.write(line.endsWith("\n") ? line : line + "\n");
|
|
4247
|
-
}
|
|
4248
|
-
}
|
|
4249
|
-
};
|
|
4250
|
-
stderrLogListener = (buf) => {
|
|
4251
|
-
const s = buf.toString();
|
|
4252
|
-
for (const line of s.split(/\r?\n/)) {
|
|
4253
|
-
if (line) {
|
|
4254
|
-
process.stderr.write(line.endsWith("\n") ? line : line + "\n");
|
|
4255
|
-
}
|
|
4256
|
-
}
|
|
4257
|
-
};
|
|
4229
|
+
stdoutLogListener = createLogListener(onStdout, process.stdout);
|
|
4230
|
+
stderrLogListener = createLogListener(onStderr, process.stderr);
|
|
4258
4231
|
child.stdout?.on("data", stdoutLogListener);
|
|
4259
4232
|
child.stderr?.on("data", stderrLogListener);
|
|
4260
4233
|
const readinessRegexes = [
|
|
@@ -4515,86 +4488,70 @@ var build = async ({
|
|
|
4515
4488
|
pythonPath: pythonVersion.pythonPath,
|
|
4516
4489
|
venvPath
|
|
4517
4490
|
});
|
|
4518
|
-
const
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
console.log(`Running "install" command: \`${installCommand}\`...`);
|
|
4526
|
-
await (0, import_build_utils8.execCommand)(installCommand, {
|
|
4491
|
+
const baseEnv = spawnEnv || process.env;
|
|
4492
|
+
const pythonEnv = createVenvEnv(venvPath, baseEnv);
|
|
4493
|
+
pythonEnv.VERCEL_PYTHON_VENV_PATH = venvPath;
|
|
4494
|
+
let assumeDepsInstalled = false;
|
|
4495
|
+
if (projectInstallCommand) {
|
|
4496
|
+
console.log(`Running "install" command: \`${projectInstallCommand}\`...`);
|
|
4497
|
+
await (0, import_build_utils8.execCommand)(projectInstallCommand, {
|
|
4527
4498
|
env: pythonEnv,
|
|
4528
4499
|
cwd: workPath
|
|
4529
4500
|
});
|
|
4501
|
+
assumeDepsInstalled = true;
|
|
4530
4502
|
} else {
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
uv = new UvRunner(uvPath);
|
|
4550
|
-
} catch (err) {
|
|
4551
|
-
console.log("Failed to install or locate uv");
|
|
4552
|
-
throw new Error(
|
|
4553
|
-
`uv is required for this project but failed to install: ${err instanceof Error ? err.message : String(err)}`
|
|
4554
|
-
);
|
|
4555
|
-
}
|
|
4556
|
-
const baseEnv = spawnEnv || process.env;
|
|
4557
|
-
useRuntime = !!baseEnv.VERCEL_RUNTIME_PYTHON_ENABLED;
|
|
4558
|
-
const runtimeDependencies = [];
|
|
4559
|
-
if (useRuntime) {
|
|
4560
|
-
runtimeDependencies.push(
|
|
4561
|
-
baseEnv.VERCEL_RUNTIME_PYTHON || `vercel-runtime==${VERCEL_RUNTIME_VERSION}`
|
|
4562
|
-
);
|
|
4563
|
-
} else {
|
|
4564
|
-
runtimeDependencies.push("werkzeug>=1.0.1");
|
|
4565
|
-
if (framework !== "flask") {
|
|
4566
|
-
runtimeDependencies.push("uvicorn>=0.24");
|
|
4567
|
-
}
|
|
4568
|
-
}
|
|
4569
|
-
const { projectDir } = await ensureUvProject({
|
|
4570
|
-
workPath,
|
|
4571
|
-
entryDirectory,
|
|
4572
|
-
fsFiles,
|
|
4573
|
-
repoRootPath,
|
|
4574
|
-
pythonPath: pythonVersion.pythonPath,
|
|
4575
|
-
pipPath: pythonVersion.pipPath,
|
|
4576
|
-
pythonVersion: pythonVersion.version,
|
|
4577
|
-
uv,
|
|
4578
|
-
venvPath,
|
|
4579
|
-
meta,
|
|
4580
|
-
runtimeDependencies
|
|
4581
|
-
});
|
|
4582
|
-
await uv.sync({
|
|
4583
|
-
venvPath,
|
|
4584
|
-
projectDir,
|
|
4585
|
-
locked: true
|
|
4586
|
-
});
|
|
4587
|
-
}
|
|
4503
|
+
assumeDepsInstalled = await runPyprojectScript(
|
|
4504
|
+
workPath,
|
|
4505
|
+
["vercel-install", "now-install", "install"],
|
|
4506
|
+
pythonEnv,
|
|
4507
|
+
/* useUserVirtualEnv */
|
|
4508
|
+
false
|
|
4509
|
+
);
|
|
4510
|
+
}
|
|
4511
|
+
let uv;
|
|
4512
|
+
try {
|
|
4513
|
+
const uvPath = await getUvBinaryOrInstall(pythonVersion.pythonPath);
|
|
4514
|
+
console.log(`Using uv at "${uvPath}"`);
|
|
4515
|
+
uv = new UvRunner(uvPath);
|
|
4516
|
+
} catch (err) {
|
|
4517
|
+
console.log("Failed to install or locate uv");
|
|
4518
|
+
throw new Error(
|
|
4519
|
+
`uv is required for this project but failed to install: ${err instanceof Error ? err.message : String(err)}`
|
|
4520
|
+
);
|
|
4588
4521
|
}
|
|
4522
|
+
if (!assumeDepsInstalled) {
|
|
4523
|
+
const { projectDir } = await ensureUvProject({
|
|
4524
|
+
workPath,
|
|
4525
|
+
entryDirectory,
|
|
4526
|
+
fsFiles,
|
|
4527
|
+
repoRootPath,
|
|
4528
|
+
pythonPath: pythonVersion.pythonPath,
|
|
4529
|
+
pipPath: pythonVersion.pipPath,
|
|
4530
|
+
pythonVersion: pythonVersion.version,
|
|
4531
|
+
uv,
|
|
4532
|
+
venvPath,
|
|
4533
|
+
meta
|
|
4534
|
+
});
|
|
4535
|
+
await uv.sync({
|
|
4536
|
+
venvPath,
|
|
4537
|
+
projectDir,
|
|
4538
|
+
locked: true
|
|
4539
|
+
});
|
|
4540
|
+
}
|
|
4541
|
+
const runtimeDep = baseEnv.VERCEL_RUNTIME_PYTHON || `vercel-runtime==${VERCEL_RUNTIME_VERSION}`;
|
|
4542
|
+
(0, import_build_utils8.debug)(`Installing ${runtimeDep}`);
|
|
4543
|
+
await uv.pip({
|
|
4544
|
+
venvPath,
|
|
4545
|
+
projectDir: entryDirectory,
|
|
4546
|
+
args: ["install", runtimeDep]
|
|
4547
|
+
});
|
|
4589
4548
|
(0, import_build_utils8.debug)("Entrypoint is", entrypoint);
|
|
4590
4549
|
const moduleName = entrypoint.replace(/\//g, ".").replace(/\.py$/i, "");
|
|
4591
4550
|
const vendorDir = resolveVendorDir();
|
|
4592
4551
|
const suffix = meta.isDev && !entrypoint.endsWith(".py") ? ".py" : "";
|
|
4593
4552
|
const entrypointWithSuffix = `${entrypoint}${suffix}`;
|
|
4594
4553
|
(0, import_build_utils8.debug)("Entrypoint with suffix is", entrypointWithSuffix);
|
|
4595
|
-
|
|
4596
|
-
if (useRuntime) {
|
|
4597
|
-
handlerPyContents = `
|
|
4554
|
+
const runtimeTrampoline = `
|
|
4598
4555
|
import importlib
|
|
4599
4556
|
import os
|
|
4600
4557
|
import os.path
|
|
@@ -4632,11 +4589,6 @@ if os.path.isdir(_vendor):
|
|
|
4632
4589
|
|
|
4633
4590
|
from vercel_runtime.vc_init import vc_handler
|
|
4634
4591
|
`;
|
|
4635
|
-
} else {
|
|
4636
|
-
const originalPyPath = (0, import_path7.join)(__dirname, "..", "vc_init.py");
|
|
4637
|
-
const originalHandlerPyContents = await readFile(originalPyPath, "utf8");
|
|
4638
|
-
handlerPyContents = originalHandlerPyContents.replace(/__VC_HANDLER_MODULE_NAME/g, moduleName).replace(/__VC_HANDLER_ENTRYPOINT/g, entrypointWithSuffix).replace(/__VC_HANDLER_VENDOR_DIR/g, vendorDir);
|
|
4639
|
-
}
|
|
4640
4592
|
const predefinedExcludes = [
|
|
4641
4593
|
".git/**",
|
|
4642
4594
|
".gitignore",
|
|
@@ -4670,7 +4622,7 @@ from vercel_runtime.vc_init import vc_handler
|
|
|
4670
4622
|
files[p] = f;
|
|
4671
4623
|
}
|
|
4672
4624
|
const handlerPyFilename = "vc__handler__python";
|
|
4673
|
-
files[`${handlerPyFilename}.py`] = new import_build_utils8.FileBlob({ data:
|
|
4625
|
+
files[`${handlerPyFilename}.py`] = new import_build_utils8.FileBlob({ data: runtimeTrampoline });
|
|
4674
4626
|
if (config.framework === "fasthtml") {
|
|
4675
4627
|
const { SESSKEY = "" } = process.env;
|
|
4676
4628
|
files[".sesskey"] = new import_build_utils8.FileBlob({ data: `"${SESSKEY}"` });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vercel/python",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.7.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"smol-toml": "1.5.2",
|
|
32
32
|
"vitest": "2.1.4",
|
|
33
33
|
"which": "3.0.0",
|
|
34
|
-
"@vercel/build-utils": "13.2
|
|
34
|
+
"@vercel/build-utils": "13.3.2",
|
|
35
35
|
"@vercel/error-utils": "2.0.3"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
package/vc_init.py
DELETED
|
@@ -1,916 +0,0 @@
|
|
|
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
|
|
20
|
-
import contextvars
|
|
21
|
-
import contextlib
|
|
22
|
-
import atexit
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
_here = os.path.dirname(__file__)
|
|
26
|
-
_vendor_rel = '__VC_HANDLER_VENDOR_DIR'
|
|
27
|
-
_vendor = os.path.normpath(os.path.join(_here, _vendor_rel))
|
|
28
|
-
|
|
29
|
-
if os.path.isdir(_vendor):
|
|
30
|
-
# Process .pth files like a real site-packages dir
|
|
31
|
-
site.addsitedir(_vendor)
|
|
32
|
-
|
|
33
|
-
# Move _vendor to the front (after script dir if present)
|
|
34
|
-
try:
|
|
35
|
-
while _vendor in sys.path:
|
|
36
|
-
sys.path.remove(_vendor)
|
|
37
|
-
except ValueError:
|
|
38
|
-
pass
|
|
39
|
-
|
|
40
|
-
# Put vendored deps ahead of site-packages but after the script dir
|
|
41
|
-
idx = 1 if (sys.path and sys.path[0] in ('', _here)) else 0
|
|
42
|
-
sys.path.insert(idx, _vendor)
|
|
43
|
-
|
|
44
|
-
importlib.invalidate_caches()
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def setup_logging(send_message: Callable[[dict], None], storage: contextvars.ContextVar[dict | None]):
|
|
48
|
-
# Override logging.Handler to send logs to the platform when a request context is available.
|
|
49
|
-
class VCLogHandler(logging.Handler):
|
|
50
|
-
def emit(self, record: logging.LogRecord):
|
|
51
|
-
try:
|
|
52
|
-
message = record.getMessage()
|
|
53
|
-
except Exception:
|
|
54
|
-
message = repr(getattr(record, "msg", ""))
|
|
55
|
-
|
|
56
|
-
with contextlib.suppress(Exception):
|
|
57
|
-
if record.exc_info:
|
|
58
|
-
# logging allows exc_info=True or a (type, value, tb) tuple
|
|
59
|
-
exc_info = record.exc_info
|
|
60
|
-
if exc_info is True:
|
|
61
|
-
exc_info = sys.exc_info()
|
|
62
|
-
if isinstance(exc_info, tuple):
|
|
63
|
-
tb = ''.join(traceback.format_exception(*exc_info))
|
|
64
|
-
if tb:
|
|
65
|
-
if message:
|
|
66
|
-
message = f"{message}\n{tb}"
|
|
67
|
-
else:
|
|
68
|
-
message = tb
|
|
69
|
-
|
|
70
|
-
if record.levelno >= logging.CRITICAL:
|
|
71
|
-
level = "fatal"
|
|
72
|
-
elif record.levelno >= logging.ERROR:
|
|
73
|
-
level = "error"
|
|
74
|
-
elif record.levelno >= logging.WARNING:
|
|
75
|
-
level = "warn"
|
|
76
|
-
elif record.levelno >= logging.INFO:
|
|
77
|
-
level = "info"
|
|
78
|
-
else:
|
|
79
|
-
level = "debug"
|
|
80
|
-
|
|
81
|
-
context = storage.get()
|
|
82
|
-
if context is not None:
|
|
83
|
-
send_message({
|
|
84
|
-
"type": "log",
|
|
85
|
-
"payload": {
|
|
86
|
-
"context": {
|
|
87
|
-
"invocationId": context['invocationId'],
|
|
88
|
-
"requestId": context['requestId'],
|
|
89
|
-
},
|
|
90
|
-
"message": base64.b64encode(message.encode()).decode(),
|
|
91
|
-
"level": level,
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
|
-
else:
|
|
95
|
-
# If IPC is not ready, enqueue the message to be sent later.
|
|
96
|
-
enqueue_or_send_message({
|
|
97
|
-
"type": "log",
|
|
98
|
-
"payload": {
|
|
99
|
-
"context": {"invocationId": "0", "requestId": 0},
|
|
100
|
-
"message": base64.b64encode(message.encode()).decode(),
|
|
101
|
-
"level": level,
|
|
102
|
-
}
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
# Override sys.stdout and sys.stderr to map logs to the correct request
|
|
106
|
-
class StreamWrapper:
|
|
107
|
-
def __init__(self, stream: TextIO, stream_name: Literal["stdout", "stderr"]):
|
|
108
|
-
self.stream = stream
|
|
109
|
-
self.stream_name = stream_name
|
|
110
|
-
|
|
111
|
-
def write(self, message: str):
|
|
112
|
-
context = storage.get()
|
|
113
|
-
if context is not None:
|
|
114
|
-
send_message({
|
|
115
|
-
"type": "log",
|
|
116
|
-
"payload": {
|
|
117
|
-
"context": {
|
|
118
|
-
"invocationId": context['invocationId'],
|
|
119
|
-
"requestId": context['requestId'],
|
|
120
|
-
},
|
|
121
|
-
"message": base64.b64encode(message.encode()).decode(),
|
|
122
|
-
"stream": self.stream_name,
|
|
123
|
-
}
|
|
124
|
-
})
|
|
125
|
-
else:
|
|
126
|
-
enqueue_or_send_message({
|
|
127
|
-
"type": "log",
|
|
128
|
-
"payload": {
|
|
129
|
-
"context": {"invocationId": "0", "requestId": 0},
|
|
130
|
-
"message": base64.b64encode(message.encode()).decode(),
|
|
131
|
-
"stream": self.stream_name,
|
|
132
|
-
}
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
def __getattr__(self, name):
|
|
136
|
-
return getattr(self.stream, name)
|
|
137
|
-
|
|
138
|
-
sys.stdout = StreamWrapper(sys.stdout, "stdout")
|
|
139
|
-
sys.stderr = StreamWrapper(sys.stderr, "stderr")
|
|
140
|
-
|
|
141
|
-
logging.basicConfig(level=logging.INFO, handlers=[VCLogHandler()], force=True)
|
|
142
|
-
|
|
143
|
-
# Ensure built-in print funnels through stdout wrapper so prints are
|
|
144
|
-
# attributed to the current request context.
|
|
145
|
-
def print_wrapper(func: Callable[..., None]) -> Callable[..., None]:
|
|
146
|
-
@functools.wraps(func)
|
|
147
|
-
def wrapper(*args, sep=' ', end='\n', file=None, flush=False):
|
|
148
|
-
if file is None:
|
|
149
|
-
file = sys.stdout
|
|
150
|
-
if file in (sys.stdout, sys.stderr):
|
|
151
|
-
file.write(sep.join(map(str, args)) + end)
|
|
152
|
-
if flush:
|
|
153
|
-
file.flush()
|
|
154
|
-
else:
|
|
155
|
-
# User specified a different file, use original print behavior
|
|
156
|
-
func(*args, sep=sep, end=end, file=file, flush=flush)
|
|
157
|
-
return wrapper
|
|
158
|
-
|
|
159
|
-
builtins.print = print_wrapper(builtins.print)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def _stderr(message: str):
|
|
163
|
-
with contextlib.suppress(Exception):
|
|
164
|
-
_original_stderr.write(message + "\n")
|
|
165
|
-
_original_stderr.flush()
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
# If running in the platform (IPC present), logging must be setup before importing user code so that
|
|
169
|
-
# logs happening outside the request context are emitted correctly.
|
|
170
|
-
ipc_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
171
|
-
storage: contextvars.ContextVar[dict | None] = contextvars.ContextVar('storage', default=None)
|
|
172
|
-
send_message = lambda m: None
|
|
173
|
-
_original_stderr = sys.stderr
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
# Buffer for pre-handshake logs (to avoid blocking IPC on startup)
|
|
177
|
-
_ipc_ready = False
|
|
178
|
-
_init_log_buf: list[dict] = []
|
|
179
|
-
_INIT_LOG_BUF_MAX_BYTES = 1_000_000
|
|
180
|
-
_init_log_buf_bytes = 0
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def enqueue_or_send_message(msg: dict):
|
|
184
|
-
global _init_log_buf_bytes
|
|
185
|
-
if _ipc_ready:
|
|
186
|
-
send_message(msg)
|
|
187
|
-
return
|
|
188
|
-
|
|
189
|
-
enc_len = len(json.dumps(msg))
|
|
190
|
-
|
|
191
|
-
if _init_log_buf_bytes + enc_len <= _INIT_LOG_BUF_MAX_BYTES:
|
|
192
|
-
_init_log_buf.append(msg)
|
|
193
|
-
_init_log_buf_bytes += enc_len
|
|
194
|
-
else:
|
|
195
|
-
# Fallback so message is not lost if buffer is full
|
|
196
|
-
with contextlib.suppress(Exception):
|
|
197
|
-
payload = msg.get("payload", {})
|
|
198
|
-
decoded = base64.b64decode(payload.get("message", "")).decode(errors="ignore")
|
|
199
|
-
_original_stderr.write(decoded + "\n")
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def flush_init_log_buf_to_stderr():
|
|
203
|
-
global _init_log_buf, _init_log_buf_bytes
|
|
204
|
-
try:
|
|
205
|
-
combined: list[str] = []
|
|
206
|
-
for m in _init_log_buf:
|
|
207
|
-
payload = m.get("payload", {})
|
|
208
|
-
msg = payload.get("message")
|
|
209
|
-
if not msg:
|
|
210
|
-
continue
|
|
211
|
-
with contextlib.suppress(Exception):
|
|
212
|
-
decoded = base64.b64decode(msg).decode(errors="ignore")
|
|
213
|
-
combined.append(decoded)
|
|
214
|
-
if combined:
|
|
215
|
-
_stderr("".join(combined))
|
|
216
|
-
except Exception:
|
|
217
|
-
pass
|
|
218
|
-
finally:
|
|
219
|
-
_init_log_buf.clear()
|
|
220
|
-
_init_log_buf_bytes = 0
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
atexit.register(flush_init_log_buf_to_stderr)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if 'VERCEL_IPC_PATH' in os.environ:
|
|
227
|
-
with contextlib.suppress(Exception):
|
|
228
|
-
ipc_sock.connect(os.getenv("VERCEL_IPC_PATH", ""))
|
|
229
|
-
|
|
230
|
-
def send_message(message: dict):
|
|
231
|
-
with contextlib.suppress(Exception):
|
|
232
|
-
ipc_sock.sendall((json.dumps(message) + '\0').encode())
|
|
233
|
-
|
|
234
|
-
setup_logging(send_message, storage)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
# Import relative path https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
|
|
238
|
-
try:
|
|
239
|
-
user_mod_path = os.path.join(_here, "__VC_HANDLER_ENTRYPOINT") # absolute
|
|
240
|
-
__vc_spec = util.spec_from_file_location("__VC_HANDLER_MODULE_NAME", user_mod_path)
|
|
241
|
-
__vc_module = util.module_from_spec(__vc_spec)
|
|
242
|
-
sys.modules["__VC_HANDLER_MODULE_NAME"] = __vc_module
|
|
243
|
-
__vc_spec.loader.exec_module(__vc_module)
|
|
244
|
-
__vc_variables = dir(__vc_module)
|
|
245
|
-
except Exception:
|
|
246
|
-
_stderr(f'Error importing __VC_HANDLER_ENTRYPOINT:')
|
|
247
|
-
_stderr(traceback.format_exc())
|
|
248
|
-
exit(1)
|
|
249
|
-
|
|
250
|
-
_use_legacy_asyncio = sys.version_info < (3, 10)
|
|
251
|
-
|
|
252
|
-
def format_headers(headers, decode=False):
|
|
253
|
-
keyToList = {}
|
|
254
|
-
for key, value in headers.items():
|
|
255
|
-
if decode and 'decode' in dir(key) and 'decode' in dir(value):
|
|
256
|
-
key = key.decode()
|
|
257
|
-
value = value.decode()
|
|
258
|
-
if key not in keyToList:
|
|
259
|
-
keyToList[key] = []
|
|
260
|
-
keyToList[key].append(value)
|
|
261
|
-
return keyToList
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
class ASGIMiddleware:
|
|
265
|
-
"""
|
|
266
|
-
ASGI middleware that preserves Vercel IPC semantics for request lifecycle:
|
|
267
|
-
- Handles /_vercel/ping
|
|
268
|
-
- Extracts x-vercel-internal-* headers and removes them from downstream app
|
|
269
|
-
- Sets request context into `storage` for logging/metrics
|
|
270
|
-
- Emits handler-started and end IPC messages
|
|
271
|
-
"""
|
|
272
|
-
def __init__(self, app):
|
|
273
|
-
self.app = app
|
|
274
|
-
|
|
275
|
-
async def __call__(self, scope, receive, send):
|
|
276
|
-
if scope.get('type') != 'http':
|
|
277
|
-
# Non-HTTP traffic is forwarded verbatim
|
|
278
|
-
await self.app(scope, receive, send)
|
|
279
|
-
return
|
|
280
|
-
|
|
281
|
-
if scope.get('path') == '/_vercel/ping':
|
|
282
|
-
await send({
|
|
283
|
-
'type': 'http.response.start',
|
|
284
|
-
'status': 200,
|
|
285
|
-
'headers': [],
|
|
286
|
-
})
|
|
287
|
-
await send({
|
|
288
|
-
'type': 'http.response.body',
|
|
289
|
-
'body': b'',
|
|
290
|
-
'more_body': False,
|
|
291
|
-
})
|
|
292
|
-
return
|
|
293
|
-
|
|
294
|
-
# Extract internal headers and set per-request context
|
|
295
|
-
headers_list = scope.get('headers', []) or []
|
|
296
|
-
new_headers = []
|
|
297
|
-
invocation_id = "0"
|
|
298
|
-
request_id = 0
|
|
299
|
-
|
|
300
|
-
def _b2s(b: bytes) -> str:
|
|
301
|
-
try:
|
|
302
|
-
return b.decode()
|
|
303
|
-
except Exception:
|
|
304
|
-
return ''
|
|
305
|
-
|
|
306
|
-
for k, v in headers_list:
|
|
307
|
-
key = _b2s(k).lower()
|
|
308
|
-
val = _b2s(v)
|
|
309
|
-
if key == 'x-vercel-internal-invocation-id':
|
|
310
|
-
invocation_id = val
|
|
311
|
-
continue
|
|
312
|
-
if key == 'x-vercel-internal-request-id':
|
|
313
|
-
request_id = int(val) if val.isdigit() else 0
|
|
314
|
-
continue
|
|
315
|
-
if key in ('x-vercel-internal-span-id', 'x-vercel-internal-trace-id'):
|
|
316
|
-
continue
|
|
317
|
-
new_headers.append((k, v))
|
|
318
|
-
|
|
319
|
-
new_scope = dict(scope)
|
|
320
|
-
new_scope['headers'] = new_headers
|
|
321
|
-
|
|
322
|
-
# Announce handler start and set context for logging/metrics
|
|
323
|
-
send_message({
|
|
324
|
-
"type": "handler-started",
|
|
325
|
-
"payload": {
|
|
326
|
-
"handlerStartedAt": int(time.time() * 1000),
|
|
327
|
-
"context": {
|
|
328
|
-
"invocationId": invocation_id,
|
|
329
|
-
"requestId": request_id,
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
token = storage.set({
|
|
335
|
-
"invocationId": invocation_id,
|
|
336
|
-
"requestId": request_id,
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
try:
|
|
340
|
-
await self.app(new_scope, receive, send)
|
|
341
|
-
finally:
|
|
342
|
-
storage.reset(token)
|
|
343
|
-
send_message({
|
|
344
|
-
"type": "end",
|
|
345
|
-
"payload": {
|
|
346
|
-
"context": {
|
|
347
|
-
"invocationId": invocation_id,
|
|
348
|
-
"requestId": request_id,
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
if 'VERCEL_IPC_PATH' in os.environ:
|
|
354
|
-
start_time = time.time()
|
|
355
|
-
|
|
356
|
-
# Override urlopen from urllib3 (& requests) to send Request Metrics
|
|
357
|
-
try:
|
|
358
|
-
import urllib3
|
|
359
|
-
from urllib.parse import urlparse
|
|
360
|
-
|
|
361
|
-
def timed_request(func):
|
|
362
|
-
fetchId = 0
|
|
363
|
-
@functools.wraps(func)
|
|
364
|
-
def wrapper(self, method, url, *args, **kwargs):
|
|
365
|
-
nonlocal fetchId
|
|
366
|
-
fetchId += 1
|
|
367
|
-
start_time = int(time.time() * 1000)
|
|
368
|
-
result = func(self, method, url, *args, **kwargs)
|
|
369
|
-
elapsed_time = int(time.time() * 1000) - start_time
|
|
370
|
-
parsed_url = urlparse(url)
|
|
371
|
-
context = storage.get()
|
|
372
|
-
if context is not None:
|
|
373
|
-
send_message({
|
|
374
|
-
"type": "metric",
|
|
375
|
-
"payload": {
|
|
376
|
-
"context": {
|
|
377
|
-
"invocationId": context['invocationId'],
|
|
378
|
-
"requestId": context['requestId'],
|
|
379
|
-
},
|
|
380
|
-
"type": "fetch-metric",
|
|
381
|
-
"payload": {
|
|
382
|
-
"pathname": parsed_url.path,
|
|
383
|
-
"search": parsed_url.query,
|
|
384
|
-
"start": start_time,
|
|
385
|
-
"duration": elapsed_time,
|
|
386
|
-
"host": parsed_url.hostname or self.host,
|
|
387
|
-
"statusCode": result.status,
|
|
388
|
-
"method": method,
|
|
389
|
-
"id": fetchId
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
})
|
|
393
|
-
return result
|
|
394
|
-
return wrapper
|
|
395
|
-
urllib3.connectionpool.HTTPConnectionPool.urlopen = timed_request(urllib3.connectionpool.HTTPConnectionPool.urlopen)
|
|
396
|
-
except:
|
|
397
|
-
pass
|
|
398
|
-
|
|
399
|
-
class BaseHandler(BaseHTTPRequestHandler):
|
|
400
|
-
# Re-implementation of BaseHTTPRequestHandler's log_message method to
|
|
401
|
-
# log to stdout instead of stderr.
|
|
402
|
-
def log_message(self, format, *args):
|
|
403
|
-
message = format % args
|
|
404
|
-
sys.stdout.write("%s - - [%s] %s\n" %
|
|
405
|
-
(self.address_string(),
|
|
406
|
-
self.log_date_time_string(),
|
|
407
|
-
message.translate(self._control_char_table)))
|
|
408
|
-
|
|
409
|
-
# Re-implementation of BaseHTTPRequestHandler's handle_one_request method
|
|
410
|
-
# to send the end message after the response is fully sent.
|
|
411
|
-
def handle_one_request(self):
|
|
412
|
-
self.raw_requestline = self.rfile.readline(65537)
|
|
413
|
-
if not self.raw_requestline:
|
|
414
|
-
self.close_connection = True
|
|
415
|
-
return
|
|
416
|
-
if not self.parse_request():
|
|
417
|
-
return
|
|
418
|
-
|
|
419
|
-
if self.path == '/_vercel/ping':
|
|
420
|
-
self.send_response(200)
|
|
421
|
-
self.end_headers()
|
|
422
|
-
return
|
|
423
|
-
|
|
424
|
-
invocationId = self.headers.get('x-vercel-internal-invocation-id')
|
|
425
|
-
requestId = int(self.headers.get('x-vercel-internal-request-id'))
|
|
426
|
-
del self.headers['x-vercel-internal-invocation-id']
|
|
427
|
-
del self.headers['x-vercel-internal-request-id']
|
|
428
|
-
del self.headers['x-vercel-internal-span-id']
|
|
429
|
-
del self.headers['x-vercel-internal-trace-id']
|
|
430
|
-
|
|
431
|
-
send_message({
|
|
432
|
-
"type": "handler-started",
|
|
433
|
-
"payload": {
|
|
434
|
-
"handlerStartedAt": int(time.time() * 1000),
|
|
435
|
-
"context": {
|
|
436
|
-
"invocationId": invocationId,
|
|
437
|
-
"requestId": requestId,
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
})
|
|
441
|
-
|
|
442
|
-
token = storage.set({
|
|
443
|
-
"invocationId": invocationId,
|
|
444
|
-
"requestId": requestId,
|
|
445
|
-
})
|
|
446
|
-
|
|
447
|
-
try:
|
|
448
|
-
self.handle_request()
|
|
449
|
-
finally:
|
|
450
|
-
storage.reset(token)
|
|
451
|
-
send_message({
|
|
452
|
-
"type": "end",
|
|
453
|
-
"payload": {
|
|
454
|
-
"context": {
|
|
455
|
-
"invocationId": invocationId,
|
|
456
|
-
"requestId": requestId,
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
})
|
|
460
|
-
|
|
461
|
-
if 'handler' in __vc_variables or 'Handler' in __vc_variables:
|
|
462
|
-
base = __vc_module.handler if ('handler' in __vc_variables) else __vc_module.Handler
|
|
463
|
-
if not issubclass(base, BaseHTTPRequestHandler):
|
|
464
|
-
_stderr('Handler must inherit from BaseHTTPRequestHandler')
|
|
465
|
-
_stderr('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
|
|
466
|
-
exit(1)
|
|
467
|
-
|
|
468
|
-
class Handler(BaseHandler, base):
|
|
469
|
-
def handle_request(self):
|
|
470
|
-
mname = 'do_' + self.command
|
|
471
|
-
if not hasattr(self, mname):
|
|
472
|
-
self.send_error(
|
|
473
|
-
http.HTTPStatus.NOT_IMPLEMENTED,
|
|
474
|
-
"Unsupported method (%r)" % self.command)
|
|
475
|
-
return
|
|
476
|
-
method = getattr(self, mname)
|
|
477
|
-
method()
|
|
478
|
-
self.wfile.flush()
|
|
479
|
-
elif 'app' in __vc_variables:
|
|
480
|
-
if (
|
|
481
|
-
not inspect.iscoroutinefunction(__vc_module.app) and
|
|
482
|
-
not inspect.iscoroutinefunction(__vc_module.app.__call__)
|
|
483
|
-
):
|
|
484
|
-
from io import BytesIO
|
|
485
|
-
|
|
486
|
-
string_types = (str,)
|
|
487
|
-
app = __vc_module.app
|
|
488
|
-
|
|
489
|
-
def wsgi_encoding_dance(s, charset="utf-8", errors="replace"):
|
|
490
|
-
if isinstance(s, str):
|
|
491
|
-
s = s.encode(charset)
|
|
492
|
-
return s.decode("latin1", errors)
|
|
493
|
-
|
|
494
|
-
class Handler(BaseHandler):
|
|
495
|
-
def handle_request(self):
|
|
496
|
-
# Prepare WSGI environment
|
|
497
|
-
if '?' in self.path:
|
|
498
|
-
path, query = self.path.split('?', 1)
|
|
499
|
-
else:
|
|
500
|
-
path, query = self.path, ''
|
|
501
|
-
content_length = int(self.headers.get('Content-Length', 0))
|
|
502
|
-
env = {
|
|
503
|
-
'CONTENT_LENGTH': str(content_length),
|
|
504
|
-
'CONTENT_TYPE': self.headers.get('content-type', ''),
|
|
505
|
-
'PATH_INFO': path,
|
|
506
|
-
'QUERY_STRING': query,
|
|
507
|
-
'REMOTE_ADDR': self.headers.get(
|
|
508
|
-
'x-forwarded-for', self.headers.get(
|
|
509
|
-
'x-real-ip')),
|
|
510
|
-
'REQUEST_METHOD': self.command,
|
|
511
|
-
'SERVER_NAME': self.headers.get('host', 'lambda'),
|
|
512
|
-
'SERVER_PORT': self.headers.get('x-forwarded-port', '80'),
|
|
513
|
-
'SERVER_PROTOCOL': 'HTTP/1.1',
|
|
514
|
-
'wsgi.errors': sys.stderr,
|
|
515
|
-
'wsgi.input': BytesIO(self.rfile.read(content_length)),
|
|
516
|
-
'wsgi.multiprocess': False,
|
|
517
|
-
'wsgi.multithread': False,
|
|
518
|
-
'wsgi.run_once': False,
|
|
519
|
-
'wsgi.url_scheme': self.headers.get('x-forwarded-proto', 'http'),
|
|
520
|
-
'wsgi.version': (1, 0),
|
|
521
|
-
}
|
|
522
|
-
for key, value in env.items():
|
|
523
|
-
if isinstance(value, string_types):
|
|
524
|
-
env[key] = wsgi_encoding_dance(value)
|
|
525
|
-
for k, v in self.headers.items():
|
|
526
|
-
env['HTTP_' + k.replace('-', '_').upper()] = v
|
|
527
|
-
|
|
528
|
-
def start_response(status, headers, exc_info=None):
|
|
529
|
-
self.send_response(int(status.split(' ')[0]))
|
|
530
|
-
for name, value in headers:
|
|
531
|
-
self.send_header(name, value)
|
|
532
|
-
self.end_headers()
|
|
533
|
-
return self.wfile.write
|
|
534
|
-
|
|
535
|
-
# Call the application
|
|
536
|
-
response = app(env, start_response)
|
|
537
|
-
try:
|
|
538
|
-
for data in response:
|
|
539
|
-
if data:
|
|
540
|
-
self.wfile.write(data)
|
|
541
|
-
self.wfile.flush()
|
|
542
|
-
finally:
|
|
543
|
-
if hasattr(response, 'close'):
|
|
544
|
-
response.close()
|
|
545
|
-
else:
|
|
546
|
-
# ASGI: Run with Uvicorn so we get proper lifespan and protocol handling
|
|
547
|
-
try:
|
|
548
|
-
import uvicorn
|
|
549
|
-
except Exception:
|
|
550
|
-
_stderr('Uvicorn is required to run ASGI apps. Please ensure it is installed.')
|
|
551
|
-
exit(1)
|
|
552
|
-
|
|
553
|
-
# Prefer a callable app.asgi when available; some frameworks expose a boolean here
|
|
554
|
-
user_app_candidate = getattr(__vc_module.app, 'asgi', None)
|
|
555
|
-
user_app = user_app_candidate if callable(user_app_candidate) else __vc_module.app
|
|
556
|
-
asgi_app = ASGIMiddleware(user_app)
|
|
557
|
-
|
|
558
|
-
# Pre-bind a socket to obtain an ephemeral port for IPC announcement
|
|
559
|
-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
560
|
-
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
561
|
-
sock.bind(('127.0.0.1', 0))
|
|
562
|
-
sock.listen(2048)
|
|
563
|
-
http_port = sock.getsockname()[1]
|
|
564
|
-
|
|
565
|
-
config = uvicorn.Config(
|
|
566
|
-
app=asgi_app,
|
|
567
|
-
fd=sock.fileno(),
|
|
568
|
-
lifespan='auto',
|
|
569
|
-
access_log=False,
|
|
570
|
-
log_config=None,
|
|
571
|
-
log_level='warning',
|
|
572
|
-
)
|
|
573
|
-
server = uvicorn.Server(config)
|
|
574
|
-
|
|
575
|
-
send_message({
|
|
576
|
-
"type": "server-started",
|
|
577
|
-
"payload": {
|
|
578
|
-
"initDuration": int((time.time() - start_time) * 1000),
|
|
579
|
-
"httpPort": http_port,
|
|
580
|
-
}
|
|
581
|
-
})
|
|
582
|
-
|
|
583
|
-
# Mark IPC as ready and flush any buffered init logs
|
|
584
|
-
_ipc_ready = True
|
|
585
|
-
for m in _init_log_buf:
|
|
586
|
-
send_message(m)
|
|
587
|
-
_init_log_buf.clear()
|
|
588
|
-
|
|
589
|
-
# Run the server (blocking)
|
|
590
|
-
server.run()
|
|
591
|
-
# If the server ever returns, exit
|
|
592
|
-
sys.exit(0)
|
|
593
|
-
|
|
594
|
-
if 'Handler' in locals():
|
|
595
|
-
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
|
|
596
|
-
send_message({
|
|
597
|
-
"type": "server-started",
|
|
598
|
-
"payload": {
|
|
599
|
-
"initDuration": int((time.time() - start_time) * 1000),
|
|
600
|
-
"httpPort": server.server_address[1],
|
|
601
|
-
}
|
|
602
|
-
})
|
|
603
|
-
# Mark IPC as ready and flush any buffered init logs
|
|
604
|
-
_ipc_ready = True
|
|
605
|
-
for m in _init_log_buf:
|
|
606
|
-
send_message(m)
|
|
607
|
-
_init_log_buf.clear()
|
|
608
|
-
server.serve_forever()
|
|
609
|
-
|
|
610
|
-
_stderr('Missing variable `handler` or `app` in file "__VC_HANDLER_ENTRYPOINT".')
|
|
611
|
-
_stderr('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
|
|
612
|
-
exit(1)
|
|
613
|
-
|
|
614
|
-
if 'handler' in __vc_variables or 'Handler' in __vc_variables:
|
|
615
|
-
base = __vc_module.handler if ('handler' in __vc_variables) else __vc_module.Handler
|
|
616
|
-
if not issubclass(base, BaseHTTPRequestHandler):
|
|
617
|
-
print('Handler must inherit from BaseHTTPRequestHandler')
|
|
618
|
-
print('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
|
|
619
|
-
exit(1)
|
|
620
|
-
|
|
621
|
-
print('using HTTP Handler')
|
|
622
|
-
from http.server import HTTPServer
|
|
623
|
-
import http
|
|
624
|
-
import _thread
|
|
625
|
-
|
|
626
|
-
server = HTTPServer(('127.0.0.1', 0), base)
|
|
627
|
-
port = server.server_address[1]
|
|
628
|
-
|
|
629
|
-
def vc_handler(event, context):
|
|
630
|
-
_thread.start_new_thread(server.handle_request, ())
|
|
631
|
-
|
|
632
|
-
payload = json.loads(event['body'])
|
|
633
|
-
path = payload['path']
|
|
634
|
-
headers = payload['headers']
|
|
635
|
-
method = payload['method']
|
|
636
|
-
encoding = payload.get('encoding')
|
|
637
|
-
body = payload.get('body')
|
|
638
|
-
|
|
639
|
-
if (
|
|
640
|
-
(body is not None and len(body) > 0) and
|
|
641
|
-
(encoding is not None and encoding == 'base64')
|
|
642
|
-
):
|
|
643
|
-
body = base64.b64decode(body)
|
|
644
|
-
|
|
645
|
-
request_body = body.encode('utf-8') if isinstance(body, str) else body
|
|
646
|
-
conn = http.client.HTTPConnection('127.0.0.1', port)
|
|
647
|
-
try:
|
|
648
|
-
conn.request(method, path, headers=headers, body=request_body)
|
|
649
|
-
except (http.client.HTTPException, socket.error) as ex:
|
|
650
|
-
print ("Request Error: %s" % ex)
|
|
651
|
-
res = conn.getresponse()
|
|
652
|
-
|
|
653
|
-
return_dict = {
|
|
654
|
-
'statusCode': res.status,
|
|
655
|
-
'headers': format_headers(res.headers),
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
data = res.read()
|
|
659
|
-
|
|
660
|
-
try:
|
|
661
|
-
return_dict['body'] = data.decode('utf-8')
|
|
662
|
-
except UnicodeDecodeError:
|
|
663
|
-
return_dict['body'] = base64.b64encode(data).decode('utf-8')
|
|
664
|
-
return_dict['encoding'] = 'base64'
|
|
665
|
-
|
|
666
|
-
return return_dict
|
|
667
|
-
|
|
668
|
-
elif 'app' in __vc_variables:
|
|
669
|
-
if (
|
|
670
|
-
not inspect.iscoroutinefunction(__vc_module.app) and
|
|
671
|
-
not inspect.iscoroutinefunction(__vc_module.app.__call__)
|
|
672
|
-
):
|
|
673
|
-
print('using Web Server Gateway Interface (WSGI)')
|
|
674
|
-
from io import BytesIO
|
|
675
|
-
from urllib.parse import urlparse
|
|
676
|
-
from werkzeug.datastructures import Headers
|
|
677
|
-
from werkzeug.wrappers import Response
|
|
678
|
-
|
|
679
|
-
string_types = (str,)
|
|
680
|
-
|
|
681
|
-
def to_bytes(x, charset=sys.getdefaultencoding(), errors="strict"):
|
|
682
|
-
if x is None:
|
|
683
|
-
return None
|
|
684
|
-
if isinstance(x, (bytes, bytearray, memoryview)):
|
|
685
|
-
return bytes(x)
|
|
686
|
-
if isinstance(x, str):
|
|
687
|
-
return x.encode(charset, errors)
|
|
688
|
-
raise TypeError("Expected bytes")
|
|
689
|
-
|
|
690
|
-
def wsgi_encoding_dance(s, charset="utf-8", errors="replace"):
|
|
691
|
-
if isinstance(s, str):
|
|
692
|
-
s = s.encode(charset)
|
|
693
|
-
return s.decode("latin1", errors)
|
|
694
|
-
|
|
695
|
-
def vc_handler(event, context):
|
|
696
|
-
payload = json.loads(event['body'])
|
|
697
|
-
|
|
698
|
-
headers = Headers(payload.get('headers', {}))
|
|
699
|
-
|
|
700
|
-
body = payload.get('body', '')
|
|
701
|
-
if body != '':
|
|
702
|
-
if payload.get('encoding') == 'base64':
|
|
703
|
-
body = base64.b64decode(body)
|
|
704
|
-
if isinstance(body, string_types):
|
|
705
|
-
body = to_bytes(body, charset='utf-8')
|
|
706
|
-
|
|
707
|
-
url = urlparse(payload['path'])
|
|
708
|
-
query = url.query
|
|
709
|
-
path = url.path
|
|
710
|
-
|
|
711
|
-
environ = {
|
|
712
|
-
'CONTENT_LENGTH': str(len(body)),
|
|
713
|
-
'CONTENT_TYPE': headers.get('content-type', ''),
|
|
714
|
-
'PATH_INFO': path,
|
|
715
|
-
'QUERY_STRING': query,
|
|
716
|
-
'REMOTE_ADDR': headers.get(
|
|
717
|
-
'x-forwarded-for', headers.get(
|
|
718
|
-
'x-real-ip', payload.get(
|
|
719
|
-
'true-client-ip', ''))),
|
|
720
|
-
'REQUEST_METHOD': payload['method'],
|
|
721
|
-
'SERVER_NAME': headers.get('host', 'lambda'),
|
|
722
|
-
'SERVER_PORT': headers.get('x-forwarded-port', '80'),
|
|
723
|
-
'SERVER_PROTOCOL': 'HTTP/1.1',
|
|
724
|
-
'event': event,
|
|
725
|
-
'context': context,
|
|
726
|
-
'wsgi.errors': sys.stderr,
|
|
727
|
-
'wsgi.input': BytesIO(body),
|
|
728
|
-
'wsgi.multiprocess': False,
|
|
729
|
-
'wsgi.multithread': False,
|
|
730
|
-
'wsgi.run_once': False,
|
|
731
|
-
'wsgi.url_scheme': headers.get('x-forwarded-proto', 'http'),
|
|
732
|
-
'wsgi.version': (1, 0),
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
for key, value in environ.items():
|
|
736
|
-
if isinstance(value, string_types):
|
|
737
|
-
environ[key] = wsgi_encoding_dance(value)
|
|
738
|
-
|
|
739
|
-
for key, value in headers.items():
|
|
740
|
-
key = 'HTTP_' + key.upper().replace('-', '_')
|
|
741
|
-
if key not in ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'):
|
|
742
|
-
environ[key] = value
|
|
743
|
-
|
|
744
|
-
response = Response.from_app(__vc_module.app, environ)
|
|
745
|
-
|
|
746
|
-
return_dict = {
|
|
747
|
-
'statusCode': response.status_code,
|
|
748
|
-
'headers': format_headers(response.headers)
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if response.data:
|
|
752
|
-
return_dict['body'] = base64.b64encode(response.data).decode('utf-8')
|
|
753
|
-
return_dict['encoding'] = 'base64'
|
|
754
|
-
|
|
755
|
-
return return_dict
|
|
756
|
-
else:
|
|
757
|
-
print('using Asynchronous Server Gateway Interface (ASGI)')
|
|
758
|
-
# Originally authored by Jordan Eremieff and included under MIT license:
|
|
759
|
-
# https://github.com/erm/mangum/blob/b4d21c8f5e304a3e17b88bc9fa345106acc50ad7/mangum/__init__.py
|
|
760
|
-
# https://github.com/erm/mangum/blob/b4d21c8f5e304a3e17b88bc9fa345106acc50ad7/LICENSE
|
|
761
|
-
import asyncio
|
|
762
|
-
import enum
|
|
763
|
-
from urllib.parse import urlparse
|
|
764
|
-
from werkzeug.datastructures import Headers
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
class ASGICycleState(enum.Enum):
|
|
768
|
-
REQUEST = enum.auto()
|
|
769
|
-
RESPONSE = enum.auto()
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
class ASGICycle:
|
|
773
|
-
def __init__(self, scope):
|
|
774
|
-
self.scope = scope
|
|
775
|
-
self.body = b''
|
|
776
|
-
self.state = ASGICycleState.REQUEST
|
|
777
|
-
self.app_queue = None
|
|
778
|
-
self.response = {}
|
|
779
|
-
|
|
780
|
-
def __call__(self, app, body):
|
|
781
|
-
"""
|
|
782
|
-
Receives the application and any body included in the request, then builds the
|
|
783
|
-
ASGI instance using the connection scope.
|
|
784
|
-
Runs until the response is completely read from the application.
|
|
785
|
-
"""
|
|
786
|
-
if _use_legacy_asyncio:
|
|
787
|
-
loop = asyncio.new_event_loop()
|
|
788
|
-
self.app_queue = asyncio.Queue(loop=loop)
|
|
789
|
-
else:
|
|
790
|
-
self.app_queue = asyncio.Queue()
|
|
791
|
-
self.put_message({'type': 'http.request', 'body': body, 'more_body': False})
|
|
792
|
-
|
|
793
|
-
asgi_instance = app(self.scope, self.receive, self.send)
|
|
794
|
-
|
|
795
|
-
if _use_legacy_asyncio:
|
|
796
|
-
asgi_task = loop.create_task(asgi_instance)
|
|
797
|
-
loop.run_until_complete(asgi_task)
|
|
798
|
-
else:
|
|
799
|
-
asyncio.run(self.run_asgi_instance(asgi_instance))
|
|
800
|
-
return self.response
|
|
801
|
-
|
|
802
|
-
async def run_asgi_instance(self, asgi_instance):
|
|
803
|
-
await asgi_instance
|
|
804
|
-
|
|
805
|
-
def put_message(self, message):
|
|
806
|
-
self.app_queue.put_nowait(message)
|
|
807
|
-
|
|
808
|
-
async def receive(self):
|
|
809
|
-
"""
|
|
810
|
-
Awaited by the application to receive messages in the queue.
|
|
811
|
-
"""
|
|
812
|
-
message = await self.app_queue.get()
|
|
813
|
-
return message
|
|
814
|
-
|
|
815
|
-
async def send(self, message):
|
|
816
|
-
"""
|
|
817
|
-
Awaited by the application to send messages to the current cycle instance.
|
|
818
|
-
"""
|
|
819
|
-
message_type = message['type']
|
|
820
|
-
|
|
821
|
-
if self.state is ASGICycleState.REQUEST:
|
|
822
|
-
if message_type != 'http.response.start':
|
|
823
|
-
raise RuntimeError(
|
|
824
|
-
f"Expected 'http.response.start', received: {message_type}"
|
|
825
|
-
)
|
|
826
|
-
|
|
827
|
-
status_code = message['status']
|
|
828
|
-
raw_headers = message.get('headers', [])
|
|
829
|
-
|
|
830
|
-
# Headers from werkzeug transform bytes header value
|
|
831
|
-
# from b'value' to "b'value'" so we need to process
|
|
832
|
-
# ASGI headers manually
|
|
833
|
-
decoded_headers = []
|
|
834
|
-
for key, value in raw_headers:
|
|
835
|
-
decoded_key = key.decode() if isinstance(key, bytes) else key
|
|
836
|
-
decoded_value = value.decode() if isinstance(value, bytes) else value
|
|
837
|
-
decoded_headers.append((decoded_key, decoded_value))
|
|
838
|
-
|
|
839
|
-
headers = Headers(decoded_headers)
|
|
840
|
-
|
|
841
|
-
self.on_request(headers, status_code)
|
|
842
|
-
self.state = ASGICycleState.RESPONSE
|
|
843
|
-
|
|
844
|
-
elif self.state is ASGICycleState.RESPONSE:
|
|
845
|
-
if message_type != 'http.response.body':
|
|
846
|
-
raise RuntimeError(
|
|
847
|
-
f"Expected 'http.response.body', received: {message_type}"
|
|
848
|
-
)
|
|
849
|
-
|
|
850
|
-
body = message.get('body', b'')
|
|
851
|
-
more_body = message.get('more_body', False)
|
|
852
|
-
|
|
853
|
-
# The body must be completely read before returning the response.
|
|
854
|
-
self.body += body
|
|
855
|
-
|
|
856
|
-
if not more_body:
|
|
857
|
-
self.on_response()
|
|
858
|
-
self.put_message({'type': 'http.disconnect'})
|
|
859
|
-
|
|
860
|
-
def on_request(self, headers, status_code):
|
|
861
|
-
self.response['statusCode'] = status_code
|
|
862
|
-
self.response['headers'] = format_headers(headers, decode=True)
|
|
863
|
-
|
|
864
|
-
def on_response(self):
|
|
865
|
-
if self.body:
|
|
866
|
-
self.response['body'] = base64.b64encode(self.body).decode('utf-8')
|
|
867
|
-
self.response['encoding'] = 'base64'
|
|
868
|
-
|
|
869
|
-
def vc_handler(event, context):
|
|
870
|
-
payload = json.loads(event['body'])
|
|
871
|
-
|
|
872
|
-
headers = payload.get('headers', {})
|
|
873
|
-
|
|
874
|
-
body = payload.get('body', b'')
|
|
875
|
-
if payload.get('encoding') == 'base64':
|
|
876
|
-
body = base64.b64decode(body)
|
|
877
|
-
elif not isinstance(body, bytes):
|
|
878
|
-
body = body.encode()
|
|
879
|
-
|
|
880
|
-
url = urlparse(payload['path'])
|
|
881
|
-
query = url.query.encode()
|
|
882
|
-
path = url.path
|
|
883
|
-
|
|
884
|
-
headers_encoded = []
|
|
885
|
-
for k, v in headers.items():
|
|
886
|
-
# Cope with repeated headers in the encoding.
|
|
887
|
-
if isinstance(v, list):
|
|
888
|
-
headers_encoded.append([k.lower().encode(), [i.encode() for i in v]])
|
|
889
|
-
else:
|
|
890
|
-
headers_encoded.append([k.lower().encode(), v.encode()])
|
|
891
|
-
|
|
892
|
-
scope = {
|
|
893
|
-
'server': (headers.get('host', 'lambda'), headers.get('x-forwarded-port', 80)),
|
|
894
|
-
'client': (headers.get(
|
|
895
|
-
'x-forwarded-for', headers.get(
|
|
896
|
-
'x-real-ip', payload.get(
|
|
897
|
-
'true-client-ip', ''))), 0),
|
|
898
|
-
'scheme': headers.get('x-forwarded-proto', 'http'),
|
|
899
|
-
'root_path': '',
|
|
900
|
-
'query_string': query,
|
|
901
|
-
'headers': headers_encoded,
|
|
902
|
-
'type': 'http',
|
|
903
|
-
'http_version': '1.1',
|
|
904
|
-
'method': payload['method'],
|
|
905
|
-
'path': path,
|
|
906
|
-
'raw_path': path.encode(),
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
asgi_cycle = ASGICycle(scope)
|
|
910
|
-
response = asgi_cycle(__vc_module.app, body)
|
|
911
|
-
return response
|
|
912
|
-
|
|
913
|
-
else:
|
|
914
|
-
print('Missing variable `handler` or `app` in file "__VC_HANDLER_ENTRYPOINT".')
|
|
915
|
-
print('See the docs: https://vercel.com/docs/functions/serverless-functions/runtimes/python')
|
|
916
|
-
exit(1)
|