Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 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
8 changes: 8 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,15 @@ jobs:
set -eux
jlpm
jlpm run lint:check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Test the extension
run: |
set -eux
jlpm run test
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Build the extension
run: |
Expand All @@ -47,6 +51,8 @@ jobs:
jupyter labextension list
jupyter labextension list 2>&1 | grep -ie "jupyterlab-deepnote.*OK"
python -m jupyterlab.browser_check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Package the extension
run: |
Expand All @@ -55,6 +61,8 @@ jobs:
pip install build
python -m build
pip uninstall -y "jupyterlab_deepnote" jupyterlab
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Upload extension packages
uses: actions/upload-artifact@v4
Expand Down
14 changes: 11 additions & 3 deletions .github/workflows/check-release.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Check Release
on:
push:
branches: ["main"]
branches: ['main']
pull_request:
branches: ["*"]
branches: ['*']

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
Expand All @@ -15,13 +15,21 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Configure npm registry for GitHub Packages
run: |
echo "@deepnote:registry=https://npm.pkg.github.com" >> ~/.npmrc
echo "//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}" >> ~/.npmrc
Copy link
Member

Choose a reason for hiding this comment

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

Writing the token to a file feels scary. If you specify NODE_AUTH_TOKEN, that should be usable by npm.

- name: Install dependencies
        run: npm ci
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Copy link
Contributor Author

@andyjakubowski andyjakubowski Oct 9, 2025

Choose a reason for hiding this comment

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

I replaced it with actions/setup-node@v5 to make it less scary. After some fun digging I found out that actions/setup-node@v5 does the exact same thing of writing the token out to the file 😀

Copy link
Member

Choose a reason for hiding this comment

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

haha okay

env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Base Setup
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
- name: Check Release
uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2
with:

token: ${{ secrets.GITHUB_TOKEN }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Upload Distributions
uses: actions/upload-artifact@v4
Expand Down
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}'
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@ Install `jupyterlab`. The extension package itself doesn’t depend on `jupyterl
uv pip install jupyterlab
```

**Configure Access to @deepnote/blocks Package**

The `@deepnote/blocks` package is published on GitHub Packages. To install it, you'll need to authenticate with GitHub:

1. Create a GitHub Personal Access Token (classic) with `read:packages` scope:
- Go to https://github.com/settings/tokens
- Click "Generate new token (classic)"
- Select the `read:packages` scope
- Generate and copy the token

2. Set the `GITHUB_TOKEN` environment variable to ensure `jlpm` (which is a wrapper around Yarn) can download the `@deepnote/blocks` package from the GitHub package registry. You can set the variable in `.zshrc` or manually like:
```shell
export GITHUB_TOKEN=your_token_here
```
Replace `YOUR_TOKEN_HERE` with your actual token.

Install the extension package in editable mode. It installs the package’s dependencies, too:

```shell
Expand Down
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,18 +58,23 @@
"watch:labextension": "jupyter labextension watch ."
},
"dependencies": {
"@deepnote/blocks": "^1.1.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",
"lodash": "^4.17.21",
"yaml": "^2.8.1",
"zod": "^4.1.11"
},
"devDependencies": {
"@jupyterlab/builder": "^4.0.0",
"@jupyterlab/testutils": "^4.0.0",
"@types/jest": "^29.2.0",
"@types/json-schema": "^7.0.11",
"@types/lodash": "^4.17.20",
"@types/react": "^18.0.26",
"@types/react-addons-linked-state-mixin": "^0.14.22",
"@typescript-eslint/eslint-plugin": "^6.1.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;
44 changes: 44 additions & 0 deletions src/convert-deepnote-block-to-jupyter-cell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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 ?? 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';
}
}
Loading