dataiku-sdk 3.6.0 → 3.7.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.
Files changed (127) hide show
  1. package/README.md +14 -6
  2. package/bin/bun-version.js +14 -0
  3. package/bin/dss.js +23 -12
  4. package/dist/build-metadata.json +1 -1
  5. package/dist/packages/types/src/index.d.ts +74 -1
  6. package/dist/packages/types/src/index.js +53 -1
  7. package/dist/src/cli/coerce.d.ts +1 -0
  8. package/dist/src/cli/coerce.js +19 -15
  9. package/dist/src/cli/command-syntax.json +3521 -416
  10. package/dist/src/cli/commands/analysis.js +19 -1
  11. package/dist/src/cli/commands/bundle.js +22 -0
  12. package/dist/src/cli/commands/connection.js +26 -2
  13. package/dist/src/cli/commands/data-quality.js +33 -1
  14. package/dist/src/cli/commands/dataset.js +35 -4
  15. package/dist/src/cli/commands/discussion.js +15 -0
  16. package/dist/src/cli/commands/doctor.js +1 -1
  17. package/dist/src/cli/commands/flow.d.ts +5 -0
  18. package/dist/src/cli/commands/flow.js +57 -0
  19. package/dist/src/cli/commands/future.js +8 -0
  20. package/dist/src/cli/commands/index.js +6 -0
  21. package/dist/src/cli/commands/job.js +29 -6
  22. package/dist/src/cli/commands/metrics.js +21 -1
  23. package/dist/src/cli/commands/ml-task.d.ts +5 -0
  24. package/dist/src/cli/commands/ml-task.js +86 -16
  25. package/dist/src/cli/commands/mlflow.d.ts +11 -0
  26. package/dist/src/cli/commands/mlflow.js +121 -0
  27. package/dist/src/cli/commands/plugin.js +5 -11
  28. package/dist/src/cli/commands/project-deployer.js +70 -0
  29. package/dist/src/cli/commands/project-library.js +2 -5
  30. package/dist/src/cli/commands/project.js +8 -0
  31. package/dist/src/cli/commands/recipe.d.ts +9 -0
  32. package/dist/src/cli/commands/recipe.js +93 -7
  33. package/dist/src/cli/commands/saved-model.d.ts +1 -1
  34. package/dist/src/cli/commands/saved-model.js +9 -76
  35. package/dist/src/cli/commands/streaming-endpoint.js +30 -1
  36. package/dist/src/cli/commands/webapp.js +8 -0
  37. package/dist/src/cli/commands/wiki.js +22 -0
  38. package/dist/src/cli/commands/workspace.js +8 -0
  39. package/dist/src/cli/contract.d.ts +16 -15
  40. package/dist/src/cli/contract.js +139 -3404
  41. package/dist/src/cli/doctor.d.ts +13 -0
  42. package/dist/src/cli/doctor.js +41 -1
  43. package/dist/src/cli/flags.d.ts +7 -1
  44. package/dist/src/cli/flags.js +78 -6
  45. package/dist/src/cli/helpers/app-successor.js +1 -2
  46. package/dist/src/cli/helpers/documentation.d.ts +21 -0
  47. package/dist/src/cli/helpers/documentation.js +53 -0
  48. package/dist/src/cli/helpers/trained-model.d.ts +26 -0
  49. package/dist/src/cli/helpers/trained-model.js +144 -0
  50. package/dist/src/cli/list-output.d.ts +7 -0
  51. package/dist/src/cli/list-output.js +77 -0
  52. package/dist/src/cli/list-shapes.d.ts +21 -0
  53. package/dist/src/cli/list-shapes.js +111 -0
  54. package/dist/src/cli/output-schemas.d.ts +0 -14
  55. package/dist/src/cli/output-schemas.js +4 -7
  56. package/dist/src/cli/output.d.ts +0 -1
  57. package/dist/src/cli/output.js +27 -19
  58. package/dist/src/cli/plans.d.ts +14 -0
  59. package/dist/src/cli/plans.js +3684 -0
  60. package/dist/src/cli/usage.d.ts +7 -0
  61. package/dist/src/cli/usage.js +20 -1
  62. package/dist/src/cli/version.d.ts +2 -2
  63. package/dist/src/cli/version.js +2 -2
  64. package/dist/src/cli.js +127 -113
  65. package/dist/src/client.d.ts +52 -43
  66. package/dist/src/client.js +267 -96
  67. package/dist/src/errors.d.ts +11 -1
  68. package/dist/src/errors.js +31 -47
  69. package/dist/src/generated/action-output-schemas.json +345 -89
  70. package/dist/src/index.d.ts +5 -1
  71. package/dist/src/index.js +3 -0
  72. package/dist/src/resources/analyses.d.ts +8 -0
  73. package/dist/src/resources/analyses.js +13 -1
  74. package/dist/src/resources/bundles.d.ts +34 -0
  75. package/dist/src/resources/bundles.js +67 -0
  76. package/dist/src/resources/connections.d.ts +9 -0
  77. package/dist/src/resources/connections.js +15 -0
  78. package/dist/src/resources/data-quality.d.ts +20 -0
  79. package/dist/src/resources/data-quality.js +30 -0
  80. package/dist/src/resources/dataset-create.d.ts +19 -0
  81. package/dist/src/resources/dataset-create.js +66 -0
  82. package/dist/src/resources/datasets.d.ts +25 -15
  83. package/dist/src/resources/datasets.js +59 -111
  84. package/dist/src/resources/discussions.d.ts +5 -0
  85. package/dist/src/resources/discussions.js +7 -0
  86. package/dist/src/resources/documentation.d.ts +25 -0
  87. package/dist/src/resources/documentation.js +16 -0
  88. package/dist/src/resources/flow.d.ts +48 -0
  89. package/dist/src/resources/flow.js +65 -0
  90. package/dist/src/resources/futures.d.ts +23 -0
  91. package/dist/src/resources/futures.js +32 -0
  92. package/dist/src/resources/jobs.js +102 -2
  93. package/dist/src/resources/metrics.d.ts +10 -0
  94. package/dist/src/resources/metrics.js +10 -0
  95. package/dist/src/resources/ml-tasks.d.ts +65 -2
  96. package/dist/src/resources/ml-tasks.js +102 -28
  97. package/dist/src/resources/mlflow-extension.d.ts +73 -0
  98. package/dist/src/resources/mlflow-extension.js +105 -0
  99. package/dist/src/resources/projects.d.ts +6 -0
  100. package/dist/src/resources/projects.js +8 -0
  101. package/dist/src/resources/recipe-create.d.ts +65 -0
  102. package/dist/src/resources/recipe-create.js +338 -0
  103. package/dist/src/resources/recipes.d.ts +28 -29
  104. package/dist/src/resources/recipes.js +126 -245
  105. package/dist/src/resources/saved-models.d.ts +10 -6
  106. package/dist/src/resources/saved-models.js +16 -14
  107. package/dist/src/resources/scenarios.js +8 -14
  108. package/dist/src/resources/streaming-endpoints.d.ts +12 -0
  109. package/dist/src/resources/streaming-endpoints.js +15 -0
  110. package/dist/src/resources/trained-model.d.ts +60 -0
  111. package/dist/src/resources/trained-model.js +98 -0
  112. package/dist/src/resources/webapps.d.ts +8 -0
  113. package/dist/src/resources/webapps.js +10 -0
  114. package/dist/src/resources/wiki.d.ts +5 -0
  115. package/dist/src/resources/wiki.js +7 -0
  116. package/dist/src/resources/workspaces.d.ts +2 -0
  117. package/dist/src/resources/workspaces.js +4 -0
  118. package/dist/src/utils/response-file.js +20 -9
  119. package/dist/src/utils/secret-sanitize.d.ts +7 -0
  120. package/dist/src/utils/secret-sanitize.js +46 -0
  121. package/package.json +25 -26
  122. package/plugin.json +1 -1
  123. package/skills/dataiku-dss/SKILL.md +1 -1
  124. package/skills/dataiku-dss/references/authentication.md +3 -1
  125. package/skills/dataiku-dss/references/coding.md +1 -0
  126. package/skills/dataiku-dss/references/discovery.md +4 -6
  127. package/skills/dataiku-dss/references/troubleshooting.md +3 -2
