@vercel/python 6.37.0 → 6.39.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.
Files changed (2) hide show
  1. package/dist/index.js +273 -52
  2. package/package.json +7 -8
package/dist/index.js CHANGED
@@ -3230,8 +3230,8 @@ var import_fs13 = __toESM(require("fs"));
3230
3230
  var import_path13 = require("path");
3231
3231
 
3232
3232
  // src/package-versions.ts
3233
- var VERCEL_RUNTIME_VERSION = "0.13.1";
3234
- var VERCEL_WORKERS_VERSION = "0.0.20";
3233
+ var VERCEL_RUNTIME_VERSION = "0.13.2";
3234
+ var VERCEL_WORKERS_VERSION = "0.0.22";
3235
3235
 
3236
3236
  // src/index.ts
3237
3237
  var import_build_utils16 = require("@vercel/build-utils");
@@ -4328,6 +4328,9 @@ var LAMBDA_SIZE_THRESHOLD_BYTES = 245 * 1024 * 1024;
4328
4328
  var LAMBDA_EPHEMERAL_STORAGE_BYTES = 500 * 1024 * 1024;
4329
4329
  var HIVE_LAMBDA_SIZE_BYTES = 1 * 1024 * 1024 * 1024;
4330
4330
  var FUNCTIONS_BETA_CTA = "Run `vercel deploy --functions-beta` to use extended function limits (up to 1 GB), or reduce your dependency footprint.";
4331
+ var BUNDLING_DOCS_LINK = "https://vercel.com/docs/functions/runtimes/python#controlling-what-gets-bundled";
4332
+ var FUNCTIONS_BETA_DOCS_LINK = "https://vercel.com/docs/functions/runtimes/python#extended-size-limits-with-functions-beta";
4333
+ var FUNCTIONS_BETA_EXCEEDED_CTA = "Your deployment is already using extended function limits (`--functions-beta`).\nReduce your dependency footprint to under 1 GB or split your application\ninto smaller functions.";
4331
4334
  function shouldShowFunctionsBetaHint() {
4332
4335
  const v = process.env.VERCEL_FUNCTIONS_BETA_HINT;
4333
4336
  return v === "1" || v === "true";
@@ -4390,10 +4393,13 @@ var PythonDependencyExternalizer = class {
4390
4393
 
4391
4394
  ` + FUNCTIONS_BETA_CTA : `Total bundle size (${totalBundleSizeMB} MB) exceeds the size limit (${limitMB} MB).
4392
4395
 
4393
- When using a custom install command, Vercel cannot automatically optimize dependency bundling. To reduce the size of your dependencies, you can:
4394
- 1. Remove unused dependencies from your project
4395
- 2. Remove the custom install command to allow Vercel to manage and optimize dependencies automatically`,
4396
- link: "https://vercel.com/docs/functions/runtimes/python#controlling-what-gets-bundled",
4396
+ When using a custom install command, Vercel cannot automatically
4397
+ optimize dependency bundling. To reduce the size of your
4398
+ dependencies, you can:
4399
+ 1. Remove unused dependencies from your project.
4400
+ 2. Remove the custom install command to allow Vercel to manage
4401
+ and optimize dependencies automatically.`,
4402
+ link: shouldShowFunctionsBetaHint() ? FUNCTIONS_BETA_DOCS_LINK : BUNDLING_DOCS_LINK,
4397
4403
  action: "Learn More"
4398
4404
  });
4399
4405
  }
@@ -4401,7 +4407,12 @@ When using a custom install command, Vercel cannot automatically optimize depend
4401
4407
  const limitMB = (HIVE_LAMBDA_SIZE_BYTES / (1024 * 1024)).toFixed(0);
4402
4408
  throw new import_build_utils5.NowBuildError({
4403
4409
  code: "LAMBDA_SIZE_EXCEEDED",
4404
- message: `Total bundle size (${totalBundleSizeMB} MB) exceeds the extended function size limit (${limitMB} MB). Consider removing unused dependencies or splitting your application into smaller functions.`,
4410
+ message: shouldShowFunctionsBetaHint() ? `Total bundle size (${totalBundleSizeMB} MB) exceeds the extended function size limit (${limitMB} MB).
4411
+
4412
+ ` + FUNCTIONS_BETA_EXCEEDED_CTA : `Total bundle size (${totalBundleSizeMB} MB) exceeds the extended function size limit (${limitMB} MB).
4413
+
4414
+ Consider removing unused dependencies or splitting your
4415
+ application into smaller functions.`,
4405
4416
  link: "https://vercel.com/docs/functions/runtimes/python#controlling-what-gets-bundled",
4406
4417
  action: "Learn More"
4407
4418
  });
