Skip to content

Commit b12b775

Browse files
committed
feat: implement init notebook behavior
1 parent 338e054 commit b12b775

10 files changed

+761
-74
lines changed

DEEPNOTE_KERNEL_IMPLEMENTATION.md

Lines changed: 356 additions & 63 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ This extension allows you to work with Deepnote notebooks in VS Code. Deepnote n
99
- **More block types** - Choose from SQL blocks, chart blocks, and more specialized data science blocks
1010
- **Seamless language switching** - Switch between Python and SQL seamlessly within the same notebook
1111
- **Database integrations** - Connect directly to Postgres, Snowflake, BigQuery and more data sources
12+
- **Init notebooks** - Automatically runs initialization code (like dependency installation) before your notebooks execute
13+
- **Project requirements** - Automatically creates `requirements.txt` from your project settings for easy dependency management
1214

1315
## Useful commands
1416

src/kernels/deepnote/deepnoteToolkitInstaller.node.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ export class DeepnoteToolkitInstaller implements IDeepnoteToolkitInstaller {
262262
}
263263
}
264264

265-
private getVenvHash(deepnoteFileUri: Uri): string {
265+
public getVenvHash(deepnoteFileUri: Uri): string {
266266
// Create a short hash from the file path for kernel naming and venv directory
267267
// This provides better uniqueness and prevents directory structure leakage
268268
const path = deepnoteFileUri.fsPath;

src/kernels/deepnote/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ export interface IDeepnoteToolkitInstaller {
8585
* @param deepnoteFileUri The URI of the .deepnote file
8686
*/
8787
getVenvInterpreter(deepnoteFileUri: vscode.Uri): Promise<PythonEnvironment | undefined>;
88+
89+
/**
90+
* Gets the hash for the venv directory/kernel spec name based on file path.
91+
* @param deepnoteFileUri The URI of the .deepnote file
92+
* @returns The hash string used for venv directory and kernel spec naming
93+
*/
94+
getVenvHash(deepnoteFileUri: vscode.Uri): string;
8895
}
8996

9097
export const IDeepnoteServerStarter = Symbol('IDeepnoteServerStarter');
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { inject, injectable } from 'inversify';
5+
import { NotebookDocument, ProgressLocation, window, CancellationTokenSource } from 'vscode';
6+
import { logger } from '../../platform/logging';
7+
import { IDeepnoteNotebookManager } from '../types';
8+
import { DeepnoteProject, DeepnoteNotebook } from './deepnoteTypes';
9+
import { IKernelProvider } from '../../kernels/types';
10+
import { getDisplayPath } from '../../platform/common/platform/fs-paths';
11+
12+
/**
13+
* Service responsible for running init notebooks before the main notebook starts.
14+
* Init notebooks typically contain setup code like pip installs.
15+
*/
16+
@injectable()
17+
export class DeepnoteInitNotebookRunner {
18+
constructor(
19+
@inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager,
20+
@inject(IKernelProvider) private readonly kernelProvider: IKernelProvider
21+
) {}
22+
23+
/**
24+
* Runs the init notebook if it exists and hasn't been run yet for this project.
25+
* This should be called after the kernel is started but before user code executes.
26+
* @param notebook The notebook document
27+
* @param projectId The Deepnote project ID
28+
*/
29+
async runInitNotebookIfNeeded(notebook: NotebookDocument, projectId: string): Promise<void> {
30+
try {
31+
// Check if init notebook has already run for this project
32+
if (this.notebookManager.hasInitNotebookRun(projectId)) {
33+
logger.info(`Init notebook already run for project ${projectId}, skipping`);
34+
return;
35+
}
36+
37+
// Get the project data
38+
const project = this.notebookManager.getOriginalProject(projectId) as DeepnoteProject | undefined;
39+
if (!project) {
40+
logger.warn(`Project ${projectId} not found, cannot run init notebook`);
41+
return;
42+
}
43+
44+
// Check if project has an init notebook ID
45+
const initNotebookId = (project.project as { initNotebookId?: string }).initNotebookId;
46+
if (!initNotebookId) {
47+
logger.info(`No init notebook configured for project ${projectId}`);
48+
// Mark as run so we don't check again
49+
this.notebookManager.markInitNotebookAsRun(projectId);
50+
return;
51+
}
52+
53+
// Find the init notebook
54+
const initNotebook = project.project.notebooks.find((nb) => nb.id === initNotebookId);
55+
if (!initNotebook) {
56+
logger.warn(
57+
`Init notebook ${initNotebookId} not found in project ${projectId}, skipping initialization`
58+
);
59+
this.notebookManager.markInitNotebookAsRun(projectId);
60+
return;
61+
}
62+
63+
logger.info(`Running init notebook "${initNotebook.name}" (${initNotebookId}) for project ${projectId}`);
64+
65+
// Execute the init notebook with progress
66+
const success = await this.executeInitNotebook(notebook, initNotebook);
67+
68+
if (success) {
69+
// Mark as run so we don't run it again
70+
this.notebookManager.markInitNotebookAsRun(projectId);
71+
logger.info(`Init notebook completed successfully for project ${projectId}`);
72+
} else {
73+
logger.warn(`Init notebook did not execute for project ${projectId} - kernel not available`);
74+
}
75+
} catch (error) {
76+
// Log error but don't throw - we want to let user continue anyway
77+
logger.error(`Error running init notebook for project ${projectId}:`, error);
78+
console.error(`Failed to run init notebook:`, error);
79+
// Still mark as run to avoid retrying on every notebook open
80+
this.notebookManager.markInitNotebookAsRun(projectId);
81+
}
82+
}
83+
84+
/**
85+
* Executes the init notebook's code blocks in the kernel.
86+
* @param notebook The notebook document (for kernel context)
87+
* @param initNotebook The init notebook to execute
88+
* @returns True if execution completed, false if kernel was not available
89+
*/
90+
private async executeInitNotebook(notebook: NotebookDocument, initNotebook: DeepnoteNotebook): Promise<boolean> {
91+
// Show progress in both notification AND window for maximum visibility
92+
const cancellationTokenSource = new CancellationTokenSource();
93+
94+
// Create a wrapper that reports to both progress locations
95+
const executeWithDualProgress = async () => {
96+
return window.withProgress(
97+
{
98+
location: ProgressLocation.Notification,
99+
title: `🚀 Initializing project environment`,
100+
cancellable: false
101+
},
102+
async (notificationProgress) => {
103+
return window.withProgress(
104+
{
105+
location: ProgressLocation.Window,
106+
title: `Init: "${initNotebook.name}"`,
107+
cancellable: false
108+
},
109+
async (windowProgress) => {
110+
// Helper to report to both progress bars
111+
const reportProgress = (message: string, increment: number) => {
112+
notificationProgress.report({ message, increment });
113+
windowProgress.report({ message, increment });
114+
};
115+
116+
return this.executeInitNotebookImpl(
117+
notebook,
118+
initNotebook,
119+
reportProgress,
120+
cancellationTokenSource.token
121+
);
122+
}
123+
);
124+
}
125+
);
126+
};
127+
128+
try {
129+
return await executeWithDualProgress();
130+
} finally {
131+
cancellationTokenSource.dispose();
132+
}
133+
}
134+
135+
private async executeInitNotebookImpl(
136+
notebook: NotebookDocument,
137+
initNotebook: DeepnoteNotebook,
138+
progress: (message: string, increment: number) => void,
139+
_token: unknown
140+
): Promise<boolean> {
141+
try {
142+
progress(`Running init notebook "${initNotebook.name}"...`, 0);
143+
144+
// Get the kernel for this notebook
145+
// Note: This should always exist because onKernelStarted already fired
146+
const kernel = this.kernelProvider.get(notebook);
147+
if (!kernel) {
148+
logger.error(
149+
`No kernel found for ${getDisplayPath(
150+
notebook.uri
151+
)} even after onDidStartKernel fired - this should not happen`
152+
);
153+
return false;
154+
}
155+
156+
logger.info(`Kernel found for ${getDisplayPath(notebook.uri)}, starting init notebook execution`);
157+
158+
// Filter out non-code blocks
159+
const codeBlocks = initNotebook.blocks.filter((block) => block.type === 'code');
160+
161+
if (codeBlocks.length === 0) {
162+
logger.info(`Init notebook has no code blocks, skipping execution`);
163+
return true; // Not an error - just nothing to execute
164+
}
165+
166+
logger.info(`Executing ${codeBlocks.length} code blocks from init notebook`);
167+
progress(
168+
`Preparing to execute ${codeBlocks.length} initialization ${
169+
codeBlocks.length === 1 ? 'block' : 'blocks'
170+
}...`,
171+
5
172+
);
173+
174+
// Get kernel execution
175+
const kernelExecution = this.kernelProvider.getKernelExecution(kernel);
176+
177+
// Execute each code block sequentially
178+
for (let i = 0; i < codeBlocks.length; i++) {
179+
const block = codeBlocks[i];
180+
const percentComplete = Math.floor((i / codeBlocks.length) * 100);
181+
182+
// Show more detailed progress with percentage
183+
progress(
184+
`[${percentComplete}%] Executing block ${i + 1} of ${codeBlocks.length}...`,
185+
90 / codeBlocks.length // Reserve 5% for start, 5% for finish
186+
);
187+
188+
logger.info(`Executing init notebook block ${i + 1}/${codeBlocks.length}`);
189+
190+
try {
191+
// Execute the code silently in the background
192+
const outputs = await kernelExecution.executeHidden(block.content ?? '');
193+
194+
// Log outputs for debugging
195+
if (outputs && outputs.length > 0) {
196+
logger.info(`Init notebook block ${i + 1} produced ${outputs.length} outputs`);
197+
198+
// Check for errors in outputs
199+
const errors = outputs.filter(
200+
(output: { output_type?: string }) => output.output_type === 'error'
201+
);
202+
if (errors.length > 0) {
203+
logger.warn(`Init notebook block ${i + 1} produced errors:`, errors);
204+
}
205+
}
206+
} catch (blockError) {
207+
// Log error but continue with next block
208+
logger.error(`Error executing init notebook block ${i + 1}:`, blockError);
209+
console.error(`Error in init notebook block ${i + 1}:`, blockError);
210+
}
211+
}
212+
213+
logger.info(`Completed executing all init notebook blocks`);
214+
progress(`✓ Initialization complete! Environment ready.`, 5);
215+
216+
// Give user a moment to see the completion message
217+
await new Promise((resolve) => setTimeout(resolve, 1000));
218+
219+
return true;
220+
} catch (error) {
221+
logger.error(`Error in executeInitNotebook:`, error);
222+
throw error;
223+
}
224+
}
225+
}
226+
227+
export const IDeepnoteInitNotebookRunner = Symbol('IDeepnoteInitNotebookRunner');
228+
export interface IDeepnoteInitNotebookRunner {
229+
runInitNotebookIfNeeded(notebook: NotebookDocument, projectId: string): Promise<void>;
230+
}

0 commit comments

Comments
 (0)