@deepnote/blocks 3.0.1 → 4.0.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/dist/index.cjs CHANGED
@@ -28,6 +28,113 @@ yaml = __toESM(yaml);
28
28
  let ts_dedent = require("ts-dedent");
29
29
  ts_dedent = __toESM(ts_dedent);
30
30
 
31
+ //#region src/errors.ts
32
+ /**
33
+ * Base error class for all Deepnote errors.
34
+ */
35
+ var DeepnoteError = class extends Error {
36
+ constructor(message, options) {
37
+ super(message, options);
38
+ this.name = this.constructor.name;
39
+ }
40
+ };
41
+ /**
42
+ * Generic parse failure, optionally associated with a file path.
43
+ */
44
+ var ParseError = class extends DeepnoteError {
45
+ filePath;
46
+ constructor(message, options) {
47
+ super(message, options);
48
+ this.filePath = options?.filePath;
49
+ }
50
+ };
51
+ /**
52
+ * YAML syntax or duplicate key errors.
53
+ */
54
+ var YamlParseError = class extends ParseError {};
55
+ /**
56
+ * BOM or invalid UTF-8 encoding errors.
57
+ */
58
+ var EncodingError = class extends ParseError {};
59
+ /**
60
+ * Zod schema validation failures.
61
+ */
62
+ var SchemaValidationError = class extends ParseError {};
63
+ /**
64
+ * Prohibited YAML features: anchors, aliases, merge keys, tags.
65
+ */
66
+ var ProhibitedYamlFeatureError = class extends ParseError {
67
+ feature;
68
+ constructor(message, options) {
69
+ super(message, options);
70
+ this.feature = options.feature;
71
+ }
72
+ };
73
+ /**
74
+ * Thrown when a block type is not supported.
75
+ */
76
+ var UnsupportedBlockTypeError = class extends DeepnoteError {};
77
+ /**
78
+ * Thrown when a value is invalid (e.g., slider value, date interval).
79
+ */
80
+ var InvalidValueError = class extends DeepnoteError {
81
+ value;
82
+ constructor(message, options) {
83
+ super(message, options);
84
+ this.value = options.value;
85
+ }
86
+ };
87
+
88
+ //#endregion
89
+ //#region src/blocks/executable-blocks.ts
90
+ /**
91
+ * Block types that represent user input widgets.
92
+ * These blocks capture user input and define variables.
93
+ */
94
+ const INPUT_BLOCK_TYPES = new Set([
95
+ "input-text",
96
+ "input-textarea",
97
+ "input-checkbox",
98
+ "input-select",
99
+ "input-slider",
100
+ "input-date",
101
+ "input-date-range",
102
+ "input-file"
103
+ ]);
104
+ const executableBlockTypes = new Set([
105
+ "code",
106
+ "sql",
107
+ "notebook-function",
108
+ "visualization",
109
+ "button",
110
+ "big-number",
111
+ ...INPUT_BLOCK_TYPES
112
+ ]);
113
+ /**
114
+ * Type guard to check if a block is an executable block.
115
+ * Executable blocks can have outputs and be executed by the runtime.
116
+ */
117
+ function isExecutableBlock(block) {
118
+ return executableBlockTypes.has(block.type);
119
+ }
120
+ /**
121
+ * Checks if a block type string represents an executable block.
122
+ * Convenience function for when you only have the type string.
123
+ */
124
+ function isExecutableBlockType(type) {
125
+ return executableBlockTypes.has(type);
126
+ }
127
+
128
+ //#endregion
129
+ //#region src/blocks/sql-utils.ts
130
+ function convertToEnvironmentVariableName(str) {
131
+ return (/^\d/.test(str) ? `_${str}` : str).toUpperCase().replace(/[^\w]/g, "_");
132
+ }
133
+ function getSqlEnvVarName(integrationId) {
134
+ return `SQL_${integrationId}`;
135
+ }
136
+
137
+ //#endregion
31
138
  //#region src/deserialize-file/deepnote-file-schema.ts
32
139
  /** Preprocesses any content value to empty string for blocks that don't use content */
33
140
  const emptyContent = () => zod.z.preprocess(() => "", zod.z.literal("").optional());
@@ -420,6 +527,17 @@ const deepnoteFileSchema = zod.z.object({
420
527
  }),