@@ -4440,8 +4451,13 @@ When using a custom install command, Vercel cannot automatically optimize depend
4440
4451
  code: "LAMBDA_SIZE_EXCEEDED",
4441
4452
  message: shouldShowFunctionsBetaHint() ? `Total bundle size (${totalBundleSizeMB} MB) exceeds the ephemeral storage limit (${ephemeralLimitMB} MB).
4442
4453
 
4443
- ` + FUNCTIONS_BETA_CTA : `Total bundle size (${totalBundleSizeMB} MB) exceeds Lambda ephemeral storage limit (${ephemeralLimitMB} MB). Even with runtime dependency installation, all packages must fit within the ${ephemeralLimitMB} MB ephemeral storage available to Lambda functions. Consider removing unused dependencies or splitting your application into smaller functions.`,
4444
- link: "https://vercel.com/docs/functions/runtimes/python#controlling-what-gets-bundled",
4454
+ ` + FUNCTIONS_BETA_CTA : `Total bundle size (${totalBundleSizeMB} MB) exceeds Lambda ephemeral storage limit (${ephemeralLimitMB} MB).
4455
+
4456
+ Even with runtime dependency installation, all packages must fit
4457
+ within the ${ephemeralLimitMB} MB ephemeral storage available to Lambda
4458
+ functions. Consider removing unused dependencies or splitting
4459
+ your application into smaller functions.`,
4460
+ link: shouldShowFunctionsBetaHint() ? FUNCTIONS_BETA_DOCS_LINK : BUNDLING_DOCS_LINK,
4445
4461
  action: "Learn More"
4446
4462
  });
4447
4463
  }
@@ -4513,12 +4529,17 @@ ${error.fileContent}`
4513
4529
  if (externalizablePublic.length === 0) {
4514
4530
  throw new import_build_utils5.NowBuildError({
4515
4531
  code: "RUNTIME_DEPENDENCY_INSTALLATION_FAILED",
4516
- message: `Bundle size exceeds the Lambda limit and requires runtime dependency installation, but no public packages have compatible pre-built wheels for the Lambda platform.
4517
- Runtime dependency installation requires packages to have binary wheels.
4532
+ message: `Bundle size exceeds the Lambda limit and requires runtime
4533
+ dependency installation, but no public packages have compatible
4534
+ pre-built wheels for the Lambda platform.
4535
+
4536
+ Runtime dependency installation requires packages to have binary
4537
+ wheels.
4518
4538
 
4519
4539
  To fix this, either:
4520
- 1. Regenerate your lock file with: uv lock --upgrade, or
4521
- 2. Switch the problematic packages to ones that have pre-built wheels available`
4540
+ 1. Regenerate your lock file with: uv lock --upgrade, or
4541
+ 2. Switch the problematic packages to ones that have pre-built
4542
+ wheels available.`
4522
4543
  });
4523
4544
  }
4524
4545
  const packageSizes = await calculatePerPackageSizes(this.venvPath);
@@ -4636,8 +4657,13 @@ To fix this, either:
4636
4657
  code: "LAMBDA_SIZE_EXCEEDED",
