dataiku-sdk 3.2.0 → 3.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.
Files changed (120) hide show
  1. package/README.md +149 -46
  2. package/dist/build-metadata.json +1 -1
  3. package/dist/packages/types/src/index.d.ts +102 -0
  4. package/dist/packages/types/src/index.js +76 -0
  5. package/dist/src/cli/commands/app.js +57 -15
  6. package/dist/src/cli/commands/bundle.js +1 -1
  7. package/dist/src/cli/commands/code-env.js +13 -7
  8. package/dist/src/cli/commands/connection.d.ts +9 -0
  9. package/dist/src/cli/commands/connection.js +267 -1
  10. package/dist/src/cli/commands/data-collection.d.ts +2 -0
  11. package/dist/src/cli/commands/data-collection.js +164 -0
  12. package/dist/src/cli/commands/dataset.js +161 -32
  13. package/dist/src/cli/commands/future.js +2 -2
  14. package/dist/src/cli/commands/group.d.ts +2 -0
  15. package/dist/src/cli/commands/group.js +109 -0
  16. package/dist/src/cli/commands/index.js +151 -78
  17. package/dist/src/cli/commands/knowledge-bank.d.ts +2 -0
  18. package/dist/src/cli/commands/knowledge-bank.js +80 -0
  19. package/dist/src/cli/commands/llm.d.ts +2 -0
  20. package/dist/src/cli/commands/llm.js +93 -0
  21. package/dist/src/cli/commands/macro.d.ts +2 -0
  22. package/dist/src/cli/commands/macro.js +221 -0
  23. package/dist/src/cli/commands/ml-task.js +18 -6
  24. package/dist/src/cli/commands/notebook.js +6 -2
  25. package/dist/src/cli/commands/plugin.d.ts +2 -0
  26. package/dist/src/cli/commands/plugin.js +799 -0
  27. package/dist/src/cli/commands/project-folder.d.ts +2 -0
  28. package/dist/src/cli/commands/project-folder.js +197 -0
  29. package/dist/src/cli/commands/project-git.js +14 -54
  30. package/dist/src/cli/commands/project.js +52 -6
  31. package/dist/src/cli/commands/recipe.js +36 -1
  32. package/dist/src/cli/commands/saved-model.d.ts +5 -0
  33. package/dist/src/cli/commands/saved-model.js +388 -1
  34. package/dist/src/cli/commands/scenario.js +138 -1
  35. package/dist/src/cli/commands/user.d.ts +8 -0
  36. package/dist/src/cli/commands/user.js +296 -0
  37. package/dist/src/cli/contract.d.ts +1 -2
  38. package/dist/src/cli/contract.js +1089 -103
  39. package/dist/src/cli/flags.js +30 -1
  40. package/dist/src/cli/helpers/app-successor.d.ts +19 -4
  41. package/dist/src/cli/helpers/app-successor.js +90 -30
  42. package/dist/src/cli/helpers/cleanup.js +3 -1
  43. package/dist/src/cli/helpers/flow-zone.js +1 -2
  44. package/dist/src/cli/output.d.ts +1 -1
  45. package/dist/src/cli/output.js +23 -10
  46. package/dist/src/cli/usage.d.ts +1 -0
  47. package/dist/src/cli/usage.js +47 -1
  48. package/dist/src/cli.js +147 -41
  49. package/dist/src/client.d.ts +83 -5
  50. package/dist/src/client.js +185 -29
  51. package/dist/src/errors.d.ts +3 -0
  52. package/dist/src/errors.js +2 -0
  53. package/dist/src/index.d.ts +16 -7
  54. package/dist/src/index.js +11 -2
  55. package/dist/src/resources/analyses.js +1 -8
  56. package/dist/src/resources/applications.d.ts +8 -6
  57. package/dist/src/resources/applications.js +21 -21
  58. package/dist/src/resources/base.d.ts +4 -0
  59. package/dist/src/resources/base.js +15 -0
  60. package/dist/src/resources/bundles.d.ts +9 -4
  61. package/dist/src/resources/bundles.js +10 -5
  62. package/dist/src/resources/connections.d.ts +78 -1
  63. package/dist/src/resources/connections.js +132 -1
  64. package/dist/src/resources/data-collections.d.ts +118 -0
  65. package/dist/src/resources/data-collections.js +49 -0
  66. package/dist/src/resources/datasets.d.ts +69 -0
  67. package/dist/src/resources/datasets.js +99 -1
  68. package/dist/src/resources/futures.d.ts +4 -1
  69. package/dist/src/resources/futures.js +38 -6
  70. package/dist/src/resources/groups.d.ts +47 -0
  71. package/dist/src/resources/groups.js +56 -0
  72. package/dist/src/resources/jobs.d.ts +8 -13
  73. package/dist/src/resources/jobs.js +41 -16
  74. package/dist/src/resources/knowledge-banks.d.ts +58 -0
  75. package/dist/src/resources/knowledge-banks.js +61 -0
  76. package/dist/src/resources/llms.d.ts +92 -0
  77. package/dist/src/resources/llms.js +62 -0
  78. package/dist/src/resources/macros.d.ts +121 -0
  79. package/dist/src/resources/macros.js +231 -0
  80. package/dist/src/resources/ml-tasks.d.ts +37 -2
  81. package/dist/src/resources/ml-tasks.js +100 -20
  82. package/dist/src/resources/model-evaluation-stores.js +1 -8
  83. package/dist/src/resources/notebooks.d.ts +43 -7
  84. package/dist/src/resources/notebooks.js +53 -13
  85. package/dist/src/resources/plugins.d.ts +265 -0
  86. package/dist/src/resources/plugins.js +458 -0
  87. package/dist/src/resources/project-folders.d.ts +71 -0
  88. package/dist/src/resources/project-folders.js +57 -0
  89. package/dist/src/resources/project-git.d.ts +6 -0
  90. package/dist/src/resources/project-git.js +69 -42
  91. package/dist/src/resources/project-library.d.ts +0 -1
  92. package/dist/src/resources/project-library.js +1 -17
  93. package/dist/src/resources/projects.d.ts +19 -1
  94. package/dist/src/resources/projects.js +27 -1
  95. package/dist/src/resources/recipes.d.ts +15 -1
  96. package/dist/src/resources/recipes.js +73 -6
  97. package/dist/src/resources/saved-models.d.ts +134 -1
  98. package/dist/src/resources/saved-models.js +214 -6
  99. package/dist/src/resources/scenarios.d.ts +105 -0
  100. package/dist/src/resources/scenarios.js +232 -106
  101. package/dist/src/resources/sql.js +5 -1
  102. package/dist/src/resources/users.d.ts +129 -0
  103. package/dist/src/resources/users.js +128 -0
  104. package/dist/src/resources/webapps.d.ts +15 -2
  105. package/dist/src/resources/webapps.js +18 -4
  106. package/dist/src/schemas.d.ts +2 -2
  107. package/dist/src/schemas.js +1 -1
  108. package/dist/src/utils/deep-merge.d.ts +1 -1
  109. package/dist/src/utils/polling.d.ts +20 -0
  110. package/dist/src/utils/polling.js +27 -0
  111. package/dist/src/utils/response-file.js +1 -1
  112. package/dist/src/utils/secret-sanitize.d.ts +54 -0
  113. package/dist/src/utils/secret-sanitize.js +101 -0
  114. package/package.json +6 -5
  115. package/packages/types/dist/index.d.ts +102 -0
  116. package/packages/types/dist/index.js +76 -0
  117. package/plugin.json +1 -1
  118. package/skills/dataiku-dss/SKILL.md +7 -5
  119. package/skills/dataiku-dss/references/app-releases.md +8 -7
  120. package/skills/dataiku-dss/references/discovery.md +4 -4
