@vercel/python 6.0.0 → 6.0.1

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.
Files changed (3) hide show
  1. package/dist/index.js +111 -85
  2. package/package.json +2 -2
  3. package/vc_init.py +165 -79
package/dist/index.js CHANGED
@@ -2651,7 +2651,7 @@ ${stderr}${stdout}`;
2651
2651
  var require_lib = __commonJS({
2652
2652
  "../../node_modules/.pnpm/which@3.0.0/node_modules/which/lib/index.js"(exports, module2) {
2653
2653
  var isexe = require_isexe();
2654
- var { join: join5, delimiter, sep, posix } = require("path");
2654
+ var { join: join6, delimiter, sep, posix } = require("path");
2655
2655
  var isWindows = process.platform === "win32";
2656
2656
  var rSlash = new RegExp(`[${posix.sep}${sep === posix.sep ? "" : sep}]`.replace(/(\\)/g, "\\$1"));
2657
2657
  var rRel = new RegExp(`^\\.${rSlash.source}`);
@@ -2680,7 +2680,7 @@ var require_lib = __commonJS({
2680
2680
  var getPathPart = (raw, cmd) => {
2681
2681
  const pathPart = /^".*"$/.test(raw) ? raw.slice(1, -1) : raw;
2682
2682
  const prefix = !pathPart && rRel.test(cmd) ? cmd.slice(0, 2) : "";
2683
- return prefix + join5(pathPart, cmd);
2683
+ return prefix + join6(pathPart, cmd);
2684
2684
  };
2685
2685
  var which3 = async (cmd, opt = {}) => {
2686
2686
  const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt);
@@ -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_utils5 = require("@vercel/build-utils");
2754
+ var import_build_utils6 = require("@vercel/build-utils");
2755
2755
 
2756
2756
  // src/install.ts
2757
2757
  var import_execa = __toESM(require_execa());
@@ -3066,7 +3066,7 @@ async function exportRequirementsFromPipfile({
3066
3066
  }
3067
3067
 
3068
3068
  // src/index.ts
3069
- var import_build_utils6 = require("@vercel/build-utils");
3069
+ var import_build_utils7 = require("@vercel/build-utils");
3070
3070
 
3071
3071
  // src/version.ts
3072
3072
  var import_build_utils2 = require("@vercel/build-utils");
@@ -3199,12 +3199,13 @@ function isInstalled2({ pipPath, pythonPath }) {
3199
3199
  var import_child_process = require("child_process");
3200
3200
  var import_fs4 = require("fs");
3201
3201
  var import_path4 = require("path");
3202
- var import_build_utils4 = require("@vercel/build-utils");
3202
+ var import_build_utils5 = require("@vercel/build-utils");
3203
3203
 
3204
3204
  // src/entrypoint.ts
3205
3205
  var import_fs2 = __toESM(require("fs"));
3206
3206
  var import_path2 = require("path");
3207
3207
  var import_build_utils3 = require("@vercel/build-utils");
3208
+ var import_build_utils4 = require("@vercel/build-utils");
3208
3209
  var FASTAPI_ENTRYPOINT_FILENAMES = ["app", "index", "server", "main"];
3209
3210
  var FASTAPI_ENTRYPOINT_DIRS = ["", "src", "app", "api"];
3210
3211
  var FASTAPI_CONTENT_REGEX = /(from\s+fastapi\s+import\s+FastAPI|import\s+fastapi|FastAPI\s*\()/;
@@ -3287,6 +3288,43 @@ async function detectFastapiEntrypoint(workPath, configuredEntrypoint) {
3287
3288
  return null;
3288
3289
  }
3289
3290
  }
3291
+ async function getPyprojectEntrypoint(workPath) {
3292
+ const pyprojectData = await (0, import_build_utils4.readConfigFile)((0, import_path2.join)(workPath, "pyproject.toml"));
3293
+ if (!pyprojectData)
3294
+ return null;
3295
+ const scripts = pyprojectData.project?.scripts;
3296
+ const appScript = scripts?.app;
3297
+ if (typeof appScript !== "string")
3298
+ return null;
3299
+ const match = appScript.match(/([A-Za-z_][\w.]*)\s*:\s*([A-Za-z_][\w]*)/);
3300
+ if (!match)
3301
+ return null;
3302
+ const modulePath = match[1];
3303
+ const relPath = modulePath.replace(/\./g, "/");
3304
+ try {
3305
+ const fsFiles = await (0, import_build_utils3.glob)("**", workPath);
3306
+ const candidates = [`${relPath}.py`, `${relPath}/__init__.py`];
3307
+ for (const candidate of candidates) {
3308
+ if (fsFiles[candidate])
3309
+ return candidate;
3310
+ }
3311
+ return null;
3312
+ } catch {
3313
+ (0, import_build_utils3.debug)("Failed to discover Python entrypoint from pyproject.toml");
3314
+ return null;
3315
+ }
3316
+ }
3317
+ async function detectPythonEntrypoint(framework, workPath, configuredEntrypoint) {
3318
+ let entrypoint = null;
3319
+ if (framework === "fastapi") {
3320
+ entrypoint = await detectFastapiEntrypoint(workPath, configuredEntrypoint);
3321
+ } else if (framework === "flask") {
3322
+ entrypoint = await detectFlaskEntrypoint(workPath, configuredEntrypoint);
3323
+ }
3324
+ if (entrypoint)
3325
+ return entrypoint;
3326
+ return await getPyprojectEntrypoint(workPath);
3327
+ }
3290
3328
 
3291
3329
  // src/utils.ts
3292
3330
  var import_fs3 = __toESM(require("fs"));
@@ -3355,12 +3393,12 @@ function installGlobalCleanupHandlers() {
3355
3393
  try {
3356
3394
  process.kill(info.pid, "SIGTERM");
3357
3395
  } catch (err) {
3358
- (0, import_build_utils4.debug)(`Error sending SIGTERM to ${info.pid}: ${err}`);
3396
+ (0, import_build_utils5.debug)(`Error sending SIGTERM to ${info.pid}: ${err}`);
3359
3397
  }
3360
3398
  try {
3361
3399
  process.kill(info.pid, "SIGKILL");
3362
3400
  } catch (err) {
3363
- (0, import_build_utils4.debug)(`Error sending SIGKILL to ${info.pid}: ${err}`);
3401
+ (0, import_build_utils5.debug)(`Error sending SIGKILL to ${info.pid}: ${err}`);
3364
3402
  }
3365
3403
  PERSISTENT_SERVERS.delete(key);
3366
3404
  }
@@ -3368,7 +3406,7 @@ function installGlobalCleanupHandlers() {
3368
3406
  try {
3369
3407
  restoreWarnings();
3370
3408
  } catch (err) {
3371
- (0, import_build_utils4.debug)(`Error restoring warnings: ${err}`);
3409
+ (0, import_build_utils5.debug)(`Error restoring warnings: ${err}`);
3372
3410
  }
3373
3411
  restoreWarnings = null;
3374
3412
  }
@@ -3394,10 +3432,10 @@ function createDevAsgiShim(workPath, modulePath) {
3394
3432
  const template = (0, import_fs4.readFileSync)(templatePath, "utf8");
3395
3433
  const shimSource = template.replace(/__VC_DEV_MODULE_PATH__/g, modulePath);
3396
3434
  (0, import_fs4.writeFileSync)(shimPath, shimSource, "utf8");
3397
- (0, import_build_utils4.debug)(`Prepared Python dev static shim at ${shimPath}`);
3435
+ (0, import_build_utils5.debug)(`Prepared Python dev static shim at ${shimPath}`);
3398
3436
  return ASGI_SHIM_MODULE;
3399
3437
  } catch (err) {
3400
- (0, import_build_utils4.debug)(`Failed to prepare dev static shim: ${err?.message || err}`);
3438
+ (0, import_build_utils5.debug)(`Failed to prepare dev static shim: ${err?.message || err}`);
3401
3439
  return null;
3402
3440
  }
3403
3441
  }
@@ -3410,10 +3448,10 @@ function createDevWsgiShim(workPath, modulePath) {
3410
3448
  const template = (0, import_fs4.readFileSync)(templatePath, "utf8");
3411
3449
  const shimSource = template.replace(/__VC_DEV_MODULE_PATH__/g, modulePath);
3412
3450
  (0, import_fs4.writeFileSync)(shimPath, shimSource, "utf8");
3413
- (0, import_build_utils4.debug)(`Prepared Python dev WSGI shim at ${shimPath}`);
3451
+ (0, import_build_utils5.debug)(`Prepared Python dev WSGI shim at ${shimPath}`);
3414
3452
  return WSGI_SHIM_MODULE;
3415
3453
  } catch (err) {
3416
- (0, import_build_utils4.debug)(`Failed to prepare dev WSGI shim: ${err?.message || err}`);
3454
+ (0, import_build_utils5.debug)(`Failed to prepare dev WSGI shim: ${err?.message || err}`);
3417
3455
  return null;
3418
3456
  }
3419
3457
  }
@@ -3426,26 +3464,19 @@ var startDevServer = async (opts) => {
3426
3464
  if (!restoreWarnings)
3427
3465
  restoreWarnings = silenceNodeWarnings();
3428
3466
  installGlobalCleanupHandlers();
3429
- let entry = null;
3430
- if (framework === "fastapi") {
3431
- const detectedFastapi = await detectFastapiEntrypoint(
3432
- workPath,
3433
- rawEntrypoint
3434
- );
3435
- if (!detectedFastapi) {
3436
- throw new Error(
3437
- `No FastAPI entrypoint found. Searched for: ${FASTAPI_CANDIDATE_ENTRYPOINTS.join(", ")}`
3438
- );
3439
- }
3440
- entry = detectedFastapi;
3441
- } else {
3442
- const detectedFlask = await detectFlaskEntrypoint(workPath, rawEntrypoint);
3443
- if (!detectedFlask) {
3444
- throw new Error(
3445
- `No Flask entrypoint found. Searched for: ${FLASK_CANDIDATE_ENTRYPOINTS.join(", ")}`
3446
- );
3447
- }
3448
- entry = detectedFlask;
3467
+ const entry = await detectPythonEntrypoint(
3468
+ framework,
3469
+ workPath,
3470
+ rawEntrypoint
3471
+ );
3472
+ if (!entry) {
3473
+ const searched = framework === "fastapi" ? FASTAPI_CANDIDATE_ENTRYPOINTS.join(", ") : FLASK_CANDIDATE_ENTRYPOINTS.join(", ");
3474
+ throw new import_build_utils5.NowBuildError({
3475
+ code: "PYTHON_ENTRYPOINT_NOT_FOUND",
3476
+ 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
+ link: `https://vercel.com/docs/frameworks/backend/${framework?.toLowerCase()}#exporting-the-${framework?.toLowerCase()}-application`,
3478
+ action: "Learn More"
3479
+ });
3449
3480
  }
