dataiku-sdk 0.12.2 → 0.12.4

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.
@@ -59,7 +59,6 @@ export const apiServiceCommands = {
59
59
  handler: async (c, a, f) => {
60
60
  requireArgs(a, 1, "dss api-service list-packages <serviceId> [--project-key KEY]");
61
61
  const projectKey = f["project-key"];
62
- await c.apiServices.getSettings(a[0], projectKey);
63
62
  return c.apiServices.listPackages(a[0], projectKey);
64
63
  },
65
64
  usage: "dss api-service list-packages <serviceId> [--project-key KEY]",
@@ -44,7 +44,11 @@ export const notebookCommands = {
44
44
  const current = await c.notebooks.getJupyter(a[0], pk);
45
45
  const next = {
46
46
  ...current,
47
- cells: current.cells.map((cell) => ({ ...cell, outputs: [], execution_count: null, })),
47
+ cells: current.cells.map((cell) => {
48
+ if (cell.cell_type !== "code")
49
+ return cell;
50
+ return { ...cell, outputs: [], execution_count: 0, };
51
+ }),
48
52
  };
49
53
  return {
50
54
  dryRun: true,
@@ -73,7 +73,7 @@ export function transientBodyWithTargetContext(body, target, elapsedMs) {
73
73
  }
74
74
  export function addTransientTargetContext(error, target, elapsedMs) {
75
75
  if (error instanceof DataikuError && error.category === "transient") {
76
- throw new DataikuError(error.status, error.statusText, transientBodyWithTargetContext(error.body, target, elapsedMs), error.retry, error.requestId);
76
+ throw new DataikuError(error.status, error.statusText, transientBodyWithTargetContext(error.body, target, elapsedMs), error.retry, error.requestId, { target, elapsedMs, });
77
77
  }
78
78
  throw error;
79
79
  }
package/dist/src/cli.js CHANGED
@@ -12,7 +12,7 @@ import { COMMANDS_RUN_HINT, missingActionError, noCommandError, requireArgs, unk
12
12
  import { cliVersionResult, } from "./cli/version.js";
13
13
  import { DataikuClient, } from "./client.js";
14
14
  import { getCredentialsPath, } from "./config.js";
15
- import { ClientValidationError, DataikuError, dataikuErrorCode, } from "./errors.js";
15
+ import { canonicalStatusText, ClientValidationError, DataikuError, dataikuErrorCode, } from "./errors.js";
16
16
  import { AGENTS, detectAgents, findWorkspaceRoot, installSkill, planSkillInstalls, } from "./skill.js";
17
17
  import { appendCleanupLedgerEntry, readCleanupLedger, } from "./utils/cleanup-ledger.js";
18
18
  // ---------------------------------------------------------------------------
@@ -833,16 +833,41 @@ function rawCommandContext() {
833
833
  : undefined),
834
834
  };
835
835
  }
836
- function requestIdFromBody(body) {
836
+ const SAFE_RESPONSE_METADATA_KEYS = [
837
+ "requestId",
838
+ "request_id",
839
+ "errorId",
840
+ "elapsedMs",
841
+ ];
842
+ function safeResponseMetadata(body) {
837
843
  try {
838
844
  const parsed = JSON.parse(body);
839
- const value = parsed.requestId ?? parsed.request_id ?? parsed.errorId;
840
- return typeof value === "string" && value.length > 0 ? value : undefined;
845
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
846
+ return {};
847
+ const metadata = {};
848
+ for (const key of SAFE_RESPONSE_METADATA_KEYS) {
849
+ const value = parsed[key];
850
+ if ((typeof value === "string" && value.length > 0)
851
+ || (typeof value === "number" && Number.isFinite(value))
852
+ || typeof value === "boolean") {
853
+ metadata[key] = value;
854
+ }
855
+ }
856
+ return metadata;
841
857
  }
842
858
  catch {
843
- return undefined;
859
+ return {};
844
860
  }
845
861
  }
862
+ function requestIdFromBody(body) {
863
+ const metadata = safeResponseMetadata(body);
864
+ for (const key of ["requestId", "request_id", "errorId",]) {
865
+ const value = metadata[key];
866
+ if (typeof value === "string")
867
+ return value;
868
+ }
869
+ return undefined;
870
+ }
846
871
  const MISSING_PROJECT_KEY_ERROR_PREFIX = "projectKey is required";
847
872
  function errorExitCode(err) {
848
873
  if (err instanceof CommandResultFailure)
@@ -884,13 +909,18 @@ function buildErrorReport(err) {
884
909
  };
885
910
  }
886
911
  if (err instanceof DataikuError) {
887
- const errorMessage = err.category === "not_found"
888
- && err.message.includes("Dataiku instance not found")
889
- && context.resource
912
+ const errorMessage = err.category === "not_found" && context.resource
890
913
  ? `Not found: ${context.resource}${context.action ? ` ${context.action}` : ""}`
891
914
  + `${context.projectKey ? ` in project ${context.projectKey}` : ""}`
892
- + ` — verify the object identifier and project key (DSS: ${err.message.split("\n")[0]}).`
893
- : err.message;
915
+ + " — verify the object identifier and project key."
916
+ + `\nHint: ${err.retryHint}`
917
+ : err.safeMessage;
918
+ const safeMetadata = safeResponseMetadata(err.body);
919
+ if (err.trustedTarget)
920
+ safeMetadata.target = err.trustedTarget;
921
+ if (err.trustedElapsedMs !== undefined)
922
+ safeMetadata.elapsedMs = err.trustedElapsedMs;
923
+ const safeBody = JSON.stringify(safeMetadata);
894
924
  return {
895
925
  type: "error",
896
926
  ok: false,
@@ -904,8 +934,8 @@ function buildErrorReport(err) {
904
934
  requestId: err.requestId ?? requestIdFromBody(err.body),
905
935
  details: {
906
936
  dssCategory: err.category,
907
- statusText: err.statusText,
908
- body: err.body,
937
+ statusText: canonicalStatusText(err.status),
938
+ body: safeBody,
909
939
  ...(err.retry ? { retry: err.retry, } : {}),
910
940
  },
911
941
  ...context,
@@ -15,6 +15,11 @@ export interface DataikuRetryMetadata {
15
15
  delaysMs: number[];
16
16
  timedOut: boolean;
17
17
  }
18
+ export interface DataikuErrorTrustedMetadata {
19
+ target?: string;
20
+ elapsedMs?: number;
21
+ }
22
+ export declare function canonicalStatusText(status: number): string;
18
23
  export declare class ClientValidationError extends Error {
19
24
  readonly code: StableErrorCode;
20
25
  readonly hint?: string;
@@ -31,7 +36,10 @@ export declare class DataikuError extends Error {
31
36
  retryHint: string;
32
37
  retry?: DataikuRetryMetadata;
33
38
  requestId?: string;
34
- constructor(status: number, statusText: string, body: string, retry?: DataikuRetryMetadata, requestId?: string);
39
+ readonly trustedTarget?: string;
40
+ readonly trustedElapsedMs?: number;
41
+ constructor(status: number, statusText: string, body: string, retry?: DataikuRetryMetadata, requestId?: string, trustedMetadata?: DataikuErrorTrustedMetadata);
42
+ get safeMessage(): string;
35
43
  private static extractSummary;
36
44
  private static formatRetryMetadata;
37
45
  private static buildDetails;
@@ -12,6 +12,25 @@ export function dataikuErrorCode(category) {
12
12
  return "internal_error";
13
13
  }
14
14
  }
15
+ const CANONICAL_STATUS_TEXT = {
16
+ 400: "Bad Request",
17
+ 401: "Unauthorized",
18
+ 403: "Forbidden",
19
+ 404: "Not Found",
20
+ 408: "Request Timeout",
21
+ 409: "Conflict",
22
+ 422: "Unprocessable Entity",
23
+ 425: "Too Early",
24
+ 429: "Too Many Requests",
25
+ 500: "Internal Server Error",
26
+ 501: "Not Implemented",
27
+ 502: "Bad Gateway",
28
+ 503: "Service Unavailable",
29
+ 504: "Gateway Timeout",
30
+ };
31
+ export function canonicalStatusText(status) {
32
+ return CANONICAL_STATUS_TEXT[status] ?? (status === 0 ? "Network Error" : "HTTP Error");
33
+ }
15
34
  export class ClientValidationError extends Error {
16
35
  code;
17
36
  hint;
@@ -276,8 +295,10 @@ export class DataikuError extends Error {
276
295
  retryHint;
277
296
  retry;
278
297
  requestId;
279
- constructor(status, statusText, body, retry, requestId) {
280
- const details = DataikuError.buildDetails(status, statusText, body, retry);
298
+ trustedTarget;
299
+ trustedElapsedMs;
300
+ constructor(status, statusText, body, retry, requestId, trustedMetadata) {
301
+ const details = DataikuError.buildDetails(status, body, retry);
281
302
  super(details.message);
282
303
  this.status = status;
283
304
  this.statusText = statusText;
@@ -288,8 +309,20 @@ export class DataikuError extends Error {
288
309
  this.retryHint = details.retryHint;
289
310
  this.retry = retry;
290
311
  this.requestId = requestId;
312
+ this.trustedTarget = trustedMetadata?.target;
313
+ this.trustedElapsedMs = trustedMetadata?.elapsedMs;
314
+ }
315
+ get safeMessage() {
316
+ const retrySummary = DataikuError.formatRetryMetadata(this.retry);
317
+ return [
318
+ `${this.status} ${canonicalStatusText(this.status)}`,
319
+ `Error type: ${this.category}`,
320
+ `Retryable: ${this.retryable ? "yes" : "no"}`,
321
+ `Hint: ${this.retryHint}`,
322
+ ...(retrySummary ? [retrySummary,] : []),
323
+ ].join("\n");
291
324
  }
292
- static extractSummary(_status, _statusText, body) {
325
+ static extractSummary(_status, body) {
293
326
  try {
294
327
  const parsed = JSON.parse(body);
295
328
  if (parsed.message)
@@ -316,14 +349,14 @@ export class DataikuError extends Error {
316
349
  `Timed out: ${retry.timedOut ? "yes" : "no"}`,
317
350
  ].join(" | ");
318
351
  }
319
- static buildDetails(status, statusText, body, retry) {
320
- const summary = DataikuError.extractSummary(status, statusText, body);
352
+ static buildDetails(status, body, retry) {
353
+ const summary = DataikuError.extractSummary(status, body);
321
354
  const taxonomy = classifyDataikuError(status, body);
322
355
  const retrySummary = DataikuError.formatRetryMetadata(retry);
323
356
  return {
324
357
  ...taxonomy,
325
358
  message: [
326
- `${status} ${statusText}: ${summary}`,
359
+ `${status} ${canonicalStatusText(status)}: ${summary}`,
327
360
  `Error type: ${taxonomy.category}`,
328
361
  `Retryable: ${taxonomy.retryable ? "yes" : "no"}`,
329
362
  `Hint: ${taxonomy.retryHint}`,
@@ -28,6 +28,7 @@ export class ApiServicesResource extends BaseResource {
28
28
  }
29
29
  /** List deployable packages for an API service. */
30
30
  async listPackages(serviceId, projectKey) {
31
+ await this.getSettings(serviceId, projectKey);
31
32
  return this.client.get(`${this.servicePath(serviceId, projectKey)}/packages`);
32
33
  }
33
34
  /** Get an API service package summary. */
@@ -36,11 +36,15 @@ export class NotebooksResource extends BaseResource {
36
36
  */
37
37
  async clearJupyterOutputs(name, projectKey) {
38
38
  const notebook = await this.getJupyter(name, projectKey);
39
- const clearedCells = notebook.cells.map((cell) => ({
40
- ...cell,
41
- outputs: [],
42
- execution_count: null,
43
- }));
39
+ const clearedCells = notebook.cells.map((cell) => {
40
+ if (cell.cell_type !== "code")
41
+ return cell;
42
+ return {
43
+ ...cell,
44
+ outputs: [],
45
+ execution_count: 0,
46
+ };
47
+ });
44
48
  await this.saveJupyter(name, { ...notebook, cells: clearedCells, }, projectKey);
45
49
  }
46
50
  /** List running kernel sessions for a Jupyter notebook. */
@@ -1,4 +1,4 @@
1
- import { DataikuError, } from "../errors.js";
1
+ import { ClientValidationError, DataikuError, } from "../errors.js";
2
2
  import { BaseResource, } from "./base.js";
3
3
  const UNSUPPORTED_SQL_DATASET_CONNECTION_DETAIL = "neither of sql nor hdfs type";
4
4
  function isUnsupportedSqlDatasetConnectionError(error) {
@@ -161,28 +161,23 @@ export class SqlResource extends BaseResource {
161
161
  const datasetFullName = opts.datasetFullName;
162
162
  if (!datasetFullName)
163
163
  return null;
164
- try {
165
- const identifier = splitDatasetIdentifier(datasetFullName, opts.projectKey);
166
- const projectKey = identifier.projectKey
167
- ? identifier.projectKey
168
- : this.resolveProjectKey(opts.projectKey);
169
- const dsEnc = encodeURIComponent(identifier.datasetName);
170
- const raw = await this.client.get(`/public/api/projects/${encodeURIComponent(projectKey)}/datasets/${dsEnc}`);
171
- const params = asRecord(raw.params);
172
- const connection = asString(params?.connection);
173
- if (!connection)
174
- return null;
175
- return {
176
- ...opts,
177
- connection,
178
- datasetFullName: undefined,
179
- database: opts.database ?? asString(params?.schema) ?? asString(params?.catalog),
180
- projectKey,
181
- };
182
- }
183
- catch {
164
+ const identifier = splitDatasetIdentifier(datasetFullName, opts.projectKey);
165
+ const projectKey = identifier.projectKey
166
+ ? identifier.projectKey
167
+ : this.resolveProjectKey(opts.projectKey);
168
+ const dsEnc = encodeURIComponent(identifier.datasetName);
169
+ const raw = await this.client.get(`/public/api/projects/${encodeURIComponent(projectKey)}/datasets/${dsEnc}`);
170
+ const params = asRecord(raw.params);
171
+ const connection = asString(params?.connection);
172
+ if (!connection)
184
173
  return null;
185
- }
174
+ return {
175
+ ...opts,
176
+ connection,
177
+ datasetFullName: undefined,
178
+ database: opts.database ?? asString(params?.schema) ?? asString(params?.catalog),
179
+ projectKey,
180
+ };
186
181
  }
187
182
  /**
188
183
  * Execute a SQL query end-to-end: start, stream all rows, verify, return combined result.
@@ -198,9 +193,7 @@ export class SqlResource extends BaseResource {
198
193
  withSqlErrorContext(error);
199
194
  const retryOpts = await this.resolveDatasetQueryFallback(queryOpts);
200
195
  if (!retryOpts) {
201
- throw new Error(buildUnsupportedSqlDatasetConnectionMessage(queryOpts.datasetFullName), {
202
- cause: error,
203
- });
196
+ throw new ClientValidationError(buildUnsupportedSqlDatasetConnectionMessage(queryOpts.datasetFullName));
204
197
  }
205
198
  try {
206
199
  return await this.executeQuery(retryOpts);
package/dist/src/skill.js CHANGED
@@ -96,6 +96,9 @@ dss recipe set-payload compute_orders --file code.py --project-key MYPROJ
96
96
  dss job build-and-wait orders --include-logs --project-key MYPROJ
97
97
  dss scenario run daily_build --project-key MYPROJ
98
98
  dss sql query --connection analytics --sql "select 1" --project-key MYPROJ
99
+ dss sql query --dataset MYPROJ.orders --sql-file query.sql
100
+ dss notebook clear-jupyter-outputs exploration --dry-run --project-key MYPROJ
101
+ dss api-service list-packages churn-service --project-key MYPROJ
99
102
  dss batch --data-file steps.json
100
103
  \`\`\`
101
104
  For fake-DSS smoke tests, return project lists as JSON arrays such as \`[{"projectKey":"MYPROJ","name":"My Project"}]\` from \`/public/api/projects/\`; recipe payload commands read \`/public/api/projects/<PROJECT>/recipes/<NAME>?includePayload=true\` and expect a JSON object shaped like \`{"recipe":{"name":"<NAME>","type":"python"},"payload":"..."}\`.
@@ -116,6 +119,9 @@ Mutations print a small JSON ack to stdout and exit 0 on success (e.g. \`{"updat
116
119
  - Build failures: \`dss job log <id> --errors-only\` surfaces just error/traceback lines, and \`--output PATH\` saves the full log to a file. Logs are one long line with JVM noise; the \`Error in Python process: At line <N>\` marker maps \`<N>\` straight to your recipe payload's source line.
117
120
  - Schema changes aren't automatic for code recipes: after changing a python/SQL/prepare recipe's output columns run \`dss dataset refresh-schema\` (or rebuild) before downstream reads, and \`dss dataset validate-build\` to catch file-backed misconfig before launching a build. Exception: \`dss recipe create --type sync\` copies the input schema onto a schemaless dataset output at create time (see \`syncOutputSchemaPropagated\` in the result), so a fresh sync output builds populated without a manual refresh.
118
121
  - \`dss dataset download\` is capped (default 100k rows) and returns \`{ path, rows, truncated, limit }\`: check \`truncated\` and raise \`--limit N\` when you need more — treat it as a sample, not a guaranteed full export. Without \`--output\` the file lands in the current working directory (and a \`dataset_download_default_location\` warning names that path); pass \`--output PATH\` to control the destination. For very large tables, aggregate in SQL or read inside a recipe instead.
122
+ - \`dss api-service list-packages SERVICE\` first verifies the parent service through its settings. A \`not_found\` error means the service is missing (verify the service ID and project key); an empty array means the service exists but has no deployable packages. Do not reinterpret \`not_found\` as an empty package list or retry the lower-level packages route.
123
+ - \`dss notebook clear-jupyter-outputs NAME --dry-run\` returns the full \`current\` and \`next\` notebook states. Applying it clears \`outputs\` and resets \`execution_count\` to \`0\` only on code cells, preserving markdown and raw cells unchanged.
124
+ - \`dss sql query ... --dataset PROJECT.NAME\` first queries through the dataset. If DSS rejects that dataset connection as neither SQL nor HDFS, the CLI reads dataset metadata and retries with \`params.connection\` (using its schema or catalog as the database when available). A readable dataset with no usable connection exits with \`validation_failed\` and advises \`--connection\`; dataset metadata \`404\` and \`403\` errors propagate as \`not_found\` and \`permission_denied\`.
119
125
 
120
126
  ## Error envelope
121
127
 
@@ -135,6 +141,7 @@ Parse each stderr line as JSON; on non-zero exit the final line is an error even
135
141
  \`\`\`
136
142
 
137
143
  Use \`code\`, \`category\`, \`exitCode\`, \`retryable\`, \`status\`, and \`details\` for recovery logic. Do not scrape message text when a structured field is available.
144
+ Treat \`details.body\` as sanitized metadata only: it contains at most \`requestId\`/\`request_id\`/\`errorId\`/\`elapsedMs\` plus locally trusted target/timing fields, not the arbitrary DSS response body. \`details.statusText\` is canonical text derived from the numeric status, not a remote reason phrase. Base recovery on top-level \`code\`, \`category\`, \`status\`, \`retryable\`, \`requestId\`, \`hint\`, and \`details.dssCategory\`; never parse or expose assumed server-body fields.
138
145
  `;
139
146
  const SKILL_FRONTMATTER = `---
140
147
  name: dataiku-dss
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dataiku-sdk",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "description": "Dataiku DSS SDK and CLI for programmatic access to DSS REST APIs",
5
5
  "type": "module",
6
6
  "workspaces": [