@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.js CHANGED
@@ -2,6 +2,113 @@ import { z } from "zod";
2
2
  import { parseDocument } from "yaml";
3
3
  import { dedent } from "ts-dedent";
4
4
 
5
+ //#region src/errors.ts
6
+ /**
7
+ * Base error class for all Deepnote errors.
8
+ */
9
+ var DeepnoteError = class extends Error {
10
+ constructor(message, options) {
11
+ super(message, options);
12
+ this.name = this.constructor.name;
13
+ }
14
+ };
15
+ /**
16
+ * Generic parse failure, optionally associated with a file path.
17
+ */
18
+ var ParseError = class extends DeepnoteError {
19
+ filePath;
20
+ constructor(message, options) {
21
+ super(message, options);
22
+ this.filePath = options?.filePath;
23
+ }
24
+ };
25
+ /**
26
+ * YAML syntax or duplicate key errors.
27
+ */
28
+ var YamlParseError = class extends ParseError {};
29
+ /**
30
+ * BOM or invalid UTF-8 encoding errors.
31
+ */
32
+ var EncodingError = class extends ParseError {};
33
+ /**
34
+ * Zod schema validation failures.
35
+ */
36
+ var SchemaValidationError = class extends ParseError {};
37
+ /**
38
+ * Prohibited YAML features: anchors, aliases, merge keys, tags.
39
+ */
40
+ var ProhibitedYamlFeatureError = class extends ParseError {
41
+ feature;
42
+ constructor(message, options) {
43
+ super(message, options);
44
+ this.feature = options.feature;
45
+ }
46
+ };
47
+ /**
48
+ * Thrown when a block type is not supported.
49
+ */
50
+ var UnsupportedBlockTypeError = class extends DeepnoteError {};
51
+ /**
52
+ * Thrown when a value is invalid (e.g., slider value, date interval).
53
+ */
54
+ var InvalidValueError = class extends DeepnoteError {
55
+ value;
56
+ constructor(message, options) {
57
+ super(message, options);
58
+ this.value = options.value;
59
+ }
60
+ };
61
+
62
+ //#endregion
63
+ //#region src/blocks/executable-blocks.ts
64
+ /**
65
+ * Block types that represent user input widgets.
66
+ * These blocks capture user input and define variables.
67
+ */
68
+ const INPUT_BLOCK_TYPES = new Set([
69
+ "input-text",
70
+ "input-textarea",
71
+ "input-checkbox",
72
+ "input-select",
73
+ "input-slider",
74
+ "input-date",
75
+ "input-date-range",
76
+ "input-file"
77
+ ]);
78
+ const executableBlockTypes = new Set([
79
+ "code",
80
+ "sql",
81
+ "notebook-function",
82
+ "visualization",
83
+ "button",
84
+ "big-number",
85
+ ...INPUT_BLOCK_TYPES
86
+ ]);
87
+ /**
88
+ * Type guard to check if a block is an executable block.
89
+ * Executable blocks can have outputs and be executed by the runtime.
90
+ */
91
+ function isExecutableBlock(block) {
92
+ return executableBlockTypes.has(block.type);
93
+ }
94
+ /**
95
+ * Checks if a block type string represents an executable block.
96
+ * Convenience function for when you only have the type string.
97
+ */
98
+ function isExecutableBlockType(type) {
99
+ return executableBlockTypes.has(type);
100
+ }
101
+
102
+ //#endregion
103
+ //#region src/blocks/sql-utils.ts
104
+ function convertToEnvironmentVariableName(str) {
105
+ return (/^\d/.test(str) ? `_${str}` : str).toUpperCase().replace(/[^\w]/g, "_");
106
+ }
107
+ function getSqlEnvVarName(integrationId) {
108
+ return `SQL_${integrationId}`;
109
+ }
110
+
111
+ //#endregion
5
112
  //#region src/deserialize-file/deepnote-file-schema.ts
6
113
  /** Preprocesses any content value to empty string for blocks that don't use content */
7
114
  const emptyContent = () => z.preprocess(() => "", z.literal("").optional());
@@ -394,6 +501,17 @@ const deepnoteFileSchema = z.object({
394
501
  }),
395
502
  version: z.string()
396
503
  });
504
+ const deepnoteSnapshotSchema = deepnoteFileSchema.extend({
505
+ environment: environmentSchema.unwrap(),
506
+ execution: executionSchema.unwrap(),
507
+ metadata: z.object({
508
+ checksum: z.string().optional(),
509
+ createdAt: z.string(),
510
+ exportedAt: z.string().optional(),
511
+ modifiedAt: z.string().optional(),
512
+ snapshotHash: z.string()
513
+ })
514
+ });
397
515
 
398
516
  //#endregion
399
517
  //#region src/deserialize-file/parse-yaml.ts
