@vercel/python 6.0.2 → 6.0.4

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 CHANGED
@@ -2751,7 +2751,7 @@ module.exports = __toCommonJS(src_exports);
2751
2751
  var import_fs5 = __toESM(require("fs"));
2752
2752
  var import_util = require("util");
2753
2753
  var import_path5 = require("path");
2754
- var import_build_utils6 = require("@vercel/build-utils");
2754
+ var import_build_utils7 = require("@vercel/build-utils");
2755
2755
 
2756
2756
  // src/install.ts
2757
2757
  var import_execa = __toESM(require_execa());
@@ -3004,7 +3004,12 @@ async function exportRequirementsFromUv(projectDir, uvPath, options = {}) {
3004
3004
  if (!uvPath) {
3005
3005
  throw new Error("uv is not available to export requirements");
3006
3006
  }
3007
- const args = ["export"];
3007
+ const args = [
3008
+ "export",
3009
+ "--no-default-groups",
3010
+ "--no-emit-workspace",
3011
+ "--no-editable"
3012
+ ];
3008
3013
  if (locked) {
3009
3014
  args.push("--frozen");
3010
3015
  }
@@ -3066,7 +3071,7 @@ async function exportRequirementsFromPipfile({
3066
3071
  }
3067
3072
 
3068
3073
  // src/index.ts
3069
- var import_build_utils7 = require("@vercel/build-utils");
3074
+ var import_build_utils8 = require("@vercel/build-utils");
3070
3075
 
3071
3076
  // src/version.ts
3072
3077
  var import_build_utils2 = require("@vercel/build-utils");
@@ -3199,7 +3204,7 @@ function isInstalled2({ pipPath, pythonPath }) {
3199
3204
  var import_child_process = require("child_process");
3200
3205
  var import_fs4 = require("fs");
3201
3206
  var import_path4 = require("path");
3202
- var import_build_utils5 = require("@vercel/build-utils");
3207
+ var import_build_utils6 = require("@vercel/build-utils");
3203
3208
 
3204
3209
  // src/entrypoint.ts
3205
3210
  var import_fs2 = __toESM(require("fs"));
@@ -3329,6 +3334,8 @@ async function detectPythonEntrypoint(framework, workPath, configuredEntrypoint)
3329
3334
  // src/utils.ts
3330
3335
  var import_fs3 = __toESM(require("fs"));
3331
3336
  var import_path3 = require("path");
3337
+ var import_execa2 = __toESM(require_execa());
3338
+ var import_build_utils5 = require("@vercel/build-utils");
3332
3339
  var isInVirtualEnv = () => {
3333
3340
  return process.env.VIRTUAL_ENV;
3334
3341
  };
@@ -3349,6 +3356,51 @@ function useVirtualEnv(workPath, env, systemPython) {
3349
3356
  }
3350
3357
  return { pythonCmd };
3351
3358
  }
3359
+ async function runPyprojectScript(workPath, scriptNames, env) {
3360
+ const pyprojectPath = (0, import_path3.join)(workPath, "pyproject.toml");
3361
+ if (!import_fs3.default.existsSync(pyprojectPath))
3362
+ return false;
3363
+ let pyproject = null;
3364
+ try {
3365
+ pyproject = await (0, import_build_utils5.readConfigFile)(pyprojectPath);
3366
+ } catch {
3367
+ console.error("Failed to parse pyproject.toml");
3368
+ return false;
3369
+ }
3370
+ const scripts = pyproject?.tool?.vercel?.scripts || {};
3371
+ const candidates = typeof scriptNames === "string" ? [scriptNames] : Array.from(scriptNames);
3372
+ const scriptToRun = candidates.find((name) => Boolean(scripts[name]));
3373
+ if (!scriptToRun)
3374
+ return false;
3375
+ const systemPython = process.platform === "win32" ? "python" : "python3";
3376
+ const finalEnv = { ...process.env, ...env };
3377
+ const { pythonCmd } = useVirtualEnv(workPath, finalEnv, systemPython);
3378
+ const uvPath = await getUvBinaryOrInstall(pythonCmd);
3379
+ const scriptCommand = scripts[scriptToRun];
3380
+ if (typeof scriptCommand === "string" && scriptCommand.trim()) {
3381
+ const uvDir = (0, import_path3.dirname)(uvPath);
3382
+ finalEnv.PATH = `${uvDir}${import_path3.delimiter}${finalEnv.PATH || ""}`;
3383
+ if (/^\s*uv(\s|$)/i.test(scriptCommand)) {
3384
+ console.log(`Executing: ${scriptCommand}`);
3385
+ await (0, import_build_utils5.execCommand)(scriptCommand, {
3386
+ cwd: workPath,
3387
+ env: finalEnv
3388
+ });
3389
+ return true;
3390
+ }
3391
+ const args = process.platform === "win32" ? ["run", "cmd", "/d", "/s", "/c", scriptCommand] : ["run", "sh", "-lc", scriptCommand];
3392
+ console.log(
3393
+ `Executing: ${uvPath} ${args.map((a) => /\s/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a).join(" ")}`
3394
+ );
3395
+ await (0, import_execa2.default)(uvPath, args, {
3396
+ cwd: workPath,
3397
+ stdio: "inherit",
3398
+ env: finalEnv
3399
+ });
3400
+ return true;
3401
+ }
3402
+ return false;
3403
+ }
3352
3404
 
3353
3405
  // src/start-dev-server.ts
3354
3406
  function silenceNodeWarnings() {
@@ -3393,12 +3445,12 @@ function installGlobalCleanupHandlers() {
3393
3445
  try {
3394
3446
  process.kill(info.pid, "SIGTERM");
3395
3447
  } catch (err) {
3396
- (0, import_build_utils5.debug)(`Error sending SIGTERM to ${info.pid}: ${err}`);
3448
+ (0, import_build_utils6.debug)(`Error sending SIGTERM to ${info.pid}: ${err}`);
3397
3449
  }
3398
3450
  try {
3399
3451
  process.kill(info.pid, "SIGKILL");
3400
3452
  } catch (err) {
3401
- (0, import_build_utils5.debug)(`Error sending SIGKILL to ${info.pid}: ${err}`);
3453
+ (0, import_build_utils6.debug)(`Error sending SIGKILL to ${info.pid}: ${err}`);
3402
3454
  }
3403
3455
  PERSISTENT_SERVERS.delete(key);
3404
3456
  }
@@ -3406,7 +3458,7 @@ function installGlobalCleanupHandlers() {
3406
3458
  try {
3407
3459
  restoreWarnings();
3408
3460
  } catch (err) {
3409
- (0, import_build_utils5.debug)(`Error restoring warnings: ${err}`);
3461
+ (0, import_build_utils6.debug)(`Error restoring warnings: ${err}`);
3410
3462
  }
3411
3463
  restoreWarnings = null;
3412
3464
  }
@@ -3432,10 +3484,10 @@ function createDevAsgiShim(workPath, modulePath) {
3432
3484
  const template = (0, import_fs4.readFileSync)(templatePath, "utf8");
3433
3485
  const shimSource = template.replace(/__VC_DEV_MODULE_PATH__/g, modulePath);
3434
3486
  (0, import_fs4.writeFileSync)(shimPath, shimSource, "utf8");
3435
- (0, import_build_utils5.debug)(`Prepared Python dev static shim at ${shimPath}`);
3487
+ (0, import_build_utils6.debug)(`Prepared Python dev static shim at ${shimPath}`);
3436
3488
  return ASGI_SHIM_MODULE;
3437
3489
  } catch (err) {
3438
- (0, import_build_utils5.debug)(`Failed to prepare dev static shim: ${err?.message || err}`);
3490
+ (0, import_build_utils6.debug)(`Failed to prepare dev static shim: ${err?.message || err}`);
3439
3491
  return null;
3440
3492
  }
3441
3493
  }
@@ -3448,10 +3500,10 @@ function createDevWsgiShim(workPath, modulePath) {
3448
3500
  const template = (0, import_fs4.readFileSync)(templatePath, "utf8");
3449
3501
  const shimSource = template.replace(/__VC_DEV_MODULE_PATH__/g, modulePath);
3450
3502
  (0, import_fs4.writeFileSync)(shimPath, shimSource, "utf8");
3451
- (0, import_build_utils5.debug)(`Prepared Python dev WSGI shim at ${shimPath}`);
3503
+ (0, import_build_utils6.debug)(`Prepared Python dev WSGI shim at ${shimPath}`);
3452
3504
  return WSGI_SHIM_MODULE;
3453
3505
  } catch (err) {
3454
- (0, import_build_utils5.debug)(`Failed to prepare dev WSGI shim: ${err?.message || err}`);
3506
+ (0, import_build_utils6.debug)(`Failed to prepare dev WSGI shim: ${err?.message || err}`);
3455
3507
  return null;
3456
3508
  }
3457
3509
  }
@@ -3471,7 +3523,7 @@ var startDevServer = async (opts) => {
3471
3523
  );
3472
3524
  if (!entry) {
3473
3525
  const searched = framework === "fastapi" ? FASTAPI_CANDIDATE_ENTRYPOINTS.join(", ") : FLASK_CANDIDATE_ENTRYPOINTS.join(", ");
3474
- throw new import_build_utils5.NowBuildError({
3526
+ throw new import_build_utils6.NowBuildError({
3475
3527
  code: "PYTHON_ENTRYPOINT_NOT_FOUND",
3476
3528
  message: `No ${framework} entrypoint found. Define a valid application entrypoint in one of the following locations: ${searched} or add an 'app' script in pyproject.toml.`,
3477
3529
  link: `https://vercel.com/docs/frameworks/backend/${framework?.toLowerCase()}#exporting-the-${framework?.toLowerCase()}-application`,
@@ -3521,7 +3573,7 @@ var startDevServer = async (opts) => {
3521
3573
  let pythonCmd = systemPython;
3522
3574
  const venv = isInVirtualEnv();
3523
3575
  if (venv) {
3524
- (0, import_build_utils5.debug)(`Running in virtualenv at ${venv}`);
3576
+ (0, import_build_utils6.debug)(`Running in virtualenv at ${venv}`);
3525
3577
  } else {
3526
3578
  const { pythonCmd: venvPythonCmd, venvRoot } = useVirtualEnv(
3527
3579
  workPath,
@@ -3530,9 +3582,9 @@ var startDevServer = async (opts) => {
3530
3582
  );
3531
3583
  pythonCmd = venvPythonCmd;
3532
3584
  if (venvRoot) {
3533
- (0, import_build_utils5.debug)(`Using virtualenv at ${venvRoot}`);
3585
+ (0, import_build_utils6.debug)(`Using virtualenv at ${venvRoot}`);
3534
3586
  } else {
3535
- (0, import_build_utils5.debug)("No virtualenv found");
3587
+ (0, import_build_utils6.debug)("No virtualenv found");
3536
3588
  try {
3537
3589
  const yellow = "\x1B[33m";
3538
3590
  const reset = "\x1B[0m";
@@ -3555,7 +3607,7 @@ If you are using a virtual environment, activate it before running "vercel dev",
3555
3607
  }
3556
3608
  const moduleToRun = devShimModule || modulePath;
3557
3609
  const argv = ["-u", "-m", moduleToRun];
3558
- (0, import_build_utils5.debug)(`Starting ASGI dev server: ${pythonCmd} ${argv.join(" ")}`);
3610
+ (0, import_build_utils6.debug)(`Starting ASGI dev server: ${pythonCmd} ${argv.join(" ")}`);
3559
3611
  const child = (0, import_child_process.spawn)(pythonCmd, argv, {
3560
3612
  cwd: workPath,
3561
3613
  env,
@@ -3633,7 +3685,7 @@ If you are using a virtual environment, activate it before running "vercel dev",
3633
3685
  }
3634
3686
  const moduleToRun = devShimModule || modulePath;
3635
3687
  const argv = ["-u", "-m", moduleToRun];
3636
- (0, import_build_utils5.debug)(`Starting Flask dev server: ${pythonCmd} ${argv.join(" ")}`);
3688
+ (0, import_build_utils6.debug)(`Starting Flask dev server: ${pythonCmd} ${argv.join(" ")}`);
3637
3689
  const child = (0, import_child_process.spawn)(pythonCmd, argv, {
3638
3690
  cwd: workPath,
3639
3691
  env,
@@ -3743,13 +3795,13 @@ async function downloadFilesInWorkPath({
3743
3795
  files,
3744
3796
  meta = {}
3745
3797
  }) {
3746
- (0, import_build_utils6.debug)("Downloading user files...");
3747
- let downloadedFiles = await (0, import_build_utils6.download)(files, workPath, meta);
3798
+ (0, import_build_utils7.debug)("Downloading user files...");
3799
+ let downloadedFiles = await (0, import_build_utils7.download)(files, workPath, meta);
3748
3800
  if (meta.isDev) {
3749
3801
  const { devCacheDir = (0, import_path5.join)(workPath, ".now", "cache") } = meta;
3750
3802
  const destCache = (0, import_path5.join)(devCacheDir, (0, import_path5.basename)(entrypoint, ".py"));
3751
- await (0, import_build_utils6.download)(downloadedFiles, destCache);
3752
- downloadedFiles = await (0, import_build_utils6.glob)("**", destCache);
3803
+ await (0, import_build_utils7.download)(downloadedFiles, destCache);
3804
+ downloadedFiles = await (0, import_build_utils7.glob)("**", destCache);
3753
3805
  workPath = destCache;
3754
3806
  }
3755
3807
  return workPath;
@@ -3777,7 +3829,50 @@ var build = async ({
3777
3829
  console.log('Failed to create "setup.cfg" file');
3778
3830
  throw err;
3779
3831
  }
3780
- let fsFiles = await (0, import_build_utils6.glob)("**", workPath);
3832
+ if (framework === "fastapi" || framework === "flask") {
3833
+ const {
3834
+ cliType,
3835
+ lockfileVersion,
3836
+ packageJsonPackageManager,
3837
+ turboSupportsCorepackHome
3838
+ } = await (0, import_build_utils7.scanParentDirs)(workPath, true);
3839
+ const spawnEnv = (0, import_build_utils7.getEnvForPackageManager)({
3840
+ cliType,
3841
+ lockfileVersion,
3842
+ packageJsonPackageManager,
3843
+ env: process.env,
3844
+ turboSupportsCorepackHome,
3845
+ projectCreatedAt: config?.projectSettings?.createdAt
3846
+ });
3847
+ const installCommand = config?.projectSettings?.installCommand;
3848
+ if (typeof installCommand === "string") {
3849
+ if (installCommand.trim()) {
3850
+ console.log(`Running "install" command: \`${installCommand}\`...`);
3851
+ await (0, import_build_utils7.execCommand)(installCommand, {
3852
+ env: spawnEnv,
3853
+ cwd: workPath
3854
+ });
3855
+ } else {
3856
+ console.log('Skipping "install" command...');
3857
+ }
3858
+ }
3859
+ const projectBuildCommand = config?.projectSettings?.buildCommand ?? // fallback if provided directly on config (some callers set this)
3860
+ config?.buildCommand;
3861
+ if (projectBuildCommand) {
3862
+ console.log(`Running "${projectBuildCommand}"`);
3863
+ await (0, import_build_utils7.execCommand)(projectBuildCommand, {
3864
+ env: spawnEnv,
3865
+ cwd: workPath
3866
+ });
3867
+ } else {
3868
+ await runPyprojectScript(
3869
+ workPath,
3870
+ ["vercel-build", "now-build", "build"],
3871
+ spawnEnv
3872
+ );
3873
+ }
3874
+ }
3875
+ let fsFiles = await (0, import_build_utils7.glob)("**", workPath);
3781
3876
  if ((framework === "fastapi" || framework === "flask") && (!fsFiles[entrypoint] || !entrypoint.endsWith(".py"))) {
3782
3877
  const detected = await detectPythonEntrypoint(
3783
3878
  config.framework,
@@ -3785,13 +3880,13 @@ var build = async ({
3785
3880
  entrypoint
3786
3881
  );
3787
3882
  if (detected) {
3788
- (0, import_build_utils6.debug)(
3883
+ (0, import_build_utils7.debug)(
3789
3884
  `Resolved Python entrypoint to "${detected}" (configured "${entrypoint}" not found).`
3790
3885
  );
3791
3886
  entrypoint = detected;
3792
3887
  } else {
3793
3888
  const searchedList = framework === "fastapi" ? FASTAPI_CANDIDATE_ENTRYPOINTS.join(", ") : FLASK_CANDIDATE_ENTRYPOINTS.join(", ");
3794
- throw new import_build_utils6.NowBuildError({
3889
+ throw new import_build_utils7.NowBuildError({
3795
3890
  code: `${framework.toUpperCase()}_ENTRYPOINT_NOT_FOUND`,
3796
3891
  message: `No ${framework} entrypoint found. Define a valid application entrypoint in one of the following locations: ${searchedList} or add an 'app' script in pyproject.toml.`,
3797
3892
  link: `https://vercel.com/docs/frameworks/backend/${framework}#exporting-the-${framework}-application`,
@@ -3820,20 +3915,20 @@ var build = async ({
3820
3915
  if (pyprojectDir) {
3821
3916
  let requiresPython;
3822
3917
  try {
3823
- const pyproject = await (0, import_build_utils7.readConfigFile)((0, import_path5.join)(pyprojectDir, "pyproject.toml"));
3918
+ const pyproject = await (0, import_build_utils8.readConfigFile)((0, import_path5.join)(pyprojectDir, "pyproject.toml"));
3824
3919
  requiresPython = pyproject?.project?.["requires-python"];
3825
3920
  } catch (err) {
3826
- (0, import_build_utils6.debug)("Failed to parse pyproject.toml", err);
3921
+ (0, import_build_utils7.debug)("Failed to parse pyproject.toml", err);
3827
3922
  }
3828
3923
  const VERSION_REGEX = /\b\d+\.\d+\b/;
3829
3924
  const exact = requiresPython?.trim().match(VERSION_REGEX)?.[0];
3830
3925
  if (exact) {
3831
3926
  declaredPythonVersion = { version: exact, source: "pyproject.toml" };
3832
- (0, import_build_utils6.debug)(
3927
+ (0, import_build_utils7.debug)(
3833
3928
  `Found Python version ${exact} in pyproject.toml (requires-python: "${requiresPython}")`
3834
3929
  );
3835
3930
  } else if (requiresPython) {
3836
- (0, import_build_utils6.debug)(
3931
+ (0, import_build_utils7.debug)(
3837
3932
  `Could not parse Python version from pyproject.toml requires-python: "${requiresPython}"`
3838
3933
  );
3839
3934
  }
@@ -3843,7 +3938,7 @@ var build = async ({
3843
3938
  const json = await readFile((0, import_path5.join)(pipfileLockDir, "Pipfile.lock"), "utf8");
3844
3939
  lock = JSON.parse(json);
3845
3940
  } catch (err) {
3846
- throw new import_build_utils6.NowBuildError({
3941
+ throw new import_build_utils7.NowBuildError({
3847
3942
  code: "INVALID_PIPFILE_LOCK",
3848
3943
  message: "Unable to parse Pipfile.lock"
3849
3944
  });
@@ -3851,14 +3946,14 @@ var build = async ({
3851
3946
  const pyFromLock = lock?._meta?.requires?.python_version;
3852
3947
  if (pyFromLock) {
3853
3948
  declaredPythonVersion = { version: pyFromLock, source: "Pipfile.lock" };
3854
- (0, import_build_utils6.debug)(`Found Python version ${pyFromLock} in Pipfile.lock`);
3949
+ (0, import_build_utils7.debug)(`Found Python version ${pyFromLock} in Pipfile.lock`);
3855
3950
  }
3856
3951
  }
3857
3952
  const pythonVersion = getSupportedPythonVersion({
3858
3953
  isDev: meta.isDev,
3859
3954
  declaredPythonVersion
3860
3955
  });
3861
- fsFiles = await (0, import_build_utils6.glob)("**", workPath);
3956
+ fsFiles = await (0, import_build_utils7.glob)("**", workPath);
3862
3957
  const requirementsTxt = (0, import_path5.join)(entryDirectory, "requirements.txt");
3863
3958
  const vendorBaseDir = (0, import_path5.join)(
3864
3959
  workPath,
@@ -3903,7 +3998,7 @@ var build = async ({
3903
3998
  `uv is required for this project but failed to install: ${err instanceof Error ? err.message : String(err)}`
3904
3999
  );
3905
4000
  }
3906
- (0, import_build_utils6.debug)("Failed to install uv", err);
4001
+ (0, import_build_utils7.debug)("Failed to install uv", err);
3907
4002
  }
3908
4003
  await installRequirement({
3909
4004
  pythonPath: pythonVersion.pythonPath,
@@ -3915,9 +4010,21 @@ var build = async ({
3915
4010
  targetDir: vendorBaseDir,
3916
4011
  meta
3917
4012
  });
4013
+ if (framework !== "flask") {
4014
+ await installRequirement({
4015
+ pythonPath: pythonVersion.pythonPath,
4016
+ pipPath: pythonVersion.pipPath,
4017
+ uvPath,
4018
+ dependency: "uvicorn",
4019
+ version: "0.38.0",
4020
+ workPath,
4021
+ targetDir: vendorBaseDir,
4022
+ meta
4023
+ });
4024
+ }
3918
4025
  let installedFromProjectFiles = false;
3919
4026
  if (uvLockDir) {
3920
- (0, import_build_utils6.debug)('Found "uv.lock"');
4027
+ (0, import_build_utils7.debug)('Found "uv.lock"');
3921
4028
  if (pyprojectDir) {
3922
4029
  const exportedReq = await exportRequirementsFromUv(pyprojectDir, uvPath, {
3923
4030
  locked: true
@@ -3933,10 +4040,10 @@ var build = async ({
3933
4040
  });
3934
4041
  installedFromProjectFiles = true;
3935
4042
  } else {
3936
- (0, import_build_utils6.debug)('Skipping uv export because "pyproject.toml" was not found');
4043
+ (0, import_build_utils7.debug)('Skipping uv export because "pyproject.toml" was not found');
3937
4044
  }
3938
4045
  } else if (pyprojectDir) {
3939
- (0, import_build_utils6.debug)('Found "pyproject.toml"');
4046
+ (0, import_build_utils7.debug)('Found "pyproject.toml"');
3940
4047
  if (hasReqLocal || hasReqGlobal) {
3941
4048
  console.log(
3942
4049
  "Detected both pyproject.toml and requirements.txt but no lockfile; using pyproject.toml"
@@ -3956,9 +4063,9 @@ var build = async ({
3956
4063
  });
3957
4064
  installedFromProjectFiles = true;
3958
4065
  } else if (pipfileLockDir || pipfileDir) {
3959
- (0, import_build_utils6.debug)(`Found ${pipfileLockDir ? '"Pipfile.lock"' : '"Pipfile"'}`);
4066
+ (0, import_build_utils7.debug)(`Found ${pipfileLockDir ? '"Pipfile.lock"' : '"Pipfile"'}`);
3960
4067
  if (hasReqLocal || hasReqGlobal) {
3961
- (0, import_build_utils6.debug)('Skipping Pipfile export because "requirements.txt" exists');
4068
+ (0, import_build_utils7.debug)('Skipping Pipfile export because "requirements.txt" exists');
3962
4069
  } else {
3963
4070
  const exportedReq = await exportRequirementsFromPipfile({
3964
4071
  pythonPath: pythonVersion.pythonPath,
@@ -3980,7 +4087,7 @@ var build = async ({
3980
4087
  }
3981
4088
  }
3982
4089
  if (!installedFromProjectFiles && fsFiles[requirementsTxt]) {
3983
- (0, import_build_utils6.debug)('Found local "requirements.txt"');
4090
+ (0, import_build_utils7.debug)('Found local "requirements.txt"');
3984
4091
  const requirementsTxtPath = fsFiles[requirementsTxt].fsPath;
3985
4092
  await installRequirementsFile({
3986
4093
  pythonPath: pythonVersion.pythonPath,
@@ -3992,7 +4099,7 @@ var build = async ({
3992
4099
  meta
3993
4100
  });
3994
4101
  } else if (!installedFromProjectFiles && fsFiles["requirements.txt"]) {
3995
- (0, import_build_utils6.debug)('Found global "requirements.txt"');
4102
+ (0, import_build_utils7.debug)('Found global "requirements.txt"');
3996
4103
  const requirementsTxtPath = fsFiles["requirements.txt"].fsPath;
3997
4104
  await installRequirementsFile({
3998
4105
  pythonPath: pythonVersion.pythonPath,
@@ -4006,12 +4113,12 @@ var build = async ({
4006
4113
  }
4007
4114
  const originalPyPath = (0, import_path5.join)(__dirname, "..", "vc_init.py");
4008
4115
  const originalHandlerPyContents = await readFile(originalPyPath, "utf8");
4009
- (0, import_build_utils6.debug)("Entrypoint is", entrypoint);
4116
+ (0, import_build_utils7.debug)("Entrypoint is", entrypoint);
4010
4117
  const moduleName = entrypoint.replace(/\//g, ".").replace(/\.py$/i, "");
4011
4118
  const vendorDir = resolveVendorDir();
4012
4119
  const suffix = meta.isDev && !entrypoint.endsWith(".py") ? ".py" : "";
4013
4120
  const entrypointWithSuffix = `${entrypoint}${suffix}`;
4014
- (0, import_build_utils6.debug)("Entrypoint with suffix is", entrypointWithSuffix);
4121
+ (0, import_build_utils7.debug)("Entrypoint with suffix is", entrypointWithSuffix);
4015
4122
  const handlerPyContents = originalHandlerPyContents.replace(/__VC_HANDLER_MODULE_NAME/g, moduleName).replace(/__VC_HANDLER_ENTRYPOINT/g, entrypointWithSuffix).replace(/__VC_HANDLER_VENDOR_DIR/g, vendorDir);
4016
4123
  const predefinedExcludes = [
4017
4124
  ".git/**",
@@ -4026,7 +4133,10 @@ var build = async ({
4026
4133
  "**/__pycache__/**",
4027
4134
  "**/.mypy_cache/**",
4028
4135
  "**/.ruff_cache/**",
4029
- "**/public/**"
4136
+ "**/public/**",
4137
+ "**/pnpm-lock.yaml",
4138
+ "**/yarn.lock",
4139
+ "**/package-lock.json"
4030
4140
  ];
4031
4141
  const lambdaEnv = {};
4032
4142
  lambdaEnv.PYTHONPATH = vendorDir;
@@ -4034,11 +4144,11 @@ var build = async ({
4034
4144
  cwd: workPath,
4035
4145
  ignore: config && typeof config.excludeFiles === "string" ? [...predefinedExcludes, config.excludeFiles] : predefinedExcludes
4036
4146
  };
4037
- const files = await (0, import_build_utils6.glob)("**", globOptions);
4147
+ const files = await (0, import_build_utils7.glob)("**", globOptions);
4038
4148
  try {
4039
4149
  const cachedVendorAbs = (0, import_path5.join)(vendorBaseDir, resolveVendorDir());
4040
4150
  if (import_fs5.default.existsSync(cachedVendorAbs)) {
4041
- const vendorFiles = await (0, import_build_utils6.glob)("**", cachedVendorAbs, resolveVendorDir());
4151
+ const vendorFiles = await (0, import_build_utils7.glob)("**", cachedVendorAbs, resolveVendorDir());
4042
4152
  for (const [p, f] of Object.entries(vendorFiles)) {
4043
4153
  files[p] = f;
4044
4154
  }
@@ -4048,12 +4158,12 @@ var build = async ({
4048
4158
  throw err;
4049
4159
  }
4050
4160
  const handlerPyFilename = "vc__handler__python";
4051
- files[`${handlerPyFilename}.py`] = new import_build_utils6.FileBlob({ data: handlerPyContents });
4161
+ files[`${handlerPyFilename}.py`] = new import_build_utils7.FileBlob({ data: handlerPyContents });
4052
4162
  if (config.framework === "fasthtml") {
4053
4163
  const { SESSKEY = "" } = process.env;
4054
- files[".sesskey"] = new import_build_utils6.FileBlob({ data: `"${SESSKEY}"` });
4164
+ files[".sesskey"] = new import_build_utils7.FileBlob({ data: `"${SESSKEY}"` });
4055
4165
  }
4056
- const output = new import_build_utils6.Lambda({
4166
+ const output = new import_build_utils7.Lambda({
4057
4167
  files,
4058
4168
  handler: `${handlerPyFilename}.vc_handler`,
4059
4169
  runtime: pythonVersion.runtime,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/python",
3
- "version": "6.0.2",
3
+ "version": "6.0.4",
4
4
  "main": "./dist/index.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
@@ -21,7 +21,7 @@
21
21
  "@types/jest": "27.4.1",
22
22
  "@types/node": "14.18.33",
23
23
  "@types/which": "3.0.0",
24
- "@vercel/build-utils": "13.0.0",
24
+ "@vercel/build-utils": "13.0.1",
25
25
  "cross-env": "7.0.3",
26
26
  "execa": "^1.0.0",
27
27
  "fs-extra": "11.1.1",
package/vc_init.py CHANGED
@@ -210,12 +210,17 @@ if 'VERCEL_IPC_PATH' in os.environ:
210
210
 
211
211
 
212
212
  # Import relative path https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
213
- user_mod_path = os.path.join(_here, "__VC_HANDLER_ENTRYPOINT") # absolute
214
- __vc_spec = util.spec_from_file_location("__VC_HANDLER_MODULE_NAME", user_mod_path)
215
- __vc_module = util.module_from_spec(__vc_spec)
216
- sys.modules["__VC_HANDLER_MODULE_NAME"] = __vc_module
217
- __vc_spec.loader.exec_module(__vc_module)
218
- __vc_variables = dir(__vc_module)
213
+ try:
214
+ user_mod_path = os.path.join(_here, "__VC_HANDLER_ENTRYPOINT") # absolute
215
+ __vc_spec = util.spec_from_file_location("__VC_HANDLER_MODULE_NAME", user_mod_path)
216
+ __vc_module = util.module_from_spec(__vc_spec)
217
+ sys.modules["__VC_HANDLER_MODULE_NAME"] = __vc_module
218
+ __vc_spec.loader.exec_module(__vc_module)
219
+ __vc_variables = dir(__vc_module)
220
+ except Exception:
221
+ _stderr(f'Error importing __VC_HANDLER_ENTRYPOINT:')
222
+ _stderr(traceback.format_exc())
223
+ exit(1)
219
224
 
220
225
  _use_legacy_asyncio = sys.version_info < (3, 10)
221
226
 
@@ -231,6 +236,95 @@ def format_headers(headers, decode=False):
231
236
  return keyToList
232
237
 
233
238
 
239
+ class ASGIMiddleware:
240
+ """
241
+ ASGI middleware that preserves Vercel IPC semantics for request lifecycle:
242
+ - Handles /_vercel/ping
243
+ - Extracts x-vercel-internal-* headers and removes them from downstream app
244
+ - Sets request context into `storage` for logging/metrics
245
+ - Emits handler-started and end IPC messages
246
+ """
247
+ def __init__(self, app):
248
+ self.app = app
249
+
250
+ async def __call__(self, scope, receive, send):
251
+ if scope.get('type') != 'http':
252
+ # Non-HTTP traffic is forwarded verbatim
253
+ await self.app(scope, receive, send)
254
+ return
255
+
256
+ if scope.get('path') == '/_vercel/ping':
257
+ await send({
258
+ 'type': 'http.response.start',
259
+ 'status': 200,
260
+ 'headers': [],
261
+ })
262
+ await send({
263
+ 'type': 'http.response.body',
264
+ 'body': b'',
265
+ 'more_body': False,
266
+ })
267
+ return
268
+
269
+ # Extract internal headers and set per-request context
270
+ headers_list = scope.get('headers', []) or []
271
+ new_headers = []
272
+ invocation_id = "0"
273
+ request_id = 0
274
+
275
+ def _b2s(b: bytes) -> str:
276
+ try:
277
+ return b.decode()
278
+ except Exception:
279
+ return ''
280
+
281
+ for k, v in headers_list:
282
+ key = _b2s(k).lower()
283
+ val = _b2s(v)
284
+ if key == 'x-vercel-internal-invocation-id':
285
+ invocation_id = val
286
+ continue
287
+ if key == 'x-vercel-internal-request-id':
288
+ request_id = int(val) if val.isdigit() else 0
289
+ continue
290
+ if key in ('x-vercel-internal-span-id', 'x-vercel-internal-trace-id'):
291
+ continue
292
+ new_headers.append((k, v))
293
+
294
+ new_scope = dict(scope)
295
+ new_scope['headers'] = new_headers
296
+
297
+ # Announce handler start and set context for logging/metrics
298
+ send_message({
299
+ "type": "handler-started",
300
+ "payload": {
301
+ "handlerStartedAt": int(time.time() * 1000),
302
+ "context": {
303
+ "invocationId": invocation_id,
304
+ "requestId": request_id,
305
+ }
306
+ }
307
+ })
308
+
309
+ token = storage.set({
310
+ "invocationId": invocation_id,
311
+ "requestId": request_id,
312
+ })
313
+
314
+ try:
315
+ await self.app(new_scope, receive, send)
316
+ finally:
317
+ storage.reset(token)
318
+ send_message({
319
+ "type": "end",
320
+ "payload": {
321
+ "context": {
322
+ "invocationId": invocation_id,
323
+ "requestId": request_id,
324
+ }
325
+ }
326
+ })
327
+
234
328
  if 'VERCEL_IPC_PATH' in os.environ:
235
329
  start_time = time.time()
236
330
 
@@ -424,80 +518,53 @@ if 'VERCEL_IPC_PATH' in os.environ:
424
518
  if hasattr(response, 'close'):
425
519
  response.close()
426
520
  else:
427
- from urllib.parse import urlparse
428
- from io import BytesIO
429
- import asyncio
430
-
431
- app = __vc_module.app
432
-
433
- class Handler(BaseHandler):
434
- def handle_request(self):
435
- # Prepare ASGI scope
436
- url = urlparse(self.path)
437
- headers_encoded = []
438
- for k, v in self.headers.items():
439
- # Cope with repeated headers in the encoding.
440
- if isinstance(v, list):
441
- headers_encoded.append([k.lower().encode(), [i.encode() for i in v]])
442
- else:
443
- headers_encoded.append([k.lower().encode(), v.encode()])
444
- scope = {
445
- 'server': (self.headers.get('host', 'lambda'), self.headers.get('x-forwarded-port', 80)),
446
- 'client': (self.headers.get(
447
- 'x-forwarded-for', self.headers.get(
448
- 'x-real-ip')), 0),
449
- 'scheme': self.headers.get('x-forwarded-proto', 'http'),
450
- 'root_path': '',
451
- 'query_string': url.query.encode(),
452
- 'headers': headers_encoded,
453
- 'type': 'http',
454
- 'http_version': '1.1',
455
- 'method': self.command,
456
- 'path': url.path,
457
- 'raw_path': url.path.encode(),
458
- }
521
+ # ASGI: Run with Uvicorn so we get proper lifespan and protocol handling
522
+ try:
523
+ import uvicorn
524
+ except Exception:
525
+ _stderr('Uvicorn is required to run ASGI apps. Please ensure it is installed.')
526
+ exit(1)
527
+
528
+ # Prefer a callable app.asgi when available; some frameworks expose a boolean here
529
+ user_app_candidate = getattr(__vc_module.app, 'asgi', None)
530
+ user_app = user_app_candidate if callable(user_app_candidate) else __vc_module.app
531
+ asgi_app = ASGIMiddleware(user_app)
532
+
533
+ # Pre-bind a socket to obtain an ephemeral port for IPC announcement
534
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
535
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
536
+ sock.bind(('127.0.0.1', 0))
537
+ sock.listen(2048)
538
+ http_port = sock.getsockname()[1]
539
+
540
+ config = uvicorn.Config(
541
+ app=asgi_app,
542
+ fd=sock.fileno(),
543
+ lifespan='auto',
544
+ access_log=False,
545
+ log_config=None,
546
+ log_level='warning',
547
+ )
548
+ server = uvicorn.Server(config)
459
549
 
460
- if 'content-length' in self.headers:
461
- content_length = int(self.headers['content-length'])
462
- body = self.rfile.read(content_length)
463
- else:
464
- body = b''
550
+ send_message({
551
+ "type": "server-started",
552
+ "payload": {
553
+ "initDuration": int((time.time() - start_time) * 1000),
554
+ "httpPort": http_port,
555
+ }
556
+ })
465
557
 
466
- if _use_legacy_asyncio:
467
- loop = asyncio.new_event_loop()
468
- app_queue = asyncio.Queue(loop=loop)
469
- else:
470
- app_queue = asyncio.Queue()
471
- app_queue.put_nowait({'type': 'http.request', 'body': body, 'more_body': False})
472
-
473
- # Prepare ASGI receive function
474
- async def receive():
475
- message = await app_queue.get()
476
- return message
477
-
478
- # Prepare ASGI send function
479
- response_started = False
480
- async def send(event):
481
- nonlocal response_started
482
- if event['type'] == 'http.response.start':
483
- self.send_response(event['status'])
484
- if 'headers' in event:
485
- for name, value in event['headers']:
486
- self.send_header(name.decode(), value.decode())
487
- self.end_headers()
488
- response_started = True
489
- elif event['type'] == 'http.response.body':
490
- self.wfile.write(event['body'])
491
- if not event.get('more_body', False):
492
- self.wfile.flush()
558
+ # Mark IPC as ready and flush any buffered init logs
559
+ _ipc_ready = True
560
+ for m in _init_log_buf:
561
+ send_message(m)
562
+ _init_log_buf.clear()
493
563
 
494
- # Run the ASGI application
495
- asgi_instance = app(scope, receive, send)
496
- if _use_legacy_asyncio:
497
- asgi_task = loop.create_task(asgi_instance)
498
- loop.run_until_complete(asgi_task)
499
- else:
500
- asyncio.run(asgi_instance)
564
+ # Run the server (blocking)
565
+ server.run()
566
+ # If the server ever returns, exit
567
+ sys.exit(0)
501
568
 
502
569
  if 'Handler' in locals():
503
570
  server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
@@ -39,8 +39,9 @@ if _app is None:
39
39
  f"Missing 'app' in module '{USER_MODULE}'. Define `app = ...` (ASGI app)."
40
40
  )
41
41
 
42
- # Sanic compatibility: prefer `app.asgi` when available
43
- USER_ASGI_APP = getattr(_app, 'asgi', _app)
42
+ # Prefer a callable app.asgi when available; some frameworks expose a boolean here
43
+ _CAND = getattr(_app, 'asgi', None)
44
+ USER_ASGI_APP = _CAND if callable(_CAND) else _app
44
45
 
45
46
  PUBLIC_DIR = 'public'
46
47