Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b9aa168
Switch notebooks
andyjakubowski Sep 12, 2025
2102e32
Add example request logs
andyjakubowski Sep 26, 2025
28cae64
Add custom content provider on frontend
andyjakubowski Sep 26, 2025
e488759
Remove Python YAML conversion logic
andyjakubowski Sep 26, 2025
a155977
Edit tsconfig.json to include all files in `src`
andyjakubowski Sep 29, 2025
50e8536
Extract `NotebookPicker` to own file
andyjakubowski Sep 29, 2025
91bc7e5
Add explanatory comments to `index.tsx`
andyjakubowski Sep 29, 2025
7e98a3c
Validate model content with Zod schema
andyjakubowski Sep 29, 2025
fab6c39
Stub out transformDeepnoteYamlToNotebookContent
andyjakubowski Sep 29, 2025
9863cae
Deserialize Deepnote file YAML into object
andyjakubowski Sep 29, 2025
aec4f6b
Enable noUncheckedIndexedAccess in TS config
andyjakubowski Sep 30, 2025
3ed238f
Stub out block conversion
andyjakubowski Sep 30, 2025
c765dad
Update TS config
andyjakubowski Oct 7, 2025
5799272
Add temporary conversion code
andyjakubowski Oct 7, 2025
d8c1a9a
Add DOM to TS config lib
andyjakubowski Oct 7, 2025
a4fa6c7
Remove unused code
andyjakubowski Oct 7, 2025
5120d9e
Test @deepnote/blocks
andyjakubowski Oct 7, 2025
af2227b
Delete `deepnote-convert` code
andyjakubowski Oct 7, 2025
07bb549
Remove duplicated code
andyjakubowski Oct 7, 2025
cb26de0
Convert Deepnote file to Jupyter notebook
andyjakubowski Oct 7, 2025
017e92f
Set GITHUB_TOKEN in CI build workflow
andyjakubowski Oct 8, 2025
1d72fb1
Add setup instructions for GitHub package registry
andyjakubowski Oct 8, 2025
08c2d1b
Fix linting
andyjakubowski Oct 8, 2025
95cebf9
Remove request examples file
andyjakubowski Oct 8, 2025
5e6f235
Set env var in Test the extension job
andyjakubowski Oct 8, 2025
0ae0e56
Move lodash to dev deps
andyjakubowski Oct 8, 2025
d7d8256
Remove pointless ternary operator
andyjakubowski Oct 8, 2025
8426c75
Bump `@deepnote/blocks` version
andyjakubowski Oct 8, 2025
8dbf78a
Set env var in Build the extension job
andyjakubowski Oct 8, 2025
44818e9
Set env var in Package the extension job
andyjakubowski Oct 8, 2025
1b3b369
Fix release CI workflow
andyjakubowski Oct 8, 2025
eac52df
Set up registry for packages
andyjakubowski Oct 8, 2025
765d531
Update CI job
andyjakubowski Oct 9, 2025
0caae37
Fix typo
andyjakubowski Oct 9, 2025
9538a18
Fix CI
andyjakubowski Oct 9, 2025
fe7c9f0
Delete .npmrc
andyjakubowski Oct 9, 2025
09a1e0a
Enable always-auth for GitHub packages
andyjakubowski Oct 9, 2025
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
5 changes: 5 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
nodeLinker: node-modules
npmScopes:
deepnote:
npmRegistryServer: 'https://npm.pkg.github.com'
npmAlwaysAuth: true
npmAuthToken: '${GITHUB_TOKEN}'
7 changes: 7 additions & 0 deletions deepnote-file-request-examples.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[W 2025-09-18 11:22:56.083 ServerApp] 404 GET /api/contents/all-block-types-no-outputs.deepnote?content=0&hash=0&1758187376077 (::1): file or directory does not exist: 'all-block-types-no-outputs.deepnote'
[D 2025-09-18 11:22:56.083 ServerApp] Accepting token-authenticated request from ::1
[D 2025-09-18 11:22:56.083 ServerApp] 200 GET /api/contents/example.ipynb?content=0&hash=0&1758187376077 (b4931ce4427142b3a2011a47bec521d2@::1) 1.80ms
[D 2025-09-18 11:22:56.083 ServerApp] Accepting token-authenticated request from ::1
[D 2025-09-18 11:22:56.083 ServerApp] 200 GET /api/contents/example.deepnote?content=0&hash=0&1758187376077 (b4931ce4427142b3a2011a47bec521d2@::1) 2.10ms
[D 2025-09-18 11:22:56.288 ServerApp] 200 GET /api/contents/example.deepnote?type=notebook&content=1&hash=1&contentProviderId=undefined&1758187376168 (b4931ce4427142b3a2011a47bec521d2@::1) 57.88ms
[D 2025-09-18 11:22:56.296 ServerApp] 200 GET /api/contents/example.deepnote/checkpoints?1758187376293 (b4931ce4427142b3a2011a47bec521d2@::1) 1.35ms
66 changes: 4 additions & 62 deletions jupyterlab_deepnote/contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,7 @@
from jupyter_server.services.contents.filemanager import FileContentsManager
from typing import cast

