@deepnote/convert 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,18 +1,28 @@
1
1
  # @deepnote/convert
2
2
 
3
- Convert Jupyter Notebook files (`.ipynb`) to Deepnote project files (`.deepnote`).
3
+ Bidirectional converter between Jupyter Notebook files (`.ipynb`) and Deepnote project files (`.deepnote`) with lossless roundtrip support.
4
+
5
+ ```bash
6
+ # Convert a Jupyter notebook to a Deepnote project
7
+ npx @deepnote/convert notebook.ipynb
8
+
9
+ # Convert a Deepnote project to Jupyter notebooks
10
+ npx @deepnote/convert project.deepnote
11
+ ```
4
12
 
5
13
  ## Installation
6
14
 
7
15
  ```bash
8
- npm install @deepnote/convert
16
+ npm install -g @deepnote/convert
9
17
  ```
10
18
 
11
19
  ## CLI Usage
12
20
 
13
- The package provides a `deepnote-convert` command-line tool for converting Jupyter notebooks to Deepnote format.
21
+ The package provides a `deepnote-convert` command-line tool for bidirectional conversion between Jupyter and Deepnote formats.
22
+
23
+ ### Convert Jupyter → Deepnote
14
24
 
15
- ### Convert a Single Notebook
25
+ #### Convert a Single Notebook
16
26
 
17
27
  Convert a single `.ipynb` file to a `.deepnote` file:
18
28
 
@@ -22,7 +32,7 @@ deepnote-convert path/to/notebook.ipynb
22
32
 
23
33
  This will create a `notebook.deepnote` file in the current directory.
24
34
 
25
- ### Convert a Directory of Notebooks
35
+ #### Convert a Directory of Notebooks
26
36
 
27
37
  Convert all `.ipynb` files in a directory to a single `.deepnote` project:
28
38
 
@@ -32,6 +42,16 @@ deepnote-convert path/to/notebooks/
32
42
 
33
43
  This will create a `notebooks.deepnote` file in the current directory containing all notebooks from the directory.
34
44
 
45
+ ### Convert Deepnote → Jupyter
46
+
47
+ Convert a `.deepnote` file to Jupyter notebooks:
48
+
49
+ ```bash
50
+ deepnote-convert path/to/project.deepnote
51
+ ```
52
+
53
+ This will create a `project/` directory containing separate `.ipynb` files for each notebook in the Deepnote project.
54
+
35
55
  ### Options
36
56
 
37
57
  #### `--projectName <name>`
