Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
419 changes: 356 additions & 63 deletions DEEPNOTE_KERNEL_IMPLEMENTATION.md

Large diffs are not rendered by default.

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

## Useful commands

Expand Down
2 changes: 2 additions & 0 deletions src/extension.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
import { Common } from './platform/common/utils/localize';
import { IServiceContainer, IServiceManager } from './platform/ioc/types';
import { initializeLoggers as init, logger } from './platform/logging';
import { ILogger } from './platform/logging/types';
import { getJupyterOutputChannel } from './standalone/devTools/jupyterOutputChannel';
import { isUsingPylance } from './standalone/intellisense/notebookPythonPathService';
import { noop } from './platform/common/utils/misc';
Expand Down Expand Up @@ -127,6 +128,7 @@ export function initializeGlobals(
getJupyterOutputChannel(context.subscriptions),
JUPYTER_OUTPUT_CHANNEL
);
serviceManager.addSingletonInstance<ILogger>(ILogger, logger);

return [serviceManager, serviceContainer];
}
Expand Down
2 changes: 1 addition & 1 deletion src/kernels/deepnote/deepnoteToolkitInstaller.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ export class DeepnoteToolkitInstaller implements IDeepnoteToolkitInstaller {
}
}

private getVenvHash(deepnoteFileUri: Uri): string {
public getVenvHash(deepnoteFileUri: Uri): string {
// Create a short hash from the file path for kernel naming and venv directory
// This provides better uniqueness and prevents directory structure leakage
const path = deepnoteFileUri.fsPath;
Expand Down
7 changes: 7 additions & 0 deletions src/kernels/deepnote/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ export interface IDeepnoteToolkitInstaller {
* @param deepnoteFileUri The URI of the .deepnote file
*/
getVenvInterpreter(deepnoteFileUri: vscode.Uri): Promise<PythonEnvironment | undefined>;

/**
* Gets the hash for the venv directory/kernel spec name based on file path.
* @param deepnoteFileUri The URI of the .deepnote file
* @returns The hash string used for venv directory and kernel spec naming
*/
getVenvHash(deepnoteFileUri: vscode.Uri): string;
}

export const IDeepnoteServerStarter = Symbol('IDeepnoteServerStarter');
Expand Down
228 changes: 228 additions & 0 deletions src/notebooks/deepnote/deepnoteInitNotebookRunner.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { inject, injectable } from 'inversify';
import { NotebookDocument, ProgressLocation, window, CancellationTokenSource } from 'vscode';
import { logger } from '../../platform/logging';
import { IDeepnoteNotebookManager } from '../types';
import { DeepnoteProject, DeepnoteNotebook } from './deepnoteTypes';
import { IKernelProvider } from '../../kernels/types';
import { getDisplayPath } from '../../platform/common/platform/fs-paths';

/**
* Service responsible for running init notebooks before the main notebook starts.
* Init notebooks typically contain setup code like pip installs.
*/
@injectable()
export class DeepnoteInitNotebookRunner {
constructor(
@inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager,
@inject(IKernelProvider) private readonly kernelProvider: IKernelProvider
) {}

/**
* Runs the init notebook if it exists and hasn't been run yet for this project.
* This should be called after the kernel is started but before user code executes.
* @param notebook The notebook document
* @param projectId The Deepnote project ID
*/
async runInitNotebookIfNeeded(notebook: NotebookDocument, projectId: string): Promise<void> {
try {
// Check if init notebook has already run for this project
if (this.notebookManager.hasInitNotebookRun(projectId)) {
logger.info(`Init notebook already run for project ${projectId}, skipping`);
return;
}

// Get the project data
const project = this.notebookManager.getOriginalProject(projectId) as DeepnoteProject | undefined;
if (!project) {
logger.warn(`Project ${projectId} not found, cannot run init notebook`);
return;
}

// Check if project has an init notebook ID
const initNotebookId = (project.project as { initNotebookId?: string }).initNotebookId;
if (!initNotebookId) {
logger.info(`No init notebook configured for project ${projectId}`);
// Mark as run so we don't check again
this.notebookManager.markInitNotebookAsRun(projectId);
return;
}

// Find the init notebook
const initNotebook = project.project.notebooks.find((nb) => nb.id === initNotebookId);
if (!initNotebook) {
logger.warn(
`Init notebook ${initNotebookId} not found in project ${projectId}, skipping initialization`
);
this.notebookManager.markInitNotebookAsRun(projectId);
return;
}

logger.info(`Running init notebook "${initNotebook.name}" (${initNotebookId}) for project ${projectId}`);

// Execute the init notebook with progress
const success = await this.executeInitNotebook(notebook, initNotebook);

if (success) {
// Mark as run so we don't run it again
this.notebookManager.markInitNotebookAsRun(projectId);
logger.info(`Init notebook completed successfully for project ${projectId}`);
} else {
logger.warn(`Init notebook did not execute for project ${projectId} - kernel not available`);
}
} catch (error) {
// Log error but don't throw - we want to let user continue anyway
logger.error(`Error running init notebook for project ${projectId}:`, error);
// Still mark as run to avoid retrying on every notebook open
this.notebookManager.markInitNotebookAsRun(projectId);
}
}

/**
* Executes the init notebook's code blocks in the kernel.
* @param notebook The notebook document (for kernel context)
* @param initNotebook The init notebook to execute
* @returns True if execution completed, false if kernel was not available
*/
private async executeInitNotebook(notebook: NotebookDocument, initNotebook: DeepnoteNotebook): Promise<boolean> {
// Show progress in both notification AND window for maximum visibility
const cancellationTokenSource = new CancellationTokenSource();

// Create a wrapper that reports to both progress locations
const executeWithDualProgress = async () => {
return window.withProgress(
{
location: ProgressLocation.Notification,
title: `🚀 Initializing project environment`,
cancellable: false
},
async (notificationProgress) => {
return window.withProgress(
{
location: ProgressLocation.Window,
title: `Init: "${initNotebook.name}"`,
cancellable: false
},
async (windowProgress) => {
// Helper to report to both progress bars
const reportProgress = (message: string, increment: number) => {
notificationProgress.report({ message, increment });
windowProgress.report({ message, increment });
};

return this.executeInitNotebookImpl(
notebook,
initNotebook,
reportProgress,
cancellationTokenSource.token
);
}
);
}
);
};

try {
return await executeWithDualProgress();
} finally {
cancellationTokenSource.dispose();
}
}

private async executeInitNotebookImpl(
notebook: NotebookDocument,
initNotebook: DeepnoteNotebook,
progress: (message: string, increment: number) => void,
_token: unknown
): Promise<boolean> {
try {
progress(`Running init notebook "${initNotebook.name}"...`, 0);

// Get the kernel for this notebook
// Note: This should always exist because onKernelStarted already fired
const kernel = this.kernelProvider.get(notebook);
if (!kernel) {
logger.error(
`No kernel found for ${getDisplayPath(
notebook.uri
)} even after onDidStartKernel fired - this should not happen`
);
return false;
}

logger.info(`Kernel found for ${getDisplayPath(notebook.uri)}, starting init notebook execution`);

// Filter out non-code blocks
const codeBlocks = initNotebook.blocks.filter((block) => block.type === 'code');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll have other types of executable blocks here soon that we will want to support. Can you expand it to include SQL, input blocks, and chart types?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since SQL blocks are not yet implemented in the extension, we agreed that it can be added to the init notebooks once implemented.


if (codeBlocks.length === 0) {
logger.info(`Init notebook has no code blocks, skipping execution`);
return true; // Not an error - just nothing to execute
}

logger.info(`Executing ${codeBlocks.length} code blocks from init notebook`);
progress(
`Preparing to execute ${codeBlocks.length} initialization ${
codeBlocks.length === 1 ? 'block' : 'blocks'
}...`,
5
);

// Get kernel execution
const kernelExecution = this.kernelProvider.getKernelExecution(kernel);

// Execute each code block sequentially
for (let i = 0; i < codeBlocks.length; i++) {
const block = codeBlocks[i];
const percentComplete = Math.floor((i / codeBlocks.length) * 100);

// Show more detailed progress with percentage
progress(
`[${percentComplete}%] Executing block ${i + 1} of ${codeBlocks.length}...`,
90 / codeBlocks.length // Reserve 5% for start, 5% for finish
);

logger.info(`Executing init notebook block ${i + 1}/${codeBlocks.length}`);

try {
// Execute the code silently in the background
const outputs = await kernelExecution.executeHidden(block.content ?? '');

// Log outputs for debugging
if (outputs && outputs.length > 0) {
logger.info(`Init notebook block ${i + 1} produced ${outputs.length} outputs`);

// Check for errors in outputs
const errors = outputs.filter(
(output: { output_type?: string }) => output.output_type === 'error'
);
if (errors.length > 0) {
logger.warn(`Init notebook block ${i + 1} produced errors:`, errors);
}
}
} catch (blockError) {
// Log error but continue with next block
logger.error(`Error executing init notebook block ${i + 1}:`, blockError);
}
}

logger.info(`Completed executing all init notebook blocks`);
progress(`✓ Initialization complete! Environment ready.`, 5);

// Give user a moment to see the completion message
await new Promise((resolve) => setTimeout(resolve, 1000));

return true;
} catch (error) {
logger.error(`Error in executeInitNotebook:`, error);
throw error;
}
}
}

export const IDeepnoteInitNotebookRunner = Symbol('IDeepnoteInitNotebookRunner');
export interface IDeepnoteInitNotebookRunner {
runInitNotebookIfNeeded(notebook: NotebookDocument, projectId: string): Promise<void>;
}
Loading
Loading