import yaml
from nbformat.v4 import new_notebook, new_code_cell, new_markdown_cell


def yaml_to_ipynb(yaml_text: str):
"""Convert Deepnote YAML into a minimal Jupyter nbformat v4 notebook."""
try:
data = yaml.safe_load(yaml_text)
except Exception:
return new_notebook(cells=[])

notebooks = (
data.get("project", {}).get("notebooks", []) if isinstance(data, dict) else []
)

if not notebooks:
return new_notebook(cells=[])

# Collect notebook names
notebook_names = [nb.get("name", "") for nb in notebooks]

# Build all_notebooks dict: name -> full nbformat notebook JSON
all_notebooks = {}
for nb in notebooks:
nb_blocks = nb.get("blocks", [])
nb_cells = []
for block in sorted(nb_blocks, key=lambda b: b.get("sortingKey", "")):
btype = block.get("type", "code")
content = block.get("content", "")
if btype == "code":
nb_cells.append(new_code_cell(content))
else:
nb_cells.append(new_markdown_cell(content))
# Use the notebook name as key
nb_name = nb.get("name", "")
all_notebooks[nb_name] = new_notebook(cells=nb_cells)

# Use first notebook's cells to render initially
nb0 = notebooks[0]
blocks = nb0.get("blocks", [])
cells = []
for block in sorted(blocks, key=lambda b: b.get("sortingKey", "")):
btype = block.get("type", "code")
content = block.get("content", "")
if btype == "code":
cells.append(new_code_cell(content))
else:
cells.append(new_markdown_cell(content))

metadata = {
"deepnote": {"notebook_names": notebook_names, "notebooks": all_notebooks}
}
return new_notebook(cells=cells, metadata=metadata)


def yaml_to_ipynb_dummy(yaml_text: str) -> dict:
return {"nbformat": 4, "nbformat_minor": 5, "metadata": {}, "cells": []}
from nbformat.v4 import new_notebook


class DeepnoteContentsManager(FileContentsManager):
Expand All @@ -74,15 +18,13 @@ def get(self, path, content=True, type=None, format=None, require_hash=False):
else:
yaml_text = cast(str, _content)

nb_node = yaml_to_ipynb(yaml_text)

model = self._base_model(path)
model["type"] = "notebook"
model["format"] = "json"
model["content"] = nb_node
model["content"] = new_notebook(
cells=[], metadata={"deepnote": {"rawYamlString": yaml_text}}
)
model["writable"] = False
self.mark_trusted_cells(nb_node, path)
self.validate_notebook_model(model, validation_error={})