421
528
  version: zod.z.string()
422
529
  });
530
+ const deepnoteSnapshotSchema = deepnoteFileSchema.extend({
531
+ environment: environmentSchema.unwrap(),
532
+ execution: executionSchema.unwrap(),
533
+ metadata: zod.z.object({
534
+ checksum: zod.z.string().optional(),
535
+ createdAt: zod.z.string(),
536
+ exportedAt: zod.z.string().optional(),
537
+ modifiedAt: zod.z.string().optional(),
538
+ snapshotHash: zod.z.string()
539
+ })
540
+ });
423
541
 
424
542
  //#endregion
425
543
  //#region src/deserialize-file/parse-yaml.ts
@@ -429,7 +547,7 @@ const deepnoteFileSchema = zod.z.object({
429
547
  *
430
548
  * @param bytes - Raw file bytes as Uint8Array
431
549
  * @returns Decoded UTF-8 string without BOM
432
- * @throws Error if BOM detected or invalid UTF-8 encoding
550
+ * @throws EncodingError if BOM detected or invalid UTF-8 encoding
433
551
  *
434
552
  * @example
435
553
  * ```typescript
@@ -439,11 +557,11 @@ const deepnoteFileSchema = zod.z.object({
439
557
  * ```
440
558
  */
441
559
  function decodeUtf8NoBom(bytes) {
442
- if (bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) throw new Error("UTF-8 BOM detected in Deepnote file - files must be UTF-8 without BOM");
560
+ if (bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) throw new EncodingError("UTF-8 BOM detected in Deepnote file - files must be UTF-8 without BOM");
443
561
  try {
444
562
  return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
445
563
  } catch {
446
- throw new Error("Invalid UTF-8 encoding detected in Deepnote file");
564
+ throw new EncodingError("Invalid UTF-8 encoding detected in Deepnote file");
447
565
  }
448
566
  }
449
567
  /**
@@ -453,10 +571,10 @@ function decodeUtf8NoBom(bytes) {
453
571
  * For proper UTF-8 validation, use decodeUtf8NoBom() on raw bytes before decoding.
454
572
  *
455
573
  * @param yamlContent - Already-decoded YAML string
456
- * @throws Error if BOM prefix detected
574
+ * @throws EncodingError if BOM prefix detected
457
575
  */
458
576
  function validateNoBomPrefix(yamlContent) {
459
- if (yamlContent.charCodeAt(0) === 65279) throw new Error("UTF-8 BOM detected in Deepnote file - files must be UTF-8 without BOM");
577
+ if (yamlContent.charCodeAt(0) === 65279) throw new EncodingError("UTF-8 BOM detected in Deepnote file - files must be UTF-8 without BOM");
460
578
  }
461
579
  /**
462
580
  * Validates that the YAML document doesn't contain prohibited features:
@@ -465,13 +583,13 @@ function validateNoBomPrefix(yamlContent) {
465
583
  * - No custom tags (!tag)
466
584
  */
467
585
  function validateYamlStructure(yamlContent) {
468
- if (/(?:^|\n)\s*(?:-\s+|[\w-]+:\s*)&\w+/.test(yamlContent)) throw new Error("YAML anchors (&) are not allowed in Deepnote files");
469
- if (/(?:^|\n)\s*(?:-\s+|[\w-]+:\s*)\*\w+/.test(yamlContent)) throw new Error("YAML aliases (*) are not allowed in Deepnote files");
470
- if (/<<:/.test(yamlContent)) throw new Error("YAML merge keys (<<) are not allowed in Deepnote files");
586
+ if (/(?:^|\n)\s*(?:-\s+|[\w-]+:\s*)&\w+/.test(yamlContent)) throw new ProhibitedYamlFeatureError("YAML anchors (&) are not allowed in Deepnote files", { feature: "anchor" });
587
+ if (/(?:^|\n)\s*(?:-\s+|[\w-]+:\s*)\*\w+/.test(yamlContent)) throw new ProhibitedYamlFeatureError("YAML aliases (*) are not allowed in Deepnote files", { feature: "alias" });
588
+ if (/<<:/.test(yamlContent)) throw new ProhibitedYamlFeatureError("YAML merge keys (<<) are not allowed in Deepnote files", { feature: "merge-key" });
471
589
  const matches = yamlContent.match(/(?:^|\n)\s*(?:-\s+|[\w-]+:\s*)(![\w/-]+)/gm);
472
590
  if (matches) {
473
591
  const tags = matches.map((m) => m.match(/(![\w/-]+)/)?.[1]).filter(Boolean);
474
- if (tags.length > 0) throw new Error(`YAML tags are not allowed in Deepnote files: ${tags.join(", ")}`);
592
+ if (tags.length > 0) throw new ProhibitedYamlFeatureError(`YAML tags are not allowed in Deepnote files: ${tags.join(", ")}`, { feature: "tag" });
475
593
  }
476
594
  }
477
595
  /**
@@ -486,8 +604,8 @@ function parseAndValidate(yamlContent) {
486
604
  });
487
605
  if (doc.errors.length > 0) {
488
606
  const duplicateKeyError = doc.errors.find((err) => err.message.includes("duplicate") || err.message.includes("key"));
489
- if (duplicateKeyError) throw new Error(`Duplicate keys detected in Deepnote file: ${duplicateKeyError.message}`);
490
- throw new Error(`YAML parsing error: ${doc.errors[0].message}`);
607
+ if (duplicateKeyError) throw new YamlParseError(`Duplicate keys detected in Deepnote file: ${duplicateKeyError.message}`);
608
+ throw new YamlParseError(`YAML parsing error: ${doc.errors[0].message}`);
491
609
  }
492
610
  return doc.toJS();
493
611
  }
@@ -506,8 +624,8 @@ function parseYaml(yamlContent) {
506
624
  validateYamlStructure(yamlContent);
507
625
  return parseAndValidate(yamlContent);
508
626
  } catch (error) {
509
- const message = error instanceof Error ? error.message : String(error);
510
- throw new Error(`Failed to parse Deepnote file: ${message}`);
627
+ if (error instanceof DeepnoteError) throw error;
628
+ throw new ParseError(`Failed to parse Deepnote file: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
511
629
  }
512
630
  }
513
631
 
@@ -521,23 +639,13 @@ function deserializeDeepnoteFile(yamlContent) {
521
639
  const result = deepnoteFileSchema.safeParse(parsed);
522
640
  if (!result.success) {
523
641
  const issue = result.error.issues[0];
524
- if (!issue) throw new Error("Invalid Deepnote file.");
642
+ if (!issue) throw new SchemaValidationError("Invalid Deepnote file.");
525
643
  const path = issue.path.join(".");
526
- const message = path ? `${path}: ${issue.message}` : issue.message;
527
- throw new Error(`Failed to parse the Deepnote file: ${message}.`);
644
+ throw new SchemaValidationError(`Failed to parse the Deepnote file: ${path ? `${path}: ${issue.message}` : issue.message}.`);
528
645
  }
529
646
  return result.data;
530
647
  }
531
648
 
532
- //#endregion
533
- //#region src/blocks.ts
534
- var UnsupportedBlockTypeError = class extends Error {
535
- constructor(message) {
536
- super(message);
537
- this.name = "UnsupportedBlockTypeError";
538
- }
539
- };
540
-
541
649
  //#endregion
542
650
  //#region src/blocks/image-blocks.ts
543
651
  function escapeHtmlAttribute(value) {
@@ -585,7 +693,7 @@ function createMarkdownForTextBlock(block) {
585
693
  if (block.type === "text-cell-todo") return `- ${block.metadata?.checked ? "[x]" : "[ ]"} ${escapeMarkdown(content)}`;
586
694
  if (block.type === "text-cell-callout") return `> ${escapeMarkdown(content)}`;
587
695
  if (block.type === "text-cell-p") return escapeMarkdown(content);
588
- throw new Error("Unhandled block type.");
696
+ throw new UnsupportedBlockTypeError("Unhandled block type.");
589
697
  }
590
698
  function stripMarkdownFromTextBlock(block) {
591
699
  const content = block.content ?? "";
@@ -596,7 +704,7 @@ function stripMarkdownFromTextBlock(block) {
596
704
  if (block.type === "text-cell-todo") return content.replace(/^-+\s+\[.\]\s+/, "").trim();
597
705
  if (block.type === "text-cell-callout") return content.replace(/^>\s+/, "").trim();
598
706
  if (block.type === "text-cell-p") return content.trim();
599
- throw new Error("Unhandled block type.");
707
+ throw new UnsupportedBlockTypeError("Unhandled block type.");
600
708
  }
601
709
  function createMarkdownForSeparatorBlock(_block) {
602
710
  return "<hr>";
@@ -870,9 +978,9 @@ function createPythonCodeForInputSelectBlock(block) {
870
978
  function createPythonCodeForInputSliderBlock(block) {
871
979
  const sanitizedPythonVariableName = sanitizePythonVariableName(block.metadata.deepnote_variable_name);
872
980
  const value = block.metadata.deepnote_variable_value;
873
- if (!/^-?\d+\.?\d*$|^-?\d*\.\d+$/.test(value)) throw new Error(`Invalid numeric value for slider input: "${value}". Expected a valid number (integer or float).`);
981
+ if (!/^-?\d+\.?\d*$|^-?\d*\.\d+$/.test(value)) throw new InvalidValueError(`Invalid numeric value for slider input: "${value}". Expected a valid number (integer or float).`, { value });
874
982
  const numericValue = Number(value);
875
- if (!Number.isFinite(numericValue)) throw new Error(`Invalid numeric value for slider input: "${value}". Value must be finite.`);
983
+ if (!Number.isFinite(numericValue)) throw new InvalidValueError(`Invalid numeric value for slider input: "${value}". Value must be finite.`, { value });
876
984
  return `${sanitizedPythonVariableName} = ${numericValue}`;
877
985
  }
878
986
  function createPythonCodeForInputFileBlock(block) {
@@ -906,7 +1014,7 @@ function createPythonCodeForInputDateRangeBlock(block) {
906
1014
  return pythonCode.dateRangeCustomDays(sanitizedPythonVariableName, Number(customDays));
907
1015
  } else if (isValidRelativeDateInterval(block.metadata.deepnote_variable_value)) {
908
1016
  const range = DATE_RANGE_INPUT_RELATIVE_RANGES.find((range$1) => range$1.value === block.metadata.deepnote_variable_value);
909
- if (!range) throw new Error(`Invalid relative date interval: "${block.metadata.deepnote_variable_value}". Expected one of: ${DATE_RANGE_INPUT_RELATIVE_RANGES.map((r) => r.value).join(", ")}.`);
1017
+ if (!range) throw new InvalidValueError(`Invalid relative date interval: "${block.metadata.deepnote_variable_value}". Expected one of: ${DATE_RANGE_INPUT_RELATIVE_RANGES.map((r) => r.value).join(", ")}.`, { value: block.metadata.deepnote_variable_value });
910
1018
  return ts_dedent.dedent`
911
1019
  ${range.pythonCode(sanitizedPythonVariableName)}`;
912
1020
  } else return ts_dedent.dedent`
@@ -939,12 +1047,50 @@ function isInputDateRangeBlock(block) {
939
1047
  }
940
1048
 
941
1049
  //#endregion
942
- //#region src/blocks/sql-utils.ts
943
- function convertToEnvironmentVariableName(str) {
944
- return (/^\d/.test(str) ? `_${str}` : str).toUpperCase().replace(/[^\w]/g, "_");
1050
+ //#region src/blocks/notebook-function-blocks.ts
1051
+ function isNotebookFunctionBlock(block) {
1052
+ return block.type === "notebook-function";
945
1053
  }
946
- function getSqlEnvVarName(integrationId) {
947
- return `SQL_${integrationId}`;
1054
+ function createPythonCodeForNotebookFunctionBlock(block) {
1055
+ const notebookId = block.metadata?.function_notebook_id;
1056
+ const inputs = block.metadata?.function_notebook_inputs ?? {};
1057
+ const exportMappings = block.metadata?.function_notebook_export_mappings ?? {};
1058
+ if (!notebookId) return ts_dedent.dedent`
1059
+ # Notebook Function: Not configured
1060
+ pass
1061
+ `;
1062
+ const enabledExports = Object.entries(exportMappings).filter(([, mapping]) => mapping.enabled);
1063
+ const inputsAsString = JSON.stringify(inputs);
1064
+ const exportMappingsDict = {};
1065
+ for (const [exportName, mapping] of enabledExports) exportMappingsDict[exportName] = mapping.variable_name;
1066
+ const exportMappingsAsString = JSON.stringify(exportMappingsDict);
1067
+ const inputsComment = `Inputs: ${inputsAsString}`;
1068
+ const exportsComment = enabledExports.length > 0 ? `Exports: ${enabledExports.map(([name, mapping]) => `${name} -> ${mapping.variable_name}`).join(", ")}` : "Exports: (none)";
1069
+ const functionCall = ts_dedent.dedent`
1070
+ _dntk.run_notebook_function(
1071
+ ${escapePythonString(notebookId)},
1072
+ inputs=${inputsAsString},
1073
+ export_mappings=${exportMappingsAsString}
1074
+ )
1075
+ `;
1076
+ if (enabledExports.length === 0) return ts_dedent.dedent`
1077
+ # Notebook Function: ${notebookId}
1078
+ # ${inputsComment}
1079
+ # ${exportsComment}
1080
+ ${functionCall}
1081
+ `;
1082
+ if (enabledExports.length === 1) return ts_dedent.dedent`
1083
+ # Notebook Function: ${notebookId}
1084
+ # ${inputsComment}
1085
+ # ${exportsComment}
1086
+ ${enabledExports[0][1]?.variable_name} = ${functionCall}
1087
+ `;
1088
+ return ts_dedent.dedent`
1089
+ # Notebook Function: ${notebookId}
1090
+ # ${inputsComment}
1091
+ # ${exportsComment}
1092
+ ${enabledExports.map(([, mapping]) => mapping.variable_name).join(", ")} = ${functionCall}
1093
+ `;
948
1094
  }
949
1095
 
950
1096
  //#endregion
@@ -1013,18 +1159,34 @@ function createPythonCode(block, executionContext) {
1013
1159
  if (isVisualizationBlock(block)) return createPythonCodeForVisualizationBlock(block);
1014
1160
  if (isButtonBlock(block)) return createPythonCodeForButtonBlock(block, executionContext);
1015
1161
  if (isBigNumberBlock(block)) return createPythonCodeForBigNumberBlock(block);
1162
+ if (isNotebookFunctionBlock(block)) return createPythonCodeForNotebookFunctionBlock(block);
1016
1163
  throw new UnsupportedBlockTypeError(`Creating python code from block type ${block.type} is not supported yet.`);
1017
1164
  }
1018
1165
 
1019
1166
  //#endregion
1167
+ exports.DeepnoteError = DeepnoteError;
1168
+ exports.EncodingError = EncodingError;
1169
+ exports.INPUT_BLOCK_TYPES = INPUT_BLOCK_TYPES;
1170
+ exports.InvalidValueError = InvalidValueError;
1171
+ exports.ParseError = ParseError;
1172
+ exports.ProhibitedYamlFeatureError = ProhibitedYamlFeatureError;
1173
+ exports.SchemaValidationError = SchemaValidationError;
1174
+ exports.UnsupportedBlockTypeError = UnsupportedBlockTypeError;
1175
+ exports.YamlParseError = YamlParseError;
1176
+ exports.convertToEnvironmentVariableName = convertToEnvironmentVariableName;
1020
1177
  exports.createMarkdown = createMarkdown;
1021
1178
  exports.createPythonCode = createPythonCode;
1022
1179
  exports.decodeUtf8NoBom = decodeUtf8NoBom;
1023
1180
  exports.deepnoteBlockSchema = deepnoteBlockSchema;
1024
1181
  exports.deepnoteFileSchema = deepnoteFileSchema;
1182
+ exports.deepnoteSnapshotSchema = deepnoteSnapshotSchema;
1025
1183
  exports.deserializeDeepnoteFile = deserializeDeepnoteFile;
1026
1184
  exports.environmentSchema = environmentSchema;
1027
1185
  exports.executionErrorSchema = executionErrorSchema;
1028
1186
  exports.executionSchema = executionSchema;
1029
1187
  exports.executionSummarySchema = executionSummarySchema;
1188
+ exports.getSqlEnvVarName = getSqlEnvVarName;
1189
+ exports.isExecutableBlock = isExecutableBlock;
1190
+ exports.isExecutableBlockType = isExecutableBlockType;
1191
+ exports.parseYaml = parseYaml;
1030
1192
  exports.stripMarkdown = stripMarkdown;