@deepnote/runtime-core 0.1.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/LICENSE +201 -0
- package/README.md +75 -0
- package/dist/index.cjs +619 -0
- package/dist/index.d.cts +14893 -0
- package/dist/index.d.ts +14893 -0
- package/dist/index.js +583 -0
- package/package.json +53 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
let node_fs_promises = require("node:fs/promises");
|
|
25
|
+
node_fs_promises = __toESM(node_fs_promises);
|
|
26
|
+
let __deepnote_blocks = require("@deepnote/blocks");
|
|
27
|
+
__deepnote_blocks = __toESM(__deepnote_blocks);
|
|
28
|
+
let __jupyterlab_services = require("@jupyterlab/services");
|
|
29
|
+
__jupyterlab_services = __toESM(__jupyterlab_services);
|
|
30
|
+
let node_child_process = require("node:child_process");
|
|
31
|
+
node_child_process = __toESM(node_child_process);
|
|
32
|
+
let tcp_port_used = require("tcp-port-used");
|
|
33
|
+
tcp_port_used = __toESM(tcp_port_used);
|
|
34
|
+
let node_path = require("node:path");
|
|
35
|
+
node_path = __toESM(node_path);
|
|
36
|
+
|
|
37
|
+
//#region src/kernel-client.ts
|
|
38
|
+
/**
|
|
39
|
+
* Client for communicating with a Jupyter kernel via the Jupyter protocol.
|
|
40
|
+
*/
|
|
41
|
+
var KernelClient = class {
|
|
42
|
+
kernelManager = null;
|
|
43
|
+
sessionManager = null;
|
|
44
|
+
session = null;
|
|
45
|
+
kernel = null;
|
|
46
|
+
/**
|
|
47
|
+
* Connect to a Jupyter server and start a kernel session.
|
|
48
|
+
*/
|
|
49
|
+
async connect(serverUrl) {
|
|
50
|
+
try {
|
|
51
|
+
const url = new URL(serverUrl);
|
|
52
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
53
|
+
const wsUrl = url.toString();
|
|
54
|
+
const serverSettings = __jupyterlab_services.ServerConnection.makeSettings({
|
|
55
|
+
baseUrl: serverUrl,
|
|
56
|
+
wsUrl
|
|
57
|
+
});
|
|
58
|
+
this.kernelManager = new __jupyterlab_services.KernelManager({ serverSettings });
|
|
59
|
+
this.sessionManager = new __jupyterlab_services.SessionManager({
|
|
60
|
+
kernelManager: this.kernelManager,
|
|
61
|
+
serverSettings
|
|
62
|
+
});
|
|
63
|
+
await this.sessionManager.ready;
|
|
64
|
+
this.session = await this.sessionManager.startNew({
|
|
65
|
+
name: "deepnote-cli",
|
|
66
|
+
path: "deepnote-cli",
|
|
67
|
+
type: "notebook",
|
|
68
|
+
kernel: { name: "python3" }
|
|
69
|
+
});
|
|
70
|
+
this.kernel = this.session.kernel;
|
|
71
|
+
if (!this.kernel) throw new Error("Failed to start kernel");
|
|
72
|
+
await this.waitForKernelIdle();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
await this.disconnect();
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Wait for the kernel to reach idle status.
|
|
80
|
+
*/
|
|
81
|
+
async waitForKernelIdle(timeoutMs = 3e4) {
|
|
82
|
+
if (!this.kernel) return;
|
|
83
|
+
const startTime = Date.now();
|
|
84
|
+
while (this.kernel.status !== "idle") {
|
|
85
|
+
if (Date.now() - startTime > timeoutMs) throw new Error(`Kernel failed to reach idle status within ${timeoutMs}ms. Current status: ${this.kernel.status}`);
|
|
86
|
+
if (this.kernel.status === "dead") throw new Error("Kernel is dead");
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Execute code on the kernel and collect outputs.
|
|
92
|
+
*/
|
|
93
|
+
async execute(code, callbacks) {
|
|
94
|
+
if (!this.kernel) throw new Error("Kernel not connected. Call connect() first.");
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const outputs = [];
|
|
97
|
+
let executionCount = null;
|
|
98
|
+
const future = this.kernel?.requestExecute({ code });
|
|
99
|
+
if (!future) {
|
|
100
|
+
reject(/* @__PURE__ */ new Error("Failed to execute code on kernel"));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
callbacks?.onStart?.();
|
|
104
|
+
future.onIOPub = (msg) => {
|
|
105
|
+
const msgType = msg.header.msg_type;
|
|
106
|
+
if (msgType === "execute_input") executionCount = msg.content.execution_count ?? null;
|
|
107
|
+
else if ([
|
|
108
|
+
"stream",
|
|
109
|
+
"execute_result",
|
|
110
|
+
"display_data",
|
|
111
|
+
"error"
|
|
112
|
+
].includes(msgType)) {
|
|
113
|
+
const output = this.messageToOutput(msg);
|
|
114
|
+
outputs.push(output);
|
|
115
|
+
callbacks?.onOutput?.(output);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
future.done.then(() => {
|
|
119
|
+
const result = {
|
|
120
|
+
success: !outputs.some((o) => o.output_type === "error"),
|
|
121
|
+
outputs,
|
|
122
|
+
executionCount
|
|
123
|
+
};
|
|
124
|
+
callbacks?.onDone?.(result);
|
|
125
|
+
resolve(result);
|
|
126
|
+
}).catch(reject).finally(() => future?.dispose());
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Disconnect from the kernel and clean up resources.
|
|
131
|
+
*/
|
|
132
|
+
async disconnect() {
|
|
133
|
+
if (this.session) {
|
|
134
|
+
try {
|
|
135
|
+
await this.session.shutdown();
|
|
136
|
+
} catch {}
|
|
137
|
+
this.session.dispose();
|
|
138
|
+
this.session = null;
|
|
139
|
+
}
|
|
140
|
+
if (this.sessionManager) {
|
|
141
|
+
this.sessionManager.dispose();
|
|
142
|
+
this.sessionManager = null;
|
|
143
|
+
}
|
|
144
|
+
if (this.kernelManager) {
|
|
145
|
+
this.kernelManager.dispose();
|
|
146
|
+
this.kernelManager = null;
|
|
147
|
+
}
|
|
148
|
+
this.kernel = null;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Convert a Jupyter message to an IOutput object.
|
|
152
|
+
*/
|
|
153
|
+
messageToOutput(msg) {
|
|
154
|
+
const msgType = msg.header.msg_type;
|
|
155
|
+
const content = msg.content;
|
|
156
|
+
switch (msgType) {
|
|
157
|
+
case "stream": return {
|
|
158
|
+
output_type: "stream",
|
|
159
|
+
name: content.name,
|
|
160
|
+
text: content.text
|
|
161
|
+
};
|
|
162
|
+
case "execute_result": return {
|
|
163
|
+
output_type: "execute_result",
|
|
164
|
+
data: content.data,
|
|
165
|
+
metadata: content.metadata ?? {},
|
|
166
|
+
execution_count: content.execution_count
|
|
167
|
+
};
|
|
168
|
+
case "display_data": return {
|
|
169
|
+
output_type: "display_data",
|
|
170
|
+
data: content.data,
|
|
171
|
+
metadata: content.metadata ?? {}
|
|
172
|
+
};
|
|
173
|
+
case "error": return {
|
|
174
|
+
output_type: "error",
|
|
175
|
+
ename: content.ename,
|
|
176
|
+
evalue: content.evalue,
|
|
177
|
+
traceback: content.traceback
|
|
178
|
+
};
|
|
179
|
+
default: return {
|
|
180
|
+
output_type: "error",
|
|
181
|
+
ename: "UnknownMsgType",
|
|
182
|
+
evalue: `Received unknown message type: ${msgType}`,
|
|
183
|
+
traceback: []
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/python-env.ts
|
|
191
|
+
const PYTHON_EXECUTABLES_UNIX = ["python", "python3"];
|
|
192
|
+
const PYTHON_EXECUTABLES_WIN = ["python.exe", "python3.exe"];
|
|
193
|
+
/**
|
|
194
|
+
* Resolves the Python executable path using smart detection.
|
|
195
|
+
*
|
|
196
|
+
* Accepts multiple input formats (similar to uv):
|
|
197
|
+
* - 'python' or 'python3' → uses system Python
|
|
198
|
+
* - '/path/to/python' (executable file) → uses it directly
|
|
199
|
+
* - '/path/to/venv/bin' (directory with python) → uses python from that directory
|
|
200
|
+
* - '/path/to/venv' (venv root with bin/python) → uses bin/python
|
|
201
|
+
*
|
|
202
|
+
* @param pythonPath - Path to Python executable, bin directory, or venv root
|
|
203
|
+
* @returns The resolved path to the Python executable
|
|
204
|
+
* @throws Error if the path doesn't exist or no Python executable is found
|
|
205
|
+
*/
|
|
206
|
+
async function resolvePythonExecutable(pythonPath) {
|
|
207
|
+
if (pythonPath === "python" || pythonPath === "python3") return pythonPath;
|
|
208
|
+
let fileStat;
|
|
209
|
+
try {
|
|
210
|
+
fileStat = await (0, node_fs_promises.stat)(pythonPath);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
const error = err;
|
|
213
|
+
if (error.code === "ENOENT") throw new Error(`Python path not found: ${pythonPath}`);
|
|
214
|
+
throw new Error(`Failed to access Python path: ${pythonPath} (${error.code}: ${error.message})`);
|
|
215
|
+
}
|
|
216
|
+
if (fileStat.isFile()) {
|
|
217
|
+
if ((0, node_path.basename)(pythonPath).toLowerCase().startsWith("python")) return pythonPath;
|
|
218
|
+
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
|
+
}
|
|
220
|
+
if (!fileStat.isDirectory()) throw new Error(`Python path is neither a file nor a directory: ${pythonPath}`);
|
|
221
|
+
const candidates = process.platform === "win32" ? PYTHON_EXECUTABLES_WIN : PYTHON_EXECUTABLES_UNIX;
|
|
222
|
+
const directPython = await findPythonInDirectory(pythonPath, candidates);
|
|
223
|
+
if (directPython) return directPython;
|
|
224
|
+
const binDir = process.platform === "win32" ? (0, node_path.join)(pythonPath, "Scripts") : (0, node_path.join)(pythonPath, "bin");
|
|
225
|
+
if ((await (0, node_fs_promises.stat)(binDir).catch(() => null))?.isDirectory()) {
|
|
226
|
+
const venvPython = await findPythonInDirectory(binDir, candidates);
|
|
227
|
+
if (venvPython) return venvPython;
|
|
228
|
+
}
|
|
229
|
+
const searchedPaths = [...candidates.map((c) => (0, node_path.join)(pythonPath, c)), ...candidates.map((c) => (0, node_path.join)(binDir, c))];
|
|
230
|
+
throw new Error(`No Python executable found at: ${pythonPath}\n\nSearched for:\n${searchedPaths.map((p) => ` - ${p}`).join("\n")}\n\nYou can pass:
|
|
231
|
+
- A Python executable: --python /path/to/venv/bin/python
|
|
232
|
+
- A bin directory: --python /path/to/venv/bin
|
|
233
|
+
- A venv root: --python /path/to/venv`);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Finds a Python executable in the given directory.
|
|
237
|
+
*/
|
|
238
|
+
async function findPythonInDirectory(dir, candidates) {
|
|
239
|
+
for (const candidate of candidates) {
|
|
240
|
+
const pythonPath = (0, node_path.join)(dir, candidate);
|
|
241
|
+
if ((await (0, node_fs_promises.stat)(pythonPath).catch(() => null))?.isFile()) return pythonPath;
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Detects the default Python command available on the system.
|
|
247
|
+
* Tries 'python' first, then falls back to 'python3'.
|
|
248
|
+
*
|
|
249
|
+
* @returns 'python' or 'python3' depending on what's available
|
|
250
|
+
* @throws Error if neither python nor python3 is found
|
|
251
|
+
*/
|
|
252
|
+
function detectDefaultPython() {
|
|
253
|
+
if (isPythonAvailable("python")) return "python";
|
|
254
|
+
if (isPythonAvailable("python3")) return "python3";
|
|
255
|
+
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
|
+
/**
|
|
258
|
+
* Checks if a Python command is available on the system.
|
|
259
|
+
*/
|
|
260
|
+
function isPythonAvailable(command) {
|
|
261
|
+
try {
|
|
262
|
+
(0, node_child_process.execSync)(`${command} --version`, { stdio: "ignore" });
|
|
263
|
+
return true;
|
|
264
|
+
} catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/server-starter.ts
|
|
271
|
+
const DEFAULT_PORT = 8888;
|
|
272
|
+
const SERVER_STARTUP_TIMEOUT_MS = 12e4;
|
|
273
|
+
const HEALTH_CHECK_INTERVAL_MS = 200;
|
|
274
|
+
/**
|
|
275
|
+
* Start the deepnote-toolkit Jupyter server.
|
|
276
|
+
* Spawns `python -m deepnote_toolkit server` and waits for it to be ready.
|
|
277
|
+
*/
|
|
278
|
+
async function startServer(options) {
|
|
279
|
+
const { pythonEnv, workingDirectory, port, startupTimeoutMs = SERVER_STARTUP_TIMEOUT_MS } = options;
|
|
280
|
+
const pythonPath = await resolvePythonExecutable(pythonEnv);
|
|
281
|
+
const jupyterPort = await findConsecutiveAvailablePorts(port ?? DEFAULT_PORT);
|
|
282
|
+
const lspPort = jupyterPort + 1;
|
|
283
|
+
const env = { ...process.env };
|
|
284
|
+
env.DEEPNOTE_RUNTIME__RUNNING_IN_DETACHED_MODE = "true";
|
|
285
|
+
env.DEEPNOTE_ENFORCE_PIP_CONSTRAINTS = "true";
|
|
286
|
+
const serverProcess = (0, node_child_process.spawn)(pythonPath, [
|
|
287
|
+
"-m",
|
|
288
|
+
"deepnote_toolkit",
|
|
289
|
+
"server",
|
|
290
|
+
"--jupyter-port",
|
|
291
|
+
String(jupyterPort),
|
|
292
|
+
"--ls-port",
|
|
293
|
+
String(lspPort)
|
|
294
|
+
], {
|
|
295
|
+
cwd: workingDirectory,
|
|
296
|
+
env,
|
|
297
|
+
stdio: [
|
|
298
|
+
"ignore",
|
|
299
|
+
"pipe",
|
|
300
|
+
"pipe"
|
|
301
|
+
]
|
|
302
|
+
});
|
|
303
|
+
const serverInfo = {
|
|
304
|
+
url: `http://localhost:${jupyterPort}`,
|
|
305
|
+
jupyterPort,
|
|
306
|
+
lspPort,
|
|
307
|
+
process: serverProcess
|
|
308
|
+
};
|
|
309
|
+
let stdout = "";
|
|
310
|
+
let stderr = "";
|
|
311
|
+
serverProcess.stdout?.on("data", (data) => {
|
|
312
|
+
stdout += data.toString();
|
|
313
|
+
if (stdout.length > 5e3) stdout = stdout.slice(-5e3);
|
|
314
|
+
});
|
|
315
|
+
serverProcess.stderr?.on("data", (data) => {
|
|
316
|
+
stderr += data.toString();
|
|
317
|
+
if (stderr.length > 5e3) stderr = stderr.slice(-5e3);
|
|
318
|
+
});
|
|
319
|
+
const exitPromise = new Promise((_, reject) => {
|
|
320
|
+
serverProcess.on("exit", (code, signal) => {
|
|
321
|
+
reject(/* @__PURE__ */ new Error(`Server process exited unexpectedly (code=${code}, signal=${signal}).\nstderr: ${stderr}`));
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
try {
|
|
325
|
+
await Promise.race([waitForServer(serverInfo, startupTimeoutMs), exitPromise]);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
serverProcess.kill("SIGKILL");
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
return serverInfo;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Stop the deepnote-toolkit server.
|
|
334
|
+
*/
|
|
335
|
+
async function stopServer(info) {
|
|
336
|
+
if (info.process.exitCode !== null) return;
|
|
337
|
+
info.process.kill("SIGTERM");
|
|
338
|
+
await new Promise((resolve) => {
|
|
339
|
+
const timeout = setTimeout(() => {
|
|
340
|
+
if (info.process.exitCode === null) info.process.kill("SIGKILL");
|
|
341
|
+
resolve();
|
|
342
|
+
}, 2e3);
|
|
343
|
+
info.process.once("exit", () => {
|
|
344
|
+
clearTimeout(timeout);
|
|
345
|
+
resolve();
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Find two consecutive available ports starting from the given port.
|
|
351
|
+
*/
|
|
352
|
+
async function findConsecutiveAvailablePorts(startPort) {
|
|
353
|
+
const maxAttempts = 100;
|
|
354
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
355
|
+
const candidatePort = startPort + attempt * 2;
|
|
356
|
+
const [portInUse, nextPortInUse] = await Promise.all([isPortInUse(candidatePort), isPortInUse(candidatePort + 1)]);
|
|
357
|
+
if (!portInUse && !nextPortInUse) return candidatePort;
|
|
358
|
+
}
|
|
359
|
+
throw new Error(`Could not find consecutive available ports after ${maxAttempts} attempts starting from ${startPort}`);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Check if a port is in use.
|
|
363
|
+
*/
|
|
364
|
+
async function isPortInUse(port) {
|
|
365
|
+
try {
|
|
366
|
+
return await tcp_port_used.default.check(port, "127.0.0.1");
|
|
367
|
+
} catch {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Wait for the server to respond to health checks.
|
|
373
|
+
*/
|
|
374
|
+
async function waitForServer(info, timeoutMs) {
|
|
375
|
+
const startTime = Date.now();
|
|
376
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
377
|
+
try {
|
|
378
|
+
if ((await fetch(`${info.url}/api`)).ok) return;
|
|
379
|
+
} catch {}
|
|
380
|
+
await sleep(HEALTH_CHECK_INTERVAL_MS);
|
|
381
|
+
}
|
|
382
|
+
throw new Error(`Server failed to start within ${timeoutMs}ms at ${info.url}`);
|
|
383
|
+
}
|
|
384
|
+
function sleep(ms) {
|
|
385
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/execution-engine.ts
|
|
390
|
+
const executableBlockTypes = [
|
|
391
|
+
"code",
|
|
392
|
+
"sql",
|
|
393
|
+
"notebook-function",
|
|
394
|
+
"visualization",
|
|
395
|
+
"button",
|
|
396
|
+
"big-number",
|
|
397
|
+
"input-text",
|
|
398
|
+
"input-textarea",
|
|
399
|
+
"input-checkbox",
|
|
400
|
+
"input-select",
|
|
401
|
+
"input-slider",
|
|
402
|
+
"input-date",
|
|
403
|
+
"input-date-range",
|
|
404
|
+
"input-file"
|
|
405
|
+
];
|
|
406
|
+
const executableBlockTypeSet = new Set(executableBlockTypes);
|
|
407
|
+
/**
|
|
408
|
+
* High-level execution engine for running Deepnote projects.
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* ```typescript
|
|
412
|
+
* const engine = new ExecutionEngine({
|
|
413
|
+
* pythonEnv: '/path/to/venv', // or 'python' for system Python
|
|
414
|
+
* workingDirectory: '/path/to/project',
|
|
415
|
+
* })
|
|
416
|
+
*
|
|
417
|
+
* await engine.start()
|
|
418
|
+
* try {
|
|
419
|
+
* const summary = await engine.runFile('./project.deepnote')
|
|
420
|
+
* console.log(`Executed ${summary.executedBlocks} blocks`)
|
|
421
|
+
* } finally {
|
|
422
|
+
* await engine.stop()
|
|
423
|
+
* }
|
|
424
|
+
* ```
|
|
425
|
+
*/
|
|
426
|
+
var ExecutionEngine = class {
|
|
427
|
+
server = null;
|
|
428
|
+
kernel = null;
|
|
429
|
+
constructor(config) {
|
|
430
|
+
this.config = config;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Get the Jupyter server port (available after start() is called).
|
|
434
|
+
*/
|
|
435
|
+
get serverPort() {
|
|
436
|
+
return this.server?.jupyterPort ?? null;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Start the deepnote-toolkit server and connect to the kernel.
|
|
440
|
+
*/
|
|
441
|
+
async start() {
|
|
442
|
+
this.server = await startServer({
|
|
443
|
+
pythonEnv: this.config.pythonEnv,
|
|
444
|
+
workingDirectory: this.config.workingDirectory,
|
|
445
|
+
port: this.config.serverPort
|
|
446
|
+
});
|
|
447
|
+
try {
|
|
448
|
+
this.kernel = new KernelClient();
|
|
449
|
+
await this.kernel.connect(this.server.url);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
await this.stop();
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Stop the server and disconnect from the kernel.
|
|
457
|
+
*/
|
|
458
|
+
async stop() {
|
|
459
|
+
if (this.kernel) {
|
|
460
|
+
await this.kernel.disconnect();
|
|
461
|
+
this.kernel = null;
|
|
462
|
+
}
|
|
463
|
+
if (this.server) {
|
|
464
|
+
await stopServer(this.server);
|
|
465
|
+
this.server = null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Run a .deepnote file.
|
|
470
|
+
*/
|
|
471
|
+
async runFile(filePath, options = {}) {
|
|
472
|
+
const file = (0, __deepnote_blocks.deserializeDeepnoteFile)((0, __deepnote_blocks.decodeUtf8NoBom)(await (0, node_fs_promises.readFile)(filePath)));
|
|
473
|
+
return this.runProject(file, options);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Run a parsed DeepnoteFile.
|
|
477
|
+
*/
|
|
478
|
+
async runProject(file, options = {}) {
|
|
479
|
+
if (!this.kernel) throw new Error("Engine not started. Call start() first.");
|
|
480
|
+
if (options.inputs && Object.keys(options.inputs).length > 0) await this.injectInputs(options.inputs);
|
|
481
|
+
const startTime = Date.now();
|
|
482
|
+
let executedBlocks = 0;
|
|
483
|
+
let failedBlocks = 0;
|
|
484
|
+
const notebooks = options.notebookName ? file.project.notebooks.filter((n) => n.name === options.notebookName) : file.project.notebooks;
|
|
485
|
+
if (options.notebookName && notebooks.length === 0) throw new Error(`Notebook "${options.notebookName}" not found in project`);
|
|
486
|
+
const blockIdFilter = options.blockIds ? new Set(options.blockIds) : options.blockId ? new Set([options.blockId]) : null;
|
|
487
|
+
const allExecutableBlocks = [];
|
|
488
|
+
for (const notebook of notebooks) {
|
|
489
|
+
const sortedBlocks = this.sortBlocks(notebook.blocks);
|
|
490
|
+
for (const block of sortedBlocks) if ((0, __deepnote_blocks.isExecutableBlock)(block)) {
|
|
491
|
+
if (blockIdFilter && !blockIdFilter.has(block.id)) continue;
|
|
492
|
+
allExecutableBlocks.push({
|
|
493
|
+
block,
|
|
494
|
+
notebookName: notebook.name
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (options.blockIds && allExecutableBlocks.length === 0 && options.blockIds.length > 0) for (const blockId of options.blockIds) this.assertExecutableBlockExists(blockId, notebooks);
|
|
499
|
+
const primaryBlockId = options.blockIds ? void 0 : options.blockId;
|
|
500
|
+
if (primaryBlockId && allExecutableBlocks.length === 0) this.assertExecutableBlockExists(primaryBlockId, notebooks);
|
|
501
|
+
const totalBlocks = allExecutableBlocks.length;
|
|
502
|
+
for (let i = 0; i < allExecutableBlocks.length; i++) {
|
|
503
|
+
const { block } = allExecutableBlocks[i];
|
|
504
|
+
const blockStart = Date.now();
|
|
505
|
+
await options.onBlockStart?.(block, i, totalBlocks);
|
|
506
|
+
try {
|
|
507
|
+
const code = (0, __deepnote_blocks.createPythonCode)(block);
|
|
508
|
+
const result = await this.kernel.execute(code, { onOutput: (output) => options.onOutput?.(block.id, output) });
|
|
509
|
+
const blockResult = {
|
|
510
|
+
blockId: block.id,
|
|
511
|
+
blockType: block.type,
|
|
512
|
+
success: result.success,
|
|
513
|
+
outputs: result.outputs,
|
|
514
|
+
executionCount: result.executionCount,
|
|
515
|
+
durationMs: Date.now() - blockStart
|
|
516
|
+
};
|
|
517
|
+
await options.onBlockDone?.(blockResult);
|
|
518
|
+
executedBlocks++;
|
|
519
|
+
if (!result.success) {
|
|
520
|
+
failedBlocks++;
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
} catch (error) {
|
|
524
|
+
failedBlocks++;
|
|
525
|
+
executedBlocks++;
|
|
526
|
+
const blockResult = {
|
|
527
|
+
blockId: block.id,
|
|
528
|
+
blockType: block.type,
|
|
529
|
+
success: false,
|
|
530
|
+
outputs: [],
|
|
531
|
+
executionCount: null,
|
|
532
|
+
durationMs: Date.now() - blockStart,
|
|
533
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
534
|
+
};
|
|
535
|
+
await options.onBlockDone?.(blockResult);
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
totalBlocks,
|
|
541
|
+
executedBlocks,
|
|
542
|
+
failedBlocks,
|
|
543
|
+
totalDurationMs: Date.now() - startTime
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Sort blocks by their sortingKey.
|
|
548
|
+
*/
|
|
549
|
+
sortBlocks(blocks) {
|
|
550
|
+
return [...blocks].sort((a, b) => a.sortingKey.localeCompare(b.sortingKey));
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Ensure a requested block exists in the selected notebooks and is executable.
|
|
554
|
+
*/
|
|
555
|
+
assertExecutableBlockExists(blockId, notebooks) {
|
|
556
|
+
for (const notebook of notebooks) {
|
|
557
|
+
const block = notebook.blocks.find((b) => b.id === blockId);
|
|
558
|
+
if (!block) continue;
|
|
559
|
+
if (!(0, __deepnote_blocks.isExecutableBlock)(block)) throw new Error(`Block "${blockId}" is not executable (type: ${block.type}).`);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
throw new Error(`Block "${blockId}" not found in project`);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Check if a string is a valid Python identifier.
|
|
566
|
+
* Python identifiers must start with a letter or underscore,
|
|
567
|
+
* followed by letters, digits, or underscores.
|
|
568
|
+
*/
|
|
569
|
+
isValidPythonIdentifier(name) {
|
|
570
|
+
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name);
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Inject input values into the kernel before execution.
|
|
574
|
+
* Converts values to Python literals and executes assignment statements.
|
|
575
|
+
*/
|
|
576
|
+
async injectInputs(inputs) {
|
|
577
|
+
if (!this.kernel) throw new Error("Engine not started. Call start() first.");
|
|
578
|
+
const assignments = [];
|
|
579
|
+
for (const [name, value] of Object.entries(inputs)) {
|
|
580
|
+
if (!this.isValidPythonIdentifier(name)) throw new Error(`Invalid variable name: "${name}". Must be a valid Python identifier.`);
|
|
581
|
+
const pythonValue = this.toPythonLiteral(value);
|
|
582
|
+
assignments.push(`${name} = ${pythonValue}`);
|
|
583
|
+
}
|
|
584
|
+
if (assignments.length > 0) {
|
|
585
|
+
const code = assignments.join("\n");
|
|
586
|
+
const result = await this.kernel.execute(code);
|
|
587
|
+
if (!result.success) {
|
|
588
|
+
const errorOutput = result.outputs.find((o) => o.output_type === "error");
|
|
589
|
+
const errorMsg = errorOutput && "evalue" in errorOutput ? String(errorOutput.evalue) : "Failed to inject inputs";
|
|
590
|
+
throw new Error(`Failed to set input values: ${errorMsg}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Convert a JavaScript value to a Python literal.
|
|
596
|
+
*/
|
|
597
|
+
toPythonLiteral(value) {
|
|
598
|
+
if (value === null || value === void 0) return "None";
|
|
599
|
+
if (typeof value === "boolean") return value ? "True" : "False";
|
|
600
|
+
if (typeof value === "number") {
|
|
601
|
+
if (!Number.isFinite(value)) throw new Error(`Cannot convert non-finite number to Python: ${value}`);
|
|
602
|
+
return String(value);
|
|
603
|
+
}
|
|
604
|
+
if (typeof value === "string") return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/\0/g, "\\x00").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, (char) => `\\x${char.charCodeAt(0).toString(16).padStart(2, "0")}`)}'`;
|
|
605
|
+
if (Array.isArray(value)) return `[${value.map((v) => this.toPythonLiteral(v)).join(", ")}]`;
|
|
606
|
+
if (typeof value === "object") return `{${Object.entries(value).map(([k, v]) => `${this.toPythonLiteral(k)}: ${this.toPythonLiteral(v)}`).join(", ")}}`;
|
|
607
|
+
throw new Error(`Cannot convert value of type ${typeof value} to Python literal`);
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
//#endregion
|
|
612
|
+
exports.ExecutionEngine = ExecutionEngine;
|
|
613
|
+
exports.KernelClient = KernelClient;
|
|
614
|
+
exports.detectDefaultPython = detectDefaultPython;
|
|
615
|
+
exports.executableBlockTypeSet = executableBlockTypeSet;
|
|
616
|
+
exports.executableBlockTypes = executableBlockTypes;
|
|
617
|
+
exports.resolvePythonExecutable = resolvePythonExecutable;
|
|
618
|
+
exports.startServer = startServer;
|
|
619
|
+
exports.stopServer = stopServer;
|