-
Notifications
You must be signed in to change notification settings - Fork 0
feat: convert Deepnote file to Jupyter on the frontend #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 20 commits
b9aa168
2102e32
28cae64
e488759
a155977
50e8536
91bc7e5
7e98a3c
fab6c39
9863cae
aec4f6b
3ed238f
c765dad
5799272
d8c1a9a
a4fa6c7
5120d9e
af2227b
07bb549
cb26de0
017e92f
1d72fb1
08c2d1b
95cebf9
5e6f235
0ae0e56
d7d8256
8426c75
8dbf78a
44818e9
1b3b369
eac52df
765d531
0caae37
9538a18
fe7c9f0
09a1e0a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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}' | ||
| 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 | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
andyjakubowski marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||||||||
andyjakubowski marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||
| // Markdown cell | ||||||||||||||||||||||||||||||||||||||||||
| const source = createMarkdown(blockCopy); | ||||||||||||||||||||||||||||||||||||||||||
| const jupyterCell: IMarkdownCell = { | ||||||||||||||||||||||||||||||||||||||||||
| cell_type: 'markdown', | ||||||||||||||||||||||||||||||||||||||||||
| metadata: {}, | ||||||||||||||||||||||||||||||||||||||||||
| source | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
| return jupyterCell; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+34
to
+43
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Markdown cells discard block metadata. Unlike code cells (line 28), markdown cells use 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| export function convertDeepnoteBlockTypeToJupyter(blockType: string) { | ||
andyjakubowski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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'; | ||
| } | ||
| } | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Strengthen cell schema validation.
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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid mutation; return immutable fallback. Line 42 mutates 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const transformedModelContent = | ||||||||||||||||||||||||||||||||||||||||||||||
| await transformDeepnoteYamlToNotebookContent( | ||||||||||||||||||||||||||||||||||||||||||||||
| validatedModelContent.data.metadata.deepnote.rawYamlString | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const transformedModel = { | ||||||||||||||||||||||||||||||||||||||||||||||
| ...model, | ||||||||||||||||||||||||||||||||||||||||||||||
| content: transformedModelContent | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return transformedModel; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| 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 | ||
| } | ||
| ], | ||
andyjakubowski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| metadata: { | ||
| deepnote: { | ||
| rawYamlString: null, | ||
| deepnoteFile: null | ||
| } | ||
| }, | ||
| nbformat: 4, | ||
| nbformat_minor: 0 | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.