@deepnote/runtime-core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +70 -17
- package/dist/index.d.cts +22 -2
- package/dist/index.d.ts +22 -2
- package/dist/index.js +71 -19
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -84,7 +84,7 @@ var KernelClient = class {
|
|
|
84
84
|
while (this.kernel.status !== "idle") {
|
|
85
85
|
if (Date.now() - startTime > timeoutMs) throw new Error(`Kernel failed to reach idle status within ${timeoutMs}ms. Current status: ${this.kernel.status}`);
|
|
86
86
|
if (this.kernel.status === "dead") throw new Error("Kernel is dead");
|
|
87
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
87
|
+
await new Promise((resolve$1) => setTimeout(resolve$1, 100));
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
@@ -92,7 +92,7 @@ var KernelClient = class {
|
|
|
92
92
|
*/
|
|
93
93
|
async execute(code, callbacks) {
|
|
94
94
|
if (!this.kernel) throw new Error("Kernel not connected. Call connect() first.");
|
|
95
|
-
return new Promise((resolve, reject) => {
|
|
95
|
+
return new Promise((resolve$1, reject) => {
|
|
96
96
|
const outputs = [];
|
|
97
97
|
let executionCount = null;
|
|
98
98
|
const future = this.kernel?.requestExecute({ code });
|
|
@@ -122,7 +122,7 @@ var KernelClient = class {
|
|
|
122
122
|
executionCount
|
|
123
123
|
};
|
|
124
124
|
callbacks?.onDone?.(result);
|
|
125
|
-
resolve(result);
|
|
125
|
+
resolve$1(result);
|
|
126
126
|
}).catch(reject).finally(() => future?.dispose());
|
|
127
127
|
});
|
|
128
128
|
}
|
|
@@ -188,8 +188,10 @@ var KernelClient = class {
|
|
|
188
188
|
|
|
189
189
|
//#endregion
|
|
190
190
|
//#region src/python-env.ts
|
|
191
|
-
const
|
|
192
|
-
const
|
|
191
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
192
|
+
const PYTHON_EXECUTABLES = IS_WINDOWS ? ["python.exe", "python3.exe"] : ["python", "python3"];
|
|
193
|
+
const VENV_BIN_DIR = IS_WINDOWS ? "Scripts" : "bin";
|
|
194
|
+
const BARE_PYTHON_COMMAND = /^python[0-9.]*$/;
|
|
193
195
|
/**
|
|
194
196
|
* Resolves the Python executable path using smart detection.
|
|
195
197
|
*
|
|
@@ -204,7 +206,7 @@ const PYTHON_EXECUTABLES_WIN = ["python.exe", "python3.exe"];
|
|
|
204
206
|
* @throws Error if the path doesn't exist or no Python executable is found
|
|
205
207
|
*/
|
|
206
208
|
async function resolvePythonExecutable(pythonPath) {
|
|
207
|
-
if (pythonPath
|
|
209
|
+
if (isBareSystemPython(pythonPath)) return pythonPath;
|
|
208
210
|
let fileStat;
|
|
209
211
|
try {
|
|
210
212
|
fileStat = await (0, node_fs_promises.stat)(pythonPath);
|
|
@@ -218,15 +220,14 @@ async function resolvePythonExecutable(pythonPath) {
|
|
|
218
220
|
throw new Error(`Path is a file but doesn't appear to be a Python executable: ${pythonPath}\nExpected a file named python, python3, python.exe, or similar.`);
|
|
219
221
|
}
|
|
220
222
|
if (!fileStat.isDirectory()) throw new Error(`Python path is neither a file nor a directory: ${pythonPath}`);
|
|
221
|
-
const
|
|
222
|
-
const directPython = await findPythonInDirectory(pythonPath, candidates);
|
|
223
|
+
const directPython = await findPythonInDirectory(pythonPath, PYTHON_EXECUTABLES);
|
|
223
224
|
if (directPython) return directPython;
|
|
224
|
-
const binDir =
|
|
225
|
+
const binDir = (0, node_path.join)(pythonPath, VENV_BIN_DIR);
|
|
225
226
|
if ((await (0, node_fs_promises.stat)(binDir).catch(() => null))?.isDirectory()) {
|
|
226
|
-
const venvPython = await findPythonInDirectory(binDir,
|
|
227
|
+
const venvPython = await findPythonInDirectory(binDir, PYTHON_EXECUTABLES);
|
|
227
228
|
if (venvPython) return venvPython;
|
|
228
229
|
}
|
|
229
|
-
const searchedPaths = [...
|
|
230
|
+
const searchedPaths = [...PYTHON_EXECUTABLES.map((c) => (0, node_path.join)(pythonPath, c)), ...PYTHON_EXECUTABLES.map((c) => (0, node_path.join)(binDir, c))];
|
|
230
231
|
throw new Error(`No Python executable found at: ${pythonPath}\n\nSearched for:\n${searchedPaths.map((p) => ` - ${p}`).join("\n")}\n\nYou can pass:
|
|
231
232
|
- A Python executable: --python /path/to/venv/bin/python
|
|
232
233
|
- A bin directory: --python /path/to/venv/bin
|
|
@@ -255,6 +256,13 @@ function detectDefaultPython() {
|
|
|
255
256
|
throw new Error("No Python executable found.\n\nPlease ensure Python is installed and available in your PATH,\nor specify the path explicitly with --python <path>");
|
|
256
257
|
}
|
|
257
258
|
/**
|
|
259
|
+
* Checks if the given string is a bare system Python command (e.g. 'python', 'python3', 'python3.11')
|
|
260
|
+
* as opposed to an absolute/relative path to a Python executable.
|
|
261
|
+
*/
|
|
262
|
+
function isBareSystemPython(pythonPath) {
|
|
263
|
+
return BARE_PYTHON_COMMAND.test(pythonPath);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
258
266
|
* Checks if a Python command is available on the system.
|
|
259
267
|
*/
|
|
260
268
|
function isPythonAvailable(command) {
|
|
@@ -265,6 +273,46 @@ function isPythonAvailable(command) {
|
|
|
265
273
|
return false;
|
|
266
274
|
}
|
|
267
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Detects the virtual environment root for a given Python executable path.
|
|
278
|
+
*
|
|
279
|
+
* Checks for `pyvenv.cfg` which is the standard marker for Python venvs.
|
|
280
|
+
* Handles both `/path/to/venv/bin/python` and `/path/to/venv/Scripts/python.exe`.
|
|
281
|
+
*
|
|
282
|
+
* @returns The venv root directory, or null if the Python is not in a venv
|
|
283
|
+
*/
|
|
284
|
+
async function detectVenvRoot(pythonExecutable) {
|
|
285
|
+
const possibleVenvRoot = (0, node_path.dirname)((0, node_path.dirname)(pythonExecutable));
|
|
286
|
+
if ((await (0, node_fs_promises.stat)((0, node_path.join)(possibleVenvRoot, "pyvenv.cfg")).catch(() => null))?.isFile()) return possibleVenvRoot;
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Builds environment variables appropriate for the resolved Python executable.
|
|
291
|
+
*
|
|
292
|
+
* When a specific Python path is provided (not just 'python' or 'python3'),
|
|
293
|
+
* this ensures the spawned process environment is consistent with the specified
|
|
294
|
+
* Python by:
|
|
295
|
+
* - Prepending the Python's directory to PATH so subprocesses find the right Python
|
|
296
|
+
* - Setting VIRTUAL_ENV if the Python is in a venv
|
|
297
|
+
* - Clearing VIRTUAL_ENV if the Python is NOT in a venv (to avoid inheriting
|
|
298
|
+
* the current shell's active venv)
|
|
299
|
+
*
|
|
300
|
+
* @param resolvedPythonPath - The resolved path from resolvePythonExecutable()
|
|
301
|
+
* @param baseEnv - The base environment to modify (defaults to process.env)
|
|
302
|
+
* @returns Environment variables object for use with child_process.spawn()
|
|
303
|
+
*/
|
|
304
|
+
async function buildPythonEnv(resolvedPythonPath, baseEnv = process.env) {
|
|
305
|
+
const env = { ...baseEnv };
|
|
306
|
+
if (isBareSystemPython(resolvedPythonPath)) return env;
|
|
307
|
+
const pythonDir = (0, node_path.dirname)((0, node_path.resolve)(resolvedPythonPath));
|
|
308
|
+
const pathKey = Object.keys(env).find((k) => k.toLowerCase() === "path") || "PATH";
|
|
309
|
+
const currentPath = env[pathKey] || "";
|
|
310
|
+
env[pathKey] = currentPath ? `${pythonDir}${node_path.delimiter}${currentPath}` : pythonDir;
|
|
311
|
+
const venvRoot = await detectVenvRoot((0, node_path.resolve)(resolvedPythonPath));
|
|
312
|
+
if (venvRoot) env.VIRTUAL_ENV = venvRoot;
|
|
313
|
+
else delete env.VIRTUAL_ENV;
|
|
314
|
+
return env;
|
|
315
|
+
}
|
|
268
316
|
|
|
269
317
|
//#endregion
|
|
270
318
|
//#region src/server-starter.ts
|
|
@@ -280,7 +328,10 @@ async function startServer(options) {
|
|
|
280
328
|
const pythonPath = await resolvePythonExecutable(pythonEnv);
|
|
281
329
|
const jupyterPort = await findConsecutiveAvailablePorts(port ?? DEFAULT_PORT);
|
|
282
330
|
const lspPort = jupyterPort + 1;
|
|
283
|
-
const env =
|
|
331
|
+
const env = await buildPythonEnv(pythonPath, {
|
|
332
|
+
...process.env,
|
|
333
|
+
...options.env
|
|
334
|
+
});
|
|
284
335
|
env.DEEPNOTE_RUNTIME__RUNNING_IN_DETACHED_MODE = "true";
|
|
285
336
|
env.DEEPNOTE_ENFORCE_PIP_CONSTRAINTS = "true";
|
|
286
337
|
const serverProcess = (0, node_child_process.spawn)(pythonPath, [
|
|
@@ -335,14 +386,14 @@ async function startServer(options) {
|
|
|
335
386
|
async function stopServer(info) {
|
|
336
387
|
if (info.process.exitCode !== null) return;
|
|
337
388
|
info.process.kill("SIGTERM");
|
|
338
|
-
await new Promise((resolve) => {
|
|
389
|
+
await new Promise((resolve$1) => {
|
|
339
390
|
const timeout = setTimeout(() => {
|
|
340
391
|
if (info.process.exitCode === null) info.process.kill("SIGKILL");
|
|
341
|
-
resolve();
|
|
392
|
+
resolve$1();
|
|
342
393
|
}, 2e3);
|
|
343
394
|
info.process.once("exit", () => {
|
|
344
395
|
clearTimeout(timeout);
|
|
345
|
-
resolve();
|
|
396
|
+
resolve$1();
|
|
346
397
|
});
|
|
347
398
|
});
|
|
348
399
|
}
|
|
@@ -382,7 +433,7 @@ async function waitForServer(info, timeoutMs) {
|
|
|
382
433
|
throw new Error(`Server failed to start within ${timeoutMs}ms at ${info.url}`);
|
|
383
434
|
}
|
|
384
435
|
function sleep(ms) {
|
|
385
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
436
|
+
return new Promise((resolve$1) => setTimeout(resolve$1, ms));
|
|
386
437
|
}
|
|
387
438
|
|
|
388
439
|
//#endregion
|
|
@@ -442,7 +493,8 @@ var ExecutionEngine = class {
|
|
|
442
493
|
this.server = await startServer({
|
|
443
494
|
pythonEnv: this.config.pythonEnv,
|
|
444
495
|
workingDirectory: this.config.workingDirectory,
|
|
445
|
-
port: this.config.serverPort
|
|
496
|
+
port: this.config.serverPort,
|
|
497
|
+
env: this.config.env
|
|
446
498
|
});
|
|
447
499
|
try {
|
|
448
500
|
this.kernel = new KernelClient();
|
|
@@ -611,6 +663,7 @@ var ExecutionEngine = class {
|
|
|
611
663
|
//#endregion
|
|
612
664
|
exports.ExecutionEngine = ExecutionEngine;
|
|
613
665
|
exports.KernelClient = KernelClient;
|
|
666
|
+
exports.buildPythonEnv = buildPythonEnv;
|
|
614
667
|
exports.detectDefaultPython = detectDefaultPython;
|
|
615
668
|
exports.executableBlockTypeSet = executableBlockTypeSet;
|
|
616
669
|
exports.executableBlockTypes = executableBlockTypes;
|
package/dist/index.d.cts
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { IDisplayData, IError, IExecuteResult, IOutput, IOutput as IOutput$1, IStream } from "@jupyterlab/nbformat";
|
|
3
3
|
import { ChildProcess } from "node:child_process";
|
|
4
4
|
|
|
5
|
-
//#region ../blocks/src/
|
|
5
|
+
//#region ../blocks/src/deepnote-file/deepnote-file-schema.d.ts
|
|
6
6
|
|
|
7
7
|
declare const codeBlockSchema: z.ZodObject<{
|
|
8
8
|
type: z.ZodLiteral<"code">;
|
|
@@ -14689,6 +14689,8 @@ interface RuntimeConfig {
|
|
|
14689
14689
|
workingDirectory: string;
|
|
14690
14690
|
/** Optional port for the Jupyter server (auto-assigned if not provided) */
|
|
14691
14691
|
serverPort?: number;
|
|
14692
|
+
/** Optional environment variables to pass to the server */
|
|
14693
|
+
env?: Record<string, string>;
|
|
14692
14694
|
}
|
|
14693
14695
|
interface BlockExecutionResult {
|
|
14694
14696
|
blockId: string;
|
|
@@ -14862,6 +14864,22 @@ declare function resolvePythonExecutable(pythonPath: string): Promise<string>;
|
|
|
14862
14864
|
* @throws Error if neither python nor python3 is found
|
|
14863
14865
|
*/
|
|
14864
14866
|
declare function detectDefaultPython(): string;
|
|
14867
|
+
/**
|
|
14868
|
+
* Builds environment variables appropriate for the resolved Python executable.
|
|
14869
|
+
*
|
|
14870
|
+
* When a specific Python path is provided (not just 'python' or 'python3'),
|
|
14871
|
+
* this ensures the spawned process environment is consistent with the specified
|
|
14872
|
+
* Python by:
|
|
14873
|
+
* - Prepending the Python's directory to PATH so subprocesses find the right Python
|
|
14874
|
+
* - Setting VIRTUAL_ENV if the Python is in a venv
|
|
14875
|
+
* - Clearing VIRTUAL_ENV if the Python is NOT in a venv (to avoid inheriting
|
|
14876
|
+
* the current shell's active venv)
|
|
14877
|
+
*
|
|
14878
|
+
* @param resolvedPythonPath - The resolved path from resolvePythonExecutable()
|
|
14879
|
+
* @param baseEnv - The base environment to modify (defaults to process.env)
|
|
14880
|
+
* @returns Environment variables object for use with child_process.spawn()
|
|
14881
|
+
*/
|
|
14882
|
+
declare function buildPythonEnv(resolvedPythonPath: string, baseEnv?: Record<string, string | undefined>): Promise<Record<string, string | undefined>>;
|
|
14865
14883
|
//#endregion
|
|
14866
14884
|
//#region src/server-starter.d.ts
|
|
14867
14885
|
interface ServerInfo {
|
|
@@ -14879,6 +14897,8 @@ interface ServerOptions {
|
|
|
14879
14897
|
port?: number;
|
|
14880
14898
|
/** Optional timeout for server startup in milliseconds */
|
|
14881
14899
|
startupTimeoutMs?: number;
|
|
14900
|
+
/** Optional environment variables to pass to the server */
|
|
14901
|
+
env?: Record<string, string>;
|
|
14882
14902
|
}
|
|
14883
14903
|
/**
|
|
14884
14904
|
* Start the deepnote-toolkit Jupyter server.
|
|
@@ -14890,4 +14910,4 @@ declare function startServer(options: ServerOptions): Promise<ServerInfo>;
|
|
|
14890
14910
|
*/
|
|
14891
14911
|
declare function stopServer(info: ServerInfo): Promise<void>;
|
|
14892
14912
|
//#endregion
|
|
14893
|
-
export { type BlockExecutionResult, type DeepnoteBlock, type DeepnoteFile, type ExecutionCallbacks, ExecutionEngine, type ExecutionOptions, type ExecutionResult, type ExecutionSummary, type IDisplayData, type IError, type IExecuteResult, type IOutput, type IStream, KernelClient, type RuntimeConfig, type ServerInfo, type ServerOptions, detectDefaultPython, executableBlockTypeSet, executableBlockTypes, resolvePythonExecutable, startServer, stopServer };
|
|
14913
|
+
export { type BlockExecutionResult, type DeepnoteBlock, type DeepnoteFile, type ExecutionCallbacks, ExecutionEngine, type ExecutionOptions, type ExecutionResult, type ExecutionSummary, type IDisplayData, type IError, type IExecuteResult, type IOutput, type IStream, KernelClient, type RuntimeConfig, type ServerInfo, type ServerOptions, buildPythonEnv, detectDefaultPython, executableBlockTypeSet, executableBlockTypes, resolvePythonExecutable, startServer, stopServer };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { ChildProcess } from "node:child_process";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { IDisplayData, IError, IExecuteResult, IOutput, IOutput as IOutput$1, IStream } from "@jupyterlab/nbformat";
|
|
4
4
|
|
|
5
|
-
//#region ../blocks/src/
|
|
5
|
+
//#region ../blocks/src/deepnote-file/deepnote-file-schema.d.ts
|
|
6
6
|
|
|
7
7
|
declare const codeBlockSchema: z.ZodObject<{
|
|
8
8
|
type: z.ZodLiteral<"code">;
|
|
@@ -14689,6 +14689,8 @@ interface RuntimeConfig {
|
|
|
14689
14689
|
workingDirectory: string;
|
|
14690
14690
|
/** Optional port for the Jupyter server (auto-assigned if not provided) */
|
|
14691
14691
|
serverPort?: number;
|
|
14692
|
+
/** Optional environment variables to pass to the server */
|
|
14693
|
+
env?: Record<string, string>;
|
|
14692
14694
|
}
|
|
14693
14695
|
interface BlockExecutionResult {
|
|
14694
14696
|
blockId: string;
|
|
@@ -14862,6 +14864,22 @@ declare function resolvePythonExecutable(pythonPath: string): Promise<string>;
|
|
|
14862
14864
|
* @throws Error if neither python nor python3 is found
|
|
14863
14865
|
*/
|
|
14864
14866
|
declare function detectDefaultPython(): string;
|
|
14867
|
+
/**
|
|
14868
|
+
* Builds environment variables appropriate for the resolved Python executable.
|
|
14869
|
+
*
|
|
14870
|
+
* When a specific Python path is provided (not just 'python' or 'python3'),
|
|
14871
|
+
* this ensures the spawned process environment is consistent with the specified
|
|
14872
|
+
* Python by:
|
|
14873
|
+
* - Prepending the Python's directory to PATH so subprocesses find the right Python
|
|
14874
|
+
* - Setting VIRTUAL_ENV if the Python is in a venv
|
|
14875
|
+
* - Clearing VIRTUAL_ENV if the Python is NOT in a venv (to avoid inheriting
|
|
14876
|
+
* the current shell's active venv)
|
|
14877
|
+
*
|
|
14878
|
+
* @param resolvedPythonPath - The resolved path from resolvePythonExecutable()
|
|
14879
|
+
* @param baseEnv - The base environment to modify (defaults to process.env)
|
|
14880
|
+
* @returns Environment variables object for use with child_process.spawn()
|
|
14881
|
+
*/
|
|
14882
|
+
declare function buildPythonEnv(resolvedPythonPath: string, baseEnv?: Record<string, string | undefined>): Promise<Record<string, string | undefined>>;
|
|
14865
14883
|
//#endregion
|
|
14866
14884
|
//#region src/server-starter.d.ts
|
|
14867
14885
|
interface ServerInfo {
|
|
@@ -14879,6 +14897,8 @@ interface ServerOptions {
|
|
|
14879
14897
|
port?: number;
|
|
14880
14898
|
/** Optional timeout for server startup in milliseconds */
|
|
14881
14899
|
startupTimeoutMs?: number;
|
|
14900
|
+
/** Optional environment variables to pass to the server */
|
|
14901
|
+
env?: Record<string, string>;
|
|
14882
14902
|
}
|
|
14883
14903
|
/**
|
|
14884
14904
|
* Start the deepnote-toolkit Jupyter server.
|
|
@@ -14890,4 +14910,4 @@ declare function startServer(options: ServerOptions): Promise<ServerInfo>;
|
|
|
14890
14910
|
*/
|
|
14891
14911
|
declare function stopServer(info: ServerInfo): Promise<void>;
|
|
14892
14912
|
//#endregion
|
|
14893
|
-
export { type BlockExecutionResult, type DeepnoteBlock, type DeepnoteFile, type ExecutionCallbacks, ExecutionEngine, type ExecutionOptions, type ExecutionResult, type ExecutionSummary, type IDisplayData, type IError, type IExecuteResult, type IOutput, type IStream, KernelClient, type RuntimeConfig, type ServerInfo, type ServerOptions, detectDefaultPython, executableBlockTypeSet, executableBlockTypes, resolvePythonExecutable, startServer, stopServer };
|
|
14913
|
+
export { type BlockExecutionResult, type DeepnoteBlock, type DeepnoteFile, type ExecutionCallbacks, ExecutionEngine, type ExecutionOptions, type ExecutionResult, type ExecutionSummary, type IDisplayData, type IError, type IExecuteResult, type IOutput, type IStream, KernelClient, type RuntimeConfig, type ServerInfo, type ServerOptions, buildPythonEnv, detectDefaultPython, executableBlockTypeSet, executableBlockTypes, resolvePythonExecutable, startServer, stopServer };
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createPythonCode, decodeUtf8NoBom, deserializeDeepnoteFile, isExecutabl
|
|
|
3
3
|
import { KernelManager, ServerConnection, SessionManager } from "@jupyterlab/services";
|
|
4
4
|
import { execSync, spawn } from "node:child_process";
|
|
5
5
|
import tcpPortUsed from "tcp-port-used";
|
|
6
|
-
import { basename, join } from "node:path";
|
|
6
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
7
7
|
|
|
8
8
|
//#region src/kernel-client.ts
|
|
9
9
|
/**
|
|
@@ -55,7 +55,7 @@ var KernelClient = class {
|
|
|
55
55
|
while (this.kernel.status !== "idle") {
|
|
56
56
|
if (Date.now() - startTime > timeoutMs) throw new Error(`Kernel failed to reach idle status within ${timeoutMs}ms. Current status: ${this.kernel.status}`);
|
|
57
57
|
if (this.kernel.status === "dead") throw new Error("Kernel is dead");
|
|
58
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
58
|
+
await new Promise((resolve$1) => setTimeout(resolve$1, 100));
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
@@ -63,7 +63,7 @@ var KernelClient = class {
|
|
|
63
63
|
*/
|
|
64
64
|
async execute(code, callbacks) {
|
|
65
65
|
if (!this.kernel) throw new Error("Kernel not connected. Call connect() first.");
|
|
66
|
-
return new Promise((resolve, reject) => {
|
|
66
|
+
return new Promise((resolve$1, reject) => {
|
|
67
67
|
const outputs = [];
|
|
68
68
|
let executionCount = null;
|
|
69
69
|
const future = this.kernel?.requestExecute({ code });
|
|
@@ -93,7 +93,7 @@ var KernelClient = class {
|
|
|
93
93
|
executionCount
|
|
94
94
|
};
|
|
95
95
|
callbacks?.onDone?.(result);
|
|
96
|
-
resolve(result);
|
|
96
|
+
resolve$1(result);
|
|
97
97
|
}).catch(reject).finally(() => future?.dispose());
|
|
98
98
|
});
|
|
99
99
|
}
|
|
@@ -159,8 +159,10 @@ var KernelClient = class {
|
|
|
159
159
|
|
|
160
160
|
//#endregion
|
|
161
161
|
//#region src/python-env.ts
|
|
162
|
-
const
|
|
163
|
-
const
|
|
162
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
163
|
+
const PYTHON_EXECUTABLES = IS_WINDOWS ? ["python.exe", "python3.exe"] : ["python", "python3"];
|
|
164
|
+
const VENV_BIN_DIR = IS_WINDOWS ? "Scripts" : "bin";
|
|
165
|
+
const BARE_PYTHON_COMMAND = /^python[0-9.]*$/;
|
|
164
166
|
/**
|
|
165
167
|
* Resolves the Python executable path using smart detection.
|
|
166
168
|
*
|
|
@@ -175,7 +177,7 @@ const PYTHON_EXECUTABLES_WIN = ["python.exe", "python3.exe"];
|
|
|
175
177
|
* @throws Error if the path doesn't exist or no Python executable is found
|
|
176
178
|
*/
|
|
177
179
|
async function resolvePythonExecutable(pythonPath) {
|
|
178
|
-
if (pythonPath
|
|
180
|
+
if (isBareSystemPython(pythonPath)) return pythonPath;
|
|
179
181
|
let fileStat;
|
|
180
182
|
try {
|
|
181
183
|
fileStat = await stat(pythonPath);
|
|
@@ -189,15 +191,14 @@ async function resolvePythonExecutable(pythonPath) {
|
|
|
189
191
|
throw new Error(`Path is a file but doesn't appear to be a Python executable: ${pythonPath}\nExpected a file named python, python3, python.exe, or similar.`);
|
|
190
192
|
}
|
|
191
193
|
if (!fileStat.isDirectory()) throw new Error(`Python path is neither a file nor a directory: ${pythonPath}`);
|
|
192
|
-
const
|
|
193
|
-
const directPython = await findPythonInDirectory(pythonPath, candidates);
|
|
194
|
+
const directPython = await findPythonInDirectory(pythonPath, PYTHON_EXECUTABLES);
|
|
194
195
|
if (directPython) return directPython;
|
|
195
|
-
const binDir =
|
|
196
|
+
const binDir = join(pythonPath, VENV_BIN_DIR);
|
|
196
197
|
if ((await stat(binDir).catch(() => null))?.isDirectory()) {
|
|
197
|
-
const venvPython = await findPythonInDirectory(binDir,
|
|
198
|
+
const venvPython = await findPythonInDirectory(binDir, PYTHON_EXECUTABLES);
|
|
198
199
|
if (venvPython) return venvPython;
|
|
199
200
|
}
|
|
200
|
-
const searchedPaths = [...
|
|
201
|
+
const searchedPaths = [...PYTHON_EXECUTABLES.map((c) => join(pythonPath, c)), ...PYTHON_EXECUTABLES.map((c) => join(binDir, c))];
|
|
201
202
|
throw new Error(`No Python executable found at: ${pythonPath}\n\nSearched for:\n${searchedPaths.map((p) => ` - ${p}`).join("\n")}\n\nYou can pass:
|
|
202
203
|
- A Python executable: --python /path/to/venv/bin/python
|
|
203
204
|
- A bin directory: --python /path/to/venv/bin
|
|
@@ -226,6 +227,13 @@ function detectDefaultPython() {
|
|
|
226
227
|
throw new Error("No Python executable found.\n\nPlease ensure Python is installed and available in your PATH,\nor specify the path explicitly with --python <path>");
|
|
227
228
|
}
|
|
228
229
|
/**
|
|
230
|
+
* Checks if the given string is a bare system Python command (e.g. 'python', 'python3', 'python3.11')
|
|
231
|
+
* as opposed to an absolute/relative path to a Python executable.
|
|
232
|
+
*/
|
|
233
|
+
function isBareSystemPython(pythonPath) {
|
|
234
|
+
return BARE_PYTHON_COMMAND.test(pythonPath);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
229
237
|
* Checks if a Python command is available on the system.
|
|
230
238
|
*/
|
|
231
239
|
function isPythonAvailable(command) {
|
|
@@ -236,6 +244,46 @@ function isPythonAvailable(command) {
|
|
|
236
244
|
return false;
|
|
237
245
|
}
|
|
238
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Detects the virtual environment root for a given Python executable path.
|
|
249
|
+
*
|
|
250
|
+
* Checks for `pyvenv.cfg` which is the standard marker for Python venvs.
|
|
251
|
+
* Handles both `/path/to/venv/bin/python` and `/path/to/venv/Scripts/python.exe`.
|
|
252
|
+
*
|
|
253
|
+
* @returns The venv root directory, or null if the Python is not in a venv
|
|
254
|
+
*/
|
|
255
|
+
async function detectVenvRoot(pythonExecutable) {
|
|
256
|
+
const possibleVenvRoot = dirname(dirname(pythonExecutable));
|
|
257
|
+
if ((await stat(join(possibleVenvRoot, "pyvenv.cfg")).catch(() => null))?.isFile()) return possibleVenvRoot;
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Builds environment variables appropriate for the resolved Python executable.
|
|
262
|
+
*
|
|
263
|
+
* When a specific Python path is provided (not just 'python' or 'python3'),
|
|
264
|
+
* this ensures the spawned process environment is consistent with the specified
|
|
265
|
+
* Python by:
|
|
266
|
+
* - Prepending the Python's directory to PATH so subprocesses find the right Python
|
|
267
|
+
* - Setting VIRTUAL_ENV if the Python is in a venv
|
|
268
|
+
* - Clearing VIRTUAL_ENV if the Python is NOT in a venv (to avoid inheriting
|
|
269
|
+
* the current shell's active venv)
|
|
270
|
+
*
|
|
271
|
+
* @param resolvedPythonPath - The resolved path from resolvePythonExecutable()
|
|
272
|
+
* @param baseEnv - The base environment to modify (defaults to process.env)
|
|
273
|
+
* @returns Environment variables object for use with child_process.spawn()
|
|
274
|
+
*/
|
|
275
|
+
async function buildPythonEnv(resolvedPythonPath, baseEnv = process.env) {
|
|
276
|
+
const env = { ...baseEnv };
|
|
277
|
+
if (isBareSystemPython(resolvedPythonPath)) return env;
|
|
278
|
+
const pythonDir = dirname(resolve(resolvedPythonPath));
|
|
279
|
+
const pathKey = Object.keys(env).find((k) => k.toLowerCase() === "path") || "PATH";
|
|
280
|
+
const currentPath = env[pathKey] || "";
|
|
281
|
+
env[pathKey] = currentPath ? `${pythonDir}${delimiter}${currentPath}` : pythonDir;
|
|
282
|
+
const venvRoot = await detectVenvRoot(resolve(resolvedPythonPath));
|
|
283
|
+
if (venvRoot) env.VIRTUAL_ENV = venvRoot;
|
|
284
|
+
else delete env.VIRTUAL_ENV;
|
|
285
|
+
return env;
|
|
286
|
+
}
|
|
239
287
|
|
|
240
288
|
//#endregion
|
|
241
289
|
//#region src/server-starter.ts
|
|
@@ -251,7 +299,10 @@ async function startServer(options) {
|
|
|
251
299
|
const pythonPath = await resolvePythonExecutable(pythonEnv);
|
|
252
300
|
const jupyterPort = await findConsecutiveAvailablePorts(port ?? DEFAULT_PORT);
|
|
253
301
|
const lspPort = jupyterPort + 1;
|
|
254
|
-
const env =
|
|
302
|
+
const env = await buildPythonEnv(pythonPath, {
|
|
303
|
+
...process.env,
|
|
304
|
+
...options.env
|
|
305
|
+
});
|
|
255
306
|
env.DEEPNOTE_RUNTIME__RUNNING_IN_DETACHED_MODE = "true";
|
|
256
307
|
env.DEEPNOTE_ENFORCE_PIP_CONSTRAINTS = "true";
|
|
257
308
|
const serverProcess = spawn(pythonPath, [
|
|
@@ -306,14 +357,14 @@ async function startServer(options) {
|
|
|
306
357
|
async function stopServer(info) {
|
|
307
358
|
if (info.process.exitCode !== null) return;
|
|
308
359
|
info.process.kill("SIGTERM");
|
|
309
|
-
await new Promise((resolve) => {
|
|
360
|
+
await new Promise((resolve$1) => {
|
|
310
361
|
const timeout = setTimeout(() => {
|
|
311
362
|
if (info.process.exitCode === null) info.process.kill("SIGKILL");
|
|
312
|
-
resolve();
|
|
363
|
+
resolve$1();
|
|
313
364
|
}, 2e3);
|
|
314
365
|
info.process.once("exit", () => {
|
|
315
366
|
clearTimeout(timeout);
|
|
316
|
-
resolve();
|
|
367
|
+
resolve$1();
|
|
317
368
|
});
|
|
318
369
|
});
|
|
319
370
|
}
|
|
@@ -353,7 +404,7 @@ async function waitForServer(info, timeoutMs) {
|
|
|
353
404
|
throw new Error(`Server failed to start within ${timeoutMs}ms at ${info.url}`);
|
|
354
405
|
}
|
|
355
406
|
function sleep(ms) {
|
|
356
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
407
|
+
return new Promise((resolve$1) => setTimeout(resolve$1, ms));
|
|
357
408
|
}
|
|
358
409
|
|
|
359
410
|
//#endregion
|
|
@@ -413,7 +464,8 @@ var ExecutionEngine = class {
|
|
|
413
464
|
this.server = await startServer({
|
|
414
465
|
pythonEnv: this.config.pythonEnv,
|
|
415
466
|
workingDirectory: this.config.workingDirectory,
|
|
416
|
-
port: this.config.serverPort
|
|
467
|
+
port: this.config.serverPort,
|
|
468
|
+
env: this.config.env
|
|
417
469
|
});
|
|
418
470
|
try {
|
|
419
471
|
this.kernel = new KernelClient();
|
|
@@ -580,4 +632,4 @@ var ExecutionEngine = class {
|
|
|
580
632
|
};
|
|
581
633
|
|
|
582
634
|
//#endregion
|
|
583
|
-
export { ExecutionEngine, KernelClient, detectDefaultPython, executableBlockTypeSet, executableBlockTypes, resolvePythonExecutable, startServer, stopServer };
|
|
635
|
+
export { ExecutionEngine, KernelClient, buildPythonEnv, detectDefaultPython, executableBlockTypeSet, executableBlockTypes, resolvePythonExecutable, startServer, stopServer };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepnote/runtime-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Core runtime for executing Deepnote projects",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepnote",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@jupyterlab/services": "^7.3.2",
|
|
36
36
|
"tcp-port-used": "^1.0.2",
|
|
37
37
|
"ws": "^8.18.0",
|
|
38
|
-
"@deepnote/blocks": "4.
|
|
38
|
+
"@deepnote/blocks": "4.3.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/tcp-port-used": "^1.0.4",
|