@@ -1,5 +1,7 @@
1
- import { ClientValidationError, DataikuError, } from "../errors.js";
1
+ import { ClientValidationError, } from "../errors.js";
2
2
  import { ProjectGitActionResultSchema, ProjectGitDiffResultSchema, ProjectGitFutureResponseSchema, ProjectGitFutureStateSchema, ProjectGitLibrariesSchema, ProjectGitLogResultSchema, ProjectGitRemoteSchema, ProjectGitStatusSchema, ProjectGitTagsSchema, } from "../schemas.js";
3
+ import { computeNextPollDelayMs, isRequestDeadlineError, } from "../utils/polling.js";
4
+ import { sanitizeErrorSecrets, } from "../utils/secret-sanitize.js";
3
5
  import { BaseResource, } from "./base.js";
4
6
  /**
5
7
  * The per-project Git routes exist only under the dataikuapi base, not under
@@ -89,28 +91,10 @@ function validatedRepository(repository, field) {
89
91
  return candidate;
90
92
  }
91
93
  /** Replace every exact occurrence of a secret so echoed errors cannot leak it. */
92
- function redactSecrets(text, secrets) {
93
- let result = text;
94
- for (const secret of secrets) {
95
- if (secret !== undefined && secret !== "") {
96
- result = result.split(secret).join("[redacted]");
97
- }
98
- }
99
- return result;
100
- }
101
- /**
102
- * Strip a password that a server misconfiguration may have echoed from an
103
- * error response. `DataikuError.message` was built at construction, so the
104
- * raw body, the message, and the stack all need scrubbing.
105
- */
106
94
  function scrubErrorSecrets(error, secrets) {
107
- if (!(error instanceof DataikuError) || secrets.length === 0)
95
+ if (secrets.length === 0)
108
96
  return;
109
- error.body = redactSecrets(error.body, secrets);
110
- error.message = redactSecrets(error.message, secrets);
111
- if (typeof error.stack === "string") {
112
- error.stack = redactSecrets(error.stack, secrets);
113
- }
97
+ sanitizeErrorSecrets(error, { sensitiveKeys: {}, secrets, });
114
98
  }