@@ -403,7 +521,7 @@ const deepnoteFileSchema = z.object({
403
521
  *
404
522
  * @param bytes - Raw file bytes as Uint8Array
405
523
  * @returns Decoded UTF-8 string without BOM
406
- * @throws Error if BOM detected or invalid UTF-8 encoding
524
+ * @throws EncodingError if BOM detected or invalid UTF-8 encoding
407
525
  *
408
526
  * @example
409
527
  * ```typescript
@@ -413,11 +531,11 @@ const deepnoteFileSchema = z.object({
413
531
  * ```
414
532
  */
415
533
  function decodeUtf8NoBom(bytes) {
416
- 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");
534
+ 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");
417
535
  try {
418
536
  return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
419
537
  } catch {
420
- throw new Error("Invalid UTF-8 encoding detected in Deepnote file");
538
+ throw new EncodingError("Invalid UTF-8 encoding detected in Deepnote file");
421
539
  }
422
540
  }
423
541
  /**
@@ -427,10 +545,10 @@ function decodeUtf8NoBom(bytes) {
427
545
  * For proper UTF-8 validation, use decodeUtf8NoBom() on raw bytes before decoding.
428
546
  *
429
547
  * @param yamlContent - Already-decoded YAML string
430
- * @throws Error if BOM prefix detected
548
+ * @throws EncodingError if BOM prefix detected
431
549
  */
432
550
  function validateNoBomPrefix(yamlContent) {
433
- if (yamlContent.charCodeAt(0) === 65279) throw new Error("UTF-8 BOM detected in Deepnote file - files must be UTF-8 without BOM");
551
+ if (yamlContent.charCodeAt(0) === 65279) throw new EncodingError("UTF-8 BOM detected in Deepnote file - files must be UTF-8 without BOM");
434
552
  }
435
553
  /**
436
554
  * Validates that the YAML document doesn't contain prohibited features:
@@ -439,13 +557,13 @@ function validateNoBomPrefix(yamlContent) {
439
557
  * - No custom tags (!tag)
440
558
  */
441
559
  function validateYamlStructure(yamlContent) {
442
- if (/(?:^|\n)\s*(?:-\s+|[\w-]+:\s*)&\w+/.test(yamlContent)) throw new Error("YAML anchors (&) are not allowed in Deepnote files");
443
- if (/(?:^|\n)\s*(?:-\s+|[\w-]+:\s*)\*\w+/.test(yamlContent)) throw new Error("YAML aliases (*) are not allowed in Deepnote files");
444
- if (/<<:/.test(yamlContent)) throw new Error("YAML merge keys (<<) are not allowed in Deepnote files");
560
+ if (/(?:^|\n)\s*(?:-\s+|[\w-]+:\s*)&\w+/.test(yamlContent)) throw new ProhibitedYamlFeatureError("YAML anchors (&) are not allowed in Deepnote files", { feature: "anchor" });
561
+ if (/(?:^|\n)\s*(?:-\s+|[\w-]+:\s*)\*\w+/.test(yamlContent)) throw new ProhibitedYamlFeatureError("YAML aliases (*) are not allowed in Deepnote files", { feature: "alias" });
562
+ if (/<<:/.test(yamlContent)) throw new ProhibitedYamlFeatureError("YAML merge keys (<<) are not allowed in Deepnote files", { feature: "merge-key" });
445
563
  const matches = yamlContent.match(/(?:^|\n)\s*(?:-\s+|[\w-]+:\s*)(![\w/-]+)/gm);
446
564
  if (matches) {
447
565
  const tags = matches.map((m) => m.match(/(![\w/-]+)/)?.[1]).filter(Boolean);
448
- if (tags.length > 0) throw new Error(`YAML tags are not allowed in Deepnote files: ${tags.join(", ")}`);
566
+ if (tags.length > 0) throw new ProhibitedYamlFeatureError(`YAML tags are not allowed in Deepnote files: ${tags.join(", ")}`, { feature: "tag" });
449
567
  }
450
568
  }
451
569
  /**
@@ -460,8 +578,8 @@ function parseAndValidate(yamlContent) {
460
578
  });
461
579
  if (doc.errors.length > 0) {
462
580
  const duplicateKeyError = doc.errors.find((err) => err.message.includes("duplicate") || err.message.includes("key"));
463
- if (duplicateKeyError) throw new Error(`Duplicate keys detected in Deepnote file: ${duplicateKeyError.message}`);
464
- throw new Error(`YAML parsing error: ${doc.errors[0].message}`);
581
+ if (duplicateKeyError) throw new YamlParseError(`Duplicate keys detected in Deepnote file: ${duplicateKeyError.message}`);
582
+ throw new YamlParseError(`YAML parsing error: ${doc.errors[0].message}`);
465
583
  }
466
584
  return doc.toJS();
467
585
  }
@@ -480,8 +598,8 @@ function parseYaml(yamlContent) {
480
598
  validateYamlStructure(yamlContent);
481
599
  return parseAndValidate(yamlContent);
482
600
  } catch (error) {
483
- const message = error instanceof Error ? error.message : String(error);
484
- throw new Error(`Failed to parse Deepnote file: ${message}`);
601
+ if (error instanceof DeepnoteError) throw error;
602
+ throw new ParseError(`Failed to parse Deepnote file: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
485
603
  }
486
604
  }
487
605
 
@@ -495,23 +613,13 @@ function deserializeDeepnoteFile(yamlContent) {
495
613
  const result = deepnoteFileSchema.safeParse(parsed);
496
614
  if (!result.success) {
497
615
  const issue = result.error.issues[0];
498
- if (!issue) throw new Error("Invalid Deepnote file.");
616
+ if (!issue) throw new SchemaValidationError("Invalid Deepnote file.");
499
617
  const path = issue.path.join(".");
500
- const message = path ? `${path}: ${issue.message}` : issue.message;
501
- throw new Error(`Failed to parse the Deepnote file: ${message}.`);
618
+ throw new SchemaValidationError(`Failed to parse the Deepnote file: ${path ? `${path}: ${issue.message}` : issue.message}.`);
502
619
  }
503
620
  return result.data;
504
621
  }
505
622
 
506
- //#endregion
507
- //#region src/blocks.ts
508
- var UnsupportedBlockTypeError = class extends Error {
509
- constructor(message) {
510
- super(message);
511
- this.name = "UnsupportedBlockTypeError";
512
- }
513
- };
514
-
515
623
  //#endregion
516
624
  //#region src/blocks/image-blocks.ts
517
625
  function escapeHtmlAttribute(value) {
@@ -559,7 +667,7 @@ function createMarkdownForTextBlock(block) {
559
667
  if (block.type === "text-cell-todo") return `- ${block.metadata?.checked ? "[x]" : "[ ]"} ${escapeMarkdown(content)}`;
560
668
  if (block.type === "text-cell-callout") return `> ${escapeMarkdown(content)}`;
561
669
  if (block.type === "text-cell-p") return escapeMarkdown(content);
562
- throw new Error("Unhandled block type.");
670
+ throw new UnsupportedBlockTypeError("Unhandled block type.");
563
671
  }
564
672
  function stripMarkdownFromTextBlock(block) {
565
673
  const content = block.content ?? "";
@@ -570,7 +678,7 @@ function stripMarkdownFromTextBlock(block) {
570
678
  if (block.type === "text-cell-todo") return content.replace(/^-+\s+\[.\]\s+/, "").trim();
571
679
  if (block.type === "text-cell-callout") return content.replace(/^>\s+/, "").trim();
572
680
  if (block.type === "text-cell-p") return content.trim();
573
- throw new Error("Unhandled block type.");
681
+ throw new UnsupportedBlockTypeError("Unhandled block type.");
574
682
  }
575
683
  function createMarkdownForSeparatorBlock(_block) {
576
684
  return "<hr>";
@@ -844,9 +952,9 @@ function createPythonCodeForInputSelectBlock(block) {
844
952
  function createPythonCodeForInputSliderBlock(block) {
845
953
  const sanitizedPythonVariableName = sanitizePythonVariableName(block.metadata.deepnote_variable_name);
846
954
  const value = block.metadata.deepnote_variable_value;
847
- if (!/^-?\d+\.?\d*$|^-?\d*\.\d+$/.test(value)) throw new Error(`Invalid numeric value for slider input: "${value}". Expected a valid number (integer or float).`);
955
+ 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 });
848
956
  const numericValue = Number(value);
849
- if (!Number.isFinite(numericValue)) throw new Error(`Invalid numeric value for slider input: "${value}". Value must be finite.`);
957
+ if (!Number.isFinite(numericValue)) throw new InvalidValueError(`Invalid numeric value for slider input: "${value}". Value must be finite.`, { value });
850
958
  return `${sanitizedPythonVariableName} = ${numericValue}`;
851
959
  }
852
960
  function createPythonCodeForInputFileBlock(block) {
@@ -880,7 +988,7 @@ function createPythonCodeForInputDateRangeBlock(block) {
880
988
  return pythonCode.dateRangeCustomDays(sanitizedPythonVariableName, Number(customDays));
881
989
  } else if (isValidRelativeDateInterval(block.metadata.deepnote_variable_value)) {
882
990
  const range = DATE_RANGE_INPUT_RELATIVE_RANGES.find((range$1) => range$1.value === block.metadata.deepnote_variable_value);
883
- 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(", ")}.`);
991
+ 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 });
884
992
  return dedent`
885
993
  ${range.pythonCode(sanitizedPythonVariableName)}`;
886
994
  } else return dedent`
@@ -913,12 +1021,50 @@ function isInputDateRangeBlock(block) {
913
1021
  }
914
1022
 
915
1023
  //#endregion
916
- //#region src/blocks/sql-utils.ts
917
- function convertToEnvironmentVariableName(str) {
918
- return (/^\d/.test(str) ? `_${str}` : str).toUpperCase().replace(/[^\w]/g, "_");
1024
+ //#region src/blocks/notebook-function-blocks.ts
1025
+ function isNotebookFunctionBlock(block) {
1026
+ return block.type === "notebook-function";
919
1027
  }
920
- function getSqlEnvVarName(integrationId) {
921
- return `SQL_${integrationId}`;
1028
+ function createPythonCodeForNotebookFunctionBlock(block) {
1029
+ const notebookId = block.metadata?.function_notebook_id;
1030
+ const inputs = block.metadata?.function_notebook_inputs ?? {};
1031
+ const exportMappings = block.metadata?.function_notebook_export_mappings ?? {};
1032
+ if (!notebookId) return dedent`
1033
+ # Notebook Function: Not configured
1034
+ pass
1035
+ `;
1036
+ const enabledExports = Object.entries(exportMappings).filter(([, mapping]) => mapping.enabled);
1037
+ const inputsAsString = JSON.stringify(inputs);
1038
+ const exportMappingsDict = {};
1039
+ for (const [exportName, mapping] of enabledExports) exportMappingsDict[exportName] = mapping.variable_name;
1040
+ const exportMappingsAsString = JSON.stringify(exportMappingsDict);
1041
+ const inputsComment = `Inputs: ${inputsAsString}`;
1042
+ const exportsComment = enabledExports.length > 0 ? `Exports: ${enabledExports.map(([name, mapping]) => `${name} -> ${mapping.variable_name}`).join(", ")}` : "Exports: (none)";
1043
+ const functionCall = dedent`
1044
+ _dntk.run_notebook_function(
1045
+ ${escapePythonString(notebookId)},
1046
+ inputs=${inputsAsString},
1047
+ export_mappings=${exportMappingsAsString}
1048
+ )
1049
+ `;
1050
+ if (enabledExports.length === 0) return dedent`
1051
+ # Notebook Function: ${notebookId}
1052
+ # ${inputsComment}
1053
+ # ${exportsComment}
1054
+ ${functionCall}
1055
+ `;
1056
+ if (enabledExports.length === 1) return dedent`
1057
+ # Notebook Function: ${notebookId}
1058
+ # ${inputsComment}
1059
+ # ${exportsComment}
1060
+ ${enabledExports[0][1]?.variable_name} = ${functionCall}
1061
+ `;
1062
+ return dedent`
1063
+ # Notebook Function: ${notebookId}
1064
+ # ${inputsComment}
1065
+ # ${exportsComment}
1066
+ ${enabledExports.map(([, mapping]) => mapping.variable_name).join(", ")} = ${functionCall}
1067
+ `;
922
1068
  }
923
1069
 
924
1070
  //#endregion
@@ -987,8 +1133,9 @@ function createPythonCode(block, executionContext) {
987
1133
  if (isVisualizationBlock(block)) return createPythonCodeForVisualizationBlock(block);
988
1134
  if (isButtonBlock(block)) return createPythonCodeForButtonBlock(block, executionContext);
989
1135
  if (isBigNumberBlock(block)) return createPythonCodeForBigNumberBlock(block);
1136
+ if (isNotebookFunctionBlock(block)) return createPythonCodeForNotebookFunctionBlock(block);
990
1137
  throw new UnsupportedBlockTypeError(`Creating python code from block type ${block.type} is not supported yet.`);
991
1138
  }
992
1139
 
993
1140
  //#endregion
994
- export { createMarkdown, createPythonCode, decodeUtf8NoBom, deepnoteBlockSchema, deepnoteFileSchema, deserializeDeepnoteFile, environmentSchema, executionErrorSchema, executionSchema, executionSummarySchema, stripMarkdown };
1141
+ export { DeepnoteError, EncodingError, INPUT_BLOCK_TYPES, InvalidValueError, ParseError, ProhibitedYamlFeatureError, SchemaValidationError, UnsupportedBlockTypeError, YamlParseError, convertToEnvironmentVariableName, createMarkdown, createPythonCode, decodeUtf8NoBom, deepnoteBlockSchema, deepnoteFileSchema, deepnoteSnapshotSchema, deserializeDeepnoteFile, environmentSchema, executionErrorSchema, executionSchema, executionSummarySchema, getSqlEnvVarName, isExecutableBlock, isExecutableBlockType, parseYaml, stripMarkdown };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepnote/blocks",
3
- "version": "3.0.1",
3
+ "version": "4.0.0",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "repository": {