if require_hash:
# Accept 2- or 3-tuple; we only need the bytes
Expand Down
20 changes: 6 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,17 @@
"watch:labextension": "jupyter labextension watch ."
},
"dependencies": {
"@deepnote/blocks": "^1.0.0",
"@jupyterlab/application": "^4.0.0",
"@jupyterlab/coreutils": "^6.0.0",
"@jupyterlab/notebook": "^4.4.7",
"@jupyterlab/services": "^7.0.0",
"@jupyterlab/settingregistry": "^4.0.0",
"@lumino/widgets": "^2.7.1"
"@lumino/widgets": "^2.7.1",
"@types/lodash": "^4.17.20",
"lodash": "^4.17.21",
"yaml": "^2.8.1",
"zod": "^4.1.11"
},
"devDependencies": {
"@jupyterlab/builder": "^4.0.0",
Expand Down Expand Up @@ -141,19 +146,6 @@
"@typescript-eslint"
],
"rules": {
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "interface",
"format": [
"PascalCase"
],
"custom": {
"regex": "^I[A-Z]",
"match": true
}
}
],
"@typescript-eslint/no-unused-vars": [
"warn",
{
Expand Down
61 changes: 4 additions & 57 deletions src/index.tsx → src/components/NotebookPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,9 @@
import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';
import React from 'react';
import { IToolbarWidgetRegistry, ReactWidget } from '@jupyterlab/apputils';
import {
INotebookWidgetFactory,
NotebookPanel,
NotebookWidgetFactory
} from '@jupyterlab/notebook';
import { Widget } from '@lumino/widgets';
import { ReactWidget } from '@jupyterlab/apputils';
import { NotebookPanel } from '@jupyterlab/notebook';
import { HTMLSelect } from '@jupyterlab/ui-components';

const plugin: JupyterFrontEndPlugin<void> = {
id: 'jupyterlab-deepnote:plugin',
description: 'Open .deepnote files as notebooks.',
autoStart: true,
requires: [INotebookWidgetFactory, IToolbarWidgetRegistry],
activate: (
app: JupyterFrontEnd,
notebookWidgetFactory: NotebookWidgetFactory,
toolbarRegistry: IToolbarWidgetRegistry
) => {
app.docRegistry.addFileType(
{
name: 'deepnote',
displayName: 'Deepnote Notebook',
extensions: ['.deepnote'],
mimeTypes: ['text/yaml', 'application/x-yaml'],
fileFormat: 'text',
contentType: 'file'
},
[notebookWidgetFactory.name]
);

app.docRegistry.setDefaultWidgetFactory(
'deepnote',
notebookWidgetFactory.name
);

toolbarRegistry.addFactory<NotebookPanel>(
notebookWidgetFactory.name,
'deepnote:switch-notebook',
panel => {
if (!panel.context.path.endsWith('.deepnote')) {
return new Widget(); // don’t render for .ipynb or others
}

return new NotebookPicker(panel);
}
);
}
};

class NotebookPicker extends ReactWidget {
export class NotebookPicker extends ReactWidget {
private selected: string | null = null;

constructor(private panel: NotebookPanel) {
Expand All @@ -68,7 +18,7 @@ class NotebookPicker extends ReactWidget {
? metadataNames
: [];

this.selected = names.length > 0 ? names[0] : null;
this.selected = names.length === 0 ? null : (names[0] ?? null);
this.update();
});
}
Expand Down Expand Up @@ -115,7 +65,6 @@ class NotebookPicker extends ReactWidget {

return (
<HTMLSelect
id="deepnote-notebook-picker"
value={this.selected ?? '-'}
onChange={this.handleChange}
onKeyDown={() => {}}
Expand All @@ -141,5 +90,3 @@ class NotebookPicker extends ReactWidget {
);
}
}

export default plugin;
47 changes: 47 additions & 0 deletions src/convert-deepnote-block-to-jupyter-cell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
createMarkdown,
createPythonCode,
DeepnoteBlock
} from '@deepnote/blocks';
import _cloneDeep from 'lodash/cloneDeep';
import { ICodeCell, IMarkdownCell } from '@jupyterlab/nbformat';
import { convertDeepnoteBlockTypeToJupyter } from './convert-deepnote-block-type-to-jupyter';

export function convertDeepnoteBlockToJupyterCell(block: DeepnoteBlock) {
const blockCopy = _cloneDeep(block);
const jupyterCellMetadata = { ...blockCopy.metadata, cell_id: blockCopy.id };
const jupyterCellType = convertDeepnoteBlockTypeToJupyter(blockCopy.type);

if (jupyterCellType === 'code') {
const blockOutputs = blockCopy.outputs ?? [];

if (Array.isArray(blockOutputs)) {
blockOutputs.forEach(output => {
delete output.truncated;
});
}

const source = createPythonCode(blockCopy);

const jupyterCell: ICodeCell = {
cell_type: 'code',
metadata: jupyterCellMetadata,
execution_count:
blockCopy.executionCount !== undefined
? blockCopy.executionCount
: null,
outputs: blockOutputs,
source
};
return jupyterCell;
} else {
// Markdown cell
const source = createMarkdown(blockCopy);
const jupyterCell: IMarkdownCell = {
cell_type: 'markdown',
metadata: {},
source
};
return jupyterCell;
}
Comment on lines +34 to +43
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Markdown cells discard block metadata.

Unlike code cells (line 28), markdown cells use metadata: {} (line 42), losing any block-level metadata.

Apply this diff if block metadata should be preserved:

     const source = createMarkdown(blockCopy);
     const jupyterCell: IMarkdownCell = {
       cell_type: 'markdown',
-      metadata: {},
+      metadata: jupyterCellMetadata,
       source
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else {
// Markdown cell
const source = createMarkdown(blockCopy);
const jupyterCell: IMarkdownCell = {
cell_type: 'markdown',
metadata: {},
source
};
return jupyterCell;
}
} else {
// Markdown cell
const source = createMarkdown(blockCopy);
const jupyterCell: IMarkdownCell = {
cell_type: 'markdown',
metadata: jupyterCellMetadata,
source
};
return jupyterCell;
}
🤖 Prompt for AI Agents
In src/convert-deepnote-block-to-jupyter-cell.ts around lines 37 to 46, the
markdown branch currently sets metadata: {} which discards the block's metadata;
replace the empty object with the block's metadata (e.g., metadata:
blockCopy.metadata || {}) so markdown cells preserve any existing block-level
metadata, keeping the IMarkdownCell shape intact.

}
32 changes: 32 additions & 0 deletions src/convert-deepnote-block-type-to-jupyter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export function convertDeepnoteBlockTypeToJupyter(blockType: string) {
switch (blockType) {
case 'big-number':
case 'code':
case 'sql':
case 'notebook-function':
case 'input-text':
case 'input-checkbox':
case 'input-textarea':
case 'input-file':
case 'input-select':
case 'input-date-range':
case 'input-date':
case 'input-slider':
case 'visualization':
return 'code';

case 'markdown':
case 'text-cell-h1':
case 'text-cell-h3':
case 'text-cell-h2':
case 'text-cell-p':
case 'text-cell-bullet':
case 'text-cell-todo':
case 'text-cell-callout':
case 'image':
case 'button':
case 'separator':
default:
return 'markdown';
}
}
58 changes: 58 additions & 0 deletions src/deepnote-content-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Contents, RestContentProvider } from '@jupyterlab/services';
import { z } from 'zod';
import { transformDeepnoteYamlToNotebookContent } from './transform-deepnote-yaml-to-notebook-content';