4637
4658
  message: shouldShowFunctionsBetaHint() ? `Total bundle size (${finalSizeMB} MB) exceeds the size limit (${limitMB} MB).
4638
4659
 
4639
- ` + FUNCTIONS_BETA_CTA : `Total bundle size (${finalSizeMB} MB) exceeds Lambda limit (${limitMB} MB) even after deferring public packages to runtime installation. This usually means your private packages or source code are too large. Consider reducing the size of private dependencies or splitting your application.`,
4640
- link: "https://vercel.com/docs/functions/runtimes/python#controlling-what-gets-bundled",
4660
+ ` + FUNCTIONS_BETA_CTA : `Total bundle size (${finalSizeMB} MB) exceeds Lambda limit (${limitMB} MB) even after
4661
+ deferring public packages to runtime installation.
4662
+
4663
+ This usually means your private packages or source code are too
4664
+ large. Consider reducing the size of private dependencies or
4665
+ splitting your application.`,
4666
+ link: shouldShowFunctionsBetaHint() ? FUNCTIONS_BETA_DOCS_LINK : BUNDLING_DOCS_LINK,
4641
4667
  action: "Learn More"
4642
4668
  });
4643
4669
  }
@@ -4943,7 +4969,8 @@ async function generateProjectManifest({
4943
4969
  pythonPackage,
4944
4970
  pythonVersion,
4945
4971
  uvLockPath,
4946
- framework
4972
+ framework,
4973
+ serviceType
4947
4974
  }) {
4948
4975
  const resolved = pythonVersionString(pythonVersion) ?? "";
4949
4976
  const constraint = pythonPackage.requiresPython?.[0];
@@ -5097,6 +5124,7 @@ async function generateProjectManifest({
5097
5124
  version: import_build_utils6.MANIFEST_VERSION,
5098
5125
  runtime: "python",
5099
5126
  ...framework ? { framework } : {},
5127
+ ...serviceType ? { serviceType } : {},
5100
5128
  runtimeVersion: {
5101
5129
  ...requested ? { requested } : {},
5102
5130
  ...constraint?.source ? { requestedSource: constraint.source } : {},
@@ -5131,7 +5159,7 @@ function buildCronRouteTable(crons) {
5131
5159
  }
5132
5160
  async function getServiceCrons(opts) {
5133
5161
  const { service, entrypoint, rawEntrypoint, handlerFunction } = opts;
5134
- const isScheduledService = service?.type === "cron" || service?.type === "job" && service.trigger === "schedule";
5162
+ const isScheduledService = !!service && (0, import_build_utils7.isScheduleTriggeredService)(service);
5135
5163
  if (!isScheduledService || !service.name || typeof service.schedule !== "string") {
5136
5164
  return void 0;
5137
5165
  }
@@ -5280,6 +5308,7 @@ var PYTHON_ENTRYPOINT_DIRS = ["", "src", "app", "api"];
5280
5308
  var PYTHON_CANDIDATE_ENTRYPOINTS = getCandidateEntrypointsInDirs(
5281
5309
  PYTHON_ENTRYPOINT_DIRS
5282
5310
  );
5311
+ var PYTHON_ENTRYPOINT_DOCS_URL = "https://vercel.com/docs/functions/runtimes/python#python-entrypoints";
5283
5312
  function getCandidateEntrypointsInDirs(dirs) {
5284
5313
  return dirs.flatMap(
5285
5314
  (dir) => PYTHON_ENTRYPOINT_FILENAMES.map(
@@ -5295,6 +5324,51 @@ async function fileExists(filePath) {
5295
5324
  return false;
5296
5325
  }
5297
5326
  }
5327
+ function getFrameworkDisplayName(framework) {
5328
+ switch (framework) {
5329
+ case "fastapi":
5330
+ return "FastAPI";
5331
+ case "flask":
5332
+ return "Flask";
5333
+ case "django":
5334
+ return "Django";
5335
+ case "fasthtml":
5336
+ return "FastHTML";
5337
+ default:
5338
+ return framework;
5339
+ }
5340
+ }
5341
+ function getFrameworkEntrypointDocsUrl(framework) {
5342
+ switch (framework) {
5343
+ case "fastapi":
5344
+ return "https://vercel.com/docs/frameworks/backend/fastapi#exporting-the-fastapi-application";
5345
+ case "flask":
5346
+ return "https://vercel.com/docs/frameworks/backend/flask#exporting-the-flask-application";
5347
+ case "django":
5348
+ return "https://vercel.com/docs/frameworks/full-stack/django#configure-the-django-entrypoint";
5349
+ default:
5350
+ return PYTHON_ENTRYPOINT_DOCS_URL;
5351
+ }
5352
+ }
5353
+ function getMissingExportMessage(framework, entrypoints) {
5354
+ const fileList = entrypoints.slice(0, 3).join(", ");
5355
+ const found = `Found ${fileList}`;
5356
+ const subject = entrypoints.length === 1 ? "it" : "none";
5357
+ const verb = entrypoints.length === 1 ? "does not define" : "define";
5358
+ const displayName = getFrameworkDisplayName(framework);
5359
+ switch (framework) {
5360
+ case "fastapi":
5361
+ return `${found} but ${subject} ${verb} a top-level "app" FastAPI instance.`;
5362
+ case "flask":
5363
+ return `${found} but ${subject} ${verb} a top-level "app" Flask instance.`;
5364
+ case "django":
5365
+ return `${found} but ${subject} ${verb} a top-level "application" ASGI or WSGI instance.`;
5366
+ case "fasthtml":
5367
+ return `${found} but ${subject} ${verb} a top-level "app" ${displayName} instance.`;
5368
+ default:
5369
+ return entrypoints.length === 1 ? `Found ${fileList} but it does not export a top-level "app", "application", or "handler" variable.` : `Found ${fileList} but none export a top-level "app", "application", or "handler" variable.`;
5370
+ }
5371
+ }
5298
5372
  async function checkEntrypoint(workPath, relPath) {
5299
5373
  const absPath = (0, import_path8.join)(workPath, relPath);
5300
5374
  if (!await fileExists(absPath))
@@ -5302,6 +5376,28 @@ async function checkEntrypoint(workPath, relPath) {
5302
5376
  const content = await import_fs8.default.promises.readFile(absPath, "utf-8");
5303
5377
  return (0, import_python_analysis5.findAppOrHandler)(content);
5304
5378
  }
5379
+ async function findOtherPyFiles(workPath, dirs, candidateSet) {
5380
+ const results = [];
5381
+ for (const dir of dirs) {
5382
+ const dirPath = (0, import_path8.join)(workPath, dir);
5383
+ try {
5384
+ const entries = await import_fs8.default.promises.readdir(dirPath);
5385
+ for (const entry of entries) {
5386
+ if (!entry.endsWith(".py"))
5387
+ continue;
5388
+ const relPath = dir ? import_path8.posix.join(dir, entry) : entry;
5389
+ if (candidateSet.has(relPath))
5390
+ continue;
5391
+ const stat = await import_fs8.default.promises.stat((0, import_path8.join)(dirPath, entry));
5392
+ if (stat.isFile()) {
5393
+ results.push(relPath);
5394
+ }
5395
+ }
5396
+ } catch {
5397
+ }
5398
+ }
5399
+ return results;
5400
+ }
5305
5401
  async function resolveModuleAttrEntrypoint(workPath, value) {
5306
5402
  const match = value.match(/([A-Za-z_][\w.]*)\s*:\s*([A-Za-z_][\w]*)/);
5307
5403
  if (!match)
@@ -5326,25 +5422,46 @@ async function getVercelToolsEntrypoint(workPath) {
5326
5422
  return null;
5327
5423
  return resolveModuleAttrEntrypoint(workPath, vercelEntrypoint);
5328
5424
  }
5329
- async function getPyprojectScriptsEntrypoint(workPath) {
5425
+ async function getPyprojectEntrypointWithDiagnostics(workPath) {
5330
5426
  const pyprojectData = await (0, import_build_utils10.readConfigFile)((0, import_path8.join)(workPath, "pyproject.toml"));
5331
5427
  if (!pyprojectData)
5332
- return null;
5428
+ return { entrypoint: null };
5333
5429
  const scripts = pyprojectData.project?.scripts;
5334
5430
  const appScript = scripts?.app;
5335
5431
  if (typeof appScript !== "string")
5336
- return null;
5337
- return resolveModuleAttrEntrypoint(workPath, appScript);
5432
+ return { entrypoint: null };
5433
+ const match = appScript.match(/([A-Za-z_][\w.]*)\s*:\s*([A-Za-z_][\w]*)/);
5434
+ if (!match)
5435
+ return { entrypoint: null, appScript };
5436
+ const modulePath = match[1];
5437
+ const variableName = match[2];
5438
+ const relPath = modulePath.replace(/\./g, "/");
5439
+ const candidates = [`${relPath}.py`, `${relPath}/__init__.py`];
5440
+ for (const candidate of candidates) {
5441
+ if (await fileExists((0, import_path8.join)(workPath, candidate))) {
5442
+ return { entrypoint: { entrypoint: candidate, variableName } };
5443
+ }
5444
+ }
5445
+ return { entrypoint: null, appScript, checkedPaths: candidates };
5338
5446
  }
5339
5447
  async function findValidEntrypoint(workPath, candidates) {
5448
+ const existingWithoutEntrypoint = [];
5340
5449
  for (const candidate of candidates) {
5341
- const varName = await checkEntrypoint(workPath, candidate);
5450
+ const absPath = (0, import_path8.join)(workPath, candidate);
5451
+ if (!await fileExists(absPath))
5452
+ continue;
5453
+ const content = await import_fs8.default.promises.readFile(absPath, "utf-8");
5454
+ const varName = await (0, import_python_analysis5.findAppOrHandler)(content);
5342
5455
  if (varName) {
5343
5456
  (0, import_build_utils9.debug)(`Detected Python entrypoint: ${candidate} (variable: ${varName})`);
5344
- return { entrypoint: candidate, variableName: varName };
5457
+ return {
5458
+ entrypoint: { entrypoint: candidate, variableName: varName },
5459
+ existingWithoutEntrypoint
5460
+ };
5345
5461
  }
5462
+ existingWithoutEntrypoint.push(candidate);
5346
5463
  }
5347
- return null;
5464
+ return { entrypoint: null, existingWithoutEntrypoint };
5348
5465
  }
5349
5466
  async function checkDjangoManage(workPath) {
5350
5467
  const managePath = (0, import_path8.join)(workPath, "manage.py");
@@ -5368,28 +5485,56 @@ async function getSubdirectories(workPath) {
5368
5485
  return [];
5369
5486
  }
5370
5487
  }
5371
- function makeDetectError(framework) {
5372
- const searchedList = PYTHON_CANDIDATE_ENTRYPOINTS.join(", ");
5373
- return new import_build_utils8.NowBuildError({
5374
- code: `${framework.toUpperCase()}_ENTRYPOINT_NOT_FOUND`,
5375
- message: `No ${framework} entrypoint found. Set \`tool.vercel.entrypoint\` in pyproject.toml or define an entrypoint in one of: ${searchedList}.`,
5376
- link: `https://vercel.com/docs/frameworks/backend/${framework}#exporting-the-${framework}-application`,
5377
- action: "Learn More"
5378
- });
5488
+ function makeDiagnosticError(framework, diagnostics2) {
5489
+ const code = `${framework.toUpperCase()}_ENTRYPOINT_NOT_FOUND`;
5490
+ const link = getFrameworkEntrypointDocsUrl(framework);
5491
+ const action = "Learn More";
5492
+ const displayName = getFrameworkDisplayName(framework);
5493
+ let message;
5494
+ if (diagnostics2.existingWithoutEntrypoint.length > 0) {
5495
+ message = getMissingExportMessage(
5496
+ framework,
5497
+ diagnostics2.existingWithoutEntrypoint
5498
+ );
5499
+ } else if (diagnostics2.pyprojectAppScript && diagnostics2.pyprojectCheckedPaths?.length) {
5500
+ const checked = diagnostics2.pyprojectCheckedPaths.join(" or ");
5501
+ message = `pyproject.toml [project.scripts] defines app = "${diagnostics2.pyprojectAppScript}" but ${checked} was not found.`;
5502
+ } else if (diagnostics2.otherPyFiles.length > 0) {
5503
+ const fileList = diagnostics2.otherPyFiles.slice(0, 5).join(", ");
5504
+ message = `No ${displayName} entrypoint found in standard locations. Found Python files: ${fileList}. Rename one to app.py or set "tool.vercel.entrypoint" in pyproject.toml.`;
5505
+ } else {
5506
+ const searchedList = PYTHON_CANDIDATE_ENTRYPOINTS.join(", ");
5507
+ message = `No ${displayName} entrypoint found. Set "tool.vercel.entrypoint" in pyproject.toml or define an entrypoint in one of: ${searchedList}.`;
5508
+ }
5509
+ return new import_build_utils8.NowBuildError({ code, message, link, action });
5379
5510
  }
