@deepnote/convert 2.1.3 → 2.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 +2 -2
- package/dist/bin.cjs +128 -56
- package/dist/bin.js +127 -57
- package/dist/index.cjs +25 -3
- package/dist/index.d.cts +334 -3
- package/dist/index.d.ts +334 -3
- package/dist/index.js +2 -2
- package/dist/{src-zxlAKpnq.js → src-C0XPNDnj.js} +673 -43
- package/dist/{src-1XGCb_OZ.cjs → src-CrnuwKOU.cjs} +803 -41
- package/package.json +3 -2
|
@@ -27,10 +27,10 @@ let node_path = require("node:path");
|
|
|
27
27
|
node_path = __toESM(node_path);
|
|
28
28
|
let __deepnote_blocks = require("@deepnote/blocks");
|
|
29
29
|
__deepnote_blocks = __toESM(__deepnote_blocks);
|
|
30
|
-
let node_crypto = require("node:crypto");
|
|
31
|
-
node_crypto = __toESM(node_crypto);
|
|
32
30
|
let yaml = require("yaml");
|
|
33
31
|
yaml = __toESM(yaml);
|
|
32
|
+
let node_crypto = require("node:crypto");
|
|
33
|
+
node_crypto = __toESM(node_crypto);
|
|
34
34
|
|
|
35
35
|
//#region src/utils.ts
|
|
36
36
|
/**
|
|
@@ -126,7 +126,7 @@ function sortKeysAlphabetically(obj) {
|
|
|
126
126
|
*/
|
|
127
127
|
function convertBlocksToJupyterNotebook(blocks, options) {
|
|
128
128
|
return {
|
|
129
|
-
cells: blocks.map((block) =>
|
|
129
|
+
cells: blocks.map((block) => convertBlockToJupyterCell(block)),
|
|
130
130
|
metadata: {
|
|
131
131
|
deepnote_notebook_id: options.notebookId,
|
|
132
132
|
deepnote_notebook_name: options.notebookName,
|
|
@@ -183,7 +183,27 @@ async function convertDeepnoteFileToJupyterFiles(deepnoteFilePath, options) {
|
|
|
183
183
|
await node_fs_promises.default.writeFile(filePath, JSON.stringify(notebook, null, 2), "utf-8");
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Converts a single Deepnote block to a Jupyter cell.
|
|
188
|
+
* This is useful for streaming export scenarios where blocks need to be
|
|
189
|
+
* processed one at a time rather than converting an entire notebook at once.
|
|
190
|
+
*
|
|
191
|
+
* @param block - A single DeepnoteBlock to convert
|
|
192
|
+
* @returns A JupyterCell object
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* import { convertBlockToJupyterCell } from '@deepnote/convert'
|
|
197
|
+
* import type { JupyterCell } from '@deepnote/convert'
|
|
198
|
+
*
|
|
199
|
+
* // Streaming export example
|
|
200
|
+
* for await (const block of blockStream) {
|
|
201
|
+
* const cell: JupyterCell = convertBlockToJupyterCell(block)
|
|
202
|
+
* outputStream.write(JSON.stringify(cell))
|
|
203
|
+
* }
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
function convertBlockToJupyterCell(block) {
|
|
187
207
|
const content = block.content || "";
|
|
188
208
|
const jupyterCellType = convertBlockTypeToJupyter(block.type);
|
|
189
209
|
const executionStartedAt = "executionStartedAt" in block ? block.executionStartedAt : void 0;
|
|
@@ -651,11 +671,49 @@ function convertBlockToCell(block) {
|
|
|
651
671
|
//#region src/format-detection.ts
|
|
652
672
|
/** Check if file content is Marimo format */
|
|
653
673
|
function isMarimoContent(content) {
|
|
654
|
-
|
|
674
|
+
const lines = content.split("\n");
|
|
675
|
+
let inDocstring = false;
|
|
676
|
+
let docstringQuote = "";
|
|
677
|
+
const firstLine = lines.find((line) => {
|
|
678
|
+
const t = line.trim();
|
|
679
|
+
if (t.length === 0) return false;
|
|
680
|
+
if (t.startsWith("#!") || /^#\s*(-\*-\s*)?(coding|encoding)[:=]/i.test(t)) return false;
|
|
681
|
+
for (const q of ["\"\"\"", "'''"]) {
|
|
682
|
+
if (!inDocstring && t.startsWith(q)) {
|
|
683
|
+
if (t.length > 3 && t.endsWith(q)) return false;
|
|
684
|
+
inDocstring = true;
|
|
685
|
+
docstringQuote = q;
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
if (inDocstring && docstringQuote === q && t.includes(q)) {
|
|
689
|
+
inDocstring = false;
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (inDocstring) return false;
|
|
694
|
+
return true;
|
|
695
|
+
}) ?? "";
|
|
696
|
+
return (/^import\s+marimo\b/.test(firstLine) || /^from\s+marimo\s+import\b/.test(firstLine)) && /@app\.cell\b/.test(content);
|
|
655
697
|
}
|
|
656
698
|
/** Check if file content is percent format */
|
|
657
699
|
function isPercentContent(content) {
|
|
658
|
-
|
|
700
|
+
const lines = content.split("\n");
|
|
701
|
+
let inTripleQuote = false;
|
|
702
|
+
let quoteChar = "";
|
|
703
|
+
for (const line of lines) {
|
|
704
|
+
for (const q of ["\"\"\"", "'''"]) {
|
|
705
|
+
let idx = line.indexOf(q, 0);
|
|
706
|
+
while (idx !== -1) {
|
|
707
|
+
if (!inTripleQuote) {
|
|
708
|
+
inTripleQuote = true;
|
|
709
|
+
quoteChar = q;
|
|
710
|
+
} else if (quoteChar === q) inTripleQuote = false;
|
|
711
|
+
idx = line.indexOf(q, idx + 3);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (!inTripleQuote && /^# %%/.test(line)) return true;
|
|
715
|
+
}
|
|
716
|
+
return false;
|
|
659
717
|
}
|
|
660
718
|
/**
|
|
661
719
|
* Detects the notebook format from filename and optionally content.
|
|
@@ -757,9 +815,14 @@ function convertJupyterNotebooksToDeepnote(notebooks, options) {
|
|
|
757
815
|
return deepnoteFile;
|
|
758
816
|
}
|
|
759
817
|
/**
|
|
760
|
-
*
|
|
818
|
+
* Reads and converts multiple Jupyter Notebook (.ipynb) files into a DeepnoteFile.
|
|
819
|
+
* This function reads the files and returns the converted DeepnoteFile without writing to disk.
|
|
820
|
+
*
|
|
821
|
+
* @param inputFilePaths - Array of paths to .ipynb files
|
|
822
|
+
* @param options - Conversion options including project name
|
|
823
|
+
* @returns A DeepnoteFile object
|
|
761
824
|
*/
|
|
762
|
-
async function
|
|
825
|
+
async function readAndConvertIpynbFiles(inputFilePaths, options) {
|
|
763
826
|
const notebooks = [];
|
|
764
827
|
for (const filePath of inputFilePaths) {
|
|
765
828
|
const notebook = await parseIpynbFile(filePath);
|
|
@@ -768,7 +831,13 @@ async function convertIpynbFilesToDeepnoteFile(inputFilePaths, options) {
|
|
|
768
831
|
notebook
|
|
769
832
|
});
|
|
770
833
|
}
|
|
771
|
-
|
|
834
|
+
return convertJupyterNotebooksToDeepnote(notebooks, { projectName: options.projectName });
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Converts multiple Jupyter Notebook (.ipynb) files into a single Deepnote project file.
|
|
838
|
+
*/
|
|
839
|
+
async function convertIpynbFilesToDeepnoteFile(inputFilePaths, options) {
|
|
840
|
+
const yamlContent = (0, yaml.stringify)(await readAndConvertIpynbFiles(inputFilePaths, { projectName: options.projectName }));
|
|
772
841
|
const parentDir = (0, node_path.dirname)(options.outputPath);
|
|
773
842
|
await node_fs_promises.default.mkdir(parentDir, { recursive: true });
|
|
774
843
|
await node_fs_promises.default.writeFile(options.outputPath, yamlContent, "utf-8");
|
|
@@ -812,7 +881,7 @@ function convertCellToBlock$3(cell, index, idGenerator) {
|
|
|
812
881
|
delete cell.block_group;
|
|
813
882
|
const executionCount = cell.execution_count ?? void 0;
|
|
814
883
|
const hasExecutionCount = executionCount !== void 0;
|
|
815
|
-
const hasOutputs = cell.cell_type === "code" && cell.outputs !== void 0;
|
|
884
|
+
const hasOutputs$1 = cell.cell_type === "code" && cell.outputs !== void 0;
|
|
816
885
|
return sortKeysAlphabetically(__deepnote_blocks.deepnoteBlockSchema.parse({
|
|
817
886
|
blockGroup,
|
|
818
887
|
content: source,
|
|
@@ -822,15 +891,444 @@ function convertCellToBlock$3(cell, index, idGenerator) {
|
|
|
822
891
|
...executionStartedAt ? { executionStartedAt } : {},
|
|
823
892
|
id: cellId ?? idGenerator(),
|
|
824
893
|
metadata: originalMetadata,
|
|
825
|
-
...hasOutputs ? { outputs: cell.outputs } : {},
|
|
894
|
+
...hasOutputs$1 ? { outputs: cell.outputs } : {},
|
|
826
895
|
sortingKey: sortingKey ?? createSortingKey(index),
|
|
827
896
|
type: blockType
|
|
828
897
|
}));
|
|
829
898
|
}
|
|
830
899
|
|
|
900
|
+
//#endregion
|
|
901
|
+
//#region src/snapshot/hash.ts
|
|
902
|
+
/**
|
|
903
|
+
* Computes a SHA-256 hash of the given content.
|
|
904
|
+
*
|
|
905
|
+
* @param content - The content to hash
|
|
906
|
+
* @returns Hash string in format 'sha256:{hex}'
|
|
907
|
+
*/
|
|
908
|
+
function computeContentHash(content) {
|
|
909
|
+
return `sha256:${(0, node_crypto.createHash)("sha256").update(content, "utf-8").digest("hex")}`;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Computes a snapshot hash from the file's key properties.
|
|
913
|
+
* The hash is based on: version, environment.hash, integrations, and all block contentHashes.
|
|
914
|
+
*
|
|
915
|
+
* @param file - The DeepnoteFile to compute hash for
|
|
916
|
+
* @returns Hash string in format 'sha256:{hex}'
|
|
917
|
+
*/
|
|
918
|
+
function computeSnapshotHash(file) {
|
|
919
|
+
const parts = [];
|
|
920
|
+
parts.push(`version:${file.version}`);
|
|
921
|
+
if (file.environment?.hash) parts.push(`env:${file.environment.hash}`);
|
|
922
|
+
const sortedIntegrations = [...file.project.integrations ?? []].sort((a, b) => a.id.localeCompare(b.id));
|
|
923
|
+
for (const integration of sortedIntegrations) parts.push(`integration:${integration.id}:${integration.type}`);
|
|
924
|
+
for (const notebook of file.project.notebooks) for (const block of notebook.blocks) if (block.contentHash) parts.push(`block:${block.id}:${block.contentHash}`);
|
|
925
|
+
const combined = parts.join("\n");
|
|
926
|
+
return `sha256:${(0, node_crypto.createHash)("sha256").update(combined, "utf-8").digest("hex")}`;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Adds content hashes to all blocks in a DeepnoteFile that don't already have them.
|
|
930
|
+
* Returns a new DeepnoteFile with hashes added (does not mutate the input).
|
|
931
|
+
*
|
|
932
|
+
* @param file - The DeepnoteFile to add hashes to
|
|
933
|
+
* @returns A new DeepnoteFile with content hashes added
|
|
934
|
+
*/
|
|
935
|
+
function addContentHashes(file) {
|
|
936
|
+
const result = structuredClone(file);
|
|
937
|
+
for (const notebook of result.project.notebooks) for (const block of notebook.blocks) if (!block.contentHash && block.content) block.contentHash = computeContentHash(block.content);
|
|
938
|
+
return result;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
//#endregion
|
|
942
|
+
//#region src/snapshot/lookup.ts
|
|
943
|
+
/** Default directory name for snapshots */
|
|
944
|
+
const DEFAULT_SNAPSHOT_DIR = "snapshots";
|
|
945
|
+
/** Regex pattern for snapshot filenames */
|
|
946
|
+
const SNAPSHOT_FILENAME_PATTERN = /^(.+)_([0-9a-f-]{36})_(latest|[\dT:-]+)\.snapshot\.deepnote$/;
|
|
947
|
+
/**
|
|
948
|
+
* Parses a snapshot filename into its components.
|
|
949
|
+
*
|
|
950
|
+
* @param filename - The snapshot filename to parse
|
|
951
|
+
* @returns Parsed components or null if filename doesn't match pattern
|
|
952
|
+
*/
|
|
953
|
+
function parseSnapshotFilename(filename) {
|
|
954
|
+
const match = SNAPSHOT_FILENAME_PATTERN.exec(filename);
|
|
955
|
+
if (!match) return null;
|
|
956
|
+
return {
|
|
957
|
+
slug: match[1],
|
|
958
|
+
projectId: match[2],
|
|
959
|
+
timestamp: match[3]
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Finds all snapshot files for a given project.
|
|
964
|
+
*
|
|
965
|
+
* @param projectDir - Directory containing the .deepnote file
|
|
966
|
+
* @param projectId - The project UUID to search for
|
|
967
|
+
* @param options - Snapshot options
|
|
968
|
+
* @returns Array of SnapshotInfo objects, sorted by timestamp (newest first)
|
|
969
|
+
*/
|
|
970
|
+
async function findSnapshotsForProject(projectDir, projectId, options = {}) {
|
|
971
|
+
const snapshotsPath = (0, node_path.resolve)(projectDir, options.snapshotDir ?? DEFAULT_SNAPSHOT_DIR);
|
|
972
|
+
try {
|
|
973
|
+
const entries = await node_fs_promises.default.readdir(snapshotsPath, { withFileTypes: true });
|
|
974
|
+
const snapshots = [];
|
|
975
|
+
for (const entry of entries) {
|
|
976
|
+
if (!entry.isFile() || !entry.name.endsWith(".snapshot.deepnote")) continue;
|
|
977
|
+
const parsed = parseSnapshotFilename(entry.name);
|
|
978
|
+
if (parsed && parsed.projectId === projectId) snapshots.push({
|
|
979
|
+
path: (0, node_path.join)(snapshotsPath, entry.name),
|
|
980
|
+
slug: parsed.slug,
|
|
981
|
+
projectId: parsed.projectId,
|
|
982
|
+
timestamp: parsed.timestamp
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
snapshots.sort((a, b) => {
|
|
986
|
+
if (a.timestamp === "latest") return -1;
|
|
987
|
+
if (b.timestamp === "latest") return 1;
|
|
988
|
+
return b.timestamp.localeCompare(a.timestamp);
|
|
989
|
+
});
|
|
990
|
+
return snapshots;
|
|
991
|
+
} catch (err) {
|
|
992
|
+
if (err.code === "ENOENT") return [];
|
|
993
|
+
throw err;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Loads the latest snapshot for a project.
|
|
998
|
+
*
|
|
999
|
+
* @param sourceFilePath - Path to the source .deepnote file
|
|
1000
|
+
* @param projectId - The project UUID
|
|
1001
|
+
* @param options - Snapshot options
|
|
1002
|
+
* @returns The parsed DeepnoteSnapshot or null if not found
|
|
1003
|
+
*/
|
|
1004
|
+
async function loadLatestSnapshot(sourceFilePath, projectId, options = {}) {
|
|
1005
|
+
const snapshots = await findSnapshotsForProject((0, node_path.dirname)(sourceFilePath), projectId, options);
|
|
1006
|
+
if (snapshots.length === 0) return null;
|
|
1007
|
+
const snapshotInfo = snapshots[0];
|
|
1008
|
+
return loadSnapshotFile(snapshotInfo.path);
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Loads and parses a snapshot file.
|
|
1012
|
+
*
|
|
1013
|
+
* @param snapshotPath - Path to the snapshot file
|
|
1014
|
+
* @returns The parsed DeepnoteSnapshot
|
|
1015
|
+
*/
|
|
1016
|
+
async function loadSnapshotFile(snapshotPath) {
|
|
1017
|
+
const parsed = (0, yaml.parse)(await node_fs_promises.default.readFile(snapshotPath, "utf-8"));
|
|
1018
|
+
return __deepnote_blocks.deepnoteSnapshotSchema.parse(parsed);
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Gets the snapshot directory path for a source file.
|
|
1022
|
+
*
|
|
1023
|
+
* @param sourceFilePath - Path to the source .deepnote file
|
|
1024
|
+
* @param options - Snapshot options
|
|
1025
|
+
* @returns The snapshot directory path
|
|
1026
|
+
*/
|
|
1027
|
+
function getSnapshotDir(sourceFilePath, options = {}) {
|
|
1028
|
+
const snapshotDir = options.snapshotDir ?? DEFAULT_SNAPSHOT_DIR;
|
|
1029
|
+
return (0, node_path.resolve)((0, node_path.dirname)(sourceFilePath), snapshotDir);
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Checks if a snapshot exists for a project.
|
|
1033
|
+
*
|
|
1034
|
+
* @param sourceFilePath - Path to the source .deepnote file
|
|
1035
|
+
* @param projectId - The project UUID
|
|
1036
|
+
* @param options - Snapshot options
|
|
1037
|
+
* @returns True if at least one snapshot exists
|
|
1038
|
+
*/
|
|
1039
|
+
async function snapshotExists(sourceFilePath, projectId, options = {}) {
|
|
1040
|
+
return (await findSnapshotsForProject((0, node_path.dirname)(sourceFilePath), projectId, options)).length > 0;
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Extracts project information from a source file path.
|
|
1044
|
+
*
|
|
1045
|
+
* @param sourceFilePath - Path to the source .deepnote file
|
|
1046
|
+
* @returns Object with directory and filename without extension
|
|
1047
|
+
*/
|
|
1048
|
+
function parseSourceFilePath(sourceFilePath) {
|
|
1049
|
+
return {
|
|
1050
|
+
dir: (0, node_path.dirname)(sourceFilePath),
|
|
1051
|
+
name: (0, node_path.basename)(sourceFilePath).replace(/\.deepnote$/, "")
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
//#endregion
|
|
1056
|
+
//#region src/snapshot/marimo-outputs.ts
|
|
1057
|
+
/**
|
|
1058
|
+
* Finds the Marimo session cache file for a given .py file.
|
|
1059
|
+
* Looks in __marimo__/session/{filename}.json relative to the file's directory.
|
|
1060
|
+
*
|
|
1061
|
+
* @param marimoFilePath - Path to the .py Marimo file
|
|
1062
|
+
* @returns Path to the session cache file, or null if not found
|
|
1063
|
+
*/
|
|
1064
|
+
async function findMarimoSessionCache(marimoFilePath) {
|
|
1065
|
+
const sessionPath = (0, node_path.join)((0, node_path.dirname)(marimoFilePath), "__marimo__", "session", `${(0, node_path.basename)(marimoFilePath)}.json`);
|
|
1066
|
+
try {
|
|
1067
|
+
await node_fs_promises.default.access(sessionPath);
|
|
1068
|
+
return sessionPath;
|
|
1069
|
+
} catch {
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Reads and parses a Marimo session cache file.
|
|
1075
|
+
*
|
|
1076
|
+
* @param sessionPath - Path to the session cache JSON file
|
|
1077
|
+
* @returns The parsed session cache, or null if reading fails
|
|
1078
|
+
*/
|
|
1079
|
+
async function readMarimoSessionCache(sessionPath) {
|
|
1080
|
+
try {
|
|
1081
|
+
const content = await node_fs_promises.default.readFile(sessionPath, "utf-8");
|
|
1082
|
+
return JSON.parse(content);
|
|
1083
|
+
} catch (_error) {
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Converts a Marimo session output to Jupyter/Deepnote output format.
|
|
1089
|
+
*
|
|
1090
|
+
* @param output - The Marimo session output
|
|
1091
|
+
* @returns A Jupyter-compatible output object
|
|
1092
|
+
*/
|
|
1093
|
+
function convertMarimoOutputToJupyter(output) {
|
|
1094
|
+
if (output.type === "error") return {
|
|
1095
|
+
output_type: "error",
|
|
1096
|
+
ename: output.ename || "Error",
|
|
1097
|
+
evalue: output.evalue || "",
|
|
1098
|
+
traceback: output.traceback || []
|
|
1099
|
+
};
|
|
1100
|
+
if (output.data) return {
|
|
1101
|
+
output_type: "execute_result",
|
|
1102
|
+
data: output.data,
|
|
1103
|
+
metadata: {},
|
|
1104
|
+
execution_count: null
|
|
1105
|
+
};
|
|
1106
|
+
return {
|
|
1107
|
+
output_type: "execute_result",
|
|
1108
|
+
data: { "text/plain": "" },
|
|
1109
|
+
metadata: {},
|
|
1110
|
+
execution_count: null
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Converts Marimo console outputs to Jupyter stream outputs.
|
|
1115
|
+
*
|
|
1116
|
+
* @param consoleOutputs - Array of Marimo console outputs
|
|
1117
|
+
* @returns Array of Jupyter stream outputs
|
|
1118
|
+
*/
|
|
1119
|
+
function convertMarimoConsoleToJupyter(consoleOutputs) {
|
|
1120
|
+
const outputs = [];
|
|
1121
|
+
for (const consoleOutput of consoleOutputs) outputs.push({
|
|
1122
|
+
output_type: "stream",
|
|
1123
|
+
name: consoleOutput.channel,
|
|
1124
|
+
text: consoleOutput.data
|
|
1125
|
+
});
|
|
1126
|
+
return outputs;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Converts a full Marimo session cell to Jupyter outputs.
|
|
1130
|
+
*
|
|
1131
|
+
* @param cell - The Marimo session cell
|
|
1132
|
+
* @returns Array of Jupyter-compatible outputs
|
|
1133
|
+
*/
|
|
1134
|
+
function convertMarimoSessionCellToOutputs(cell) {
|
|
1135
|
+
const outputs = [];
|
|
1136
|
+
outputs.push(...convertMarimoConsoleToJupyter(cell.console ?? []));
|
|
1137
|
+
for (const output of cell.outputs) outputs.push(convertMarimoOutputToJupyter(output));
|
|
1138
|
+
return outputs;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Gets outputs from the Marimo session cache file.
|
|
1142
|
+
* This is the preferred method as it doesn't require the marimo CLI.
|
|
1143
|
+
* Outputs are keyed by code_hash for reliable matching even when cells are reordered.
|
|
1144
|
+
*
|
|
1145
|
+
* @param marimoFilePath - Path to the .py Marimo file
|
|
1146
|
+
* @returns Map of code_hash to outputs, or null if session cache is not found
|
|
1147
|
+
*/
|
|
1148
|
+
async function getMarimoOutputsFromCache(marimoFilePath) {
|
|
1149
|
+
const sessionPath = await findMarimoSessionCache(marimoFilePath);
|
|
1150
|
+
if (!sessionPath) return null;
|
|
1151
|
+
const cache = await readMarimoSessionCache(sessionPath);
|
|
1152
|
+
if (!cache) return null;
|
|
1153
|
+
const outputMap = /* @__PURE__ */ new Map();
|
|
1154
|
+
for (const cell of cache.cells) {
|
|
1155
|
+
const outputs = convertMarimoSessionCellToOutputs(cell);
|
|
1156
|
+
if (outputs.length > 0) {
|
|
1157
|
+
const nonEmptyOutputs = outputs.filter((o) => {
|
|
1158
|
+
if (o.output_type === "execute_result" && o.data) {
|
|
1159
|
+
const data = o.data;
|
|
1160
|
+
return Object.values(data).some((v) => {
|
|
1161
|
+
if (typeof v === "string") return v.trim() !== "";
|
|
1162
|
+
return v != null;
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
return true;
|
|
1166
|
+
});
|
|
1167
|
+
if (nonEmptyOutputs.length > 0) outputMap.set(cell.code_hash, nonEmptyOutputs);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return outputMap;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
//#endregion
|
|
1174
|
+
//#region src/snapshot/merge.ts
|
|
1175
|
+
/**
|
|
1176
|
+
* Merges outputs from a snapshot into a source file.
|
|
1177
|
+
* Returns a new DeepnoteFile with outputs added from the snapshot.
|
|
1178
|
+
*
|
|
1179
|
+
* @param source - The source DeepnoteFile (without outputs)
|
|
1180
|
+
* @param snapshot - The snapshot containing outputs
|
|
1181
|
+
* @param options - Merge options
|
|
1182
|
+
* @returns A new DeepnoteFile with outputs merged in
|
|
1183
|
+
*/
|
|
1184
|
+
function mergeSnapshotIntoSource(source, snapshot, options = {}) {
|
|
1185
|
+
const { skipMismatched = false } = options;
|
|
1186
|
+
const outputMap = /* @__PURE__ */ new Map();
|
|
1187
|
+
for (const notebook of snapshot.project.notebooks) for (const block of notebook.blocks) {
|
|
1188
|
+
const execBlock = block;
|
|
1189
|
+
if (execBlock.outputs && execBlock.outputs.length > 0) outputMap.set(block.id, {
|
|
1190
|
+
contentHash: block.contentHash,
|
|
1191
|
+
executionCount: execBlock.executionCount,
|
|
1192
|
+
executionStartedAt: execBlock.executionStartedAt,
|
|
1193
|
+
executionFinishedAt: execBlock.executionFinishedAt,
|
|
1194
|
+
outputs: execBlock.outputs
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
...source,
|
|
1199
|
+
environment: snapshot.environment ?? source.environment,
|
|
1200
|
+
execution: snapshot.execution ?? source.execution,
|
|
1201
|
+
project: {
|
|
1202
|
+
...source.project,
|
|
1203
|
+
notebooks: source.project.notebooks.map((notebook) => ({
|
|
1204
|
+
...notebook,
|
|
1205
|
+
blocks: notebook.blocks.map((block) => {
|
|
1206
|
+
const snapshotData = outputMap.get(block.id);
|
|
1207
|
+
if (!snapshotData) return block;
|
|
1208
|
+
if (skipMismatched && snapshotData.contentHash && block.contentHash) {
|
|
1209
|
+
if (snapshotData.contentHash !== block.contentHash) return block;
|
|
1210
|
+
}
|
|
1211
|
+
return {
|
|
1212
|
+
...block,
|
|
1213
|
+
...snapshotData.executionCount !== void 0 ? { executionCount: snapshotData.executionCount } : {},
|
|
1214
|
+
...snapshotData.executionStartedAt ? { executionStartedAt: snapshotData.executionStartedAt } : {},
|
|
1215
|
+
...snapshotData.executionFinishedAt ? { executionFinishedAt: snapshotData.executionFinishedAt } : {},
|
|
1216
|
+
...snapshotData.outputs ? { outputs: snapshotData.outputs } : {}
|
|
1217
|
+
};
|
|
1218
|
+
})
|
|
1219
|
+
}))
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Counts blocks with outputs in a file.
|
|
1225
|
+
*
|
|
1226
|
+
* @param file - The DeepnoteFile to count outputs in
|
|
1227
|
+
* @returns Number of blocks that have outputs
|
|
1228
|
+
*/
|
|
1229
|
+
function countBlocksWithOutputs(file) {
|
|
1230
|
+
let count = 0;
|
|
1231
|
+
for (const notebook of file.project.notebooks) for (const block of notebook.blocks) {
|
|
1232
|
+
const execBlock = block;
|
|
1233
|
+
if (execBlock.outputs && execBlock.outputs.length > 0) count++;
|
|
1234
|
+
}
|
|
1235
|
+
return count;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
//#endregion
|
|
1239
|
+
//#region src/snapshot/split.ts
|
|
1240
|
+
/**
|
|
1241
|
+
* Creates a slug from a project name.
|
|
1242
|
+
* Normalizes accented characters to ASCII equivalents (e.g., é → e),
|
|
1243
|
+
* converts to lowercase, replaces spaces and special chars with hyphens,
|
|
1244
|
+
* removes consecutive hyphens, and trims leading/trailing hyphens.
|
|
1245
|
+
*
|
|
1246
|
+
* @param name - The project name to slugify
|
|
1247
|
+
* @returns A URL-safe slug
|
|
1248
|
+
*/
|
|
1249
|
+
function slugifyProjectName(name) {
|
|
1250
|
+
return name.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Generates a snapshot filename from project info.
|
|
1254
|
+
*
|
|
1255
|
+
* @param slug - The project name slug
|
|
1256
|
+
* @param projectId - The project UUID
|
|
1257
|
+
* @param timestamp - Timestamp string or 'latest'
|
|
1258
|
+
* @returns Filename in format '{slug}_{projectId}_{timestamp}.snapshot.deepnote'
|
|
1259
|
+
*/
|
|
1260
|
+
function generateSnapshotFilename(slug, projectId, timestamp = "latest") {
|
|
1261
|
+
return `${slug}_${projectId}_${timestamp}.snapshot.deepnote`;
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Removes output-related fields from a block, returning a clean source block.
|
|
1265
|
+
*/
|
|
1266
|
+
function stripOutputsFromBlock(block) {
|
|
1267
|
+
if (!(0, __deepnote_blocks.isExecutableBlockType)(block.type)) return block;
|
|
1268
|
+
const { executionCount: _executionCount, executionStartedAt: _executionStartedAt, executionFinishedAt: _executionFinishedAt, outputs: _outputs,...rest } = block;
|
|
1269
|
+
return rest;
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Splits a DeepnoteFile into a source file (no outputs) and a snapshot file (outputs only).
|
|
1273
|
+
*
|
|
1274
|
+
* @param file - The complete DeepnoteFile with outputs
|
|
1275
|
+
* @returns Object containing source and snapshot files
|
|
1276
|
+
*/
|
|
1277
|
+
function splitDeepnoteFile(file) {
|
|
1278
|
+
const fileWithHashes = addContentHashes(file);
|
|
1279
|
+
const snapshotHash = computeSnapshotHash(fileWithHashes);
|
|
1280
|
+
const { snapshotHash: _snapshotHash,...sourceMetadata } = fileWithHashes.metadata ?? {};
|
|
1281
|
+
return {
|
|
1282
|
+
source: {
|
|
1283
|
+
...fileWithHashes,
|
|
1284
|
+
metadata: sourceMetadata,
|
|
1285
|
+
project: {
|
|
1286
|
+
...fileWithHashes.project,
|
|
1287
|
+
notebooks: fileWithHashes.project.notebooks.map((notebook) => ({
|
|
1288
|
+
...notebook,
|
|
1289
|
+
blocks: notebook.blocks.map(stripOutputsFromBlock)
|
|
1290
|
+
}))
|
|
1291
|
+
}
|
|
1292
|
+
},
|
|
1293
|
+
snapshot: {
|
|
1294
|
+
...fileWithHashes,
|
|
1295
|
+
environment: fileWithHashes.environment ?? {},
|
|
1296
|
+
execution: fileWithHashes.execution ?? {},
|
|
1297
|
+
metadata: {
|
|
1298
|
+
...fileWithHashes.metadata,
|
|
1299
|
+
snapshotHash
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Checks if a DeepnoteFile has any outputs.
|
|
1306
|
+
*
|
|
1307
|
+
* @param file - The DeepnoteFile to check
|
|
1308
|
+
* @returns True if any block has outputs
|
|
1309
|
+
*/
|
|
1310
|
+
function hasOutputs(file) {
|
|
1311
|
+
for (const notebook of file.project.notebooks) for (const block of notebook.blocks) {
|
|
1312
|
+
if (!(0, __deepnote_blocks.isExecutableBlockType)(block.type)) continue;
|
|
1313
|
+
const execBlock = block;
|
|
1314
|
+
if (execBlock.outputs && execBlock.outputs.length > 0) return true;
|
|
1315
|
+
}
|
|
1316
|
+
return false;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
831
1319
|
//#endregion
|
|
832
1320
|
//#region src/marimo-to-deepnote.ts
|
|
833
1321
|
/**
|
|
1322
|
+
* Computes a code hash for a cell's content.
|
|
1323
|
+
* This matches how Marimo computes code_hash for the session cache.
|
|
1324
|
+
*
|
|
1325
|
+
* @param content - The cell's code content
|
|
1326
|
+
* @returns An MD5 hash string (32 hex chars)
|
|
1327
|
+
*/
|
|
1328
|
+
function computeCodeHash(content) {
|
|
1329
|
+
return (0, node_crypto.createHash)("md5").update(content, "utf-8").digest("hex");
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
834
1332
|
* Splits a string on commas that are at the top level (not inside parentheses, brackets, braces, or string literals).
|
|
835
1333
|
* This handles cases like "func(a, b), other" and 'return "a,b", x' correctly.
|
|
836
1334
|
* Supports single quotes, double quotes, and backticks, with proper escape handling.
|
|
@@ -901,13 +1399,26 @@ function parseMarimoFormat(content) {
|
|
|
901
1399
|
const generatedWith = /__generated_with\s*=\s*["']([^"']+)["']/.exec(content)?.[1];
|
|
902
1400
|
const width = /(?:marimo|mo)\.App\([^)]*width\s*=\s*["']([^"']+)["']/.exec(content)?.[1];
|
|
903
1401
|
const title = /(?:marimo|mo)\.App\([^)]*title\s*=\s*["']([^"']+)["']/.exec(content)?.[1];
|
|
904
|
-
const cellRegex = /@app\.cell(?:\(([^)]*)\))?\s*\n\s*def\s+(\w+)\s*\(([^)]*)\)\s*(?:->.*?)?\s*:\s*\n([\s\S]*?)(?=@app\.cell|if\s+__name__|$)/g;
|
|
1402
|
+
const cellRegex = /@app\.(cell|function)(?:\(([^)]*)\))?\s*\n\s*def\s+(\w+)\s*\(([^)]*)\)\s*(?:->.*?)?\s*:\s*\n([\s\S]*?)(?=@app\.cell|@app\.function|if\s+__name__|$)/g;
|
|
905
1403
|
let match = cellRegex.exec(content);
|
|
906
1404
|
while (match !== null) {
|
|
907
|
-
const
|
|
908
|
-
const
|
|
909
|
-
const
|
|
910
|
-
|
|
1405
|
+
const decoratorType = match[1];
|
|
1406
|
+
const decoratorArgs = match[2] || "";
|
|
1407
|
+
const functionName = match[3];
|
|
1408
|
+
const params = match[4].trim();
|
|
1409
|
+
let body = match[5];
|
|
1410
|
+
if (decoratorType === "function") {
|
|
1411
|
+
const funcDef = `def ${functionName}(${params})${/@app\.function(?:\([^)]*\))?\s*\n\s*def\s+\w+\s*\([^)]*\)\s*(->.*?)?\s*:/.exec(content.slice(match.index))?.[1] || ""}:\n${body}`;
|
|
1412
|
+
cells.push({
|
|
1413
|
+
cellType: "code",
|
|
1414
|
+
content: funcDef.trim(),
|
|
1415
|
+
functionName,
|
|
1416
|
+
hidden: /hide_code\s*=\s*True/.test(decoratorArgs),
|
|
1417
|
+
disabled: /disabled\s*=\s*True/.test(decoratorArgs)
|
|
1418
|
+
});
|
|
1419
|
+
match = cellRegex.exec(content);
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
911
1422
|
const dependencies = params ? params.split(",").map((p) => p.trim()).filter((p) => p.length > 0) : void 0;
|
|
912
1423
|
const hidden = /hide_code\s*=\s*True/.test(decoratorArgs);
|
|
913
1424
|
const disabled = /disabled\s*=\s*True/.test(decoratorArgs);
|
|
@@ -995,41 +1506,70 @@ function parseMarimoFormat(content) {
|
|
|
995
1506
|
* This is the lowest-level conversion function.
|
|
996
1507
|
*
|
|
997
1508
|
* @param app - The Marimo app object to convert
|
|
998
|
-
* @param options - Optional conversion options including custom ID generator
|
|
1509
|
+
* @param options - Optional conversion options including custom ID generator and outputs
|
|
999
1510
|
* @returns Array of DeepnoteBlock objects
|
|
1000
1511
|
*/
|
|
1001
1512
|
function convertMarimoAppToBlocks(app, options) {
|
|
1002
1513
|
const idGenerator = options?.idGenerator ?? node_crypto.randomUUID;
|
|
1003
|
-
|
|
1514
|
+
const outputs = options?.outputs;
|
|
1515
|
+
return app.cells.map((cell, index) => {
|
|
1516
|
+
const codeHash = computeCodeHash(cell.content);
|
|
1517
|
+
const cellOutputs = outputs?.get(codeHash);
|
|
1518
|
+
return convertCellToBlock$2(cell, index, idGenerator, cellOutputs);
|
|
1519
|
+
});
|
|
1004
1520
|
}
|
|
1005
1521
|
/**
|
|
1006
|
-
*
|
|
1007
|
-
* This is a
|
|
1008
|
-
*
|
|
1009
|
-
* @param apps - Array of Marimo apps with filenames
|
|
1010
|
-
* @param options - Conversion options including project name and optional ID generator
|
|
1011
|
-
* @returns A DeepnoteFile object
|
|
1522
|
+
* Creates a base DeepnoteFile structure with empty notebooks.
|
|
1523
|
+
* This is a helper to reduce duplication in conversion functions.
|
|
1012
1524
|
*/
|
|
1013
|
-
function
|
|
1014
|
-
|
|
1015
|
-
const firstNotebookId = apps.length > 0 ? idGenerator() : void 0;
|
|
1016
|
-
const deepnoteFile = {
|
|
1525
|
+
function createDeepnoteFileSkeleton(projectName, idGenerator, firstNotebookId) {
|
|
1526
|
+
return {
|
|
1017
1527
|
metadata: { createdAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1018
1528
|
project: {
|
|
1019
1529
|
id: idGenerator(),
|
|
1020
1530
|
initNotebookId: firstNotebookId,
|
|
1021
1531
|
integrations: [],
|
|
1022
|
-
name:
|
|
1532
|
+
name: projectName,
|
|
1023
1533
|
notebooks: [],
|
|
1024
1534
|
settings: {}
|
|
1025
1535
|
},
|
|
1026
1536
|
version: "1.0.0"
|
|
1027
1537
|
};
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Converts Marimo app objects into a Deepnote project file.
|
|
1541
|
+
* This is a pure conversion function that doesn't perform any file I/O.
|
|
1542
|
+
*
|
|
1543
|
+
* @param apps - Array of Marimo apps with filenames
|
|
1544
|
+
* @param options - Conversion options including project name and optional ID generator
|
|
1545
|
+
* @returns A DeepnoteFile object
|
|
1546
|
+
*/
|
|
1547
|
+
function convertMarimoAppsToDeepnote(apps, options) {
|
|
1548
|
+
return convertMarimoAppsToDeepnoteFile(apps.map((app) => ({
|
|
1549
|
+
...app,
|
|
1550
|
+
outputs: void 0
|
|
1551
|
+
})), options);
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Converts Marimo app objects with outputs into a Deepnote project file.
|
|
1555
|
+
* This variant includes outputs from the Marimo session cache.
|
|
1556
|
+
*
|
|
1557
|
+
* @param apps - Array of Marimo apps with filenames and optional outputs
|
|
1558
|
+
* @param options - Conversion options including project name and optional ID generator
|
|
1559
|
+
* @returns A DeepnoteFile object
|
|
1560
|
+
*/
|
|
1561
|
+
function convertMarimoAppsToDeepnoteFile(apps, options) {
|
|
1562
|
+
const idGenerator = options.idGenerator ?? node_crypto.randomUUID;
|
|
1563
|
+
const firstNotebookId = apps.length > 0 ? idGenerator() : void 0;
|
|
1564
|
+
const deepnoteFile = createDeepnoteFileSkeleton(options.projectName, idGenerator, firstNotebookId);
|
|
1028
1565
|
for (let i = 0; i < apps.length; i++) {
|
|
1029
|
-
const { filename, app } = apps[i];
|
|
1566
|
+
const { filename, app, outputs } = apps[i];
|
|
1030
1567
|
const filenameWithoutExt = (0, node_path.basename)(filename, (0, node_path.extname)(filename)) || "Untitled notebook";
|
|
1031
1568
|
const notebookName = app.title || filenameWithoutExt;
|
|
1032
|
-
const blocks = convertMarimoAppToBlocks(app, {
|
|
1569
|
+
const blocks = convertMarimoAppToBlocks(app, {
|
|
1570
|
+
idGenerator,
|
|
1571
|
+
outputs
|
|
1572
|
+
});
|
|
1033
1573
|
const notebookId = i === 0 && firstNotebookId ? firstNotebookId : idGenerator();
|
|
1034
1574
|
deepnoteFile.project.notebooks.push({
|
|
1035
1575
|
blocks,
|
|
@@ -1042,15 +1582,45 @@ function convertMarimoAppsToDeepnote(apps, options) {
|
|
|
1042
1582
|
return deepnoteFile;
|
|
1043
1583
|
}
|
|
1044
1584
|
/**
|
|
1585
|
+
* Reads and converts multiple Marimo (.py) files into a DeepnoteFile.
|
|
1586
|
+
* This function reads the files and returns the converted DeepnoteFile without writing to disk.
|
|
1587
|
+
*
|
|
1588
|
+
* @param inputFilePaths - Array of paths to Marimo .py files
|
|
1589
|
+
* @param options - Conversion options including project name
|
|
1590
|
+
* @returns A DeepnoteFile object
|
|
1591
|
+
*/
|
|
1592
|
+
async function readAndConvertMarimoFiles(inputFilePaths, options) {
|
|
1593
|
+
const apps = [];
|
|
1594
|
+
for (const filePath of inputFilePaths) try {
|
|
1595
|
+
const app = parseMarimoFormat(await node_fs_promises.default.readFile(filePath, "utf-8"));
|
|
1596
|
+
const outputs = await getMarimoOutputsFromCache(filePath);
|
|
1597
|
+
apps.push({
|
|
1598
|
+
filename: (0, node_path.basename)(filePath),
|
|
1599
|
+
app,
|
|
1600
|
+
outputs: outputs ?? void 0
|
|
1601
|
+
});
|
|
1602
|
+
} catch (err) {
|
|
1603
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1604
|
+
const errorStack = err instanceof Error ? err.stack : void 0;
|
|
1605
|
+
throw new Error(`Failed to read or parse file ${(0, node_path.basename)(filePath)}: ${errorMessage}`, { cause: errorStack ? {
|
|
1606
|
+
originalError: err,
|
|
1607
|
+
stack: errorStack
|
|
1608
|
+
} : err });
|
|
1609
|
+
}
|
|
1610
|
+
return convertMarimoAppsToDeepnoteFile(apps, { projectName: options.projectName });
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1045
1613
|
* Converts multiple Marimo (.py) files into a single Deepnote project file.
|
|
1046
1614
|
*/
|
|
1047
1615
|
async function convertMarimoFilesToDeepnoteFile(inputFilePaths, options) {
|
|
1048
1616
|
const apps = [];
|
|
1049
1617
|
for (const filePath of inputFilePaths) try {
|
|
1050
1618
|
const app = parseMarimoFormat(await node_fs_promises.default.readFile(filePath, "utf-8"));
|
|
1619
|
+
const outputs = await getMarimoOutputsFromCache(filePath);
|
|
1051
1620
|
apps.push({
|
|
1052
1621
|
filename: (0, node_path.basename)(filePath),
|
|
1053
|
-
app
|
|
1622
|
+
app,
|
|
1623
|
+
outputs: outputs ?? void 0
|
|
1054
1624
|
});
|
|
1055
1625
|
} catch (err) {
|
|
1056
1626
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -1060,12 +1630,12 @@ async function convertMarimoFilesToDeepnoteFile(inputFilePaths, options) {
|
|
|
1060
1630
|
stack: errorStack
|
|
1061
1631
|
} : err });
|
|
1062
1632
|
}
|
|
1063
|
-
const yamlContent = (0, yaml.stringify)(
|
|
1633
|
+
const yamlContent = (0, yaml.stringify)(convertMarimoAppsToDeepnoteFile(apps, { projectName: options.projectName }));
|
|
1064
1634
|
const parentDir = (0, node_path.dirname)(options.outputPath);
|
|
1065
1635
|
await node_fs_promises.default.mkdir(parentDir, { recursive: true });
|
|
1066
1636
|
await node_fs_promises.default.writeFile(options.outputPath, yamlContent, "utf-8");
|
|
1067
1637
|
}
|
|
1068
|
-
function convertCellToBlock$2(cell, index, idGenerator) {
|
|
1638
|
+
function convertCellToBlock$2(cell, index, idGenerator, outputs) {
|
|
1069
1639
|
let blockType;
|
|
1070
1640
|
if (cell.cellType === "markdown") blockType = "markdown";
|
|
1071
1641
|
else if (cell.cellType === "sql") blockType = "sql";
|
|
@@ -1083,7 +1653,8 @@ function convertCellToBlock$2(cell, index, idGenerator) {
|
|
|
1083
1653
|
id: idGenerator(),
|
|
1084
1654
|
metadata: Object.keys(metadata).length > 0 ? metadata : {},
|
|
1085
1655
|
sortingKey: createSortingKey(index),
|
|
1086
|
-
type: blockType
|
|
1656
|
+
type: blockType,
|
|
1657
|
+
...outputs && outputs.length > 0 && (blockType === "code" || blockType === "sql") ? { outputs } : {}
|
|
1087
1658
|
};
|
|
1088
1659
|
}
|
|
1089
1660
|
|
|
@@ -1210,9 +1781,14 @@ function convertPercentNotebooksToDeepnote(notebooks, options) {
|
|
|
1210
1781
|
return deepnoteFile;
|
|
1211
1782
|
}
|
|
1212
1783
|
/**
|
|
1213
|
-
*
|
|
1784
|
+
* Reads and converts multiple percent format (.py) files into a DeepnoteFile.
|
|
1785
|
+
* This function reads the files and returns the converted DeepnoteFile without writing to disk.
|
|
1786
|
+
*
|
|
1787
|
+
* @param inputFilePaths - Array of paths to percent format .py files
|
|
1788
|
+
* @param options - Conversion options including project name
|
|
1789
|
+
* @returns A DeepnoteFile object
|
|
1214
1790
|
*/
|
|
1215
|
-
async function
|
|
1791
|
+
async function readAndConvertPercentFiles(inputFilePaths, options) {
|
|
1216
1792
|
const notebooks = [];
|
|
1217
1793
|
for (const filePath of inputFilePaths) {
|
|
1218
1794
|
const notebook = parsePercentFormat(await node_fs_promises.default.readFile(filePath, "utf-8"));
|
|
@@ -1221,7 +1797,13 @@ async function convertPercentFilesToDeepnoteFile(inputFilePaths, options) {
|
|
|
1221
1797
|
notebook
|
|
1222
1798
|
});
|
|
1223
1799
|
}
|
|
1224
|
-
|
|
1800
|
+
return convertPercentNotebooksToDeepnote(notebooks, { projectName: options.projectName });
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Converts multiple percent format (.py) files into a single Deepnote project file.
|
|
1804
|
+
*/
|
|
1805
|
+
async function convertPercentFilesToDeepnoteFile(inputFilePaths, options) {
|
|
1806
|
+
const yamlContent = (0, yaml.stringify)(await readAndConvertPercentFiles(inputFilePaths, { projectName: options.projectName }));
|
|
1225
1807
|
const parentDir = (0, node_path.dirname)(options.outputPath);
|
|
1226
1808
|
await node_fs_promises.default.mkdir(parentDir, { recursive: true });
|
|
1227
1809
|
await node_fs_promises.default.writeFile(options.outputPath, yamlContent, "utf-8");
|
|
@@ -1472,9 +2054,14 @@ function convertQuartoDocumentsToDeepnote(documents, options) {
|
|
|
1472
2054
|
return deepnoteFile;
|
|
1473
2055
|
}
|
|
1474
2056
|
/**
|
|
1475
|
-
*
|
|
2057
|
+
* Reads and converts multiple Quarto (.qmd) files into a DeepnoteFile.
|
|
2058
|
+
* This function reads the files and returns the converted DeepnoteFile without writing to disk.
|
|
2059
|
+
*
|
|
2060
|
+
* @param inputFilePaths - Array of paths to .qmd files
|
|
2061
|
+
* @param options - Conversion options including project name
|
|
2062
|
+
* @returns A DeepnoteFile object
|
|
1476
2063
|
*/
|
|
1477
|
-
async function
|
|
2064
|
+
async function readAndConvertQuartoFiles(inputFilePaths, options) {
|
|
1478
2065
|
const documents = [];
|
|
1479
2066
|
for (const filePath of inputFilePaths) {
|
|
1480
2067
|
const document = parseQuartoFormat(await node_fs_promises.default.readFile(filePath, "utf-8"));
|
|
@@ -1483,7 +2070,13 @@ async function convertQuartoFilesToDeepnoteFile(inputFilePaths, options) {
|
|
|
1483
2070
|
document
|
|
1484
2071
|
});
|
|
1485
2072
|
}
|
|
1486
|
-
|
|
2073
|
+
return convertQuartoDocumentsToDeepnote(documents, { projectName: options.projectName });
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
2076
|
+
* Converts multiple Quarto (.qmd) files into a single Deepnote project file.
|
|
2077
|
+
*/
|
|
2078
|
+
async function convertQuartoFilesToDeepnoteFile(inputFilePaths, options) {
|
|
2079
|
+
const yamlContent = (0, yaml.stringify)(await readAndConvertQuartoFiles(inputFilePaths, { projectName: options.projectName }));
|
|
1487
2080
|
const parentDir = (0, node_path.dirname)(options.outputPath);
|
|
1488
2081
|
await node_fs_promises.default.mkdir(parentDir, { recursive: true });
|
|
1489
2082
|
await node_fs_promises.default.writeFile(options.outputPath, yamlContent, "utf-8");
|
|
@@ -1509,6 +2102,43 @@ function convertCellToBlock(cell, index, idGenerator) {
|
|
|
1509
2102
|
};
|
|
1510
2103
|
}
|
|
1511
2104
|
|
|
2105
|
+
//#endregion
|
|
2106
|
+
//#region src/write-deepnote-file.ts
|
|
2107
|
+
/**
|
|
2108
|
+
* Writes a DeepnoteFile to disk, optionally splitting outputs into a snapshot file.
|
|
2109
|
+
*
|
|
2110
|
+
* When singleFile is false (default) and the file contains outputs:
|
|
2111
|
+
* - Splits the file in memory into source (no outputs) and snapshot (with outputs)
|
|
2112
|
+
* - Writes both files in parallel
|
|
2113
|
+
*
|
|
2114
|
+
* When singleFile is true or there are no outputs:
|
|
2115
|
+
* - Writes the complete file as-is
|
|
2116
|
+
*
|
|
2117
|
+
* @param options - Write options including the file, output path, and project name
|
|
2118
|
+
* @returns Object containing paths to the written files
|
|
2119
|
+
*/
|
|
2120
|
+
async function writeDeepnoteFile(options) {
|
|
2121
|
+
const { file, outputPath, projectName, singleFile = false } = options;
|
|
2122
|
+
const parentDir = (0, node_path.dirname)(outputPath);
|
|
2123
|
+
await node_fs_promises.default.mkdir(parentDir, { recursive: true });
|
|
2124
|
+
if (singleFile || !hasOutputs(file)) {
|
|
2125
|
+
const yamlContent = (0, yaml.stringify)(file);
|
|
2126
|
+
await node_fs_promises.default.writeFile(outputPath, yamlContent, "utf-8");
|
|
2127
|
+
return { sourcePath: outputPath };
|
|
2128
|
+
}
|
|
2129
|
+
const { source, snapshot } = splitDeepnoteFile(file);
|
|
2130
|
+
const snapshotDir = getSnapshotDir(outputPath);
|
|
2131
|
+
const snapshotPath = (0, node_path.resolve)(snapshotDir, generateSnapshotFilename(slugifyProjectName(projectName) || "project", file.project.id));
|
|
2132
|
+
const sourceYaml = (0, yaml.stringify)(source);
|
|
2133
|
+
const snapshotYaml = (0, yaml.stringify)(snapshot);
|
|
2134
|
+
await node_fs_promises.default.mkdir(snapshotDir, { recursive: true });
|
|
2135
|
+
await Promise.all([node_fs_promises.default.writeFile(outputPath, sourceYaml, "utf-8"), node_fs_promises.default.writeFile(snapshotPath, snapshotYaml, "utf-8")]);
|
|
2136
|
+
return {
|
|
2137
|
+
sourcePath: outputPath,
|
|
2138
|
+
snapshotPath
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
|
|
1512
2142
|
//#endregion
|
|
1513
2143
|
Object.defineProperty(exports, '__toESM', {
|
|
1514
2144
|
enumerable: true,
|
|
@@ -1516,6 +2146,30 @@ Object.defineProperty(exports, '__toESM', {
|
|
|
1516
2146
|
return __toESM;
|
|
1517
2147
|
}
|
|
1518
2148
|
});
|
|
2149
|
+
Object.defineProperty(exports, 'addContentHashes', {
|
|
2150
|
+
enumerable: true,
|
|
2151
|
+
get: function () {
|
|
2152
|
+
return addContentHashes;
|
|
2153
|
+
}
|
|
2154
|
+
});
|
|
2155
|
+
Object.defineProperty(exports, 'computeContentHash', {
|
|
2156
|
+
enumerable: true,
|
|
2157
|
+
get: function () {
|
|
2158
|
+
return computeContentHash;
|
|
2159
|
+
}
|
|
2160
|
+
});
|
|
2161
|
+
Object.defineProperty(exports, 'computeSnapshotHash', {
|
|
2162
|
+
enumerable: true,
|
|
2163
|
+
get: function () {
|
|
2164
|
+
return computeSnapshotHash;
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
Object.defineProperty(exports, 'convertBlockToJupyterCell', {
|
|
2168
|
+
enumerable: true,
|
|
2169
|
+
get: function () {
|
|
2170
|
+
return convertBlockToJupyterCell;
|
|
2171
|
+
}
|
|
2172
|
+
});
|
|
1519
2173
|
Object.defineProperty(exports, 'convertBlocksToJupyterNotebook', {
|
|
1520
2174
|
enumerable: true,
|
|
1521
2175
|
get: function () {
|
|
@@ -1660,12 +2314,42 @@ Object.defineProperty(exports, 'convertQuartoFilesToDeepnoteFile', {
|
|
|
1660
2314
|
return convertQuartoFilesToDeepnoteFile;
|
|
1661
2315
|
}
|
|
1662
2316
|
});
|
|
2317
|
+
Object.defineProperty(exports, 'countBlocksWithOutputs', {
|
|
2318
|
+
enumerable: true,
|
|
2319
|
+
get: function () {
|
|
2320
|
+
return countBlocksWithOutputs;
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
1663
2323
|
Object.defineProperty(exports, 'detectFormat', {
|
|
1664
2324
|
enumerable: true,
|
|
1665
2325
|
get: function () {
|
|
1666
2326
|
return detectFormat;
|
|
1667
2327
|
}
|
|
1668
2328
|
});
|
|
2329
|
+
Object.defineProperty(exports, 'findSnapshotsForProject', {
|
|
2330
|
+
enumerable: true,
|
|
2331
|
+
get: function () {
|
|
2332
|
+
return findSnapshotsForProject;
|
|
2333
|
+
}
|
|
2334
|
+
});
|
|
2335
|
+
Object.defineProperty(exports, 'generateSnapshotFilename', {
|
|
2336
|
+
enumerable: true,
|
|
2337
|
+
get: function () {
|
|
2338
|
+
return generateSnapshotFilename;
|
|
2339
|
+
}
|
|
2340
|
+
});
|
|
2341
|
+
Object.defineProperty(exports, 'getSnapshotDir', {
|
|
2342
|
+
enumerable: true,
|
|
2343
|
+
get: function () {
|
|
2344
|
+
return getSnapshotDir;
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
Object.defineProperty(exports, 'hasOutputs', {
|
|
2348
|
+
enumerable: true,
|
|
2349
|
+
get: function () {
|
|
2350
|
+
return hasOutputs;
|
|
2351
|
+
}
|
|
2352
|
+
});
|
|
1669
2353
|
Object.defineProperty(exports, 'isMarimoContent', {
|
|
1670
2354
|
enumerable: true,
|
|
1671
2355
|
get: function () {
|
|
@@ -1678,6 +2362,24 @@ Object.defineProperty(exports, 'isPercentContent', {
|
|
|
1678
2362
|
return isPercentContent;
|
|
1679
2363
|
}
|
|
1680
2364
|
});
|
|
2365
|
+
Object.defineProperty(exports, 'loadLatestSnapshot', {
|
|
2366
|
+
enumerable: true,
|
|
2367
|
+
get: function () {
|
|
2368
|
+
return loadLatestSnapshot;
|
|
2369
|
+
}
|
|
2370
|
+
});
|
|
2371
|
+
Object.defineProperty(exports, 'loadSnapshotFile', {
|
|
2372
|
+
enumerable: true,
|
|
2373
|
+
get: function () {
|
|
2374
|
+
return loadSnapshotFile;
|
|
2375
|
+
}
|
|
2376
|
+
});
|
|
2377
|
+
Object.defineProperty(exports, 'mergeSnapshotIntoSource', {
|
|
2378
|
+
enumerable: true,
|
|
2379
|
+
get: function () {
|
|
2380
|
+
return mergeSnapshotIntoSource;
|
|
2381
|
+
}
|
|
2382
|
+
});
|
|
1681
2383
|
Object.defineProperty(exports, 'parseMarimoFormat', {
|
|
1682
2384
|
enumerable: true,
|
|
1683
2385
|
get: function () {
|
|
@@ -1696,6 +2398,42 @@ Object.defineProperty(exports, 'parseQuartoFormat', {
|
|
|
1696
2398
|
return parseQuartoFormat;
|
|
1697
2399
|
}
|
|
1698
2400
|
});
|
|
2401
|
+
Object.defineProperty(exports, 'parseSnapshotFilename', {
|
|
2402
|
+
enumerable: true,
|
|
2403
|
+
get: function () {
|
|
2404
|
+
return parseSnapshotFilename;
|
|
2405
|
+
}
|
|
2406
|
+
});
|
|
2407
|
+
Object.defineProperty(exports, 'parseSourceFilePath', {
|
|
2408
|
+
enumerable: true,
|
|
2409
|
+
get: function () {
|
|
2410
|
+
return parseSourceFilePath;
|
|
2411
|
+
}
|
|
2412
|
+
});
|
|
2413
|
+
Object.defineProperty(exports, 'readAndConvertIpynbFiles', {
|
|
2414
|
+
enumerable: true,
|
|
2415
|
+
get: function () {
|
|
2416
|
+
return readAndConvertIpynbFiles;
|
|
2417
|
+
}
|
|
2418
|
+
});
|
|
2419
|
+
Object.defineProperty(exports, 'readAndConvertMarimoFiles', {
|
|
2420
|
+
enumerable: true,
|
|
2421
|
+
get: function () {
|
|
2422
|
+
return readAndConvertMarimoFiles;
|
|
2423
|
+
}
|
|
2424
|
+
});
|
|
2425
|
+
Object.defineProperty(exports, 'readAndConvertPercentFiles', {
|
|
2426
|
+
enumerable: true,
|
|
2427
|
+
get: function () {
|
|
2428
|
+
return readAndConvertPercentFiles;
|
|
2429
|
+
}
|
|
2430
|
+
});
|
|
2431
|
+
Object.defineProperty(exports, 'readAndConvertQuartoFiles', {
|
|
2432
|
+
enumerable: true,
|
|
2433
|
+
get: function () {
|
|
2434
|
+
return readAndConvertQuartoFiles;
|
|
2435
|
+
}
|
|
2436
|
+
});
|
|
1699
2437
|
Object.defineProperty(exports, 'serializeMarimoFormat', {
|
|
1700
2438
|
enumerable: true,
|
|
1701
2439
|
get: function () {
|
|
@@ -1713,4 +2451,28 @@ Object.defineProperty(exports, 'serializeQuartoFormat', {
|
|
|
1713
2451
|
get: function () {
|
|
1714
2452
|
return serializeQuartoFormat;
|
|
1715
2453
|
}
|
|
2454
|
+
});
|
|
2455
|
+
Object.defineProperty(exports, 'slugifyProjectName', {
|
|
2456
|
+
enumerable: true,
|
|
2457
|
+
get: function () {
|
|
2458
|
+
return slugifyProjectName;
|
|
2459
|
+
}
|
|
2460
|
+
});
|
|
2461
|
+
Object.defineProperty(exports, 'snapshotExists', {
|
|
2462
|
+
enumerable: true,
|
|
2463
|
+
get: function () {
|
|
2464
|
+
return snapshotExists;
|
|
2465
|
+
}
|
|
2466
|
+
});
|
|
2467
|
+
Object.defineProperty(exports, 'splitDeepnoteFile', {
|
|
2468
|
+
enumerable: true,
|
|
2469
|
+
get: function () {
|
|
2470
|
+
return splitDeepnoteFile;
|
|
2471
|
+
}
|
|
2472
|
+
});
|
|
2473
|
+
Object.defineProperty(exports, 'writeDeepnoteFile', {
|
|
2474
|
+
enumerable: true,
|
|
2475
|
+
get: function () {
|
|
2476
|
+
return writeDeepnoteFile;
|
|
2477
|
+
}
|
|
1716
2478
|
});
|