@@ -46,49 +66,106 @@ If not specified, the project name will default to the filename (without extensi
46
66
 
47
67
  #### `-o, --outputPath <path>`
48
68
 
49
- Specify where to save the output `.deepnote` file:
69
+ Specify where to save the output file(s):
50
70
 
51
71
  ```bash
52
- # Save to a specific file
72
+ # For Jupyter → Deepnote: Save to a specific file
53
73
  deepnote-convert notebook.ipynb -o output/project.deepnote
54
74
 
55
- # Save to a directory (filename will be auto-generated)
75
+ # For Jupyter → Deepnote: Save to a directory (filename will be auto-generated)
56
76
  deepnote-convert notebook.ipynb -o output/
77
+
78
+ # For Deepnote → Jupyter: Specify output directory for notebooks
79
+ deepnote-convert project.deepnote -o output/jupyter-notebooks/
57
80
  ```
58
81
 
59
- If not specified, the output file will be saved in the current directory.
82
+ If not specified:
83
+
84
+ - For Jupyter → Deepnote: Output file will be saved in the current directory
85
+ - For Deepnote → Jupyter: A directory will be created using the `.deepnote` filename (e.g., `project.deepnote` → `project/`)
60
86
 
61
87
  ### Examples
62
88
 
63
89
  ```bash
64
- # Convert a single notebook with custom name
90
+ # Jupyter → Deepnote: Convert a single notebook with custom name
65
91
  deepnote-convert titanic.ipynb --projectName "Titanic Analysis"
66
92
 
67
- # Convert all notebooks in a directory
93
+ # Jupyter → Deepnote: Convert all notebooks in a directory
68
94
  deepnote-convert ./analysis --projectName "Data Science Project" -o ./output
69
95
 
70
- # Convert multiple notebooks from a folder
96
+ # Jupyter → Deepnote: Convert multiple notebooks from a folder
71
97
  deepnote-convert ~/notebooks/ml-experiments -o ~/projects/
98
+
99
+ # Deepnote → Jupyter: Convert a Deepnote project to Jupyter notebooks
100
+ deepnote-convert my-project.deepnote
101
+
102
+ # Deepnote → Jupyter: Specify output directory
103
+ deepnote-convert my-project.deepnote -o ./jupyter-notebooks/
72
104
  ```
73
105
 
106
+ ### Lossless Roundtrip Conversion
107
+
108
+ The converter supports lossless roundtrip conversions:
109
+
110
+ - **Deepnote → Jupyter → Deepnote**: Preserves all Deepnote-specific metadata in Jupyter cell metadata, enabling faithful reconstruction of the original notebook's structure and metadata (note: serialization formatting or key ordering may differ)
111
+ - **Jupyter → Deepnote → Jupyter**: Preserves original Jupyter content while adding Deepnote metadata
112
+
113
+ This is achieved by storing Deepnote-specific metadata as flat `deepnote_*` keys directly on Jupyter notebook metadata (e.g., `deepnote_notebook_id`, `deepnote_execution_mode`) and cell metadata (e.g., `deepnote_cell_type`, `deepnote_sorting_key`, `deepnote_source`).
114
+
74
115
  ## Programmatic Usage
75
116
 
76
- You can also use the conversion function programmatically in your Node.js or TypeScript applications.
117
+ You can also use the conversion functions programmatically in your Node.js or TypeScript applications.
77
118
 
78
- ### Basic Usage
119
+ ### Jupyter → Deepnote
79
120
 
80
121
  ```typescript
81
- import { convertIpynbFilesToDeepnoteFile } from '@deepnote/convert'
122
+ import { convertIpynbFilesToDeepnoteFile } from "@deepnote/convert";
82
123
 
83
- await convertIpynbFilesToDeepnoteFile(
84
- ['path/to/notebook.ipynb'],
85
- {
86
- outputPath: 'output.deepnote',
87
- projectName: 'My Project'
88
- }
89
- )
124
+ await convertIpynbFilesToDeepnoteFile(["path/to/notebook.ipynb"], {
125
+ outputPath: "output.deepnote",
126
+ projectName: "My Project",
127
+ });
128
+ ```
129
+
130
+ ### Deepnote → Jupyter
131
+
132
+ #### File-based Conversion
133
+
134
+ For automatic file I/O (reading and writing files):
135
+
136
+ ```typescript
137
+ import { convertDeepnoteFileToJupyter } from "@deepnote/convert";
138
+
139
+ await convertDeepnoteFileToJupyter("path/to/project.deepnote", {
140
+ outputDir: "./jupyter-notebooks",
141
+ });
142
+ ```
143
+
144
+ #### Pure Conversion (No File I/O)
145
+
146
+ For programmatic use with in-memory data:
147
+
148
+ ```typescript
149
+ import fs from "node:fs/promises";
150
+ import { deserializeDeepnoteFile } from "@deepnote/blocks";
151
+ import { convertDeepnoteToJupyterNotebooks } from "@deepnote/convert";
152
+
153
+ // Read and deserialize the Deepnote file
154
+ const yamlContent = await fs.readFile("project.deepnote", "utf-8");
155
+ const deepnoteFile = deserializeDeepnoteFile(yamlContent);
156
+
157
+ // Convert to Jupyter notebooks (pure function, no I/O)
158
+ const notebooks = convertDeepnoteToJupyterNotebooks(deepnoteFile);
159
+
160
+ // Now you can work with the notebooks in memory
161
+ for (const { filename, notebook } of notebooks) {
162
+ console.log(`${filename}: ${notebook.cells.length} cells`);
163
+
164
+ // Or save them yourself
165
+ await fs.writeFile(filename, JSON.stringify(notebook, null, 2));
166
+ }
167
+ ```
90
168
 
91
169
  ## License
92
170
 
93
171
  Apache-2.0
94
- ```
package/dist/bin.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/bin.js ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ import { a as convertDeepnoteFileToJupyterFiles, t as convertIpynbFilesToDeepnoteFile } from "./src-j4HyYJfD.js";
3
+ import { cli } from "cleye";
4
+ import fs from "node:fs/promises";
5
+ import { basename, extname, resolve } from "node:path";
6
+ import chalk from "chalk";
7
+ import ora from "ora";
8
+
9
+ //#region src/cli.ts
10
+ async function convert(options) {
11
+ const { inputPath, projectName: customProjectName, outputPath: customOutputPath, cwd = process.cwd() } = options;
12
+ const resolveProjectName = (possibleName) => {
13
+ if (customProjectName) return customProjectName;
14
+ if (possibleName) return possibleName;
15
+ return "Untitled project";
16
+ };
17
+ const resolveOutputPath = async (outputFilename) => {
18
+ if (customOutputPath) {
19
+ const absoluteOutputPath = resolve(cwd, customOutputPath);
20
+ if ((await fs.stat(absoluteOutputPath).catch(() => null))?.isDirectory()) return resolve(absoluteOutputPath, outputFilename);
21
+ return absoluteOutputPath;
22
+ }
23
+ return resolve(cwd, outputFilename);
24
+ };
25
+ const absolutePath = resolve(cwd, inputPath);
26
+ if ((await fs.stat(absolutePath)).isDirectory()) {
27
+ const ipynbFiles = (await fs.readdir(absolutePath, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".ipynb")).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
28
+ if (ipynbFiles.length === 0) throw new Error("No .ipynb files found in the specified directory.");
29
+ const spinner = ora("Converting Jupyter Notebooks to a Deepnote project...").start();
30
+ try {
31
+ const filenameWithoutExtension = basename(absolutePath);
32
+ const projectName = resolveProjectName(filenameWithoutExtension);
33
+ const outputPath = await resolveOutputPath(`${filenameWithoutExtension}.deepnote`);
34
+ await convertIpynbFilesToDeepnoteFile(ipynbFiles.map((file) => resolve(absolutePath, file)), {
35
+ projectName,
36
+ outputPath
37
+ });
38
+ spinner.succeed(`The Deepnote project has been saved to ${chalk.bold(outputPath)}`);
39
+ return outputPath;
40
+ } catch (error) {
41
+ spinner.fail("Conversion failed");
42
+ throw error;
43
+ }
44
+ }
45
+ const ext = extname(absolutePath).toLowerCase();
46
+ if (ext === ".ipynb") {
47
+ const spinner = ora("Converting the Jupyter Notebook to a Deepnote project...").start();
48
+ try {
49
+ const filenameWithoutExtension = basename(absolutePath, ext);
50
+ const projectName = resolveProjectName(filenameWithoutExtension);
51
+ const outputPath = await resolveOutputPath(`${filenameWithoutExtension}.deepnote`);
52
+ await convertIpynbFilesToDeepnoteFile([absolutePath], {
53
+ projectName,
54
+ outputPath
55
+ });
56
+ spinner.succeed(`The Deepnote project has been saved to ${chalk.bold(outputPath)}`);
57
+ return outputPath;
58
+ } catch (error) {
59
+ spinner.fail("Conversion failed");
60
+ throw error;
61
+ }
62
+ }
63
+ if (ext === ".deepnote") {
64
+ const spinner = ora("Converting Deepnote project to Jupyter Notebooks...").start();
65
+ try {
66
+ const outputDirName = basename(absolutePath, ext);
67
+ const outputDir = customOutputPath ? resolve(cwd, customOutputPath) : resolve(cwd, outputDirName);
68
+ await convertDeepnoteFileToJupyterFiles(absolutePath, { outputDir });
69
+ spinner.succeed(`Jupyter Notebooks have been saved to ${chalk.bold(outputDir)}`);
70
+ return outputDir;
71
+ } catch (error) {
72
+ spinner.fail("Conversion failed");
73
+ throw error;
74
+ }
75
+ }
76
+ throw new Error("Unsupported file type. Please provide a .ipynb or .deepnote file.");
77
+ }
78
+
79
+ //#endregion
80
+ //#region src/bin.ts
81
+ async function main() {
82
+ const argv = cli({
83
+ name: "deepnote-convert",
84
+ parameters: ["<path>"],
85
+ flags: {
86
+ projectName: {
87
+ description: "The name of the Deepnote project.",
88
+ type: String
89
+ },
90
+ outputPath: {
91
+ alias: "o",
92
+ description: "The path where the .deepnote file will be saved.",
93
+ type: String
94
+ },
95
+ cwd: {
96
+ description: "The working directory to resolve paths relative to.",
97
+ type: String
98
+ }
99
+ }
100
+ });
101
+ await convert({
102
+ inputPath: argv._.path,
103
+ projectName: argv.flags.projectName,
104
+ outputPath: argv.flags.outputPath,
105
+ cwd: argv.flags.cwd ?? process.cwd()
106
+ });
107
+ }
108
+ if (process.env.NODE_ENV !== "test" && process.env.VITEST !== "true") main().catch((error) => {
109
+ console.error(error);
110
+ process.exit(1);
111
+ });
112
+
113
+ //#endregion
114
+ export { };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,158 @@
1
+ import { DeepnoteBlock, DeepnoteFile } from "@deepnote/blocks";
2
+
3
+ //#region src/types/jupyter.d.ts
4
+
5
+ /**
6
+ * Shared Jupyter Notebook type definitions used by both
7
+ * deepnote-to-jupyter and jupyter-to-deepnote converters.
8
+ */
9
+ interface JupyterCell {
10
+ /** Top-level block_group field present in cloud-exported notebooks */
11
+ block_group?: string;
12
+ cell_type: 'code' | 'markdown';
13
+ execution_count?: number | null;
14
+ metadata: JupyterCellMetadata;
15
+ outputs?: any[];
16
+ outputs_reference?: string;
17
+ source: string | string[];
18
+ }
19
+ interface JupyterCellMetadata {
20
+ cell_id?: string;
21
+ deepnote_cell_type?: string;
22
+ deepnote_block_group?: string;
23
+ deepnote_sorting_key?: string;
24
+ deepnote_source?: string;
25
+ [key: string]: unknown;
26
+ }
27
+ interface JupyterNotebook {
28
+ cells: JupyterCell[];
29
+ metadata: JupyterNotebookMetadata;
30
+ nbformat?: number;
31
+ nbformat_minor?: number;
32
+ }
33
+ interface JupyterNotebookMetadata {
34
+ deepnote_notebook_id?: string;
35
+ deepnote_notebook_name?: string;
36
+ deepnote_execution_mode?: 'block' | 'downstream';
37
+ deepnote_is_module?: boolean;
38
+ deepnote_working_directory?: string;
39
+ [key: string]: unknown;
40
+ }
41
+ //#endregion
42
+ //#region src/deepnote-to-jupyter.d.ts
43
+ interface ConvertDeepnoteFileToJupyterOptions {
44
+ outputDir: string;
45
+ }
46
+ interface ConvertBlocksToJupyterOptions {
47
+ /** Unique identifier for the notebook */
48
+ notebookId: string;
49
+ /** Display name of the notebook */
50
+ notebookName: string;
51
+ /** Execution mode: 'block' runs cells individually, 'downstream' runs dependent cells */
52
+ executionMode?: 'block' | 'downstream';
53
+ /** Whether this notebook is a module (importable by other notebooks) */
54
+ isModule?: boolean;
55
+ /** Working directory for the notebook */
56
+ workingDirectory?: string;
57
+ }
58
+ /**
59
+ * Converts an array of Deepnote blocks into a single Jupyter Notebook.
60
+ * This is the lowest-level conversion function, suitable for use in Deepnote Cloud.
61
+ *
62
+ * @param blocks - Array of DeepnoteBlock objects to convert
63
+ * @param options - Notebook metadata options
64
+ * @returns A JupyterNotebook object
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * import { convertBlocksToJupyterNotebook } from '@deepnote/convert'
69
+ *
70
+ * const notebook = convertBlocksToJupyterNotebook(blocks, {
71
+ * notebookId: 'abc123',
72
+ * notebookName: 'My Notebook',
73
+ * executionMode: 'block'
74
+ * })
75
+ * ```
76
+ */
77
+ declare function convertBlocksToJupyterNotebook(blocks: DeepnoteBlock[], options: ConvertBlocksToJupyterOptions): JupyterNotebook;
78
+ /**
79
+ * Converts a Deepnote project into Jupyter Notebook objects.
80
+ * This is a pure conversion function that doesn't perform any file I/O.
81
+ * Each notebook in the Deepnote project is converted to a separate Jupyter notebook.
82
+ *
83
+ * @param deepnoteFile - The deserialized Deepnote project file
84
+ * @returns Array of objects containing filename and corresponding Jupyter notebook
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * import { deserializeDeepnoteFile } from '@deepnote/blocks'
89
+ * import { convertDeepnoteToJupyterNotebooks } from '@deepnote/convert'
90
+ *
91
+ * const yamlContent = await fs.readFile('project.deepnote', 'utf-8')
92
+ * const deepnoteFile = deserializeDeepnoteFile(yamlContent)
93
+ * const notebooks = convertDeepnoteToJupyterNotebooks(deepnoteFile)
94
+ *
95
+ * for (const { filename, notebook } of notebooks) {
96
+ * console.log(`${filename}: ${notebook.cells.length} cells`)
97
+ * }
98
+ * ```
99
+ */
100
+ declare function convertDeepnoteToJupyterNotebooks(deepnoteFile: DeepnoteFile): Array<{
101
+ filename: string;
102
+ notebook: JupyterNotebook;
103
+ }>;
104
+ /**
105
+ * Converts a Deepnote project file into separate Jupyter Notebook (.ipynb) files.
106
+ * Each notebook in the Deepnote project becomes a separate .ipynb file.
107
+ */
108
+ declare function convertDeepnoteFileToJupyterFiles(deepnoteFilePath: string, options: ConvertDeepnoteFileToJupyterOptions): Promise<void>;
109
+ //#endregion
1
110
  //#region src/jupyter-to-deepnote.d.ts
2
111
  interface ConvertIpynbFilesToDeepnoteFileOptions {
3
112
  outputPath: string;
4
113
  projectName: string;
5
114
  }
115
+ interface ConvertJupyterNotebookOptions {
116
+ /** Custom ID generator function. Defaults to uuid v4. */
117
+ idGenerator?: () => string;
118
+ }
119
+ interface JupyterNotebookInput {
120
+ filename: string;
121
+ notebook: JupyterNotebook;
122
+ }
123
+ /**
124
+ * Converts a single Jupyter Notebook into an array of Deepnote blocks.
125
+ * This is the lowest-level conversion function, suitable for use in Deepnote Cloud.
126
+ *
127
+ * @param notebook - The Jupyter notebook object to convert
128
+ * @param options - Optional conversion options including custom ID generator
129
+ * @returns Array of DeepnoteBlock objects
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * import { convertJupyterNotebookToBlocks } from '@deepnote/convert'
134
+ *
135
+ * const notebook = JSON.parse(ipynbContent)
136
+ * const blocks = convertJupyterNotebookToBlocks(notebook, {
137
+ * idGenerator: () => myCustomIdGenerator()
138
+ * })
139
+ * ```
140
+ */
141
+ declare function convertJupyterNotebookToBlocks(notebook: JupyterNotebook, options?: ConvertJupyterNotebookOptions): DeepnoteBlock[];
142
+ /**
143
+ * Converts Jupyter Notebook objects into a Deepnote project file.
144
+ * This is a pure conversion function that doesn't perform any file I/O.
145
+ *
146
+ * @param notebooks - Array of Jupyter notebooks with filenames
147
+ * @param options - Conversion options including project name
148
+ * @returns A DeepnoteFile object
149
+ */
150
+ declare function convertJupyterNotebooksToDeepnote(notebooks: JupyterNotebookInput[], options: {
151
+ projectName: string;
152
+ }): DeepnoteFile;
6
153
  /**
7
154
  * Converts multiple Jupyter Notebook (.ipynb) files into a single Deepnote project file.
8
155
  */
9
156
  declare function convertIpynbFilesToDeepnoteFile(inputFilePaths: string[], options: ConvertIpynbFilesToDeepnoteFileOptions): Promise<void>;
10
157
  //#endregion
11
- export { type ConvertIpynbFilesToDeepnoteFileOptions, convertIpynbFilesToDeepnoteFile };
158
+ export { type ConvertBlocksToJupyterOptions, type ConvertIpynbFilesToDeepnoteFileOptions, type ConvertJupyterNotebookOptions, type JupyterCell, type JupyterNotebook, type JupyterNotebookInput, convertBlocksToJupyterNotebook, convertDeepnoteFileToJupyterFiles as convertDeepnoteFileToJupyter, convertDeepnoteToJupyterNotebooks, convertIpynbFilesToDeepnoteFile, convertJupyterNotebookToBlocks, convertJupyterNotebooksToDeepnote };
package/dist/index.js CHANGED
@@ -1,87 +1,3 @@
1
- import fs from "node:fs/promises";
2
- import { basename, dirname, extname } from "node:path";
3
- import { v4 } from "uuid";
4
- import { stringify } from "yaml";
1
+ import { a as convertDeepnoteFileToJupyterFiles, i as convertBlocksToJupyterNotebook, n as convertJupyterNotebookToBlocks, o as convertDeepnoteToJupyterNotebooks, r as convertJupyterNotebooksToDeepnote, t as convertIpynbFilesToDeepnoteFile } from "./src-j4HyYJfD.js";
5
2
 
6
- //#region src/jupyter-to-deepnote.ts
7
- /**
8
- * Converts multiple Jupyter Notebook (.ipynb) files into a single Deepnote project file.
9
- */
10
- async function convertIpynbFilesToDeepnoteFile(inputFilePaths, options) {
11
- const deepnoteFile = {
12
- metadata: { createdAt: (/* @__PURE__ */ new Date()).toISOString() },
13
- project: {
14
- id: v4(),
15
- initNotebookId: void 0,
16
- integrations: [],
17
- name: options.projectName,
18
- notebooks: [],
19
- settings: {}
20
- },
21
- version: "1.0.0"
22
- };
23
- for (const filePath of inputFilePaths) {
24
- const name = basename(filePath, extname(filePath)) || "Untitled notebook";
25
- const blocks = (await parseIpynbFile(filePath)).cells.map((cell, index) => {
26
- const source = Array.isArray(cell.source) ? cell.source.join("") : cell.source;
27
- return {
28
- blockGroup: v4(),
29
- content: source,
30
- executionCount: cell.execution_count ?? void 0,
31
- id: v4(),
32
- metadata: {},
33
- outputs: cell.cell_type === "code" ? cell.outputs : void 0,
34
- sortingKey: createSortingKey(index),
35
- type: cell.cell_type === "code" ? "code" : "markdown",
36
- version: 1
37
- };
38
- });
39
- deepnoteFile.project.notebooks.push({
40
- blocks,
41
- executionMode: "block",
42
- id: v4(),
43
- isModule: false,
44
- name,
45
- workingDirectory: void 0
46
- });
47
- }
48
- const yamlContent = stringify(deepnoteFile);
49
- const parentDir = dirname(options.outputPath);
50
- await fs.mkdir(parentDir, { recursive: true });
51
- await fs.writeFile(options.outputPath, yamlContent, "utf-8");
52
- }
53
- async function parseIpynbFile(filePath) {
54
- let ipynbJson;
55
- try {
56
- ipynbJson = await fs.readFile(filePath, "utf-8");
57
- } catch (error) {
58
- const message = error instanceof Error ? error.message : String(error);
59
- throw new Error(`Failed to read ${filePath}: ${message}`);
60
- }
61
- try {
62
- return JSON.parse(ipynbJson);
63
- } catch (error) {
64
- const message = error instanceof Error ? error.message : String(error);
65
- throw new Error(`Failed to parse ${filePath}: invalid JSON - ${message}`);
66
- }
67
- }
68
- function createSortingKey(index) {
69
- const maxLength = 6;
70
- const chars = "0123456789abcdefghijklmnopqrstuvwxyz";
71
- const base = 36;
72
- if (index < 0) throw new Error("Index must be non-negative");
73
- let result = "";
74
- let num = index + 1;
75
- let iterations = 0;
76
- while (num > 0 && iterations < maxLength) {
77
- num--;
78
- result = chars[num % base] + result;
79
- num = Math.floor(num / base);
80
- iterations++;
81
- }
82
- if (num > 0) throw new Error(`Index ${index} exceeds maximum key length of ${maxLength}`);
83
- return result;
84
- }
85
-
86
- //#endregion
87
- export { convertIpynbFilesToDeepnoteFile };
3
+ export { convertBlocksToJupyterNotebook, convertDeepnoteFileToJupyterFiles as convertDeepnoteFileToJupyter, convertDeepnoteToJupyterNotebooks, convertIpynbFilesToDeepnoteFile, convertJupyterNotebookToBlocks, convertJupyterNotebooksToDeepnote };
@@ -0,0 +1,279 @@
1
+ import fs from "node:fs/promises";
2
+ import { basename, dirname, extname, join } from "node:path";
3
+ import { createMarkdown, createPythonCode, deserializeDeepnoteFile } from "@deepnote/blocks";
4
+ import { v4 } from "uuid";
5
+ import { stringify } from "yaml";
6
+
7
+ //#region src/deepnote-to-jupyter.ts
8
+ /**
9
+ * Converts an array of Deepnote blocks into a single Jupyter Notebook.
10
+ * This is the lowest-level conversion function, suitable for use in Deepnote Cloud.
11
+ *
12
+ * @param blocks - Array of DeepnoteBlock objects to convert
13
+ * @param options - Notebook metadata options
14
+ * @returns A JupyterNotebook object
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * import { convertBlocksToJupyterNotebook } from '@deepnote/convert'
19
+ *
20
+ * const notebook = convertBlocksToJupyterNotebook(blocks, {
21
+ * notebookId: 'abc123',
22
+ * notebookName: 'My Notebook',
23
+ * executionMode: 'block'
24
+ * })
25
+ * ```
26
+ */
27
+ function convertBlocksToJupyterNotebook(blocks, options) {
28
+ return {
29
+ cells: blocks.map((block) => convertBlockToCell(block)),
30
+ metadata: {
31
+ deepnote_notebook_id: options.notebookId,
32
+ deepnote_notebook_name: options.notebookName,
33
+ deepnote_execution_mode: options.executionMode,
34
+ deepnote_is_module: options.isModule,
35
+ deepnote_working_directory: options.workingDirectory
36
+ },
37
+ nbformat: 4,
38
+ nbformat_minor: 0
39
+ };
40
+ }
41
+ /**
42
+ * Converts a Deepnote project into Jupyter Notebook objects.
43
+ * This is a pure conversion function that doesn't perform any file I/O.
44
+ * Each notebook in the Deepnote project is converted to a separate Jupyter notebook.
45
+ *
46
+ * @param deepnoteFile - The deserialized Deepnote project file
47
+ * @returns Array of objects containing filename and corresponding Jupyter notebook
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * import { deserializeDeepnoteFile } from '@deepnote/blocks'
52
+ * import { convertDeepnoteToJupyterNotebooks } from '@deepnote/convert'
53
+ *
54
+ * const yamlContent = await fs.readFile('project.deepnote', 'utf-8')
55
+ * const deepnoteFile = deserializeDeepnoteFile(yamlContent)
56
+ * const notebooks = convertDeepnoteToJupyterNotebooks(deepnoteFile)
57
+ *
58
+ * for (const { filename, notebook } of notebooks) {
59
+ * console.log(`${filename}: ${notebook.cells.length} cells`)
60
+ * }
61
+ * ```
62
+ */
63
+ function convertDeepnoteToJupyterNotebooks(deepnoteFile) {
64
+ return deepnoteFile.project.notebooks.map((notebook) => {
65
+ const jupyterNotebook = convertNotebookToJupyter(deepnoteFile, notebook);
66
+ return {
67
+ filename: `${sanitizeFileName(notebook.name)}.ipynb`,
68
+ notebook: jupyterNotebook
69
+ };
70
+ });
71
+ }
72
+ /**
73
+ * Converts a Deepnote project file into separate Jupyter Notebook (.ipynb) files.
74
+ * Each notebook in the Deepnote project becomes a separate .ipynb file.
75
+ */
76
+ async function convertDeepnoteFileToJupyterFiles(deepnoteFilePath, options) {
77
+ const notebooks = convertDeepnoteToJupyterNotebooks(deserializeDeepnoteFile(await fs.readFile(deepnoteFilePath, "utf-8")));
78
+ await fs.mkdir(options.outputDir, { recursive: true });
79
+ for (const { filename, notebook } of notebooks) {
80
+ const filePath = join(options.outputDir, filename);
81
+ await fs.writeFile(filePath, JSON.stringify(notebook, null, 2), "utf-8");
82
+ }
83
+ }
84
+ function convertBlockToCell(block) {
85
+ const content = block.content || "";
86
+ const jupyterCellType = convertBlockTypeToJupyter(block.type);
87
+ const metadata = {
88
+ cell_id: block.id,
89
+ deepnote_block_group: block.blockGroup,
90
+ deepnote_cell_type: block.type,
91
+ deepnote_sorting_key: block.sortingKey,
92
+ ...block.metadata || {}
93
+ };
94
+ metadata.deepnote_source = content;
95
+ return {
96
+ block_group: block.blockGroup,
97
+ cell_type: jupyterCellType,
98
+ execution_count: block.executionCount ?? null,
99
+ metadata,
100
+ outputs: block.outputs,
101
+ source: getSourceForBlock(block, jupyterCellType, content)
102
+ };
103
+ }
104
+ function getSourceForBlock(block, jupyterCellType, content) {
105
+ if (jupyterCellType === "markdown") return createMarkdown(block);
106
+ if (block.type === "code") return content;
107
+ return createPythonCode(block);
108
+ }
109
+ function convertBlockTypeToJupyter(blockType) {
110
+ const codeTypes = [
111
+ "big-number",
112
+ "button",
113
+ "code",
114
+ "notebook-function",
115
+ "sql",
116
+ "visualization"
117
+ ];
118
+ if (blockType.startsWith("input-")) return "code";
119
+ return codeTypes.includes(blockType) ? "code" : "markdown";
120
+ }
121
+ function convertNotebookToJupyter(_deepnoteFile, notebook) {
122
+ return convertBlocksToJupyterNotebook(notebook.blocks, {
123
+ notebookId: notebook.id,
124
+ notebookName: notebook.name,
125
+ executionMode: notebook.executionMode,
126
+ isModule: notebook.isModule,
127
+ workingDirectory: notebook.workingDirectory
128
+ });
129
+ }
130
+ function sanitizeFileName(name) {
131
+ return name.replace(/[<>:"/\\|?*]/g, "_").replace(/\s+/g, "-");
132
+ }
133
+
134
+ //#endregion
135
+ //#region src/jupyter-to-deepnote.ts
136
+ /**
137
+ * Converts a single Jupyter Notebook into an array of Deepnote blocks.
138
+ * This is the lowest-level conversion function, suitable for use in Deepnote Cloud.
139
+ *
140
+ * @param notebook - The Jupyter notebook object to convert
141
+ * @param options - Optional conversion options including custom ID generator
142
+ * @returns Array of DeepnoteBlock objects
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * import { convertJupyterNotebookToBlocks } from '@deepnote/convert'
147
+ *
148
+ * const notebook = JSON.parse(ipynbContent)
149
+ * const blocks = convertJupyterNotebookToBlocks(notebook, {
150
+ * idGenerator: () => myCustomIdGenerator()
151
+ * })
152
+ * ```
153
+ */
154
+ function convertJupyterNotebookToBlocks(notebook, options) {
155
+ const idGenerator = options?.idGenerator ?? v4;
156
+ return notebook.cells.map((cell, index) => convertCellToBlock(cell, index, idGenerator));
157
+ }
158
+ /**
159
+ * Converts Jupyter Notebook objects into a Deepnote project file.
160
+ * This is a pure conversion function that doesn't perform any file I/O.
161
+ *
162
+ * @param notebooks - Array of Jupyter notebooks with filenames
163
+ * @param options - Conversion options including project name
164
+ * @returns A DeepnoteFile object
165
+ */
166
+ function convertJupyterNotebooksToDeepnote(notebooks, options) {
167
+ const deepnoteFile = {
168
+ metadata: { createdAt: (/* @__PURE__ */ new Date()).toISOString() },
169
+ project: {
170
+ id: v4(),
171
+ initNotebookId: void 0,
172
+ integrations: [],
173
+ name: options.projectName,
174
+ notebooks: [],
175
+ settings: {}
176
+ },
177
+ version: "1.0.0"
178
+ };
179
+ for (const { filename, notebook } of notebooks) {
180
+ const filenameWithoutExt = basename(filename, extname(filename)) || "Untitled notebook";
181
+ const blocks = convertJupyterNotebookToBlocks(notebook);
182
+ const notebookId = notebook.metadata?.deepnote_notebook_id;
183
+ const notebookName = notebook.metadata?.deepnote_notebook_name;
184
+ const executionMode = notebook.metadata?.deepnote_execution_mode;
185
+ const isModule = notebook.metadata?.deepnote_is_module;
186
+ const workingDirectory = notebook.metadata?.deepnote_working_directory;
187
+ deepnoteFile.project.notebooks.push({
188
+ blocks,
189
+ executionMode: executionMode ?? "block",
190
+ id: notebookId ?? v4(),
191
+ isModule: isModule ?? false,
192
+ name: notebookName ?? filenameWithoutExt,
193
+ workingDirectory
194
+ });
195
+ }
196
+ return deepnoteFile;
197
+ }
198
+ /**
199
+ * Converts multiple Jupyter Notebook (.ipynb) files into a single Deepnote project file.
200
+ */
201
+ async function convertIpynbFilesToDeepnoteFile(inputFilePaths, options) {
202
+ const notebooks = [];
203
+ for (const filePath of inputFilePaths) {
204
+ const notebook = await parseIpynbFile(filePath);
205
+ notebooks.push({
206
+ filename: basename(filePath),
207
+ notebook
208
+ });
209
+ }
210
+ const yamlContent = stringify(convertJupyterNotebooksToDeepnote(notebooks, { projectName: options.projectName }));
211
+ const parentDir = dirname(options.outputPath);
212
+ await fs.mkdir(parentDir, { recursive: true });
213
+ await fs.writeFile(options.outputPath, yamlContent, "utf-8");
214
+ }
215
+ async function parseIpynbFile(filePath) {
216
+ let ipynbJson;
217
+ try {
218
+ ipynbJson = await fs.readFile(filePath, "utf-8");
219
+ } catch (error) {
220
+ const message = error instanceof Error ? error.message : String(error);
221
+ throw new Error(`Failed to read ${filePath}: ${message}`);
222
+ }
223
+ try {
224
+ return JSON.parse(ipynbJson);
225
+ } catch (error) {
226
+ const message = error instanceof Error ? error.message : String(error);
227
+ throw new Error(`Failed to parse ${filePath}: invalid JSON - ${message}`);
228
+ }
229
+ }
230
+ function convertCellToBlock(cell, index, idGenerator) {
231
+ let source = Array.isArray(cell.source) ? cell.source.join("") : cell.source;
232
+ const cellId = cell.metadata?.cell_id;
233
+ const deepnoteCellType = cell.metadata?.deepnote_cell_type;
234
+ const sortingKey = cell.metadata?.deepnote_sorting_key;
235
+ const blockGroup = cell.metadata?.deepnote_block_group ?? cell.block_group ?? idGenerator();
236
+ const deepnoteSource = cell.metadata?.deepnote_source;
237
+ if (deepnoteSource !== void 0) source = deepnoteSource;
238
+ const blockType = deepnoteCellType ?? (cell.cell_type === "code" ? "code" : "markdown");
239
+ const originalMetadata = { ...cell.metadata };
240
+ delete originalMetadata.cell_id;
241
+ delete originalMetadata.deepnote_cell_type;
242
+ delete originalMetadata.deepnote_block_group;
243
+ delete originalMetadata.deepnote_sorting_key;
244
+ delete originalMetadata.deepnote_source;
245
+ delete cell.block_group;
246
+ const executionCount = cell.execution_count ?? void 0;
247
+ const hasExecutionCount = executionCount !== void 0;
248
+ const hasOutputs = cell.cell_type === "code" && cell.outputs !== void 0;
249
+ return {
250
+ blockGroup,
251
+ content: source,
252
+ ...hasExecutionCount ? { executionCount } : {},
253
+ id: cellId ?? idGenerator(),
254
+ metadata: originalMetadata,
255
+ ...hasOutputs ? { outputs: cell.outputs } : {},
256
+ sortingKey: sortingKey ?? createSortingKey(index),
257
+ type: blockType
258
+ };
259
+ }
260
+ function createSortingKey(index) {
261
+ const maxLength = 6;
262
+ const chars = "0123456789abcdefghijklmnopqrstuvwxyz";
263
+ const base = 36;
264
+ if (index < 0) throw new Error("Index must be non-negative");
265
+ let result = "";
266
+ let num = index + 1;
267
+ let iterations = 0;
268
+ while (num > 0 && iterations < maxLength) {
269
+ num--;
270
+ result = chars[num % base] + result;
271
+ num = Math.floor(num / base);
272
+ iterations++;
273
+ }
274
+ if (num > 0) throw new Error(`Index ${index} exceeds maximum key length of ${maxLength}`);
275
+ return result;
276
+ }
277
+
278
+ //#endregion
279
+ export { convertDeepnoteFileToJupyterFiles as a, convertBlocksToJupyterNotebook as i, convertJupyterNotebookToBlocks as n, convertDeepnoteToJupyterNotebooks as o, convertJupyterNotebooksToDeepnote as r, convertIpynbFilesToDeepnoteFile as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepnote/convert",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "repository": {
@@ -29,11 +29,11 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "chalk": "^5.6.2",
32
- "cleye": "^1.3.4",
32
+ "cleye": "^2.0.0",
33
33
  "ora": "^9.0.0",
34
34
  "uuid": "^13.0.0",
35
35
  "yaml": "^2.8.1",
36
- "@deepnote/blocks": "1.3.5"
36
+ "@deepnote/blocks": "1.3.6"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^22.0.0",
@@ -41,15 +41,16 @@
41
41
  "typescript": "^5.0.0"
42
42
  },
43
43
  "engines": {
44
- "node": ">=18"
44
+ "node": ">=22.14.0",
45
+ "pnpm": ">=10.17.1"
45
46
  },
46
47
  "publishConfig": {
47
48
  "access": "public",
48
49
  "registry": "https://registry.npmjs.org"
49
50
  },
50
51
  "scripts": {
51
- "build": "tsdown --format esm --dts",
52
+ "build": "tsdown --format esm --dts src/bin.ts src/index.ts",
52
53
  "test": "vitest",
53
- "watch": "tsdown --watch --format esm --dts"
54
+ "watch": "tsdown --watch --format esm --dts src/bin.ts src/index.ts"
54
55
  }
55
56
  }