3450
3481
  const modulePath = entry.replace(/\.py$/i, "").replace(/[\\/]/g, ".");
3451
3482
  const env = { ...process.env, ...meta.env || {} };
@@ -3490,7 +3521,7 @@ var startDevServer = async (opts) => {
3490
3521
  let pythonCmd = systemPython;
3491
3522
  const venv = isInVirtualEnv();
3492
3523
  if (venv) {
3493
- (0, import_build_utils4.debug)(`Running in virtualenv at ${venv}`);
3524
+ (0, import_build_utils5.debug)(`Running in virtualenv at ${venv}`);
3494
3525
  } else {
3495
3526
  const { pythonCmd: venvPythonCmd, venvRoot } = useVirtualEnv(
3496
3527
  workPath,
@@ -3499,9 +3530,9 @@ var startDevServer = async (opts) => {
3499
3530
  );
3500
3531
  pythonCmd = venvPythonCmd;
3501
3532
  if (venvRoot) {
3502
- (0, import_build_utils4.debug)(`Using virtualenv at ${venvRoot}`);
3533
+ (0, import_build_utils5.debug)(`Using virtualenv at ${venvRoot}`);
3503
3534
  } else {
3504
- (0, import_build_utils4.debug)("No virtualenv found");
3535
+ (0, import_build_utils5.debug)("No virtualenv found");
3505
3536
  try {
3506
3537
  const yellow = "\x1B[33m";
3507
3538
  const reset = "\x1B[0m";
@@ -3524,7 +3555,7 @@ If you are using a virtual environment, activate it before running "vercel dev",
3524
3555
  }
3525
3556
  const moduleToRun = devShimModule || modulePath;
3526
3557
  const argv = ["-u", "-m", moduleToRun];
3527
- (0, import_build_utils4.debug)(`Starting ASGI dev server: ${pythonCmd} ${argv.join(" ")}`);
3558
+ (0, import_build_utils5.debug)(`Starting ASGI dev server: ${pythonCmd} ${argv.join(" ")}`);
3528
3559
  const child = (0, import_child_process.spawn)(pythonCmd, argv, {
3529
3560
  cwd: workPath,
3530
3561
  env,
@@ -3602,7 +3633,7 @@ If you are using a virtual environment, activate it before running "vercel dev",
3602
3633
  }
3603
3634
  const moduleToRun = devShimModule || modulePath;
3604
3635
  const argv = ["-u", "-m", moduleToRun];
3605
- (0, import_build_utils4.debug)(`Starting Flask dev server: ${pythonCmd} ${argv.join(" ")}`);
3636
+ (0, import_build_utils5.debug)(`Starting Flask dev server: ${pythonCmd} ${argv.join(" ")}`);
3606
3637
  const child = (0, import_child_process.spawn)(pythonCmd, argv, {
3607
3638
  cwd: workPath,
3608
3639
  env,
@@ -3712,13 +3743,13 @@ async function downloadFilesInWorkPath({
3712
3743
  files,
3713
3744
  meta = {}
3714
3745
  }) {
3715
- (0, import_build_utils5.debug)("Downloading user files...");
3716
- let downloadedFiles = await (0, import_build_utils5.download)(files, workPath, meta);
3746
+ (0, import_build_utils6.debug)("Downloading user files...");
3747
+ let downloadedFiles = await (0, import_build_utils6.download)(files, workPath, meta);
3717
3748
  if (meta.isDev) {
3718
3749
  const { devCacheDir = (0, import_path5.join)(workPath, ".now", "cache") } = meta;
3719
3750
  const destCache = (0, import_path5.join)(devCacheDir, (0, import_path5.basename)(entrypoint, ".py"));
3720
- await (0, import_build_utils5.download)(downloadedFiles, destCache);
3721
- downloadedFiles = await (0, import_build_utils5.glob)("**", destCache);
3751
+ await (0, import_build_utils6.download)(downloadedFiles, destCache);
3752
+ downloadedFiles = await (0, import_build_utils6.glob)("**", destCache);
3722
3753
  workPath = destCache;
3723
3754
  }
3724
3755
  return workPath;
@@ -3730,6 +3761,7 @@ var build = async ({
3730
3761
  meta = {},
3731
3762
  config
3732
3763
  }) => {
3764
+ const framework = config?.framework;
3733
3765
  workPath = await downloadFilesInWorkPath({
3734
3766
  workPath,
3735
3767
  files: originalFiles,
@@ -3745,33 +3777,25 @@ var build = async ({
3745
3777
  console.log('Failed to create "setup.cfg" file');
3746
3778
  throw err;
3747
3779
  }
3748
- let fsFiles = await (0, import_build_utils5.glob)("**", workPath);
3749
- if (!fsFiles[entrypoint] && config?.framework === "fastapi") {
3750
- const detected = await detectFastapiEntrypoint(workPath, entrypoint);
3751
- if (detected) {
3752
- (0, import_build_utils5.debug)(
3753
- `Resolved Python entrypoint to "${detected}" (configured "${entrypoint}" not found).`
3754
- );
3755
- entrypoint = detected;
3756
- } else {
3757
- const searchedList = FASTAPI_CANDIDATE_ENTRYPOINTS.join(", ");
3758
- throw new import_build_utils5.NowBuildError({
3759
- code: "FASTAPI_ENTRYPOINT_NOT_FOUND",
3760
- message: `No FastAPI entrypoint found. Searched for: ${searchedList}`
3761
- });
3762
- }
3763
- } else if (!fsFiles[entrypoint] && config?.framework === "flask") {
3764
- const detected = await detectFlaskEntrypoint(workPath, entrypoint);
3780
+ let fsFiles = await (0, import_build_utils6.glob)("**", workPath);
3781
+ if ((framework === "fastapi" || framework === "flask") && (!fsFiles[entrypoint] || !entrypoint.endsWith(".py"))) {
3782
+ const detected = await detectPythonEntrypoint(
3783
+ config.framework,
3784
+ workPath,
3785
+ entrypoint
3786
+ );
3765
3787
  if (detected) {
3766
- (0, import_build_utils5.debug)(
3788
+ (0, import_build_utils6.debug)(
3767
3789
  `Resolved Python entrypoint to "${detected}" (configured "${entrypoint}" not found).`
3768
3790
  );
3769
3791
  entrypoint = detected;
3770
3792
  } else {
3771
- const searchedList = FLASK_CANDIDATE_ENTRYPOINTS.join(", ");
3772
- throw new import_build_utils5.NowBuildError({
3773
- code: "FLASK_ENTRYPOINT_NOT_FOUND",
3774
- message: `No Flask entrypoint found. Searched for: ${searchedList}`
3793
+ const searchedList = framework === "fastapi" ? FASTAPI_CANDIDATE_ENTRYPOINTS.join(", ") : FLASK_CANDIDATE_ENTRYPOINTS.join(", ");
3794
+ throw new import_build_utils6.NowBuildError({
3795
+ code: `${framework.toUpperCase()}_ENTRYPOINT_NOT_FOUND`,
3796
+ 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
+ link: `https://vercel.com/docs/frameworks/backend/${framework}#exporting-the-${framework}-application`,
3798
+ action: "Learn More"
3775
3799
  });
3776
3800
  }
3777
3801
  }
@@ -3796,20 +3820,20 @@ var build = async ({
3796
3820
  if (pyprojectDir) {
3797
3821
  let requiresPython;
3798
3822
  try {
3799
- const pyproject = await (0, import_build_utils6.readConfigFile)((0, import_path5.join)(pyprojectDir, "pyproject.toml"));
3823
+ const pyproject = await (0, import_build_utils7.readConfigFile)((0, import_path5.join)(pyprojectDir, "pyproject.toml"));
3800
3824
  requiresPython = pyproject?.project?.["requires-python"];
3801
3825
  } catch (err) {
3802
- (0, import_build_utils5.debug)("Failed to parse pyproject.toml", err);
3826
+ (0, import_build_utils6.debug)("Failed to parse pyproject.toml", err);
3803
3827
  }
3804
3828
  const VERSION_REGEX = /\b\d+\.\d+\b/;
3805
3829
  const exact = requiresPython?.trim().match(VERSION_REGEX)?.[0];
3806
3830
  if (exact) {
3807
3831
  declaredPythonVersion = { version: exact, source: "pyproject.toml" };
3808
- (0, import_build_utils5.debug)(
3832
+ (0, import_build_utils6.debug)(
3809
3833
  `Found Python version ${exact} in pyproject.toml (requires-python: "${requiresPython}")`
3810
3834
  );
3811
3835
  } else if (requiresPython) {
3812
- (0, import_build_utils5.debug)(
3836
+ (0, import_build_utils6.debug)(
3813
3837
  `Could not parse Python version from pyproject.toml requires-python: "${requiresPython}"`
3814
3838
  );
3815
3839
  }
@@ -3819,7 +3843,7 @@ var build = async ({
3819
3843
  const json = await readFile((0, import_path5.join)(pipfileLockDir, "Pipfile.lock"), "utf8");
3820
3844
  lock = JSON.parse(json);
3821
3845
  } catch (err) {
3822
- throw new import_build_utils5.NowBuildError({
3846
+ throw new import_build_utils6.NowBuildError({
3823
3847
  code: "INVALID_PIPFILE_LOCK",
3824
3848
  message: "Unable to parse Pipfile.lock"
3825
3849
  });
@@ -3827,14 +3851,14 @@ var build = async ({
3827
3851
  const pyFromLock = lock?._meta?.requires?.python_version;
3828
3852
  if (pyFromLock) {
3829
3853
  declaredPythonVersion = { version: pyFromLock, source: "Pipfile.lock" };
3830
- (0, import_build_utils5.debug)(`Found Python version ${pyFromLock} in Pipfile.lock`);
3854
+ (0, import_build_utils6.debug)(`Found Python version ${pyFromLock} in Pipfile.lock`);
3831
3855
  }
3832
3856
  }
3833
3857
  const pythonVersion = getSupportedPythonVersion({
3834
3858
  isDev: meta.isDev,
3835
3859
  declaredPythonVersion
3836
3860
  });
3837
- fsFiles = await (0, import_build_utils5.glob)("**", workPath);
3861
+ fsFiles = await (0, import_build_utils6.glob)("**", workPath);
3838
3862
  const requirementsTxt = (0, import_path5.join)(entryDirectory, "requirements.txt");
3839
3863
  const vendorBaseDir = (0, import_path5.join)(
3840
3864
  workPath,
@@ -3879,7 +3903,7 @@ var build = async ({
3879
3903
  `uv is required for this project but failed to install: ${err instanceof Error ? err.message : String(err)}`
3880
3904
  );
3881
3905
  }
3882
- (0, import_build_utils5.debug)("Failed to install uv", err);
3906
+ (0, import_build_utils6.debug)("Failed to install uv", err);
3883
3907
  }
3884
3908
  await installRequirement({
3885
3909
  pythonPath: pythonVersion.pythonPath,
@@ -3893,7 +3917,7 @@ var build = async ({
3893
3917
  });
3894
3918
  let installedFromProjectFiles = false;
3895
3919
  if (uvLockDir) {
3896
- (0, import_build_utils5.debug)('Found "uv.lock"');
3920
+ (0, import_build_utils6.debug)('Found "uv.lock"');
3897
3921
  if (pyprojectDir) {
3898
3922
  const exportedReq = await exportRequirementsFromUv(pyprojectDir, uvPath, {
3899
3923
  locked: true
@@ -3909,10 +3933,10 @@ var build = async ({
3909
3933
  });
3910
3934
  installedFromProjectFiles = true;
3911
3935
  } else {
3912
- (0, import_build_utils5.debug)('Skipping uv export because "pyproject.toml" was not found');
3936
+ (0, import_build_utils6.debug)('Skipping uv export because "pyproject.toml" was not found');
3913
3937
  }
3914
3938
  } else if (pyprojectDir) {
3915
- (0, import_build_utils5.debug)('Found "pyproject.toml"');
3939
+ (0, import_build_utils6.debug)('Found "pyproject.toml"');
3916
3940
  if (hasReqLocal || hasReqGlobal) {
3917
3941
  console.log(
3918
3942
  "Detected both pyproject.toml and requirements.txt but no lockfile; using pyproject.toml"
@@ -3932,9 +3956,9 @@ var build = async ({
3932
3956
  });
3933
3957
  installedFromProjectFiles = true;
3934
3958
  } else if (pipfileLockDir || pipfileDir) {
3935
- (0, import_build_utils5.debug)(`Found ${pipfileLockDir ? '"Pipfile.lock"' : '"Pipfile"'}`);
3959
+ (0, import_build_utils6.debug)(`Found ${pipfileLockDir ? '"Pipfile.lock"' : '"Pipfile"'}`);
3936
3960
  if (hasReqLocal || hasReqGlobal) {
3937
- (0, import_build_utils5.debug)('Skipping Pipfile export because "requirements.txt" exists');
3961
+ (0, import_build_utils6.debug)('Skipping Pipfile export because "requirements.txt" exists');
3938
3962
  } else {
3939
3963
  const exportedReq = await exportRequirementsFromPipfile({
3940
3964
  pythonPath: pythonVersion.pythonPath,
@@ -3956,7 +3980,7 @@ var build = async ({
3956
3980
  }
3957
3981
  }
3958
3982
  if (!installedFromProjectFiles && fsFiles[requirementsTxt]) {
3959
- (0, import_build_utils5.debug)('Found local "requirements.txt"');
3983
+ (0, import_build_utils6.debug)('Found local "requirements.txt"');
3960
3984
  const requirementsTxtPath = fsFiles[requirementsTxt].fsPath;
3961
3985
  await installRequirementsFile({
3962
3986
  pythonPath: pythonVersion.pythonPath,
@@ -3968,7 +3992,7 @@ var build = async ({
3968
3992
  meta
3969
3993
  });
3970
3994
  } else if (!installedFromProjectFiles && fsFiles["requirements.txt"]) {
3971
- (0, import_build_utils5.debug)('Found global "requirements.txt"');
3995
+ (0, import_build_utils6.debug)('Found global "requirements.txt"');
3972
3996
  const requirementsTxtPath = fsFiles["requirements.txt"].fsPath;
3973
3997
  await installRequirementsFile({
3974
3998
  pythonPath: pythonVersion.pythonPath,
@@ -3982,12 +4006,12 @@ var build = async ({
3982
4006
  }
3983
4007
  const originalPyPath = (0, import_path5.join)(__dirname, "..", "vc_init.py");
3984
4008
  const originalHandlerPyContents = await readFile(originalPyPath, "utf8");
3985
- (0, import_build_utils5.debug)("Entrypoint is", entrypoint);
4009
+ (0, import_build_utils6.debug)("Entrypoint is", entrypoint);
3986
4010
  const moduleName = entrypoint.replace(/\//g, ".").replace(/\.py$/i, "");
3987
4011
  const vendorDir = resolveVendorDir();
3988
4012
  const suffix = meta.isDev && !entrypoint.endsWith(".py") ? ".py" : "";
3989
4013
  const entrypointWithSuffix = `${entrypoint}${suffix}`;
3990
- (0, import_build_utils5.debug)("Entrypoint with suffix is", entrypointWithSuffix);
4014
+ (0, import_build_utils6.debug)("Entrypoint with suffix is", entrypointWithSuffix);
3991
4015
  const handlerPyContents = originalHandlerPyContents.replace(/__VC_HANDLER_MODULE_NAME/g, moduleName).replace(/__VC_HANDLER_ENTRYPOINT/g, entrypointWithSuffix).replace(/__VC_HANDLER_VENDOR_DIR/g, vendorDir);
3992
4016
  const predefinedExcludes = [
3993
4017
  ".git/**",
@@ -4000,6 +4024,8 @@ var build = async ({
4000
4024
  "**/.venv/**",
4001
4025
  "**/venv/**",
4002
4026
  "**/__pycache__/**",
4027
+ "**/.mypy_cache/**",
4028
+ "**/.ruff_cache/**",
4003
4029
  "**/public/**"
4004
4030
  ];
4005
4031
  const lambdaEnv = {};
@@ -4008,11 +4034,11 @@ var build = async ({
4008
4034
  cwd: workPath,
4009
4035
  ignore: config && typeof config.excludeFiles === "string" ? [...predefinedExcludes, config.excludeFiles] : predefinedExcludes
4010
4036
  };
4011
- const files = await (0, import_build_utils5.glob)("**", globOptions);
4037
+ const files = await (0, import_build_utils6.glob)("**", globOptions);
4012
4038
  try {
4013
4039
  const cachedVendorAbs = (0, import_path5.join)(vendorBaseDir, resolveVendorDir());
4014
4040
  if (import_fs5.default.existsSync(cachedVendorAbs)) {
4015
- const vendorFiles = await (0, import_build_utils5.glob)("**", cachedVendorAbs, resolveVendorDir());
4041
+ const vendorFiles = await (0, import_build_utils6.glob)("**", cachedVendorAbs, resolveVendorDir());
4016
4042
  for (const [p, f] of Object.entries(vendorFiles)) {
4017
4043
  files[p] = f;
4018
4044
  }
@@ -4022,12 +4048,12 @@ var build = async ({
4022
4048
  throw err;
4023
4049
  }
4024
4050
  const handlerPyFilename = "vc__handler__python";
4025
- files[`${handlerPyFilename}.py`] = new import_build_utils5.FileBlob({ data: handlerPyContents });
4051
+ files[`${handlerPyFilename}.py`] = new import_build_utils6.FileBlob({ data: handlerPyContents });
4026
4052
  if (config.framework === "fasthtml") {
4027
4053
  const { SESSKEY = "" } = process.env;
4028
- files[".sesskey"] = new import_build_utils5.FileBlob({ data: `"${SESSKEY}"` });
4054
+ files[".sesskey"] = new import_build_utils6.FileBlob({ data: `"${SESSKEY}"` });
4029
4055
  }
4030
- const output = new import_build_utils5.Lambda({
4056
+ const output = new import_build_utils6.Lambda({
4031
4057
  files,
4032
4058
  handler: `${handlerPyFilename}.vc_handler`,
4033
4059
  runtime: pythonVersion.runtime,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/python",
3
- "version": "6.0.0",
3
+ "version": "6.0.1",
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": "12.2.3",
24
+ "@vercel/build-utils": "12.2.4",
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
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  import sys
2
3
  import os
3
4
  import site
@@ -5,9 +6,19 @@ import importlib
5
6
  import base64
6
7
  import json
7
8
  import inspect
9
+ import asyncio
10
+ import http
11
+ import time
8
12
  from importlib import util
9
- from http.server import BaseHTTPRequestHandler
13
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
10
14
  import socket
15
+ import functools
16
+ import logging
17
+ import builtins
18
+ from typing import Callable, Literal, TextIO
19
+ import contextvars
20
+ import contextlib
21
+
11
22
 
12
23
  _here = os.path.dirname(__file__)
13
24
  _vendor_rel = '__VC_HANDLER_VENDOR_DIR'
@@ -30,6 +41,153 @@ if os.path.isdir(_vendor):
30
41
 
31
42
  importlib.invalidate_caches()
32
43
 
44
+
45
+ def setup_logging(send_message: Callable[[dict], None], storage: contextvars.ContextVar[dict | None]):
46
+ # Override logging.Handler to send logs to the platform when a request context is available.
47
+ class VCLogHandler(logging.Handler):
48
+ def emit(self, record: logging.LogRecord):
49
+ try:
50
+ message = record.getMessage()
51
+ except Exception:
52
+ message = repr(getattr(record, "msg", ""))
53
+
54
+ if record.levelno >= logging.CRITICAL:
55
+ level = "fatal"
56
+ elif record.levelno >= logging.ERROR:
57
+ level = "error"
58
+ elif record.levelno >= logging.WARNING:
59
+ level = "warn"
60
+ elif record.levelno >= logging.INFO:
61
+ level = "info"
62
+ else:
63
+ level = "debug"
64
+
65
+ context = storage.get()
66
+ if context is not None:
67
+ send_message({
68
+ "type": "log",
69
+ "payload": {
70
+ "context": {
71
+ "invocationId": context['invocationId'],
72
+ "requestId": context['requestId'],
73
+ },
74
+ "message": base64.b64encode(message.encode()).decode(),
75
+ "level": level,
76
+ }
77
+ })
78
+ else:
79
+ # If IPC is not ready, enqueue the message to be sent later.
80
+ enqueue_or_send_message({
81
+ "type": "log",
82
+ "payload": {
83
+ "context": {"invocationId": "0", "requestId": 0},
84
+ "message": base64.b64encode(message.encode()).decode(),
85
+ "level": level,
86
+ }
87
+ })
88
+
89
+ # Override sys.stdout and sys.stderr to map logs to the correct request
90
+ class StreamWrapper:
91
+ def __init__(self, stream: TextIO, stream_name: Literal["stdout", "stderr"]):
92
+ self.stream = stream
93
+ self.stream_name = stream_name
94
+
95
+ def write(self, message: str):
96
+ context = storage.get()
97
+ if context is not None:
98
+ send_message({
99
+ "type": "log",
100
+ "payload": {
101
+ "context": {
102
+ "invocationId": context['invocationId'],
103
+ "requestId": context['requestId'],
104
+ },
105
+ "message": base64.b64encode(message.encode()).decode(),
106
+ "stream": self.stream_name,
107
+ }
108
+ })
109
+ else:
110
+ enqueue_or_send_message({
111
+ "type": "log",
112
+ "payload": {
113
+ "context": {"invocationId": "0", "requestId": 0},
114
+ "message": base64.b64encode(message.encode()).decode(),
115
+ "stream": self.stream_name,
116
+ }
117
+ })
118
+
119
+ def __getattr__(self, name):
120
+ return getattr(self.stream, name)
121
+
122
+ sys.stdout = StreamWrapper(sys.stdout, "stdout")
123
+ sys.stderr = StreamWrapper(sys.stderr, "stderr")
124
+
125
+ logging.basicConfig(level=logging.INFO, handlers=[VCLogHandler()], force=True)
126
+
127
+ # Ensure built-in print funnels through stdout wrapper so prints are
128
+ # attributed to the current request context.
129
+ def print_wrapper(func: Callable[..., None]) -> Callable[..., None]:
130
+ @functools.wraps(func)
131
+ def wrapper(*args, sep=' ', end='\n', file=None, flush=False):
132
+ if file is None:
133
+ file = sys.stdout
134
+ if file in (sys.stdout, sys.stderr):
135
+ file.write(sep.join(map(str, args)) + end)
136
+ if flush:
137
+ file.flush()
138
+ else:
139
+ # User specified a different file, use original print behavior
140
+ func(*args, sep=sep, end=end, file=file, flush=flush)
141
+ return wrapper
142
+
143
+ builtins.print = print_wrapper(builtins.print)
144
+
145
+
146
+ # If running in the platform (IPC present), logging must be setup before importing user code so that
147
+ # logs happening outside the request context are emitted correctly.
148
+ ipc_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
149
+ storage: contextvars.ContextVar[dict | None] = contextvars.ContextVar('storage', default=None)
150
+ send_message = lambda m: None
151
+ _original_stderr = sys.stderr
152
+
153
+
154
+ # Buffer for pre-handshake logs (to avoid blocking IPC on startup)
155
+ _ipc_ready = False
156
+ _init_log_buf: list[dict] = []
157
+ _INIT_LOG_BUF_MAX_BYTES = 1_000_000
158
+ _init_log_buf_bytes = 0
159
+
160
+
161
+ def enqueue_or_send_message(msg: dict):
162
+ global _init_log_buf_bytes
163
+ if _ipc_ready:
164
+ send_message(msg)
165
+ return
166
+
167
+ enc_len = len(json.dumps(msg))
168
+
169
+ if _init_log_buf_bytes + enc_len <= _INIT_LOG_BUF_MAX_BYTES:
170
+ _init_log_buf.append(msg)
171
+ _init_log_buf_bytes += enc_len
172
+ else:
173
+ # Fallback so message is not lost if buffer is full
174
+ with contextlib.suppress(Exception):
175
+ payload = msg.get("payload", {})
176
+ decoded = base64.b64decode(payload.get("message", "")).decode(errors="ignore")
177
+ _original_stderr.write(decoded + "\n")
178
+
179
+
180
+ if 'VERCEL_IPC_PATH' in os.environ:
181
+ with contextlib.suppress(Exception):
182
+ ipc_sock.connect(os.getenv("VERCEL_IPC_PATH", ""))
183
+
184
+ def send_message(message: dict):
185
+ with contextlib.suppress(Exception):
186
+ ipc_sock.sendall((json.dumps(message) + '\0').encode())
187
+
188
+ setup_logging(send_message, storage)
189
+
190
+
33
191
  # Import relative path https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
34
192
  user_mod_path = os.path.join(_here, "__VC_HANDLER_ENTRYPOINT") # absolute
35
193
  __vc_spec = util.spec_from_file_location("__VC_HANDLER_MODULE_NAME", user_mod_path)
@@ -51,21 +209,9 @@ def format_headers(headers, decode=False):
51
209
  keyToList[key].append(value)
52
210
  return keyToList
53
211
 
54
- if 'VERCEL_IPC_PATH' in os.environ:
55
- from http.server import ThreadingHTTPServer
56
- import http
57
- import time
58
- import contextvars
59
- import functools
60
- import builtins
61
- import logging
62
212
 
213
+ if 'VERCEL_IPC_PATH' in os.environ:
63
214
  start_time = time.time()
64
- sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
65
- sock.connect(os.getenv("VERCEL_IPC_PATH", ""))
66
-
67
- send_message = lambda message: sock.sendall((json.dumps(message) + '\0').encode())
68
- storage = contextvars.ContextVar('storage', default=None)
69
215
 
70
216
  # Override urlopen from urllib3 (& requests) to send Request Metrics
71
217
  try:
@@ -110,71 +256,6 @@ if 'VERCEL_IPC_PATH' in os.environ:
110
256
  except:
111
257
  pass
112
258
 
113
- # Override sys.stdout and sys.stderr to map logs to the correct request
114
- class StreamWrapper:
115
- def __init__(self, stream, stream_name):
116
- self.stream = stream
117
- self.stream_name = stream_name
118
-
119
- def write(self, message):
120
- context = storage.get()
121
- if context is not None:
122
- send_message({
123
- "type": "log",
124
- "payload": {
125
- "context": {
126
- "invocationId": context['invocationId'],
127
- "requestId": context['requestId'],
128
- },
129
- "message": base64.b64encode(message.encode()).decode(),
130
- "stream": self.stream_name,
131
- }
132
- })
133
- else:
134
- self.stream.write(message)
135
-
136
- def __getattr__(self, name):
137
- return getattr(self.stream, name)
138
-
139
- sys.stdout = StreamWrapper(sys.stdout, "stdout")
140
- sys.stderr = StreamWrapper(sys.stderr, "stderr")
141
-
142
- # Override the global print to log to stdout
143
- def print_wrapper(func):
144
- @functools.wraps(func)
145
- def wrapper(*args, **kwargs):
146
- sys.stdout.write(' '.join(map(str, args)) + '\n')
147
- return wrapper
148
- builtins.print = print_wrapper(builtins.print)
149
-
150
- # Override logging to maps logs to the correct request
151
- def logging_wrapper(func, level="info"):
152
- @functools.wraps(func)
153
- def wrapper(*args, **kwargs):
154
- context = storage.get()
155
- if context is not None:
156
- send_message({
157
- "type": "log",
158
- "payload": {
159
- "context": {
160
- "invocationId": context['invocationId'],
161
- "requestId": context['requestId'],
162
- },
163
- "message": base64.b64encode(f"{args[0]}".encode()).decode(),
164
- "level": level,
165
- }
166
- })
167
- else:
168
- func(*args, **kwargs)
169
- return wrapper
170
-
171
- logging.basicConfig(level=logging.INFO)
172
- logging.debug = logging_wrapper(logging.debug)
173
- logging.info = logging_wrapper(logging.info)
174
- logging.warning = logging_wrapper(logging.warning, "warn")
175
- logging.error = logging_wrapper(logging.error, "error")
176
- logging.critical = logging_wrapper(logging.critical, "error")
177
-
178
259
  class BaseHandler(BaseHTTPRequestHandler):
179
260
  # Re-implementation of BaseHTTPRequestHandler's log_message method to
180
261
  # log to stdout instead of stderr.
@@ -406,6 +487,11 @@ if 'VERCEL_IPC_PATH' in os.environ:
406
487
  "httpPort": server.server_address[1],
407
488
  }
408
489
  })
490
+ # Mark IPC as ready and flush any buffered init logs
491
+ _ipc_ready = True
492
+ for m in _init_log_buf:
493
+ send_message(m)
494
+ _init_log_buf.clear()
409
495
  server.serve_forever()
410
496
 
411
497
  print('Missing variable `handler` or `app` in file "__VC_HANDLER_ENTRYPOINT".')