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.
- package/dist/src/cli/commands/api-service.js +0 -1
- package/dist/src/cli/commands/notebook.js +5 -1
- package/dist/src/cli/output.js +1 -1
- package/dist/src/cli.js +42 -12
- package/dist/src/errors.d.ts +9 -1
- package/dist/src/errors.js +39 -6
- package/dist/src/resources/api-services.js +1 -0
- package/dist/src/resources/notebooks.js +9 -5
- package/dist/src/resources/sql.js +18 -25
- package/dist/src/skill.js +7 -0
- package/package.json +1 -1
|
@@ -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) =>
|
|
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,
|
package/dist/src/cli/output.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
840
|
-
|
|
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
|
|
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
|
-
+
|
|
893
|
-
|
|
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.
|
|
908
|
-
body:
|
|
937
|
+
statusText: canonicalStatusText(err.status),
|
|
938
|
+
body: safeBody,
|
|
909
939
|
...(err.retry ? { retry: err.retry, } : {}),
|
|
910
940
|
},
|
|
911
941
|
...context,
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/src/errors.js
CHANGED
|
@@ -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
|
-
|
|
280
|
-
|
|
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,
|
|
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,
|
|
320
|
-
const summary = DataikuError.extractSummary(status,
|
|
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} ${
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
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
|