5380
5511
  async function detectGenericPythonEntrypoint(workPath) {
5381
5512
  try {
5382
- const found = await findValidEntrypoint(
5513
+ const result = await findValidEntrypoint(
5383
5514
  workPath,
5384
5515
  PYTHON_CANDIDATE_ENTRYPOINTS
5385
5516
  );
5386
- return found ? { entrypoint: found } : null;
5517
+ return {
5518
+ detected: result.entrypoint ? { entrypoint: result.entrypoint } : null,
5519
+ findDiagnostics: result
5520
+ };
5387
5521
  } catch {
5388
5522
  (0, import_build_utils9.debug)("Failed to discover Python entrypoint");
5389
- return null;
5523
+ return {
5524
+ detected: null,
5525
+ findDiagnostics: { entrypoint: null, existingWithoutEntrypoint: [] }
5526
+ };
5390
5527
  }
5391
5528
  }
5392
5529
  async function detectDjangoPythonEntrypoint(workPath) {
5530
+ const emptyResult = {
5531
+ detected: null,
5532
+ findDiagnostics: {
5533
+ entrypoint: null,
5534
+ existingWithoutEntrypoint: []
5535
+ },
5536
+ dirs: PYTHON_ENTRYPOINT_DIRS
5537
+ };
5393
5538
  try {
5394
5539
  const subdirs = await getSubdirectories(workPath);
5395
5540
  const rootDirs = ["", ...subdirs];
@@ -5397,21 +5542,40 @@ async function detectDjangoPythonEntrypoint(workPath) {
5397
5542
  const currPath = (0, import_path8.join)(workPath, rootDir);
5398
5543
  const isDjango = await checkDjangoManage(currPath);
5399
5544
  if (isDjango) {
5400
- return { baseDir: rootDir, error: makeDetectError("django") };
5545
+ return {
5546
+ detected: { baseDir: rootDir },
5547
+ findDiagnostics: { entrypoint: null, existingWithoutEntrypoint: [] },
5548
+ dirs: rootDirs
5549
+ };
5401
5550
  }
5402
5551
  }
5403
5552
  const candidates = getCandidateEntrypointsInDirs(rootDirs);
5404
- const found = await findValidEntrypoint(workPath, candidates);
5405
- return found ? { entrypoint: found } : null;
5553
+ const result = await findValidEntrypoint(workPath, candidates);
5554
+ return {
5555
+ detected: result.entrypoint ? { entrypoint: result.entrypoint } : null,
5556
+ findDiagnostics: result,
5557
+ dirs: rootDirs
5558
+ };
5406
5559
  } catch {
5407
5560
  (0, import_build_utils9.debug)("Failed to discover Django Python entrypoint");
5408
- return null;
5561
+ return emptyResult;
5409
5562
  }
5410
5563
  }
5411
5564
  async function detectPythonEntrypoint(framework, workPath, configuredEntrypoint, service) {
5412
5565
  if (configuredEntrypoint) {
5413
5566
  const { filePath: configEntryFile, varName: configEntryVar } = configuredEntrypoint;
5414
5567
  const entrypoint = configEntryFile.endsWith(".py") ? configEntryFile : `${configEntryFile}.py`;
5568
+ const entrypointExists = await fileExists((0, import_path8.join)(workPath, entrypoint));
5569
+ if (!entrypointExists) {
5570
+ return {
5571
+ error: new import_build_utils8.NowBuildError({
5572
+ code: "PYTHON_ENTRYPOINT_NOT_FOUND",
5573
+ message: `Configured Python entrypoint "${entrypoint}" was not found.`,
5574
+ link: PYTHON_ENTRYPOINT_DOCS_URL,
5575
+ action: "Learn More"
5576
+ })
5577
+ };
5578
+ }
5415
5579
  let varName = configEntryVar ?? await checkEntrypoint(workPath, entrypoint);
5416
5580
  if (!varName) {
5417
5581
  const needsDynamicApp = !!service && ((0, import_build_utils8.isScheduleTriggeredService)(service) || (0, import_build_utils8.isQueueTriggeredService)(service));
@@ -5427,7 +5591,7 @@ async function detectPythonEntrypoint(framework, workPath, configuredEntrypoint,
5427
5591
  error: new import_build_utils8.NowBuildError({
5428
5592
  code: "PYTHON_ENTRYPOINT_NOT_FOUND",
5429
5593
  message: `Could not find a top-level "app", "application", or "handler" in "${entrypoint}".`,
5430
- link: "https://vercel.com/docs/functions/serverless-functions/runtimes/python",
5594
+ link: PYTHON_ENTRYPOINT_DOCS_URL,
5431
5595
  action: "Learn More"
5432
5596
  })
5433
5597
  };
@@ -5439,11 +5603,57 @@ async function detectPythonEntrypoint(framework, workPath, configuredEntrypoint,
5439
5603
  const vercelEntry = await getVercelToolsEntrypoint(workPath);
5440
5604
  if (vercelEntry)
5441
5605
  return { entrypoint: vercelEntry };
5442
- const result = framework === "django" ? await detectDjangoPythonEntrypoint(workPath) : await detectGenericPythonEntrypoint(workPath);
5443
- if (result)
5444
- return result;
5445
- const scriptsEntry = await getPyprojectScriptsEntrypoint(workPath);
5446
- return scriptsEntry ? { entrypoint: scriptsEntry } : { error: makeDetectError(framework) };
5606
+ let findDiagnostics;
5607
+ let searchDirs;
5608
+ if (framework === "django") {
5609
+ const djangoResult = await detectDjangoPythonEntrypoint(workPath);
5610
+ findDiagnostics = djangoResult.findDiagnostics;
5611
+ searchDirs = djangoResult.dirs;
5612
+ if (djangoResult.detected) {
5613
+ if (djangoResult.detected.entrypoint)
5614
+ return djangoResult.detected;
5615
+ const candidateSet2 = new Set(getCandidateEntrypointsInDirs(searchDirs));
5616
+ const otherPyFiles2 = await findOtherPyFiles(
5617
+ workPath,
5618
+ searchDirs,
5619
+ candidateSet2
5620
+ );
5621
+ const pyprojectResult2 = await getPyprojectEntrypointWithDiagnostics(workPath);
5622
+ const diagnostics3 = {
5623
+ existingWithoutEntrypoint: findDiagnostics.existingWithoutEntrypoint,
5624
+ otherPyFiles: otherPyFiles2,
5625
+ pyprojectAppScript: pyprojectResult2.appScript,
5626
+ pyprojectCheckedPaths: pyprojectResult2.checkedPaths
5627
+ };
5628
+ return {
5629
+ baseDir: djangoResult.detected.baseDir,
5630
+ error: makeDiagnosticError("django", diagnostics3)
5631
+ };
5632
+ }
5633
+ } else {
5634
+ const genericResult = await detectGenericPythonEntrypoint(workPath);
5635
+ findDiagnostics = genericResult.findDiagnostics;
5636
+ searchDirs = PYTHON_ENTRYPOINT_DIRS;
5637
+ if (genericResult.detected)
5638
+ return genericResult.detected;
5639
+ }
5640
+ const pyprojectResult = await getPyprojectEntrypointWithDiagnostics(workPath);
5641
+ if (pyprojectResult.entrypoint) {
5642
+ return { entrypoint: pyprojectResult.entrypoint };
5643
+ }
5644
+ const candidateSet = new Set(getCandidateEntrypointsInDirs(searchDirs));
5645
+ const otherPyFiles = await findOtherPyFiles(
5646
+ workPath,
5647
+ searchDirs,
5648
+ candidateSet
5649
+ );
5650
+ const diagnostics2 = {
5651
+ existingWithoutEntrypoint: findDiagnostics.existingWithoutEntrypoint,
5652
+ otherPyFiles,
5653
+ pyprojectAppScript: pyprojectResult.appScript,
5654
+ pyprojectCheckedPaths: pyprojectResult.checkedPaths
5655
+ };
5656
+ return { error: makeDiagnosticError(framework, diagnostics2) };
5447
5657
  }
5448
5658
 
5449
5659
  // src/start-dev-server.ts
@@ -5997,7 +6207,7 @@ var startDevServer = async (opts) => {
5997
6207
  filePath: entrypoint,
5998
6208
  // Schedule-triggered services create their own "app" wrapper dynamically.
5999
6209
  // Other services use handlerFunction as the entrypoint variable name.
6000
- varName: service?.type === "cron" || service?.type === "job" && service.trigger === "schedule" ? void 0 : handlerFunction
6210
+ varName: service && (0, import_build_utils11.isScheduleTriggeredService)(service) ? void 0 : handlerFunction
6001
6211
  } : void 0,
6002
6212
  service
6003
6213
  );
@@ -6794,6 +7004,7 @@ async function runDjangoCollectStatic(venvPath, workPath, env, outputStaticDir,
6794
7004
  // src/index.ts
6795
7005
  var import_python_analysis9 = require("@vercel/python-analysis");
6796
7006
  var writeFile = import_fs13.default.promises.writeFile;
7007
+ var PYTHON_ENTRYPOINT_DOCS_URL2 = "https://vercel.com/docs/functions/runtimes/python#python-entrypoints";
6797
7008
  var version = -1;
6798
7009
  async function runFrameworkHook(framework, ctx) {
6799
7010
  const hook = framework ? frameworkHooks[framework] : void 0;
@@ -6881,6 +7092,15 @@ async function downloadFilesInWorkPath({
6881
7092
  (0, import_build_utils16.debug)("Downloading user files...");
6882
7093
  let downloadedFiles = await (0, import_build_utils16.download)(files, workPath, meta);
6883
7094
  if (meta.isDev && entrypoint) {
7095
+ const normalizedEntrypoint = entrypoint.endsWith(".py") ? entrypoint : `${entrypoint}.py`;
7096
+ if (!hasProp(downloadedFiles, entrypoint) && !hasProp(downloadedFiles, normalizedEntrypoint)) {
7097
+ throw new import_build_utils16.NowBuildError({
7098
+ code: "PYTHON_ENTRYPOINT_NOT_FOUND",
7099
+ message: `Configured Python entrypoint "${normalizedEntrypoint}" was not found.`,
7100
+ link: PYTHON_ENTRYPOINT_DOCS_URL2,
7101
+ action: "Learn More"
7102
+ });
7103
+ }
6884
7104
  const { devCacheDir = (0, import_path13.join)(workPath, ".now", "cache") } = meta;
6885
7105
  const cacheKey = (0, import_path13.basename)(entrypoint).replace(/\./g, "_");
6886
7106
  const destCache = (0, import_path13.join)(devCacheDir, cacheKey);
@@ -6932,7 +7152,7 @@ var build = async ({
6932
7152
  filePath: entrypoint,
6933
7153
  // For schedule-triggered jobs, the WSGI variable is always 'app' (created dynamically).
6934
7154
  // For other services, handlerFunction is used as the entrypoint variable name.
6935
- varName: service?.type === "cron" || service?.type === "job" && service.trigger === "schedule" ? void 0 : handlerFunction
7155
+ varName: service && (0, import_build_utils16.isScheduleTriggeredService)(service) ? void 0 : handlerFunction
6936
7156
  } : void 0,
6937
7157
  service
6938
7158
  ) ?? void 0;
@@ -6986,6 +7206,7 @@ var build = async ({
6986
7206
  const venvPath = service?.name ? (0, import_path13.join)(workPath, ".vercel", "python", "services", service.name, ".venv") : (0, import_path13.join)(workPath, ".vercel", "python", ".venv");
6987
7207
  const hasCachedVenv = import_fs13.default.existsSync((0, import_path13.join)(venvPath, "pyvenv.cfg"));
6988
7208
  const hasCachedUv = import_fs13.default.existsSync(uvCacheDir);
7209
+ const restoredCache = hasCachedVenv && hasCachedUv ? "both" : hasCachedVenv ? "venv" : hasCachedUv ? "uv" : "none";
6989
7210
  if (hasCachedVenv || hasCachedUv) {
6990
7211
  (0, import_build_utils16.debug)(
6991
7212
  `Build cache detected: venv=${hasCachedVenv}, uv-cache=${hasCachedUv}`
@@ -7032,7 +7253,9 @@ var build = async ({
7032
7253
  let uvProjectDir = null;
7033
7254
  let projectName;
7034
7255
  await builderSpan.child(import_build_utils16.BUILDER_INSTALLER_STEP, {
7035
- installCommand: projectInstallCommand || void 0
7256
+ installCommand: projectInstallCommand || void 0,
7257
+ runtime: "python",
7258
+ "python.cache.restored": restoredCache
7036
7259
  }).trace(async () => {
7037
7260
  if (projectInstallCommand) {
7038
7261
  await import_fs13.default.promises.rm(venvPath, { recursive: true, force: true });
@@ -7313,7 +7536,8 @@ from vercel_runtime.vc_init import vc_handler
7313
7536
  pythonPackage,
7314
7537
  pythonVersion,
7315
7538
  uvLockPath,
7316
- framework
7539
+ framework,
7540
+ serviceType: service ? (0, import_build_utils16.getReportedServiceType)(service) : void 0
7317
7541
  });
7318
7542
  } catch (err) {
7319
7543
  (0, import_build_utils16.debug)(
@@ -7343,9 +7567,6 @@ var prepareCache = async ({
7343
7567
  repoRootPath,
7344
7568
  workPath
7345
7569
  }) => {
7346
- if (process.env.VERCEL_PYTHON_PREPARE_CACHE !== "1") {
7347
- return {};
7348
- }
7349
7570
  const root = repoRootPath || workPath;
7350
7571
  const ignore = ["**/*.pyc", "**/__pycache__/**"];
7351
7572
  const uvCacheDir = getUvCacheDir(workPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/python",
3
- "version": "6.37.0",
3
+ "version": "6.39.0",
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,6 @@
21
21
  "@renovatebot/pep440": "4.2.1",
22
22
  "@types/execa": "^0.9.0",
23
23
  "@types/fs-extra": "11.0.2",
24
- "@types/jest": "27.4.1",
25
24
  "@types/node": "20.11.0",
26
25
  "@types/which": "3.0.0",
27
26
  "cross-env": "7.0.3",
@@ -30,23 +29,23 @@
30
29
  "fs-extra": "11.1.1",
31
30
  "get-port": "5.1.1",
32
31
  "is-port-reachable": "3.1.0",
33
- "jest-junit": "16.0.0",
34
32
  "minimatch": "10.1.1",
35
33
  "pip-requirements-js": "1.0.2",
36
34
  "smol-toml": "1.5.2",
37
35
  "vitest": "2.1.4",
38
36
  "which": "3.0.0",
39
- "@vercel/python-runtime": "0.13.1",
40
- "@vercel/build-utils": "13.21.0",
41
- "@vercel/error-utils": "2.1.0"
37
+ "@vercel/error-utils": "2.1.0",
38
+ "@vercel/python-runtime": "0.13.2",
39
+ "@vercel/build-utils": "13.22.0"
42
40
  },
43
41
  "scripts": {
44
42
  "build": "node ../../utils/build-builder.mjs",
45
43
  "type-check": "tsc --noEmit",
46
- "test": "cross-env VERCEL_FORCE_PYTHON_STREAMING=1 NODE_OPTIONS=--experimental-vm-modules jest --reporters=default --reporters=jest-junit --env node --verbose --runInBand --bail",
44
+ "test": "cross-env VERCEL_FORCE_PYTHON_STREAMING=1 NODE_OPTIONS=--experimental-vm-modules vitest run --config ../../vitest.config.mts",
47
45
  "test-unit": "vitest run --config ../../vitest.config.mts test/unit.test.ts",
48
46
  "test-e2e": "pnpm test test/integration-*",
49
47
  "vitest-run": "vitest -c ../../vitest.config.mts",
50
- "vitest-unit": "glob --absolute 'test/unit.test.ts'"
48
+ "vitest-unit": "glob --absolute 'test/unit.test.ts'",
49
+ "vitest-e2e": "glob --absolute 'test/integration-*'"
51
50
  }
52
51
  }