export const deepnoteContentProviderName = 'deepnote-content-provider';

const deepnoteNotebookSchema = z.object({
cells: z.array(z.any()), // or refine further with nbformat
metadata: z.object({
deepnote: z.object({
rawYamlString: z.string()
})
}),
nbformat: z.number(),
nbformat_minor: z.number()
});
Comment on lines +7 to +16
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Strengthen cell schema validation.

z.array(z.any()) at line 8 disables type checking for cells. Consider validating cell structure or using z.unknown() if cells are intentionally opaque.

 const deepnoteNotebookSchema = z.object({
-  cells: z.array(z.any()), // or refine further with nbformat
+  cells: z.array(z.unknown()), // cells replaced by transformation
   metadata: z.object({
     deepnote: z.object({
       rawYamlString: z.string()
     })
   }),
   nbformat: z.number(),
   nbformat_minor: z.number()
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const deepnoteNotebookSchema = z.object({
cells: z.array(z.any()), // or refine further with nbformat
metadata: z.object({
deepnote: z.object({
rawYamlString: z.string()
})
}),
nbformat: z.number(),
nbformat_minor: z.number()
});
const deepnoteNotebookSchema = z.object({
cells: z.array(z.unknown()), // cells replaced by transformation
metadata: z.object({
deepnote: z.object({
rawYamlString: z.string()
})
}),
nbformat: z.number(),
nbformat_minor: z.number()
});


export class DeepnoteContentProvider extends RestContentProvider {
async get(
localPath: string,
options?: Contents.IFetchOptions
): Promise<Contents.IModel> {
const model = await super.get(localPath, options);
const isDeepnoteFile =
localPath.endsWith('.deepnote') && model.type === 'notebook';

if (!isDeepnoteFile) {
// Not a .deepnote file, return as-is
return model;
}

const validatedModelContent = deepnoteNotebookSchema.safeParse(
model.content
);

if (!validatedModelContent.success) {
console.error(
'Invalid .deepnote file content:',
validatedModelContent.error
);
// Return an empty notebook instead of throwing an error
model.content.cells = [];
return model;
}
Comment on lines +36 to +44
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid mutation; return immutable fallback.

Line 42 mutates model.content.cells. Create a new content object instead:

     if (!validatedModelContent.success) {
       console.error(
         'Invalid .deepnote file content:',
         validatedModelContent.error
       );
-      // Return an empty notebook instead of throwing an error
-      model.content.cells = [];
-      return model;
+      return {
+        ...model,
+        content: {
+          ...model.content,
+          cells: []
+        }
+      };
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!validatedModelContent.success) {
console.error(
'Invalid .deepnote file content:',
validatedModelContent.error
);
// Return an empty notebook instead of throwing an error
model.content.cells = [];
return model;
}
if (!validatedModelContent.success) {
console.error(
'Invalid .deepnote file content:',
validatedModelContent.error
);
return {
...model,
content: {
...model.content,
cells: []
}
};
}
🤖 Prompt for AI Agents
In src/deepnote-content-provider.ts around lines 36 to 44, the code mutates
model.content.cells when validation fails; instead create and return a new model
object with an immutable fallback content (e.g., construct a new content object
copying existing properties but replacing cells with an empty array, and return
a new model via object spread or a shallow clone) so the original model remains
unchanged.


const transformedModelContent =
await transformDeepnoteYamlToNotebookContent(
validatedModelContent.data.metadata.deepnote.rawYamlString
);

const transformedModel = {
...model,
content: transformedModelContent
};

return transformedModel;
}
}
30 changes: 30 additions & 0 deletions src/fallback-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ICodeCell } from '@jupyterlab/nbformat';
import { IDeepnoteNotebookContent } from './types';

export const blankCodeCell: ICodeCell = {
cell_type: 'code',
source: '',
metadata: {},
outputs: [],
execution_count: null
};

export const blankDeepnoteNotebookContent: IDeepnoteNotebookContent = {
cells: [
{
cell_type: 'code',
source: '# Transformed from Deepnote YAML\n',
metadata: {},
outputs: [],
execution_count: null
}
],
metadata: {
deepnote: {
rawYamlString: null,
deepnoteFile: null
}
},
nbformat: 4,
nbformat_minor: 0
};
Loading
Loading