115
99
  function describeFutureFailure(error) {
116
100
  if (error === undefined || error === null)
@@ -129,6 +113,31 @@ function describeFutureFailure(error) {
129
113
  return String(error);
130
114
  }
131
115
  /* ------------------------------------------------------------------ */
116
+ /**
117
+ * DSS answers the lib-git-refs listing as an object map of attached external
118
+ * libraries keyed by target path alongside code-library metadata (observed
119
+ * raw on DSS 15: `{gitReferences, pythonPath, rsrcPath,
120
+ * importLibrariesFromProjects}`), while the official Python client and this
121
+ * resource's typed contract expose a plain array. Normalize the map into
122
+ * entries, preserving each key as the library's `localTargetPath`; a bare
123
+ * array passes through untouched.
124
+ */
125
+ function normalizeLibraryList(raw) {
126
+ if (Array.isArray(raw) || raw === undefined || raw === null)
127
+ return raw ?? [];
128
+ if (typeof raw !== "object")
129
+ return raw;
130
+ const record = raw;
131
+ const refs = record["gitReferences"];
132
+ if (refs === undefined || refs === null || typeof refs !== "object" || Array.isArray(refs)) {
133
+ return raw;
134
+ }
135
+ return Object.entries(refs).map(([localTargetPath, spec,]) => ({
136
+ localTargetPath,
137
+ ...(typeof spec === "object" && spec !== null ? spec : {}),
138
+ }));
139
+ }
140
+ /* ------------------------------------------------------------------ */
132
141
  /* Resource */
133
142
  /* ------------------------------------------------------------------ */
134
143
  export class ProjectGitResource extends BaseResource {
@@ -287,7 +296,7 @@ export class ProjectGitResource extends BaseResource {
287
296
  /* ---- external libraries (async futures) ---- */
288
297
  async listLibraries(projectKey) {
289
298
  const raw = await this.request("GET", `${this.libraryPath(projectKey)}/`);
290
- return this.client.safeParse(ProjectGitLibrariesSchema, raw ?? [], "projectGit.listLibraries");
299
+ return this.client.safeParse(ProjectGitLibrariesSchema, normalizeLibraryList(raw), "projectGit.listLibraries");
291
300
  }
292
301
  /** Attach an external Git library. Runs as a DSS future. */
293
302
  async addLibrary(projectKey, options) {
@@ -355,7 +364,7 @@ export class ProjectGitResource extends BaseResource {
355
364
  * what polling loops want; the default consumes it like the Python client.
356
365
  */
357
366
  async getFutureState(jobId, options = {}) {
358
- const raw = await this.request("GET", `/futures/${encodeURIComponent(jobId)}${this.query({ peek: options.peek === true, })}`);
367
+ const raw = await this.request("GET", `/futures/${encodeURIComponent(jobId)}${this.query({ peek: options.peek === true, })}`, undefined, [], { timeoutMs: options.timeoutMs, });
359
368
  return this.client.safeParse(ProjectGitFutureStateSchema, raw ?? {}, "projectGit.getFutureState");
360
369
  }
361
370
  /** Abort a running future. */
@@ -376,10 +385,36 @@ export class ProjectGitResource extends BaseResource {
376
385
  if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
377
386
  throw new ClientValidationError("timeoutMs must be a finite non-negative number.");
378
387
  }
388
+ // Adaptive backoff applies only when the caller did not pin an exact
389
+ // interval; an explicit pollIntervalMs is an exact contract.
390
+ const adaptiveEnabled = options.pollIntervalMs === undefined;
391
+ let pollCount = 0;
379
392
  const startedAt = Date.now();
380
393
  while (true) {
381
394
  // Unlike peek state, the ordinary state includes the completed result.
382
- const state = await this.getFutureState(jobId, { peek: false, });
395
+ // One state observation always happens, even when the budget is
396
+ // already spent (the poll precedes the deadline check); only the
397
+ // requests issued while budget remains are bounded by the remaining
398
+ // time, so a stalled endpoint can never defeat the caller's deadline.
399
+ const remainingMs = timeoutMs - (Date.now() - startedAt);
400
+ pollCount += 1;
401
+ let state;
402
+ try {
403
+ state = await this.getFutureState(jobId, remainingMs > 0
404
+ ? {
405
+ peek: false,
406
+ timeoutMs: remainingMs,
407
+ }
408
+ : { peek: false, });
409
+ }
410
+ catch (error) {
411
+ // The poll budget ran out mid-request: report the documented
412
+ // timeout failure instead of letting the transport deadline
413
+ // error escape.
414
+ if (!isRequestDeadlineError(error, startedAt + timeoutMs))
415
+ throw error;
416
+ throw new Error(`Timed out after ${String(Date.now() - startedAt)}ms waiting for Dataiku future ${jobId}`, { cause: error, });
417
+ }
383
418
  const failure = describeFutureFailure(state.error);
384
419
  if (failure !== undefined) {
385
420
  throw new Error(`Dataiku future ${jobId} failed: ${failure}`);
@@ -395,13 +430,18 @@ export class ProjectGitResource extends BaseResource {
395
430
  if (state.alive === false) {
396
431
  throw new Error(`Dataiku future ${jobId} ended without producing a result`);
397
432
  }
398
- // Only the remaining budget is slept, so a caller's deadline is never
399
- // overshot by a whole poll interval.
400
433
  const elapsedMs = Date.now() - startedAt;
401
434
  if (elapsedMs >= timeoutMs) {
402
435
  throw new Error(`Timed out after ${String(elapsedMs)}ms waiting for Dataiku future ${jobId}`);
403
436
  }
404
- await sleep(Math.min(pollIntervalMs, timeoutMs - elapsedMs));
437
+ const nextDelayMs = computeNextPollDelayMs({
438
+ pollCount,
439
+ baseIntervalMs: pollIntervalMs,
440
+ adaptiveEnabled,
441
+ });
442
+ // Only the remaining budget is slept, so a caller's deadline is never
443
+ // overshot by a whole interval.
444
+ await sleep(Math.min(nextDelayMs, timeoutMs - elapsedMs));
405
445
  }
406
446
  }
407
447
  /* ---- private ---- */
@@ -424,22 +464,9 @@ export class ProjectGitResource extends BaseResource {
424
464
  const queryString = search.toString();
425
465
  return queryString === "" ? "" : `?${queryString}`;
426
466
  }
427
- async request(method, path, body, secrets = []) {
428
- const transport = this.client;
429
- const init = {
430
- method,
431
- headers: {
432
- // The Git routes are only reachable with dataikuapi-style Basic auth:
433
- // API key as the username, empty password.
434
- Authorization: `Basic ${Buffer.from(`${transport.apiKey}:`, "utf8").toString("base64")}`,
435
- Accept: "application/json",
436
- "Content-Type": "application/json",
437
- },
438
- };
439
- if (body !== undefined)
440
- init.body = JSON.stringify(body);
467
+ async request(method, path, body, secrets = [], opts = {}) {
441
468
  try {
442
- const res = await transport.fetchWithRetry(`${transport.baseUrl}${GIT_API_BASE}${path}`, init);
469
+ const res = await this.client.requestGit(method, `${GIT_API_BASE}${path}`, body, opts.timeoutMs === undefined ? undefined : Date.now() + opts.timeoutMs);
443
470
  const text = await res.text();
444
471
  if (text.trim() === "")
445
472
  return undefined;
@@ -147,7 +147,6 @@ export declare class ProjectLibraryResource extends BaseResource {
147
147
  /** Move a project code-library file or folder into another library folder. */
148
148
  move(path: string, destinationFolderPath: string, projectKey?: string): Promise<void>;
149
149
  private contentsPath;
150
- private postRawBody;
151
150
  private alreadyExistsError;
152
151
  private assertLibraryFileAbsent;
153
152
  }
@@ -403,7 +403,7 @@ export class ProjectLibraryResource extends BaseResource {
403
403
  }
404
404
  const bytes = typeof content === "string" ? Buffer.from(content, "utf8") : content;
405
405
  const sha256 = sha256Hex(bytes);
406
- await this.postRawBody(this.contentsPath(valid, projectKey), content);
406
+ await this.client.postRawBody(this.contentsPath(valid, projectKey), content);
407
407
  return {
408
408
  path: valid,
409
409
  bytes: bytes.length,
@@ -518,22 +518,6 @@ export class ProjectLibraryResource extends BaseResource {
518
518
  const projectKeyPart = this.enc(projectKey);
519
519
  return `/public/api/projects/${projectKeyPart}/libraries/contents/${encodeLibraryPath(path)}`;
520
520
  }
521
- async postRawBody(path, body) {
522
- // DSS library writes use dataikuapi's raw_body; route through the client's
523
- // transport so auth, retries, TLS options, and DSS error handling stay aligned.
524
- // BodyInit rejects plain Uint8Array views, so non-Buffer bytes are copied
525
- // into a Buffer (an ArrayBufferView BodyInit accepts).
526
- const payload = body instanceof Uint8Array && !(body instanceof Buffer)
527
- ? Buffer.from(body.buffer, body.byteOffset, body.byteLength)
528
- : body;
529
- const rawClient = this.client;
530
- const res = await rawClient.fetchWithRetry(`${rawClient.baseUrl}${path}`, {
531
- method: "POST",
532
- headers: rawClient.getAnyHeaders(),
533
- body: payload,
534
- });
535
- await res.text();
536
- }
537
521
  alreadyExistsError(path) {
538
522
  return new ClientValidationError(`Project library item "${path}" already exists.`, "validation_failed", "Refuse to overwrite an existing item; delete it first, or use the create-or-replace update to replace an existing file.", { path, });
539
523
  }
@@ -1,4 +1,4 @@
1
- import type { FlowMapOptions, ProjectDetails, ProjectMetadata, ProjectSummary } from "../schemas.js";
1
+ import type { FlowMapOptions, ProjectDetails, ProjectMetadata, ProjectSummary, ProjectTags } from "../schemas.js";
2
2
  import type { AnalyzedFlowMap, FlowMapRendering } from "../utils/flow-analysis.js";
3
3
  import { BaseResource } from "./base.js";
4
4
  interface FlowMapTruncationSummary {
@@ -124,6 +124,24 @@ export declare class ProjectsResource extends BaseResource {
124
124
  get(projectKey?: string): Promise<ProjectDetails>;
125
125
  /** Get metadata (tags, custom fields, checklists) for a project. */
126
126
  metadata(projectKey?: string): Promise<ProjectMetadata>;
127
+ /**
128
+ * Replace project metadata with the supplied full object.
129
+ *
130
+ * DSS PUT semantics replace the whole metadata object: pass a metadata
131
+ * object obtained from {@link metadata}, edit it, and send it back — fields
132
+ * absent from the payload are removed from the project.
133
+ */
134
+ setMetadata(projectKey: string | undefined, metadata: ProjectMetadata): Promise<void>;
135
+ /** Get project-level tags (tag name → {color?}). */
136
+ tags(projectKey?: string): Promise<ProjectTags>;
137
+ /**
138
+ * Replace project-level tags with the supplied full object.
139
+ *
140
+ * DSS PUT semantics replace the whole tags dictionary: pass a tags object
141
+ * obtained from {@link tags}, edit it, and send it back — tags absent from
142
+ * the payload are removed from the project.
143
+ */
144
+ setTags(projectKey: string | undefined, tags: ProjectTags): Promise<void>;
127
145
  /** Get the raw flow graph for a project. */
128
146
  flow(projectKey?: string): Promise<unknown>;
129
147
  /** Compute a stable fingerprint of flow nodes and edges, excluding visual metadata. */
@@ -1,6 +1,6 @@
1
1
  import { ClientValidationError, DataikuError, } from "../errors.js";
2
2
  import { writeResponseToFile, } from "../utils/response-file.js";
3
- import { ProjectDetailsSchema, ProjectMetadataSchema, ProjectSummaryArraySchema, } from "../schemas.js";
3
+ import { ProjectDetailsSchema, ProjectMetadataSchema, ProjectSummaryArraySchema, ProjectTagsSchema, } from "../schemas.js";
4
4
  import { analyzeFlowMap, flowTopologyFingerprint, renderFlowMap, } from "../utils/flow-analysis.js";
5
5
  import { normalizeFlowGraph, } from "../utils/flow-map.js";
6
6
  import { assertImportableProjectArchive, } from "../utils/project-archive.js";
@@ -302,6 +302,32 @@ export class ProjectsResource extends BaseResource {
302
302
  const raw = await this.client.get(`/public/api/projects/${enc}/metadata`);
303
303
  return this.client.safeParse(ProjectMetadataSchema, raw, "projects.metadata");
304
304
  }
305
+ /**
306
+ * Replace project metadata with the supplied full object.
307
+ *
308
+ * DSS PUT semantics replace the whole metadata object: pass a metadata
309
+ * object obtained from {@link metadata}, edit it, and send it back — fields
310
+ * absent from the payload are removed from the project.
311
+ */
312
+ async setMetadata(projectKey, metadata) {
313
+ await this.client.putVoid(`/public/api/projects/${this.enc(projectKey)}/metadata`, metadata);
314
+ }
315
+ /** Get project-level tags (tag name → {color?}). */
316
+ async tags(projectKey) {
317
+ const enc = this.enc(projectKey);
318
+ const raw = await this.client.get(`/public/api/projects/${enc}/tags`);
319
+ return this.client.safeParse(ProjectTagsSchema, raw, "projects.tags");
320
+ }
321
+ /**
322
+ * Replace project-level tags with the supplied full object.
323
+ *
324
+ * DSS PUT semantics replace the whole tags dictionary: pass a tags object
325
+ * obtained from {@link tags}, edit it, and send it back — tags absent from
326
+ * the payload are removed from the project.
327
+ */
328
+ async setTags(projectKey, tags) {
329
+ await this.client.putVoid(`/public/api/projects/${this.enc(projectKey)}/tags`, tags);
330
+ }
305
331
  /** Get the raw flow graph for a project. */
306
332
  async flow(projectKey) {
307
333
  const enc = this.enc(projectKey);
@@ -1,4 +1,4 @@
1
- import type { BuildMode, JobWaitResult, RecipeCreateOptions, RecipeCreateResult, RecipeSummary } from "../schemas.js";
1
+ import type { BuildMode, JobWaitResult, ProjectMetadata, RecipeCreateOptions, RecipeCreateResult, RecipeSummary } from "../schemas.js";
2
2
  import { BaseResource } from "./base.js";
3
3
  import type { JobBuildTarget, JobBuildTargetType, JobLogFilter, JobLogSummary } from "./jobs.js";
4
4
  export interface RecipeRunOutput extends JobBuildTarget {
@@ -68,6 +68,20 @@ export interface RecipeCloneResult {
68
68
  export declare class RecipesResource extends BaseResource {
69
69
  /** List all recipes in a project. */
70
70
  list(projectKey?: string): Promise<RecipeSummary[]>;
71
+ /** Get recipe metadata (label, tags, description, custom fields). */
72
+ metadata(recipeName: string, opts?: {
73
+ projectKey?: string;
74
+ }): Promise<ProjectMetadata>;
75
+ /**
76
+ * Replace recipe metadata with the supplied full object.
77
+ *
78
+ * DSS PUT semantics replace the whole metadata object: pass a metadata
79
+ * object obtained from {@link metadata}, edit it, and send it back — fields
80
+ * absent from the payload are removed from the recipe.
81
+ */
82
+ setMetadata(recipeName: string, metadata: ProjectMetadata, opts?: {
83
+ projectKey?: string;
84
+ }): Promise<void>;
71
85
  /**
72
86
  * Get a recipe definition (and optionally its payload).
73
87
  * Returns the raw API response shape: `{ recipe, payload }`.
@@ -1,7 +1,7 @@
1
1
  import { writeFile, } from "node:fs/promises";
2
2
  import { resolve, } from "node:path";
3
3
  import { DataikuError, } from "../errors.js";
4
- import { RecipeSummaryArraySchema, } from "../schemas.js";
4
+ import { ProjectMetadataSchema, RecipeSummaryArraySchema, } from "../schemas.js";
5
5
  import { deepMerge, } from "../utils/deep-merge.js";
6
6
  import { sanitizeFileName, } from "../utils/sanitize.js";
7
7
  import { BaseResource, } from "./base.js";
@@ -95,6 +95,8 @@ function normalizeRecipeOutputType(value) {
95
95
  return "DATASET";
96
96
  if (normalized === "MANAGED_FOLDER" || normalized === "FOLDER")
97
97
  return "MANAGED_FOLDER";
98
+ if (normalized === "MODEL_EVALUATION_STORE")
99
+ return "MODEL_EVALUATION_STORE";
98
100
  return undefined;
99
101
  }
100
102
  function recipeOutputItems(recipe) {
@@ -116,7 +118,12 @@ function recipeOutputItems(recipe) {
116
118
  if (seen.has(seenKey))
117
119
  continue;
118
120
  seen.add(seenKey);
119
- const type = normalizeRecipeOutputType(item?.type ?? item?.targetType ?? item?.objectType);
121
+ // The evaluation recipe's "evaluationStore" role targets a model
122
+ // evaluation store even when the item carries no explicit type
123
+ // (recipe.py EvaluationRecipeCreator.with_output_evaluation_store);
124
+ // the store is a valid job target (run object_type_map).
125
+ const type = normalizeRecipeOutputType(item?.type ?? item?.targetType ?? item?.objectType)
126
+ ?? (role === "evaluationStore" ? "MODEL_EVALUATION_STORE" : undefined);
120
127
  result.push({
121
128
  ref,
122
129
  role,
@@ -292,6 +299,25 @@ export class RecipesResource extends BaseResource {
292
299
  const raw = await this.client.get(`/public/api/projects/${enc}/recipes/`);
293
300
  return this.client.safeParse(RecipeSummaryArraySchema, raw, "recipes.list");
294
301
  }
302
+ /** Get recipe metadata (label, tags, description, custom fields). */
303
+ async metadata(recipeName, opts) {
304
+ const enc = this.enc(opts?.projectKey);
305
+ const rnEnc = encodeURIComponent(recipeName);
306
+ const raw = await this.client.get(`/public/api/projects/${enc}/recipes/${rnEnc}/metadata`);
307
+ return this.client.safeParse(ProjectMetadataSchema, raw, "recipes.metadata");
308
+ }
309
+ /**
310
+ * Replace recipe metadata with the supplied full object.
311
+ *
312
+ * DSS PUT semantics replace the whole metadata object: pass a metadata
313
+ * object obtained from {@link metadata}, edit it, and send it back — fields
314
+ * absent from the payload are removed from the recipe.
315
+ */
316
+ async setMetadata(recipeName, metadata, opts) {
317
+ const enc = this.enc(opts?.projectKey);
318
+ const rnEnc = encodeURIComponent(recipeName);
319
+ await this.client.putVoid(`/public/api/projects/${enc}/recipes/${rnEnc}/metadata`, metadata);
320
+ }
295
321
  /**
296
322
  * Get a recipe definition (and optionally its payload).
297
323
  * Returns the raw API response shape: `{ recipe, payload }`.
@@ -320,11 +346,13 @@ export class RecipesResource extends BaseResource {
320
346
  const { recipe, } = await this.get(recipeName, { projectKey: pk, });
321
347
  const inputItems = recipeInputItems(recipe);
322
348
  const outputItems = recipeOutputItems(recipe);
323
- const [datasets, folders,] = await Promise.all([
349
+ const [datasets, folders, stores,] = await Promise.all([
324
350
  this.client.datasets.list(pk),
325
351
  this.client.folders.list(pk),
352
+ this.client.modelEvaluationStores.list(pk),
326
353
  ]);
327
354
  const datasetNames = new Set(datasets.map((dataset) => dataset.name));
355
+ const storeIds = new Set(stores.map((store) => store.id));
328
356
  const folderIdByRef = new Map();
329
357
  for (const folder of folders) {
330
358
  folderIdByRef.set(folder.id, folder.id);
@@ -337,6 +365,15 @@ export class RecipesResource extends BaseResource {
337
365
  if (item.type === "DATASET") {
338
366
  return { ref: item.ref, role: item.role, type: "DATASET", exists: isDataset, id: item.ref, };
339
367
  }
368
+ if (item.type === "MODEL_EVALUATION_STORE") {
369
+ return {
370
+ ref: item.ref,
371
+ role: item.role,
372
+ type: "MODEL_EVALUATION_STORE",
373
+ exists: storeIds.has(item.ref),
374
+ id: item.ref,
375
+ };
376
+ }
340
377
  if (item.type === "MANAGED_FOLDER") {
341
378
  return {
342
379
  ref: item.ref,
@@ -420,6 +457,19 @@ export class RecipesResource extends BaseResource {
420
457
  partition: opts?.partition,
421
458
  };
422
459
  }
460
+ if (item.type === "MODEL_EVALUATION_STORE") {
461
+ // The store id is the target id; evaluation builds into the store
462
+ // exactly like the official client (recipe.run maps
463
+ // COMPUTABLE_MODEL_EVALUATION_STORE -> MODEL_EVALUATION_STORE).
464
+ return {
465
+ ref: item.ref,
466
+ role: item.role,
467
+ id: item.ref,
468
+ type: "MODEL_EVALUATION_STORE",
469
+ projectKey: pk,
470
+ partition: opts?.partition,
471
+ };
472
+ }
423
473
  const folderId = folderIdByRef.get(item.ref);
424
474
  if (item.type === "MANAGED_FOLDER") {
425
475
  return {
@@ -632,10 +682,22 @@ export class RecipesResource extends BaseResource {
632
682
  }
633
683
  }
634
684
  };
685
+ let finalRecipeName;
686
+ // DSS renames some recipe types server-side (e.g. prediction_scoring
687
+ // becomes "score_<inputDataset>") and documents the creation response as
688
+ // the final unique name: Body {"name": "recipe1"} ("Returns the final
689
+ // unique name of the recipe"). The response name is the contract —
690
+ // falling back to the requested name would mask a missing/invalid
691
+ // receipt, so a body without it is an error.
635
692
  try {
636
693
  if (rawConnection)
637
694
  await provisionOutputDatasets();
638
- await createRecipe();
695
+ const created = await createRecipe();
696
+ const receivedName = asString(created?.["name"]);
697
+ if (!receivedName) {
698
+ throw new Error('DSS create-recipe response did not include the final recipe name (documented body: {"name": ...}).');
699
+ }
700
+ finalRecipeName = receivedName;
639
701
  }
640
702
  catch (error) {
641
703
  if (!shouldRetryRecipeCreateWithOutputProvisioning(error)) {
@@ -643,7 +705,12 @@ export class RecipesResource extends BaseResource {
643
705
  }
644
706
  usedOutputProvisioningFallback = true;
645
707
  await provisionOutputDatasets();
646
- await createRecipe();
708
+ const retryCreated = await createRecipe();
709
+ const retryName = asString(retryCreated?.["name"]);
710
+ if (!retryName) {
711
+ throw new Error('DSS create-recipe response did not include the final recipe name (documented body: {"name": ...}).', { cause: error, });
712
+ }
713
+ finalRecipeName = retryName;
647
714
  }
648
715
  // For sync recipes (a 1:1 copy) DSS does not propagate schema on build, so a
649
716
  // schemaless dataset output silently builds to an empty dataset. Copy the
@@ -781,7 +848,7 @@ export class RecipesResource extends BaseResource {
781
848
  }
782
849
  }
783
850
  return {
784
- recipeName: name,
851
+ recipeName: finalRecipeName,
785
852
  type,
786
853
  createdDatasets,
787
854
  joinConfigured,
@@ -24,18 +24,151 @@ export interface SavedModelVersionDetails extends Record<string, unknown> {
24
24
  export interface SavedModelActionResult extends Record<string, unknown> {
25
25
  message?: string;
26
26
  }
27
+ /**
28
+ * Body of POST /projects/{pkey}/savedmodels/ (official Python client
29
+ * `create_mlflow_model` / `create_external_model`; the REST reference's
30
+ * `/savedmodels/create-external` path returns 405 on DSS 15.0.1).
31
+ * `savedModelType` is only "MLFLOW_PYFUNC" or "PROXY_MODEL"; `proxyModelConfiguration`
32
+ * carries the protocol settings for PROXY_MODEL creation (sagemaker, azure-ml,
33
+ * vertex-ai, databricks — per the official Python client `create_external_model`).
34
+ */
35
+ export interface SavedModelCreateRequest extends Record<string, unknown> {
36
+ savedModelType: "MLFLOW_PYFUNC" | "PROXY_MODEL";
37
+ name: string;
38
+ predictionType?: "BINARY_CLASSIFICATION" | "MULTICLASS" | "REGRESSION" | null;
39
+ /** PROXY_MODEL only: e.g. { protocol: "sagemaker", region: "eu-west-1" }. */
40
+ proxyModelConfiguration?: Record<string, unknown>;
41
+ }
42
+ /** Full saved-model settings document as returned by GET, sent back by PUT. */
43
+ export type SavedModelSettings = Record<string, unknown>;
44
+ /** Body of POST .../actions/delete-versions (official Python client, see docs note below). */
45
+ export interface SavedModelDeleteVersionsRequest extends Record<string, unknown> {
46
+ versions: string[];
47
+ removeIntermediate: boolean;
48
+ }
49
+ /** Query options for MLflow version import (POST .../versions/{versionId}). */
50
+ export interface SavedModelVersionImportOptions extends Record<string, unknown> {
51
+ codeEnvName?: string;
52
+ containerExecConfigName?: string;
53
+ setActive?: boolean;
54
+ binaryClassificationThreshold?: number;
55
+ }
56
+ /** Body of POST .../external-ml/actions/evaluate. */
57
+ export interface ExternalModelVersionEvaluateRequest extends Record<string, unknown> {
58
+ datasetRef: string;
59
+ containerExecConfigName?: string;
60
+ samplingParam?: Record<string, unknown> | null;
61
+ }
62
+ /** Query flags for .../external-ml/actions/evaluate. */
63
+ export interface ExternalModelVersionEvaluateOptions extends Record<string, unknown> {
64
+ useOptimalThreshold?: boolean;
65
+ skipExpensiveReports?: boolean;
66
+ }
67
+ /** Query options for the scoring JAR export (GET .../scoring-jar). */
68
+ export interface SavedModelScoringJarOptions extends Record<string, unknown> {
69
+ fullClassName?: string;
70
+ includeLibs?: boolean;
71
+ }
27
72
  export declare class SavedModelsResource extends BaseResource {
28
73
  /** List saved models in a project. */
29
74
  list(projectKey?: string): Promise<SavedModelListItem[]>;
30
- /** Get saved-model settings and metadata. */
75
+ /**
76
+ * Create a saved model for storing and managing MLflow or external
77
+ * (proxy) models. POST /projects/{pkey}/savedmodels/ (the official Python
78
+ * client's collection-root creation endpoint; `/create-external` 405s).
79
+ */
80
+ createExternal(request: SavedModelCreateRequest, projectKey?: string): Promise<SavedModel>;
81
+ /**
82
+ * Get saved-model settings and metadata.
83
+ * GET /projects/{pkey}/savedmodels/{savedModelId}
84
+ */
31
85
  get(savedModelId: string, projectKey?: string): Promise<SavedModel>;
86
+ /**
87
+ * Save the full settings document of a saved model. The body must be the
88
+ * complete settings document as returned by {@link get} (GET-then-PUT
89
+ * workflow), per the official Python client's DSSSavedModelSettings.save().
90
+ */
91
+ updateSettings(savedModelId: string, settings: SavedModelSettings, projectKey?: string): Promise<void>;
32
92
  /** List all versions of a saved model. */
33
93
  listVersions(savedModelId: string, projectKey?: string): Promise<SavedModelVersionListItem[]>;
34
94
  /** Get full details for one saved-model version. */
35
95
  versionDetails(savedModelId: string, versionId: string, projectKey?: string): Promise<SavedModelVersionDetails>;
96
+ /** Get the short snippet (summary) for one saved-model version. */
97
+ versionSnippet(savedModelId: string, versionId: string, projectKey?: string): Promise<Record<string, unknown>>;
36
98
  /** Make one saved-model version active. */
37
99
  setActiveVersion(savedModelId: string, versionId: string, projectKey?: string): Promise<SavedModelActionResult | undefined>;
100
+ /**
101
+ * Delete one or more versions of a saved model.
102
+ *
103
+ * Transport note: the DSS 15 REST reference documents this action as GET
104
+ * `/projects/{pkey}/savedmodels/{savedModelId}/delete-versions{?versions}{?removeIntermediate}`,
105
+ * but the official Python client (dataikuapi/dss/savedmodel.py,
106
+ * DSSSavedModel.delete_versions, verified against dataiku-api-client-python
107
+ * master) performs `POST .../actions/delete-versions` with a JSON body
108
+ * `{versions, removeIntermediate}`. The REST doc is erroneous; this SDK
109
+ * follows the official client. The request is destructive and must never be
110
+ * retried automatically: retryMaxAttempts is pinned to 1 regardless of the
111
+ * client-level retry configuration.
112
+ */
113
+ deleteVersions(versions: string[], options?: {
114
+ removeIntermediate?: boolean;
115
+ }, savedModelId?: string, projectKey?: string): Promise<void>;
116
+ /**
117
+ * Import a new MLflow version from a local MLflow model archive
118
+ * (zip) uploaded as multipart/form-data.
119
+ * POST /projects/{pkey}/savedmodels/{savedModelId}/versions/{versionId}
120
+ * with query codeEnvName, containerExecConfigName, setActive,
121
+ * binaryClassificationThreshold.
122
+ */
123
+ importMlflowVersion(archivePath: string, versionId: string, options?: SavedModelVersionImportOptions, savedModelId?: string, projectKey?: string): Promise<Record<string, unknown> | undefined>;
124
+ /**
125
+ * Import a new MLflow version from a path inside a managed folder. The
126
+ * server-side backend mandates a multipart request, so the part set is the
127
+ * same as the archive upload with a zero-byte "file" part, and the model
128
+ * location is carried by the folderRef (PROJECT_KEY.FOLDER_ID) and path
129
+ * query parameters — matching the official Python client, which sends
130
+ * `files={"file": (None, None)}`.
131
+ */
132
+ importMlflowVersionFromFolder(folderRef: string, folderPath: string, versionId: string, options?: SavedModelVersionImportOptions, savedModelId?: string, projectKey?: string): Promise<Record<string, unknown> | undefined>;
133
+ /**
134
+ * Get the external-ml metadata of a saved-model version (MLflow or proxy
135
+ * model). Opaque server document; no strict schema is enforced.
136
+ */
137
+ externalMetadataGet(savedModelId: string, versionId: string, projectKey?: string): Promise<Record<string, unknown>>;
138
+ /**
139
+ * PUT the external-ml metadata of a saved-model version. The body must be
140
+ * the full metadata document as returned by {@link externalMetadataGet}
141
+ * (GET-then-PUT). DSS requires the containerExecConfigName query parameter
142
+ * on this endpoint (live: 400 "Required request parameter
143
+ * 'containerExecConfigName' not present" without it); for an external API
144
+ * caller the container exec config resolves as LOCAL-CONFIG -> NONE, so an
145
+ * omitted option sends NONE rather than dropping the parameter.
146
+ */
147
+ externalMetadataPut(savedModelId: string, versionId: string, metadata: Record<string, unknown>, options?: {
148
+ containerExecConfigName?: string;
149
+ }, projectKey?: string): Promise<void>;
150
+ /**
151
+ * Evaluate the performance of an external/MLflow model version on a
152
+ * dataset. POST .../external-ml/actions/evaluate with
153
+ * useOptimalThreshold/skipExpensiveReports as query parameters and the
154
+ * dataset/container/sampling payload in the JSON body.
155
+ */
156
+ evaluateVersion(request: ExternalModelVersionEvaluateRequest, options?: ExternalModelVersionEvaluateOptions, savedModelId?: string, versionId?: string, projectKey?: string): Promise<void>;
157
+ /**
158
+ * GET the scoring JAR of a version as a binary stream. Caller is
159
+ * responsible for consuming the response body (e.g. writeResponseToFile);
160
+ * the stream is read under the client body deadline.
161
+ */
162
+ downloadScoringJar(options: SavedModelScoringJarOptions | undefined, savedModelId: string, versionId: string, projectKey?: string): Promise<Response>;
163
+ /** GET the scoring PMML of a version as a binary/XML stream. */
164
+ downloadScoringPmml(savedModelId: string, versionId: string, projectKey?: string): Promise<Response>;
165
+ /**
166
+ * Update the user metadata of a model version. The body must only be the
167
+ * "userMeta" field of a previously-retrieved version-details object.
168
+ */
169
+ setUserMeta(savedModelId: string, versionId: string, userMeta: Record<string, unknown>, projectKey?: string): Promise<void>;
38
170
  /** Delete a saved model and all of its versions. */
39
171
  delete(savedModelId: string, projectKey?: string): Promise<void>;
172
+ private mlflowImportQuery;
40
173
  private savedModelPath;
41
174
  }