Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
48 changes: 29 additions & 19 deletions src/components/NotebookPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { ReactWidget } from '@jupyterlab/apputils';
import { NotebookPanel } from '@jupyterlab/notebook';
import { HTMLSelect } from '@jupyterlab/ui-components';
import { deepnoteMetadataSchema } from '../types';

export class NotebookPicker extends ReactWidget {
private selected: string | null = null;
Expand Down Expand Up @@ -31,22 +32,30 @@ export class NotebookPicker extends ReactWidget {

const selected = event.target.value;
const deepnoteMetadata = this.panel.context.model.getMetadata('deepnote');
const notebooks = deepnoteMetadata?.notebooks;
const deepnoteMetadataValidated =
deepnoteMetadataSchema.safeParse(deepnoteMetadata);

if (notebooks && selected in notebooks) {
// clone the notebook JSON
const newModelData = { ...notebooks[selected] };
if (!deepnoteMetadataValidated.success) {
console.error(
'Invalid deepnote metadata:',
deepnoteMetadataValidated.error
);
return;
}

// preserve deepnote metadata *without* re-inserting all notebooks
newModelData.metadata = {
...(newModelData.metadata ?? {}),
deepnote: {
notebook_names: deepnoteMetadata?.notebook_names ?? [],
notebooks: deepnoteMetadata?.notebooks ?? {}
}
};
const notebooks = deepnoteMetadataValidated.data.notebooks;

model.fromJSON(newModelData);
if (selected in notebooks) {
model.fromJSON({
cells: notebooks[selected]?.cells ?? [],
metadata: {
deepnote: {
notebooks
}
},
nbformat: 4,
nbformat_minor: 0
});
model.dirty = false;
}

Expand All @@ -56,12 +65,13 @@ export class NotebookPicker extends ReactWidget {

render(): JSX.Element {
const deepnoteMetadata = this.panel.context.model.getMetadata('deepnote');
const metadataNames = deepnoteMetadata?.notebook_names;
const names =
Array.isArray(metadataNames) &&
metadataNames.every(n => typeof n === 'string')
? metadataNames
: [];

const deepnoteMetadataValidated =
deepnoteMetadataSchema.safeParse(deepnoteMetadata);

const names = deepnoteMetadataValidated.success
? Object.values(deepnoteMetadataValidated.data.notebooks).map(n => n.name)
: [];

return (
<HTMLSelect
Expand Down
5 changes: 3 additions & 2 deletions src/deepnote-content-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { transformDeepnoteYamlToNotebookContent } from './transform-deepnote-yam

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

const deepnoteNotebookSchema = z.object({
const deepnoteFileFromServerSchema = z.object({
cells: z.array(z.any()), // or refine further with nbformat
metadata: z.object({
deepnote: z.object({
Expand All @@ -29,7 +29,7 @@ export class DeepnoteContentProvider extends RestContentProvider {
return model;
}

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

Expand All @@ -43,6 +43,7 @@ export class DeepnoteContentProvider extends RestContentProvider {
return model;
}

// Transform the Deepnote YAML to Jupyter notebook content
const transformedModelContent =
await transformDeepnoteYamlToNotebookContent(
validatedModelContent.data.metadata.deepnote.rawYamlString
Expand Down
3 changes: 1 addition & 2 deletions src/fallback-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ export const blankDeepnoteNotebookContent: IDeepnoteNotebookContent = {
],
metadata: {
deepnote: {
rawYamlString: null,
deepnoteFile: null
notebooks: {}
}
},
nbformat: 4,
Expand Down
24 changes: 21 additions & 3 deletions src/transform-deepnote-yaml-to-notebook-content.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IDeepnoteNotebookContent } from './types';
import { IDeepnoteNotebookContent, IDeepnoteNotebookMetadata } from './types';
import { blankCodeCell, blankDeepnoteNotebookContent } from './fallback-data';
import { deserializeDeepnoteFile } from '@deepnote/blocks';
import { convertDeepnoteBlockToJupyterCell } from './convert-deepnote-block-to-jupyter-cell';
Expand All @@ -9,6 +9,18 @@ export async function transformDeepnoteYamlToNotebookContent(
try {
const deepnoteFile = await deserializeDeepnoteFile(yamlString);

const notebooks = deepnoteFile.project.notebooks.reduce(
(acc, notebook) => {
acc[notebook.name] = {
id: notebook.id,
name: notebook.name,
cells: notebook.blocks.map(convertDeepnoteBlockToJupyterCell)
};
return acc;
},
{} as IDeepnoteNotebookMetadata['deepnote']['notebooks']
);

const selectedNotebook = deepnoteFile.project.notebooks[0];

if (!selectedNotebook) {
Expand All @@ -28,8 +40,14 @@ export async function transformDeepnoteYamlToNotebookContent(
);

return {
...blankDeepnoteNotebookContent,
cells
cells,
metadata: {
deepnote: {
notebooks
}
},
nbformat: 4,
nbformat_minor: 0
};
} catch (error) {
console.error('Failed to deserialize Deepnote file:', error);
Expand Down
31 changes: 27 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
import { INotebookContent, INotebookMetadata } from '@jupyterlab/nbformat';
import type { DeepnoteFile } from '@deepnote/blocks';
import {
ICodeCell,
IMarkdownCell,
INotebookContent,
INotebookMetadata
} from '@jupyterlab/nbformat';

import { z } from 'zod';

export const deepnoteMetadataSchema = z.object({
notebooks: z.record(
z.string(),
z.object({
id: z.string(),
name: z.string(),
cells: z.array(z.any())
})
)
});
Comment on lines +10 to +19
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 9, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Tighten schema cell typing to match the interface.

The schema uses z.array(z.any()) for cells (line 16), while the interface specifies Array<ICodeCell | IMarkdownCell> (line 28). This inconsistency allows invalid cells to pass validation.

Apply this diff to align the schema with the interface:

+const cellSchema = z.object({
+  cell_type: z.enum(['code', 'markdown']),
+  source: z.union([z.string(), z.array(z.string())]),
+  metadata: z.record(z.any())
+});
+
 export const deepnoteMetadataSchema = z.object({
   notebooks: z.record(
     z.string(),
     z.object({
       id: z.string(),
       name: z.string(),
-      cells: z.array(z.any())
+      cells: z.array(cellSchema)
     })
   )
 });
🤖 Prompt for AI Agents
In src/types.ts around lines 10 to 19, the deepnoteMetadataSchema currently uses
z.array(z.any()) for cells which is too permissive; change it to validate
against the two cell shapes declared by the interface (ICodeCell and
IMarkdownCell). Replace z.array(z.any()) with a z.array(z.union([<ICodeCell
schema>, <IMarkdownCell schema>])) — or better, if the cell objects have a
distinguishing "type" field, use z.discriminatedUnion('type', [{ type:
z.literal('code'), ...code cell fields schema }, { type: z.literal('markdown'),
...markdown cell fields schema }]) — ensuring each schema includes the exact
fields and types from the ICodeCell and IMarkdownCell interfaces so invalid
cells are rejected.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, I don’t want to recreate schemas for ICodeCell and IMarkdownCell types. This is fine

Copy link
Contributor

Choose a reason for hiding this comment

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

@andyjakubowski, understood. I'll leave the schema as is.


🧠 Learnings used
Learnt from: CR
PR: deepnote/vscode-deepnote#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-09T11:21:57.482Z
Learning: Applies to src/notebooks/deepnote/deepnoteTypes.ts : deepnoteTypes.ts contains Deepnote-related type definitions

Learnt from: CR
PR: deepnote/vscode-deepnote#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-09T11:21:57.482Z
Learning: Applies to src/notebooks/deepnote/deepnoteDataConverter.ts : deepnoteDataConverter.ts performs Deepnote data transformations


export interface IDeepnoteNotebookMetadata extends INotebookMetadata {
deepnote: {
rawYamlString: string | null;
deepnoteFile: DeepnoteFile | null;
notebooks: Record<
string,
{
id: string;
name: string;
cells: Array<ICodeCell | IMarkdownCell>;
}
>;
};
}

Expand Down