@@ -1,15 +1,13 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as nodePath from "node:path";
3
- import { Readable, Transform, } from "node:stream";
4
- import { pipeline, } from "node:stream/promises";
5
3
  import { StringDecoder, } from "node:string_decoder";
6
- import { createGzip, } from "node:zlib";
7
4
  import { ClientValidationError, DataikuError, } from "../errors.js";
8
5
  import { DatasetDetailsSchema, DatasetSchemaSchema, DatasetSummaryArraySchema, } from "../schemas.js";
9
6
  import { deepMerge, } from "../utils/deep-merge.js";
10
7
  import { sanitizeFileName, } from "../utils/sanitize.js";
11
8
  import { BaseResource, } from "./base.js";
12
9
  import { resolveAdminManagedStorageConnection, } from "./connections.js";
10
+ import { buildDatasetCreateBody, } from "./dataset-create.js";
13
11
  // ---------------------------------------------------------------------------
14
12
  // Helpers: TSV → CSV streaming conversion
15
13
  // ---------------------------------------------------------------------------
@@ -404,13 +402,14 @@ const CSV_BATCH_BYTES = 64 * 1024;
404
402
  function tsvToCsvTransform(maxDataRows, stats, onHeader, spreadsheetSafe = true) {
405
403
  const state = createTsvStreamState();
406
404
  const decoder = new StringDecoder("utf-8");
405
+ const encoder = new TextEncoder();
407
406
  const maxRows = Math.max(1, maxDataRows);
408
407
  let headerSeen = false;
409
408
  let done = false;
410
409
  // Bounded CSV output batching: emitted rows accumulate here and are joined
411
- // into a single push once the batch reaches CSV_BATCH_BYTES, so memory stays
412
- // bounded by the batch budget plus one escaped row (the join briefly holds a
413
- // second copy of the batch) while per-row push/callback overhead disappears.
410
+ // into a single enqueue once the batch reaches CSV_BATCH_BYTES, so memory
411
+ // stays bounded by the batch budget plus one escaped row (the join briefly
412
+ // holds a second copy of the batch) while per-row chunk overhead disappears.
414
413
  let batchLines = [];
415
414
  let batchBytes = 0;
416
415
  const flushBatch = (target) => {
@@ -419,7 +418,7 @@ function tsvToCsvTransform(maxDataRows, stats, onHeader, spreadsheetSafe = true)
419
418
  const csv = batchLines.join("");
420
419
  batchLines = [];
421
420
  batchBytes = 0;
422
- target.push(csv);
421
+ target.enqueue(encoder.encode(csv));
423
422
  };
424
423
  const queueLine = (target, line) => {
425
424
  batchLines.push(line);
@@ -450,48 +449,27 @@ function tsvToCsvTransform(maxDataRows, stats, onHeader, spreadsheetSafe = true)
450
449
  push(`${rowToCsv(row, spreadsheetSafe)}\n`);
451
450
  stats.rows += 1;
452
451
  };
453
- return new Transform({
454
- transform(chunk, _encoding, callback) {
452
+ // A byte-limit overflow throws out of transform/flush: the stream errors,
453
+ // which cancels the response and makes download() remove the partial file.
454
+ return new TransformStream({
455
+ transform(chunk, controller) {
456
+ consumeTsvChunk(decoder.write(chunk), state, (row) => handleRow(row, (line) => queueLine(controller, line)));
455
457
  if (done) {
456
- callback();
457
- return;
458
- }
459
- try {
460
- consumeTsvChunk(decoder.write(chunk), state, (row) => handleRow(row, (line) => queueLine(this, line)));
461
- }
462
- catch (error) {
463
- // Byte-limit overflow: destroy the transform so the pipeline
464
- // cancels the response and the partial file is removed.
465
- callback(error instanceof Error ? error : new ClientValidationError(String(error), "internal_error"));
466
- return;
467
- }
468
- if (done) {
469
- // Truncation reached: emit the batch queued so far before EOF.
470
- flushBatch(this);
471
- this.push(null);
458
+ // Truncation reached: emit the batch queued so far, end the output,
459
+ // and cancel the rest of the response.
460
+ flushBatch(controller);
461
+ controller.terminate();
472
462
  }
473
- callback();
474
463
  },
475
- flush(callback) {
476
- if (done) {
477
- callback();
478
- return;
479
- }
480
- try {
481
- const tail = decoder.end();
482
- if (tail !== "") {
483
- consumeTsvChunk(tail, state, (row) => handleRow(row, (line) => queueLine(this, line)));
484
- }
485
- flushTsvStream(state, (row) => handleRow(row, (line) => queueLine(this, line)));
486
- }
487
- catch (error) {
488
- callback(error instanceof Error ? error : new ClientValidationError(String(error), "internal_error"));
489
- return;
464
+ flush(controller) {
465
+ const tail = decoder.end();
466
+ if (tail !== "") {
467
+ consumeTsvChunk(tail, state, (row) => handleRow(row, (line) => queueLine(controller, line)));
490
468
  }
469
+ flushTsvStream(state, (row) => handleRow(row, (line) => queueLine(controller, line)));
491
470
  // Emit the trailing partial batch; without this the last rows of a
492
471
  // download (below the batch budget) would be silently dropped.
493
- flushBatch(this);
494
- callback();
472
+ flushBatch(controller);
495
473
  },
496
474
  });
497
475
  }
@@ -518,60 +496,6 @@ function isMissingUploadedFilesTargetConnection(error) {
518
496
  return (detail.includes("without a target connection")
519
497
  || detail.includes("target connection is required"));
520
498
  }
521
- /** Wire body for POST /datasets/; shared with `dataset create --plan` so the plan matches the request. */
522
- export function buildDatasetCreateBody(opts) {
523
- if (opts.dsType.toLowerCase() === "uploadedfiles") {
524
- return {
525
- projectKey: opts.projectKey,
526
- name: opts.datasetName,
527
- type: opts.dsType,
528
- params: opts.connection ? { uploadConnection: opts.connection, } : {},
529
- };
530
- }
531
- if (!opts.connection) {
532
- throw new ClientValidationError("connection is required unless dsType is UploadedFiles.");
533
- }
534
- if (opts.table) {
535
- const params = {
536
- connection: opts.connection,
537
- mode: "table",
538
- table: opts.table,
539
- };
540
- if (opts.dbSchema)
541
- params.schema = opts.dbSchema;
542
- if (opts.catalog)
543
- params.catalog = opts.catalog;
544
- return {
545
- projectKey: opts.projectKey,
546
- name: opts.datasetName,
547
- type: opts.dsType,
548
- params,
549
- managed: opts.managed ?? false,
550
- };
551
- }
552
- return {
553
- projectKey: opts.projectKey,
554
- name: opts.datasetName,
555
- type: opts.dsType,
556
- params: {
557
- connection: opts.connection,
558
- path: `/dataiku/${opts.projectKey}/${opts.datasetName}`,
559
- },
560
- formatType: opts.formatType ?? "csv",
561
- formatParams: opts.formatParams ?? {
562
- style: "excel",
563
- charset: "utf8",
564
- separator: "\t",
565
- quoteChar: '"',
566
- escapeChar: "\\",
567
- dateSerializationFormat: "ISO",
568
- arrayMapFormat: "json",
569
- parseHeaderRow: true,
570
- compress: "gz",
571
- },
572
- managed: opts.managed ?? true,
573
- };
574
- }
575
499
  const DATASET_CLONE_PARAM_KEYS = [
576
500
  "connection",
577
501
  "uploadConnection",
@@ -676,6 +600,23 @@ export class DatasetsResource extends BaseResource {
676
600
  const dsEnc = encodeURIComponent(datasetName);
677
601
  return this.client.get(`/public/api/projects/${this.enc(projectKey)}/datasets/${dsEnc}/schema`);
678
602
  }
603
+ /**
604
+ * TSV data stream of at most `limit` rows. A column or partition selection
605
+ * uses the documented POST variant of the data endpoint (body `columns`,
606
+ * `partitions`, `format`, `sampling`); otherwise the GET endpoint.
607
+ */
608
+ async dataStream(datasetName, limit, projectKey, selection) {
609
+ const path = `/public/api/projects/${this.enc(projectKey)}/datasets/${encodeURIComponent(datasetName)}/data/`;
610
+ if (!selection?.columns?.length && !selection?.partitions) {
611
+ return this.client.stream(`${path}?format=tsv-excel-header&limit=${limit}`);
612
+ }
613
+ return this.client.postStream(path, {
614
+ format: "tsv-excel-header",
615
+ ...(selection.columns?.length ? { columns: selection.columns, } : {}),
616
+ ...(selection.partitions ? { partitions: selection.partitions, } : {}),
617
+ sampling: { samplingMethod: "HEAD_SEQUENTIAL", maxRecords: limit, },
618
+ });
619
+ }
679
620
  /**
680
621
  * Preview dataset rows as structured data: column names plus row arrays,
681
622
  * mirroring the sql query shape ({ columns, rows, rowCount }). Streams TSV
@@ -691,10 +632,9 @@ export class DatasetsResource extends BaseResource {
691
632
  async preview(datasetName, opts) {
692
633
  const maxRows = Math.max(1, Math.min(opts?.maxRows ?? 50, 500));
693
634
  const timeoutMs = Math.max(1, opts?.timeoutMs ?? this.client.getRequestTimeoutMs());
694
- const dsEnc = encodeURIComponent(datasetName);
695
635
  // Probe one row past the cap so callers learn whether the dataset holds
696
636
  // more rows than requested without materializing the full dataset.
697
- const res = await this.client.stream(`/public/api/projects/${this.enc(opts?.projectKey)}/datasets/${dsEnc}/data/?format=tsv-excel-header&limit=${maxRows + 1}`);
637
+ const res = await this.dataStream(datasetName, maxRows + 1, opts?.projectKey, opts);
698
638
  const onHeader = opts?.validateColumns
699
639
  ? (headerRow) => {
700
640
  const warnings = validateStreamColumns(headerRow, opts.validateColumns);
@@ -761,6 +701,20 @@ export class DatasetsResource extends BaseResource {
761
701
  const dsEnc = encodeURIComponent(datasetName);
762
702
  return this.client.put(`/public/api/projects/${this.enc(projectKey)}/datasets/${dsEnc}/metadata`, metadata);
763
703
  }
704
+ /**
705
+ * Create or update the Hive table of an HDFS dataset so it matches the
706
+ * dataset schema (POST /actions/synchronizeHiveMetastore).
707
+ */
708
+ async synchronizeHiveMetastore(datasetName, projectKey) {
709
+ await this.client.post(`/public/api/projects/${this.enc(projectKey)}/datasets/${encodeURIComponent(datasetName)}/actions/synchronizeHiveMetastore`);
710
+ }
711
+ /**
712
+ * Update an HDFS dataset (path, schema, partitioning, format) from its
713
+ * Hive table (POST /actions/updateFromHive).
714
+ */
715
+ async updateFromHive(datasetName, projectKey) {
716
+ await this.client.post(`/public/api/projects/${this.enc(projectKey)}/datasets/${encodeURIComponent(datasetName)}/actions/updateFromHive`);
717
+ }
764
718
  /**
765
719
  * Create a managed dataset through the dedicated
766
720
  * `POST /projects/{projectKey}/datasets/managed` endpoint. DSS handles the
@@ -862,8 +816,7 @@ export class DatasetsResource extends BaseResource {
862
816
  */
863
817
  async download(datasetName, opts) {
864
818
  const limit = Math.max(1, opts?.limit ?? 100_000);
865
- const dsEnc = encodeURIComponent(datasetName);
866
- const res = await this.client.stream(`/public/api/projects/${this.enc(opts?.projectKey)}/datasets/${dsEnc}/data/?format=tsv-excel-header&limit=${limit + 1}`);
819
+ const res = await this.dataStream(datasetName, limit + 1, opts?.projectKey, opts);
867
820
  const safeDatasetName = sanitizeFileName(datasetName, "dataset");
868
821
  const filePath = opts?.outputPath?.endsWith(".gz") || opts?.outputPath?.endsWith(".csv")
869
822
  ? nodePath.resolve(opts.outputPath)
@@ -878,18 +831,13 @@ export class DatasetsResource extends BaseResource {
878
831
  : undefined;
879
832
  const shouldGzip = filePath.endsWith(".gz");
880
833
  const stats = { rows: 0, truncated: false, };
881
- const nodeStream = Readable.from(res.body, { objectMode: false, });
882
- const csvTransform = tsvToCsvTransform(limit, stats, onHeader, opts?.rawData !== true);
834
+ let csv = res.body.pipeThrough(tsvToCsvTransform(limit, stats, onHeader, opts?.rawData !== true));
835
+ if (shouldGzip)
836
+ csv = csv.pipeThrough(new CompressionStream("gzip"));
883
837
  fs.mkdirSync(nodePath.dirname(filePath), { recursive: true, });
884
- const fileOut = fs.createWriteStream(filePath);
885
838
  try {
886
- if (shouldGzip) {
887
- const gzip = createGzip();
888
- await pipeline(nodeStream, csvTransform, gzip, fileOut);
889
- }
890
- else {
891
- await pipeline(nodeStream, csvTransform, fileOut);
892
- }
839
+ // Native streaming write with backpressure; no Node stream layer.
840
+ await Bun.write(filePath, new Response(csv));
893
841
  }
894
842
  catch (error) {
895
843
  // A failed export (byte-limit overflow, network error) must never
@@ -21,6 +21,11 @@ export declare class DiscussionsResource extends BaseResource {
21
21
  create(objectType: string, objectId: string, topic: string, reply: string, projectKey?: string): Promise<Discussion>;
22
22
  /** Add a reply to an existing discussion. */
23
23
  reply(objectType: string, objectId: string, discussionId: string, text: string, projectKey?: string): Promise<Discussion>;
24
+ /**
25
+ * Update a discussion (topic, or the object it is attached to within the
26
+ * project): PUT the full discussion object obtained from `get`, edited.
27
+ */
28
+ update(objectType: string, objectId: string, discussionId: string, discussion: Record<string, unknown>, projectKey?: string): Promise<Discussion>;
24
29
  private objectDiscussionsPath;
25
30
  private discussionPath;
26
31
  }
@@ -16,6 +16,13 @@ export class DiscussionsResource extends BaseResource {
16
16
  async reply(objectType, objectId, discussionId, text, projectKey) {
17
17
  return this.client.post(`${this.discussionPath(objectType, objectId, discussionId, projectKey)}/replies/`, { reply: text, });
18
18
  }
19
+ /**
20
+ * Update a discussion (topic, or the object it is attached to within the
21
+ * project): PUT the full discussion object obtained from `get`, edited.
22
+ */
23
+ async update(objectType, objectId, discussionId, discussion, projectKey) {
24
+ return this.client.put(this.discussionPath(objectType, objectId, discussionId, projectKey), discussion);
25
+ }
19
26
  objectDiscussionsPath(objectType, objectId, projectKey) {
20
27
  const pk = this.enc(projectKey);
21
28
  const encodedType = encodeURIComponent(objectType);
@@ -0,0 +1,25 @@
1
+ import type { DataikuClient } from "../client.js";
2
+ import { type DssTask } from "./futures.js";
3
+ /** Documentation template: the DSS default, an uploaded docx, or a docx in a managed folder. */
4
+ export type DocumentationTemplate = {
5
+ kind: "default";
6
+ } | {
7
+ kind: "file";
8
+ filePath: string;
9
+ } | {
10
+ kind: "folder";
11
+ folderId: string;
12
+ path: string;
13
+ };
14
+ /** The three generation endpoints of one documented object (flow, lab model, saved-model version). */
15
+ export interface DocumentationRoutes {
16
+ default: string;
17
+ file: string;
18
+ folder: string;
19
+ }
20
+ /**
21
+ * Start a DSS documentation export (docx): POST the default route, upload the
22
+ * template as multipart `file`, or POST the folder route with `folderId` and
23
+ * `path`. The finished task's result carries the `exportId` to download.
24
+ */
25
+ export declare function startDocumentation(client: DataikuClient, routes: DocumentationRoutes, template: DocumentationTemplate, operation: string): Promise<DssTask>;
@@ -0,0 +1,16 @@
1
+ import { dssTask, } from "./futures.js";
2
+ /**
3
+ * Start a DSS documentation export (docx): POST the default route, upload the
4
+ * template as multipart `file`, or POST the folder route with `folderId` and
5
+ * `path`. The finished task's result carries the `exportId` to download.
6
+ */
7
+ export async function startDocumentation(client, routes, template, operation) {
8
+ if (template.kind === "file") {
9
+ return dssTask(await client.uploadJson(routes.file, template.filePath), operation);
10
+ }
11
+ if (template.kind === "folder") {
12
+ const query = new URLSearchParams({ folderId: template.folderId, path: template.path, });
13
+ return dssTask(await client.post(`${routes.folder}?${query}`), operation);
14
+ }
15
+ return dssTask(await client.post(routes.default), operation);
16
+ }
@@ -0,0 +1,48 @@
1
+ import type { FutureWaitResult } from "../schemas.js";
2
+ import { BaseResource } from "./base.js";
3
+ import { type DocumentationTemplate } from "./documentation.js";
4
+ import { type DssTask, type FutureWaitOptions } from "./futures.js";
5
+ /** Schema propagation options (dataikuapi `DSSSchemaPropagationRunBuilder` settings). */
6
+ export interface SchemaPropagationOptions {
7
+ projectKey?: string;
8
+ /** Rebuild datasets when propagation needs their data (DSS default: true). */
9
+ autoRebuild?: boolean;
10
+ /** Recipes where propagation stops (dataikuapi `stop_at`). */
11
+ excludedRecipes?: string[];
12
+ /** Recipes always considered OK (dataikuapi `mark_recipe_as_ok`). */
13
+ markAsOkRecipes?: string[];
14
+ /** Partitioned flows: default partition value per dimension when rebuilding. */
15
+ defaultPartitionValuesByDimension?: Record<string, string>;
16
+ /** Partitioned flows: partition per computable, keyed `PROJECTKEY.id`. */
17
+ partitionsByComputable?: Record<string, string>;
18
+ /** Per-recipe-type and per-recipe update options, as dataikuapi sends them. */
19
+ recipeUpdateOptions?: {
20
+ byType?: Record<string, unknown>;
21
+ byName?: Record<string, unknown>;
22
+ };
23
+ }
24
+ /**
25
+ * Flow-wide tools: schema propagation from a dataset through every downstream
26
+ * recipe, and flow documentation export. Zones and the flow graph live on
27
+ * `flowZones`; per-recipe schema updates on `recipes.updateSchema`.
28
+ */
29
+ export declare class FlowResource extends BaseResource {
30
+ private flowPath;
31
+ /**
32
+ * Start propagating `datasetName`'s schema through the flow
33
+ * (POST /flow/tools/propagate-schema/, dataikuapi
34
+ * `new_schema_propagation(...).start()`). Returns the DSS future.
35
+ */
36
+ propagateSchema(datasetName: string, opts?: SchemaPropagationOptions): Promise<DssTask>;
37
+ propagateSchemaAndWait(datasetName: string, opts?: SchemaPropagationOptions & FutureWaitOptions): Promise<FutureWaitResult | DssTask>;
38
+ /**
39
+ * Start generating the flow documentation (docx) from the DSS default
40
+ * template, an uploaded docx template, or a docx in a managed folder.
41
+ * The finished future's result carries the `exportId` to download.
42
+ */
43
+ generateDocumentation(template?: DocumentationTemplate, projectKey?: string): Promise<DssTask>;
44
+ /** GET a generated flow documentation (docx) as a binary stream. */
45
+ downloadDocumentation(exportId: string, projectKey?: string): Promise<Response>;
46
+ }
47
+ /** Request body of POST /flow/tools/propagate-schema/ (pure; shared with `--plan`). */
48
+ export declare function schemaPropagationBody(projectKey: string, datasetName: string, opts: SchemaPropagationOptions): Record<string, unknown>;
@@ -0,0 +1,65 @@
1
+ import { ClientValidationError, } from "../errors.js";
2
+ import { BaseResource, } from "./base.js";
3
+ import { startDocumentation, } from "./documentation.js";
4
+ import { dssTask, } from "./futures.js";
5
+ /**
6
+ * Flow-wide tools: schema propagation from a dataset through every downstream
7
+ * recipe, and flow documentation export. Zones and the flow graph live on
8
+ * `flowZones`; per-recipe schema updates on `recipes.updateSchema`.
9
+ */
10
+ export class FlowResource extends BaseResource {
11
+ flowPath(projectKey) {
12
+ return `/public/api/projects/${this.enc(projectKey)}/flow`;
13
+ }
14
+ /**
15
+ * Start propagating `datasetName`'s schema through the flow
16
+ * (POST /flow/tools/propagate-schema/, dataikuapi
17
+ * `new_schema_propagation(...).start()`). Returns the DSS future.
18
+ */
19
+ async propagateSchema(datasetName, opts = {}) {
20
+ const name = datasetName.trim();
21
+ if (name.length === 0) {
22
+ throw new ClientValidationError("propagateSchema requires a dataset name.", "validation_failed");
23
+ }
24
+ const pk = this.resolveProjectKey(opts.projectKey);
25
+ const raw = await this.client.post(`${this.flowPath(pk)}/tools/propagate-schema/`, schemaPropagationBody(pk, name, opts));
26
+ return dssTask(raw, "Schema propagation");
27
+ }
28
+ async propagateSchemaAndWait(datasetName, opts = {}) {
29
+ return this.client.futures.waitTask(await this.propagateSchema(datasetName, opts), opts);
30
+ }
31
+ /**
32
+ * Start generating the flow documentation (docx) from the DSS default
33
+ * template, an uploaded docx template, or a docx in a managed folder.
34
+ * The finished future's result carries the `exportId` to download.
35
+ */
36
+ async generateDocumentation(template = { kind: "default", }, projectKey) {
37
+ const base = `${this.flowPath(projectKey)}/documentation`;
38
+ return startDocumentation(this.client, {
39
+ default: `${base}/generate`,
40
+ file: `${base}/generate-with-template`,
41
+ folder: `${base}/generate-with-template-in-folder`,
42
+ }, template, "Flow documentation generation");
43
+ }
44
+ /** GET a generated flow documentation (docx) as a binary stream. */
45
+ async downloadDocumentation(exportId, projectKey) {
46
+ return this.client.stream(`${this.flowPath(projectKey)}/documentation/generated/${encodeURIComponent(exportId)}`);
47
+ }
48
+ }
49
+ /** Request body of POST /flow/tools/propagate-schema/ (pure; shared with `--plan`). */
50
+ export function schemaPropagationBody(projectKey, datasetName, opts) {
51
+ return {
52
+ options: {
53
+ recipeUpdateOptions: {
54
+ byType: opts.recipeUpdateOptions?.byType ?? {},
55
+ byName: opts.recipeUpdateOptions?.byName ?? {},
56
+ },
57
+ defaultPartitionValuesByDimension: opts.defaultPartitionValuesByDimension ?? {},
58
+ partitionsByComputable: opts.partitionsByComputable ?? {},
59
+ excludedRecipes: opts.excludedRecipes ?? [],
60
+ markAsOkRecipes: opts.markAsOkRecipes ?? [],
61
+ autoRebuild: opts.autoRebuild ?? true,
62
+ },
63
+ sources: [{ projectKey, id: datasetName, },],
64
+ };
65
+ }
@@ -4,7 +4,30 @@ export interface FutureWaitOptions {
4
4
  pollIntervalMs?: number;
5
5
  timeoutMs?: number;
6
6
  }
7
+ /**
8
+ * A DSS long task as started (dataikuapi `DSSFuture.from_resp`): `jobId`
9
+ * addresses a running task in `futures.wait`; a task DSS finished inline comes
10
+ * back without one, as its final state (`hasResult`, `result`).
11
+ */
12
+ export interface DssTask {
13
+ jobId?: string;
14
+ hasResult?: boolean;
15
+ result?: unknown;
16
+ [key: string]: unknown;
17
+ }
18
+ /** Validate a task-start response: an object whose `jobId`, when present, is a string. */
19
+ export declare function dssTask(raw: unknown, operation: string): DssTask;
7
20
  export declare class FuturesResource extends BaseResource {
21
+ /**
22
+ * Long tasks in progress (GET /futures/): the caller's own by default,
23
+ * every user's with `allUsers`, running scenarios too with `withScenarios`.
24
+ */
25
+ list(opts?: {
26
+ allUsers?: boolean;
27
+ withScenarios?: boolean;
28
+ }): Promise<FutureState[]>;
29
+ /** Wait for a started task; a task DSS already finished inline is returned as is. */
30
+ waitTask(task: DssTask, opts?: FutureWaitOptions): Promise<FutureWaitResult | DssTask>;
8
31
  get(futureId: string, opts?: {
9
32
  timeoutMs?: number;
10
33
  noRetry?: boolean;
@@ -1,6 +1,18 @@
1
+ import { unexpectedResponseError, } from "../errors.js";
1
2
  import { FutureStateSchema, FutureWaitResultSchema, } from "../schemas.js";
2
3
  import { computeNextPollDelayMs, DEFAULT_POLL_INTERVAL_MS, DEFAULT_TIMEOUT_MS, isRequestDeadlineError, } from "../utils/polling.js";
3
4
  import { BaseResource, } from "./base.js";
5
+ /** Validate a task-start response: an object whose `jobId`, when present, is a string. */
6
+ export function dssTask(raw, operation) {
7
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
8
+ throw unexpectedResponseError(`${operation} returned no task state.`);
9
+ }
10
+ const state = { ...raw, };
11
+ if (state.jobId !== undefined && typeof state.jobId !== "string") {
12
+ throw unexpectedResponseError(`${operation} returned a non-string jobId.`);
13
+ }
14
+ return state;
15
+ }
4
16
  function sleep(ms) {
5
17
  return new Promise((resolve) => setTimeout(resolve, ms));
6
18
  }
@@ -22,6 +34,26 @@ function waitState(state) {
22
34
  return "RUNNING";
23
35
  }
24
36
  export class FuturesResource extends BaseResource {
37
+ /**
38
+ * Long tasks in progress (GET /futures/): the caller's own by default,
39
+ * every user's with `allUsers`, running scenarios too with `withScenarios`.
40
+ */
41
+ async list(opts = {}) {
42
+ const query = new URLSearchParams({
43
+ allUsers: String(opts.allUsers === true),
44
+ withScenarios: String(opts.withScenarios === true),
45
+ });
46
+ return this.client.get(`/public/api/futures/?${query}`);
47
+ }
48
+ /** Wait for a started task; a task DSS already finished inline is returned as is. */
49
+ async waitTask(task, opts = {}) {
50
+ if (task.jobId === undefined)
51
+ return task;
52
+ return this.wait(task.jobId, {
53
+ pollIntervalMs: opts.pollIntervalMs,
54
+ timeoutMs: opts.timeoutMs,
55
+ });
56
+ }
25
57
  async get(futureId, opts = {}) {
26
58
  return this.state(futureId, {
27
59
  peek: false,
@@ -73,6 +73,36 @@ function jobLogLines(log) {
73
73
  */
74
74
  const LEGACY_STDOUT_RECORD = /^\s*stdout:/i;
75
75
  const LEGACY_STDERR_RECORD = /^\s*stderr:/i;
76
+ /**
77
+ * A DSS backend log record starts with a timestamped header line
78
+ * (`[YYYY/MM/DD-HH:MM:SS.mmm]`); every following non-timestamped line is a
79
+ * continuation of that record (exception chain, JVM frames, ...).
80
+ */
81
+ const DSS_RECORD_HEADER = /^\[\d{4}\/\d{2}\/\d{2}-\d{2}:\d{2}:\d{2}\.\d{3}\]/;
82
+ const ERROR_KEYWORDS = /\b(error|failed|failure|exception|traceback)\b/i;
83
+ /**
84
+ * Exception class names ending in the `Exception`/`Error` suffix (e.g.
85
+ * `java.lang.ClassCastException`, `ValueError`): no word boundary precedes the
86
+ * suffix inside a compound name, so the keyword regex alone misses them.
87
+ */
88
+ const ERROR_CLASS_NAME = /\b\w*(?:Exception|Error)\b/;
89
+ const PY_TRACEBACK_HEAD = /^\s*Traceback \(most recent call last\):$/;
90
+ const PY_FILE_FRAME = /^\s*File "(?:[^"\\]|\\.)*", line \d+/;
91
+ const JVM_STACK_FRAME = /^\s+at\s/;
92
+ const JVM_MORE_FRAMES = /^\s*\.\.\. \d+ more\b/;
93
+ /**
94
+ * Message payload of a DSS record line: timestamp header and the
95
+ * `[thread] [LEVEL] [logger] -` prefix stripped, so continuation logic can be
96
+ * shared between timestamped records and raw log lines.
97
+ */
98
+ function dssRecordMessage(line) {
99
+ return line
100
+ .replace(/^\[\d{4}\/\d{2}\/\d{2}-\d{2}:\d{2}:\d{2}\.\d{3}\]\s?/, "")
101
+ .replace(/^\[[^\]]*\] \[[^\]]*\] \[[^\]]*\]\s+-\s?/, "");
102
+ }
103
+ function isErrorContent(line) {
104
+ return ERROR_KEYWORDS.test(line) || ERROR_CLASS_NAME.test(line) || PY_FILE_FRAME.test(line);
105
+ }
76
106
  function lineMatchesLogFilter(line, filter) {
77
107
  const normalized = line.toLowerCase();
78
108
  switch (filter) {
@@ -82,17 +112,87 @@ function lineMatchesLogFilter(line, filter) {
82
112
  case "stderr":
83
113
  return normalized.includes("[null-err-") || LEGACY_STDERR_RECORD.test(line);
84
114
  case "errors":
85
- return /\b(error|failed|failure|exception|traceback)\b/i.test(line);
115
+ return isErrorContent(line);
86
116
  case "user":
87
117
  return !/^\d{4}[-/]\d{2}[-/]\d{2}/.test(line)
88
118
  && !normalized.includes("backend-log")
89
119
  && !normalized.includes("debug");
90
120
  }
91
121
  }
122
+ /**
123
+ * Errors filter over DSS records. A record's exception spans lines: the
124
+ * timestamped header carries the level/message, untimestamped continuations
125
+ * carry the exception chain. A record whose header matches the error keywords
126
+ * keeps its cause-bearing continuation lines (exception headers like
127
+ * `pkg.FooException: msg`, `Caused by:` chains, Python `Traceback` essentials)
128
+ * and drops bare JVM stack frames. A Python traceback body is tracked as state,
129
+ * so `File "..."` frames and the user-code source line survive even though they
130
+ * carry no error keyword — including when DSS re-logs each child-stderr line as
131
+ * its own timestamped record.
132
+ */
133
+ function filterJobLogToErrors(lines) {
134
+ const kept = [];
135
+ let recordMatches = false;
136
+ let pyTraceback = false;
137
+ for (const line of lines) {
138
+ const isHeader = DSS_RECORD_HEADER.test(line);
139
+ const message = dssRecordMessage(line);
140
+ if (pyTraceback) {
141
+ if (JVM_STACK_FRAME.test(line) || JVM_MORE_FRAMES.test(line)) {
142
+ if (isHeader)
143
+ recordMatches = false;
144
+ continue;
145
+ }
146
+ if (/^\s+\S/.test(message)) {
147
+ kept.push(line); // `File "..."` frame or user-code source line
148
+ if (isHeader)
149
+ recordMatches = false;
150
+ continue;
151
+ }
152
+ if (ERROR_CLASS_NAME.test(message)) {
153
+ kept.push(line); // terminating exception line
154
+ pyTraceback = false;
155
+ if (isHeader)
156
+ recordMatches = false;
157
+ continue;
158
+ }
159
+ pyTraceback = false;
160
+ }
161
+ if (isHeader) {
162
+ recordMatches = isErrorContent(line) || PY_FILE_FRAME.test(message);
163
+ if (recordMatches && (PY_TRACEBACK_HEAD.test(message) || PY_FILE_FRAME.test(message))) {
164
+ pyTraceback = true;
165
+ }
166
+ if (recordMatches)
167
+ kept.push(line);
168
+ continue;
169
+ }
170
+ if (PY_TRACEBACK_HEAD.test(message)) {
171
+ kept.push(line);
172
+ pyTraceback = true;
173
+ continue;
174
+ }
175
+ if (recordMatches
176
+ && !JVM_STACK_FRAME.test(line)
177
+ && !JVM_MORE_FRAMES.test(line)
178
+ && isErrorContent(line))
179
+ kept.push(line);
180
+ }
181
+ return kept.join("\n");
182
+ }
183
+ /**
184
+ * Filter a job log. DSS logs (any timestamped record) use the record-aware
185
+ * errors pass; untimestamped (non-DSS) logs keep the per-line behaviour, since
186
+ * no record structure can be inferred.
187
+ */
92
188
  function filterJobLog(log, filter) {
93
189
  if (!filter)
94
190
  return log;
95
- return jobLogLines(log).filter((line) => lineMatchesLogFilter(line, filter)).join("\n");
191
+ const lines = jobLogLines(log);
192
+ if (filter !== "errors" || !lines.some((line) => DSS_RECORD_HEADER.test(line))) {
193
+ return lines.filter((line) => lineMatchesLogFilter(line, filter)).join("\n");
194
+ }
195
+ return filterJobLogToErrors(lines);
96
196
  }
97
197
  function limitJobLog(log, maxLines) {
98
198
  if (!log)
@@ -37,6 +37,16 @@ export declare class MetricsResource extends BaseResource {
37
37
  getDatasetMetrics(datasetName: string, projectKey?: string): Promise<MetricValues>;
38
38
  /** Compute the dataset metrics configured in DSS for the default partition. */
39
39
  computeDatasetMetrics(datasetName: string, projectKey?: string): Promise<MetricComputationReport>;
40
+ /**
41
+ * Run the dataset's checks (POST /actions/runChecks/{?partitions}). Without
42
+ * `checks` DSS runs the checks configured on the dataset; with `checks` it
43
+ * runs those definitions instead (body `{"checks": [...]}`).
44
+ */
45
+ runDatasetChecks(datasetName: string, opts?: {
46
+ projectKey?: string;
47
+ partitions?: string;
48
+ checks?: unknown[];
49
+ }): Promise<MetricComputationReport>;
40
50
  /** Get the last metric values for a managed folder. */
41
51
  getFolderMetrics(folderId: string, projectKey?: string): Promise<MetricValues>;
42
52
  /** Get the metric history for a dataset's global partition. */