dataiku-sdk 0.1.0 → 0.1.2
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/bin/dss.js +3 -1
- package/dist/packages/types/src/index.d.ts +4 -4
- package/dist/packages/types/src/index.js +2 -2
- package/dist/src/cli.js +175 -49
- package/dist/src/client.d.ts +5 -1
- package/dist/src/client.js +14 -1
- package/dist/src/errors.js +12 -0
- package/dist/src/resources/connections.d.ts +1 -1
- package/dist/src/resources/connections.js +7 -4
- package/dist/src/resources/datasets.js +8 -2
- package/dist/src/resources/folders.d.ts +1 -0
- package/dist/src/resources/folders.js +8 -0
- package/dist/src/resources/jobs.d.ts +2 -1
- package/dist/src/resources/jobs.js +7 -3
- package/dist/src/resources/notebooks.d.ts +18 -3
- package/dist/src/resources/notebooks.js +25 -5
- package/dist/src/resources/recipes.d.ts +10 -1
- package/dist/src/resources/recipes.js +44 -7
- package/dist/src/resources/sql.js +32 -5
- package/dist/src/resources/variables.d.ts +1 -0
- package/dist/src/resources/variables.js +9 -1
- package/package.json +4 -2
- package/packages/types/dist/index.d.ts +4 -4
- package/packages/types/dist/index.js +2 -2
package/bin/dss.js
CHANGED
|
@@ -298,8 +298,8 @@ export declare const JupyterNotebookSummarySchema: import("@sinclair/typebox").T
|
|
|
298
298
|
language: import("@sinclair/typebox").TString;
|
|
299
299
|
kernelSpec: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
300
300
|
name: import("@sinclair/typebox").TString;
|
|
301
|
-
display_name: import("@sinclair/typebox").TString
|
|
302
|
-
language: import("@sinclair/typebox").TString
|
|
301
|
+
display_name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
302
|
+
language: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
303
303
|
}>>;
|
|
304
304
|
}>;
|
|
305
305
|
export type JupyterNotebookSummary = Static<typeof JupyterNotebookSummarySchema>;
|
|
@@ -438,8 +438,8 @@ export declare const JupyterNotebookSummaryArraySchema: import("@sinclair/typebo
|
|
|
438
438
|
language: import("@sinclair/typebox").TString;
|
|
439
439
|
kernelSpec: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
440
440
|
name: import("@sinclair/typebox").TString;
|
|
441
|
-
display_name: import("@sinclair/typebox").TString
|
|
442
|
-
language: import("@sinclair/typebox").TString
|
|
441
|
+
display_name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
442
|
+
language: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
443
443
|
}>>;
|
|
444
444
|
}>>;
|
|
445
445
|
export declare const SqlNotebookSummaryArraySchema: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
@@ -306,8 +306,8 @@ export const JupyterNotebookSummarySchema = Type.Object({
|
|
|
306
306
|
language: Type.String(),
|
|
307
307
|
kernelSpec: Type.Optional(Type.Object({
|
|
308
308
|
name: Type.String(),
|
|
309
|
-
display_name: Type.String(),
|
|
310
|
-
language: Type.String(),
|
|
309
|
+
display_name: Type.Optional(Type.String()),
|
|
310
|
+
language: Type.Optional(Type.String()),
|
|
311
311
|
}, { additionalProperties: true, })),
|
|
312
312
|
}, { additionalProperties: true, });
|
|
313
313
|
export const JupyterNotebookContentSchema = Type.Object({
|
package/dist/src/cli.js
CHANGED
|
@@ -18,6 +18,87 @@ function json(v) {
|
|
|
18
18
|
return undefined;
|
|
19
19
|
return JSON.parse(v);
|
|
20
20
|
}
|
|
21
|
+
function jsonInput(flags) {
|
|
22
|
+
if (flags["stdin"] === true) {
|
|
23
|
+
return JSON.parse(readFileSync(0, "utf-8"));
|
|
24
|
+
}
|
|
25
|
+
if (typeof flags["data-file"] === "string") {
|
|
26
|
+
return JSON.parse(readFileSync(flags["data-file"], "utf-8"));
|
|
27
|
+
}
|
|
28
|
+
if (typeof flags["data"] === "string") {
|
|
29
|
+
return JSON.parse(flags["data"]);
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
async function resolveFolderId(client, nameOrId, flags) {
|
|
34
|
+
return client.folders.resolveId(nameOrId, flags["project-key"]);
|
|
35
|
+
}
|
|
36
|
+
function formatLineDiff(remoteName, localPath, remoteContent, localContent) {
|
|
37
|
+
if (localContent === remoteContent) {
|
|
38
|
+
return "No differences.";
|
|
39
|
+
}
|
|
40
|
+
const localLines = localContent.split("\n");
|
|
41
|
+
const remoteLines = remoteContent.split("\n");
|
|
42
|
+
const lines = [`--- remote:${remoteName}`, `+++ local:${localPath}`, "",];
|
|
43
|
+
const maxLen = Math.max(localLines.length, remoteLines.length);
|
|
44
|
+
for (let i = 0; i < maxLen; i++) {
|
|
45
|
+
const remoteLine = remoteLines[i];
|
|
46
|
+
const localLine = localLines[i];
|
|
47
|
+
if (remoteLine === localLine)
|
|
48
|
+
continue;
|
|
49
|
+
if (remoteLine !== undefined && localLine !== undefined) {
|
|
50
|
+
lines.push(`@@ line ${String(i + 1)} @@`);
|
|
51
|
+
lines.push(`- ${remoteLine}`);
|
|
52
|
+
lines.push(`+ ${localLine}`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (remoteLine !== undefined) {
|
|
56
|
+
lines.push(`- ${remoteLine}`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
lines.push(`+ ${localLine}`);
|
|
60
|
+
}
|
|
61
|
+
return lines.join("\n");
|
|
62
|
+
}
|
|
63
|
+
function parseOutputFormat(v) {
|
|
64
|
+
if (v === undefined)
|
|
65
|
+
return "json";
|
|
66
|
+
if (v === "json" || v === "quiet" || v === "tsv")
|
|
67
|
+
return v;
|
|
68
|
+
throw new UsageError(`Invalid --format value: ${String(v)}. Use json, tsv, or quiet.`);
|
|
69
|
+
}
|
|
70
|
+
function writeCommandResult(result, format) {
|
|
71
|
+
if (result === undefined || result === null) {
|
|
72
|
+
if (format !== "quiet") {
|
|
73
|
+
process.stdout.write(`${JSON.stringify({ ok: true, }, null, 2)}\n`);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (typeof result === "string") {
|
|
78
|
+
if (format !== "quiet") {
|
|
79
|
+
process.stdout.write(result);
|
|
80
|
+
if (!result.endsWith("\n"))
|
|
81
|
+
process.stdout.write("\n");
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (format === "quiet")
|
|
86
|
+
return;
|
|
87
|
+
if (format === "tsv"
|
|
88
|
+
&& Array.isArray(result)
|
|
89
|
+
&& result.every((item) => item !== null && typeof item === "object" && !Array.isArray(item))) {
|
|
90
|
+
const items = result;
|
|
91
|
+
if (items.length === 0)
|
|
92
|
+
return;
|
|
93
|
+
const keys = Object.keys(items[0]);
|
|
94
|
+
process.stdout.write(`${keys.join("\t")}\n`);
|
|
95
|
+
for (const item of items) {
|
|
96
|
+
process.stdout.write(`${keys.map((key) => String(item[key] ?? "")).join("\t")}\n`);
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
101
|
+
}
|
|
21
102
|
function parseArgs(argv) {
|
|
22
103
|
const positional = [];
|
|
23
104
|
const flags = {};
|
|
@@ -142,14 +223,14 @@ const commands = {
|
|
|
142
223
|
},
|
|
143
224
|
update: {
|
|
144
225
|
handler: (c, a, f) => {
|
|
145
|
-
requireArgs(a, 1, "dss dataset update <name> --data '{...}'");
|
|
146
|
-
const data =
|
|
226
|
+
requireArgs(a, 1, "dss dataset update <name> [--data '{...}' | --data-file PATH | --stdin]");
|
|
227
|
+
const data = jsonInput(f);
|
|
147
228
|
if (!data) {
|
|
148
|
-
throw new UsageError("--data is required. Usage: dss dataset update <name> --data '{...}'");
|
|
229
|
+
throw new UsageError("--data, --data-file, or --stdin is required. Usage: dss dataset update <name> [--data '{...}' | --data-file PATH | --stdin]");
|
|
149
230
|
}
|
|
150
231
|
return c.datasets.update(a[0], data, f["project-key"]);
|
|
151
232
|
},
|
|
152
|
-
usage: "dss dataset update <name> --data '{...}' [--project-key KEY]",
|
|
233
|
+
usage: "dss dataset update <name> [--data '{...}' | --data-file PATH | --stdin] [--project-key KEY]",
|
|
153
234
|
},
|
|
154
235
|
},
|
|
155
236
|
recipe: {
|
|
@@ -178,37 +259,71 @@ const commands = {
|
|
|
178
259
|
requireArgs(a, 1, "dss recipe download <name>");
|
|
179
260
|
return c.recipes.download(a[0], {
|
|
180
261
|
outputPath: f["output"],
|
|
262
|
+
projectKey: f["project-key"],
|
|
263
|
+
});
|
|
264
|
+
},
|
|
265
|
+
usage: "dss recipe download <name> [--output PATH] [--project-key KEY]",
|
|
266
|
+
},
|
|
267
|
+
"download-code": {
|
|
268
|
+
handler: (c, a, f) => {
|
|
269
|
+
requireArgs(a, 1, "dss recipe download-code <name>");
|
|
270
|
+
return c.recipes.downloadCode(a[0], {
|
|
271
|
+
outputPath: f["output"],
|
|
272
|
+
projectKey: f["project-key"],
|
|
181
273
|
});
|
|
182
274
|
},
|
|
183
|
-
usage: "dss recipe download <name> [--output PATH]",
|
|
275
|
+
usage: "dss recipe download-code <name> [--output PATH] [--project-key KEY]",
|
|
184
276
|
},
|
|
185
277
|
create: {
|
|
186
278
|
handler: (c, _a, f) => {
|
|
187
279
|
const type = f["type"];
|
|
188
280
|
if (!type) {
|
|
189
|
-
throw new UsageError("--type is required. Usage: dss recipe create --type TYPE --input DS");
|
|
281
|
+
throw new UsageError("--type is required. Usage: dss recipe create --type TYPE --input DS --output DS");
|
|
282
|
+
}
|
|
283
|
+
const outputDataset = f["output"];
|
|
284
|
+
if (!outputDataset) {
|
|
285
|
+
throw new UsageError("--output is required. Usage: dss recipe create --type TYPE --input DS --output DS");
|
|
190
286
|
}
|
|
191
287
|
return c.recipes.create({
|
|
192
288
|
type,
|
|
193
289
|
name: f["name"],
|
|
194
290
|
inputDatasets: f["input"] ? [f["input"],] : undefined,
|
|
195
|
-
outputDataset
|
|
291
|
+
outputDataset,
|
|
196
292
|
outputConnection: f["output-connection"],
|
|
197
293
|
projectKey: f["project-key"],
|
|
198
294
|
});
|
|
199
295
|
},
|
|
200
|
-
usage: "dss recipe create --type TYPE --input DS
|
|
296
|
+
usage: "dss recipe create --type TYPE --input DS --output DS [--output-connection CONN] [--project-key KEY]",
|
|
297
|
+
},
|
|
298
|
+
diff: {
|
|
299
|
+
handler: async (c, a, f) => {
|
|
300
|
+
requireArgs(a, 1, "dss recipe diff <name> --file PATH");
|
|
301
|
+
const filePath = f["file"];
|
|
302
|
+
if (!filePath) {
|
|
303
|
+
throw new UsageError("--file is required. Usage: dss recipe diff <name> --file PATH");
|
|
304
|
+
}
|
|
305
|
+
const result = await c.recipes.get(a[0], {
|
|
306
|
+
includePayload: true,
|
|
307
|
+
projectKey: f["project-key"],
|
|
308
|
+
});
|
|
309
|
+
if (!result.payload) {
|
|
310
|
+
throw new Error(`Recipe "${a[0]}" has no code payload to diff.`);
|
|
311
|
+
}
|
|
312
|
+
const localContent = readFileSync(filePath, "utf-8");
|
|
313
|
+
return formatLineDiff(a[0], filePath, result.payload, localContent);
|
|
314
|
+
},
|
|
315
|
+
usage: "dss recipe diff <name> --file PATH [--project-key KEY]",
|
|
201
316
|
},
|
|
202
317
|
update: {
|
|
203
318
|
handler: (c, a, f) => {
|
|
204
|
-
requireArgs(a, 1, "dss recipe update <name> --data '{...}'");
|
|
205
|
-
const data =
|
|
319
|
+
requireArgs(a, 1, "dss recipe update <name> [--data '{...}' | --data-file PATH | --stdin]");
|
|
320
|
+
const data = jsonInput(f);
|
|
206
321
|
if (!data) {
|
|
207
|
-
throw new UsageError("--data is required. Usage: dss recipe update <name> --data '{...}'");
|
|
322
|
+
throw new UsageError("--data, --data-file, or --stdin is required. Usage: dss recipe update <name> [--data '{...}' | --data-file PATH | --stdin]");
|
|
208
323
|
}
|
|
209
324
|
return c.recipes.update(a[0], data, f["project-key"]);
|
|
210
325
|
},
|
|
211
|
-
usage: "dss recipe update <name> --data '{...}' [--project-key KEY]",
|
|
326
|
+
usage: "dss recipe update <name> [--data '{...}' | --data-file PATH | --stdin] [--project-key KEY]",
|
|
212
327
|
},
|
|
213
328
|
},
|
|
214
329
|
job: {
|
|
@@ -316,14 +431,14 @@ const commands = {
|
|
|
316
431
|
},
|
|
317
432
|
update: {
|
|
318
433
|
handler: (c, a, f) => {
|
|
319
|
-
requireArgs(a, 1, "dss scenario update <id> --data '{...}'");
|
|
320
|
-
const data =
|
|
434
|
+
requireArgs(a, 1, "dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin]");
|
|
435
|
+
const data = jsonInput(f);
|
|
321
436
|
if (!data) {
|
|
322
|
-
throw new UsageError("--data is required. Usage: dss scenario update <id> --data '{...}'");
|
|
437
|
+
throw new UsageError("--data, --data-file, or --stdin is required. Usage: dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin]");
|
|
323
438
|
}
|
|
324
439
|
return c.scenarios.update(a[0], data, f["project-key"]);
|
|
325
440
|
},
|
|
326
|
-
usage: "dss scenario update <id> --data '{...}' [--project-key KEY]",
|
|
441
|
+
usage: "dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin] [--project-key KEY]",
|
|
327
442
|
},
|
|
328
443
|
},
|
|
329
444
|
folder: {
|
|
@@ -332,41 +447,44 @@ const commands = {
|
|
|
332
447
|
usage: "dss folder list [--project-key KEY]",
|
|
333
448
|
},
|
|
334
449
|
get: {
|
|
335
|
-
handler: (c, a, f) => {
|
|
336
|
-
requireArgs(a, 1, "dss folder get <id>");
|
|
337
|
-
return c.folders.get(a[0], f["project-key"]);
|
|
450
|
+
handler: async (c, a, f) => {
|
|
451
|
+
requireArgs(a, 1, "dss folder get <name-or-id>");
|
|
452
|
+
return c.folders.get(await resolveFolderId(c, a[0], f), f["project-key"]);
|
|
338
453
|
},
|
|
339
|
-
usage: "dss folder get <id> [--project-key KEY]",
|
|
454
|
+
usage: "dss folder get <name-or-id> [--project-key KEY]",
|
|
340
455
|
},
|
|
341
456
|
contents: {
|
|
342
|
-
handler: (c, a,
|
|
343
|
-
requireArgs(a, 1, "dss folder contents <id>");
|
|
344
|
-
return c.folders.contents(a[0])
|
|
457
|
+
handler: async (c, a, f) => {
|
|
458
|
+
requireArgs(a, 1, "dss folder contents <name-or-id>");
|
|
459
|
+
return c.folders.contents(await resolveFolderId(c, a[0], f), {
|
|
460
|
+
projectKey: f["project-key"],
|
|
461
|
+
});
|
|
345
462
|
},
|
|
346
|
-
usage: "dss folder contents <id>",
|
|
463
|
+
usage: "dss folder contents <name-or-id> [--project-key KEY]",
|
|
347
464
|
},
|
|
348
465
|
download: {
|
|
349
|
-
handler: (c, a, f) => {
|
|
350
|
-
requireArgs(a, 2, "dss folder download <id> <path>");
|
|
351
|
-
return c.folders.download(a[0], a[1], {
|
|
466
|
+
handler: async (c, a, f) => {
|
|
467
|
+
requireArgs(a, 2, "dss folder download <name-or-id> <path>");
|
|
468
|
+
return c.folders.download(await resolveFolderId(c, a[0], f), a[1], {
|
|
352
469
|
localPath: f["output"],
|
|
470
|
+
projectKey: f["project-key"],
|
|
353
471
|
});
|
|
354
472
|
},
|
|
355
|
-
usage: "dss folder download <id> <path> [--output PATH]",
|
|
473
|
+
usage: "dss folder download <name-or-id> <path> [--output PATH] [--project-key KEY]",
|
|
356
474
|
},
|
|
357
475
|
upload: {
|
|
358
|
-
handler: (c, a, f) => {
|
|
359
|
-
requireArgs(a, 3, "dss folder upload <id> <path> <localPath>");
|
|
360
|
-
return c.folders.upload(a[0], a[1], a[2], f["project-key"]);
|
|
476
|
+
handler: async (c, a, f) => {
|
|
477
|
+
requireArgs(a, 3, "dss folder upload <name-or-id> <path> <localPath>");
|
|
478
|
+
return c.folders.upload(await resolveFolderId(c, a[0], f), a[1], a[2], f["project-key"]);
|
|
361
479
|
},
|
|
362
|
-
usage: "dss folder upload <id> <path> <localPath> [--project-key KEY]",
|
|
480
|
+
usage: "dss folder upload <name-or-id> <path> <localPath> [--project-key KEY]",
|
|
363
481
|
},
|
|
364
482
|
"delete-file": {
|
|
365
|
-
handler: (c, a, f) => {
|
|
366
|
-
requireArgs(a, 2, "dss folder delete-file <id> <path>");
|
|
367
|
-
return c.folders.deleteFile(a[0], a[1], f["project-key"]);
|
|
483
|
+
handler: async (c, a, f) => {
|
|
484
|
+
requireArgs(a, 2, "dss folder delete-file <name-or-id> <path>");
|
|
485
|
+
return c.folders.deleteFile(await resolveFolderId(c, a[0], f), a[1], f["project-key"]);
|
|
368
486
|
},
|
|
369
|
-
usage: "dss folder delete-file <id> <path> [--project-key KEY]",
|
|
487
|
+
usage: "dss folder delete-file <name-or-id> <path> [--project-key KEY]",
|
|
370
488
|
},
|
|
371
489
|
},
|
|
372
490
|
variable: {
|
|
@@ -378,8 +496,10 @@ const commands = {
|
|
|
378
496
|
handler: (c, _a, f) => c.variables.set({
|
|
379
497
|
standard: json(f["standard"]),
|
|
380
498
|
local: json(f["local"]),
|
|
499
|
+
replace: f["replace"] === true,
|
|
500
|
+
projectKey: f["project-key"],
|
|
381
501
|
}),
|
|
382
|
-
usage: 'dss variable set --standard \'{"k":"v"}\' --local \'{"k":"v"}\'',
|
|
502
|
+
usage: 'dss variable set --standard \'{"k":"v"}\' --local \'{"k":"v"}\' [--replace] [--project-key KEY]',
|
|
383
503
|
},
|
|
384
504
|
},
|
|
385
505
|
connection: {
|
|
@@ -492,23 +612,25 @@ const commands = {
|
|
|
492
612
|
},
|
|
493
613
|
"save-jupyter": {
|
|
494
614
|
handler: (c, a, f) => {
|
|
495
|
-
requireArgs(a, 1, "dss notebook save-jupyter <name> --data '{...}'");
|
|
496
|
-
const data =
|
|
497
|
-
if (!data)
|
|
498
|
-
throw new UsageError("--data is required (notebook JSON content)");
|
|
615
|
+
requireArgs(a, 1, "dss notebook save-jupyter <name> [--data '{...}' | --data-file PATH | --stdin]");
|
|
616
|
+
const data = jsonInput(f);
|
|
617
|
+
if (!data) {
|
|
618
|
+
throw new UsageError("--data, --data-file, or --stdin is required (notebook JSON content).");
|
|
619
|
+
}
|
|
499
620
|
return c.notebooks.saveJupyter(a[0], data, f["project-key"]);
|
|
500
621
|
},
|
|
501
|
-
usage: "dss notebook save-jupyter <name> --data '{...}' [--project-key KEY]",
|
|
622
|
+
usage: "dss notebook save-jupyter <name> [--data '{...}' | --data-file PATH | --stdin] [--project-key KEY]",
|
|
502
623
|
},
|
|
503
624
|
"save-sql": {
|
|
504
625
|
handler: (c, a, f) => {
|
|
505
|
-
requireArgs(a, 1, "dss notebook save-sql <id> --data '{...}'");
|
|
506
|
-
const data =
|
|
507
|
-
if (!data)
|
|
508
|
-
throw new UsageError("--data is required (SQL notebook content JSON)");
|
|
626
|
+
requireArgs(a, 1, "dss notebook save-sql <id> [--data '{...}' | --data-file PATH | --stdin]");
|
|
627
|
+
const data = jsonInput(f);
|
|
628
|
+
if (!data) {
|
|
629
|
+
throw new UsageError("--data, --data-file, or --stdin is required (SQL notebook content JSON).");
|
|
630
|
+
}
|
|
509
631
|
return c.notebooks.saveSql(a[0], data, f["project-key"]);
|
|
510
632
|
},
|
|
511
|
-
usage: "dss notebook save-sql <id> --data '{...}' [--project-key KEY]",
|
|
633
|
+
usage: "dss notebook save-sql <id> [--data '{...}' | --data-file PATH | --stdin] [--project-key KEY]",
|
|
512
634
|
},
|
|
513
635
|
"clear-sql-history": {
|
|
514
636
|
handler: (c, a, f) => {
|
|
@@ -535,6 +657,8 @@ function printTopLevelHelp() {
|
|
|
535
657
|
" --url URL Dataiku DSS base URL (env: DATAIKU_URL)",
|
|
536
658
|
" --api-key KEY API key (env: DATAIKU_API_KEY)",
|
|
537
659
|
" --project-key KEY Default project key (env: DATAIKU_PROJECT_KEY)",
|
|
660
|
+
" --format FORMAT Output format: json|tsv|quiet",
|
|
661
|
+
" --verbose Log HTTP requests to stderr",
|
|
538
662
|
" --help Show help",
|
|
539
663
|
"",
|
|
540
664
|
"Resources:",
|
|
@@ -662,10 +786,12 @@ async function main() {
|
|
|
662
786
|
url,
|
|
663
787
|
apiKey,
|
|
664
788
|
projectKey: flags["project-key"] ?? process.env.DATAIKU_PROJECT_KEY,
|
|
789
|
+
verbose: flags["verbose"] === true,
|
|
665
790
|
});
|
|
666
791
|
const args = positional.slice(2);
|
|
792
|
+
const format = parseOutputFormat(flags["format"]);
|
|
667
793
|
const result = await actionMeta.handler(client, args, flags);
|
|
668
|
-
|
|
794
|
+
writeCommandResult(result, format);
|
|
669
795
|
}
|
|
670
796
|
main().catch((err) => {
|
|
671
797
|
if (err instanceof UsageError) {
|
|
@@ -681,7 +807,7 @@ main().catch((err) => {
|
|
|
681
807
|
if (err.retryHint)
|
|
682
808
|
payload.retryHint = err.retryHint;
|
|
683
809
|
process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
684
|
-
process.exit(
|
|
810
|
+
process.exit(err.category === "transient" ? 3 : 2);
|
|
685
811
|
}
|
|
686
812
|
const message = err instanceof Error ? err.message : String(err);
|
|
687
813
|
process.stderr.write(`${JSON.stringify({ error: message, }, null, 2)}\n`);
|
package/dist/src/client.d.ts
CHANGED
|
@@ -21,9 +21,11 @@ export interface DataikuClientConfig {
|
|
|
21
21
|
requestTimeoutMs?: number;
|
|
22
22
|
/** Max retry attempts for idempotent requests (default 4, capped at 10) */
|
|
23
23
|
retryMaxAttempts?: number;
|
|
24
|
+
/** Emit HTTP request/response logs to stderr for CLI debugging. */
|
|
25
|
+
verbose?: boolean;
|
|
24
26
|
/**
|
|
25
27
|
* Called when an API response fails schema validation but data is still usable.
|
|
26
|
-
* Default:
|
|
28
|
+
* Default: writes to stderr. Set to a throwing function for strict mode.
|
|
27
29
|
* @param method - resource method that triggered the warning (e.g. "datasets.list")
|
|
28
30
|
* @param errors - human-readable validation error strings
|
|
29
31
|
*/
|
|
@@ -35,6 +37,7 @@ export declare class DataikuClient {
|
|
|
35
37
|
private readonly defaultProjectKey;
|
|
36
38
|
private readonly requestTimeoutMs;
|
|
37
39
|
private readonly retryMaxAttempts;
|
|
40
|
+
private readonly verbose;
|
|
38
41
|
private readonly onValidationWarning;
|
|
39
42
|
private _projects?;
|
|
40
43
|
private _datasets?;
|
|
@@ -70,6 +73,7 @@ export declare class DataikuClient {
|
|
|
70
73
|
stream(path: string): Promise<Response>;
|
|
71
74
|
private getHeaders;
|
|
72
75
|
private getAnyHeaders;
|
|
76
|
+
private logVerbose;
|
|
73
77
|
/**
|
|
74
78
|
* Validate raw data against a TypeBox schema, throwing on structural mismatch.
|
|
75
79
|
* Resources call this instead of bare `as T` casts for validated responses.
|
package/dist/src/client.js
CHANGED
|
@@ -24,7 +24,7 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
|
24
24
|
/* Helpers */
|
|
25
25
|
/* ------------------------------------------------------------------ */
|
|
26
26
|
function defaultValidationWarning(method, errors) {
|
|
27
|
-
|
|
27
|
+
process.stderr.write(`[dataiku-sdk] Schema validation warning in ${method}:\n ${errors.join("\n ")}\n`);
|
|
28
28
|
}
|
|
29
29
|
function sleep(ms) {
|
|
30
30
|
return new Promise((r) => setTimeout(r, ms));
|
|
@@ -59,6 +59,7 @@ export class DataikuClient {
|
|
|
59
59
|
defaultProjectKey;
|
|
60
60
|
requestTimeoutMs;
|
|
61
61
|
retryMaxAttempts;
|
|
62
|
+
verbose;
|
|
62
63
|
onValidationWarning;
|
|
63
64
|
/* Resource namespaces — lazily initialized to break circular imports */
|
|
64
65
|
_projects;
|
|
@@ -118,6 +119,7 @@ export class DataikuClient {
|
|
|
118
119
|
this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
119
120
|
const rawMax = config.retryMaxAttempts ?? DEFAULT_RETRY_MAX_ATTEMPTS;
|
|
120
121
|
this.retryMaxAttempts = Math.min(Math.max(1, rawMax), MAX_RETRY_ATTEMPTS_CAP);
|
|
122
|
+
this.verbose = config.verbose === true;
|
|
121
123
|
this.onValidationWarning = config.onValidationWarning ?? defaultValidationWarning;
|
|
122
124
|
}
|
|
123
125
|
/* ---- public: project key resolution ---- */
|
|
@@ -206,6 +208,10 @@ export class DataikuClient {
|
|
|
206
208
|
Accept: "*/*",
|
|
207
209
|
};
|
|
208
210
|
}
|
|
211
|
+
logVerbose(message) {
|
|
212
|
+
if (this.verbose)
|
|
213
|
+
process.stderr.write(`[dss] ${message}\n`);
|
|
214
|
+
}
|
|
209
215
|
/* ---- public: schema-validated parsing ---- */
|
|
210
216
|
/**
|
|
211
217
|
* Validate raw data against a TypeBox schema, throwing on structural mismatch.
|
|
@@ -235,6 +241,9 @@ export class DataikuClient {
|
|
|
235
241
|
/* ---- private: JSON parsing ---- */
|
|
236
242
|
async parseJsonResponse(res) {
|
|
237
243
|
const text = await res.text();
|
|
244
|
+
// SAFETY: Empty 2xx responses from DSS are surfaced to callers as undefined
|
|
245
|
+
// cast to T. This keeps existing call sites stable, but callers that rely on
|
|
246
|
+
// an object shape must guard explicitly before dereferencing the result.
|
|
238
247
|
if (!text)
|
|
239
248
|
return undefined;
|
|
240
249
|
try {
|
|
@@ -253,13 +262,16 @@ export class DataikuClient {
|
|
|
253
262
|
const delaysMs = [];
|
|
254
263
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
255
264
|
let timedOut = false;
|
|
265
|
+
const startedAt = Date.now();
|
|
256
266
|
const controller = new AbortController();
|
|
257
267
|
const timeout = setTimeout(() => {
|
|
258
268
|
timedOut = true;
|
|
259
269
|
controller.abort();
|
|
260
270
|
}, this.requestTimeoutMs);
|
|
271
|
+
this.logVerbose(`${method} ${url}`);
|
|
261
272
|
try {
|
|
262
273
|
const res = await fetch(url, { ...init, method, signal: controller.signal, });
|
|
274
|
+
this.logVerbose(`${method} ${url} → ${res.status} (${Date.now() - startedAt}ms)`);
|
|
263
275
|
if (!res.ok) {
|
|
264
276
|
const text = await res.text();
|
|
265
277
|
const canRetry = retryEnabled && attempt < maxAttempts && isTransientError(res.status, text);
|
|
@@ -288,6 +300,7 @@ export class DataikuClient {
|
|
|
288
300
|
: error instanceof Error
|
|
289
301
|
? error.message
|
|
290
302
|
: "Unknown transport error";
|
|
303
|
+
this.logVerbose(`${method} ${url} → ERROR (${Date.now() - startedAt}ms) ${detail}`);
|
|
291
304
|
const statusText = timedOut ? "Request Timeout" : "Network Error";
|
|
292
305
|
throw new DataikuError(0, statusText, detail, buildRetryMetadata(method, retryEnabled, maxAttempts, attempt, delaysMs, timedOut));
|
|
293
306
|
}
|
package/dist/src/errors.js
CHANGED
|
@@ -39,6 +39,18 @@ export function classifyDataikuError(status, body) {
|
|
|
39
39
|
retryHint: "Request appears invalid for this endpoint. Fix parameters/payload before retrying.",
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
+
const isServerPermissionLike = status >= 500
|
|
43
|
+
&& (lowerBody.includes("not allowed to access")
|
|
44
|
+
|| lowerBody.includes("access denied")
|
|
45
|
+
|| (lowerBody.includes("permission")
|
|
46
|
+
&& (lowerBody.includes("cannot use") || lowerBody.includes("not allowed"))));
|
|
47
|
+
if (isServerPermissionLike) {
|
|
48
|
+
return {
|
|
49
|
+
category: "forbidden",
|
|
50
|
+
retryable: false,
|
|
51
|
+
retryHint: "Check API key validity and project permissions for the requested action.",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
42
54
|
if (status === 404) {
|
|
43
55
|
const isHtmlGatewayResponse = lowerBody.includes("<!doctype html>");
|
|
44
56
|
return {
|
|
@@ -9,7 +9,7 @@ export declare class ConnectionsResource extends BaseResource {
|
|
|
9
9
|
* Infers available connections.
|
|
10
10
|
*
|
|
11
11
|
* - fast (default): fetches the connection name list and maps to ConnectionSummary.
|
|
12
|
-
* Falls back to rich mode on any failure.
|
|
12
|
+
* Falls back to rich mode on any failure or empty result set.
|
|
13
13
|
* - rich: inspects project datasets to derive connection metadata
|
|
14
14
|
* (types, managed flag, db schemas).
|
|
15
15
|
*/
|
|
@@ -55,7 +55,7 @@ export class ConnectionsResource extends BaseResource {
|
|
|
55
55
|
* Infers available connections.
|
|
56
56
|
*
|
|
57
57
|
* - fast (default): fetches the connection name list and maps to ConnectionSummary.
|
|
58
|
-
* Falls back to rich mode on any failure.
|
|
58
|
+
* Falls back to rich mode on any failure or empty result set.
|
|
59
59
|
* - rich: inspects project datasets to derive connection metadata
|
|
60
60
|
* (types, managed flag, db schemas).
|
|
61
61
|
*/
|
|
@@ -65,13 +65,16 @@ export class ConnectionsResource extends BaseResource {
|
|
|
65
65
|
if (mode === "rich") {
|
|
66
66
|
return inferRichConnectionsFromDatasets(this.client, projectEnc);
|
|
67
67
|
}
|
|
68
|
-
// fast — attempt name list, fall back to rich on any error
|
|
68
|
+
// fast — attempt name list, fall back to rich on any error or empty result
|
|
69
69
|
try {
|
|
70
70
|
const names = await this.list();
|
|
71
|
-
|
|
71
|
+
if (names.length > 0) {
|
|
72
|
+
return names.map((name) => ({ name, }));
|
|
73
|
+
}
|
|
72
74
|
}
|
|
73
75
|
catch {
|
|
74
|
-
|
|
76
|
+
// Fall through to rich inference.
|
|
75
77
|
}
|
|
78
|
+
return inferRichConnectionsFromDatasets(this.client, projectEnc);
|
|
76
79
|
}
|
|
77
80
|
}
|
|
@@ -346,11 +346,17 @@ export class DatasetsResource extends BaseResource {
|
|
|
346
346
|
}
|
|
347
347
|
}
|
|
348
348
|
: undefined;
|
|
349
|
+
const shouldGzip = filePath.endsWith(".gz");
|
|
349
350
|
const nodeStream = Readable.fromWeb(res.body);
|
|
350
351
|
const csvTransform = tsvToCsvTransform(downloadLimit, onHeader);
|
|
351
|
-
const gzip = createGzip();
|
|
352
352
|
const fileOut = createWriteStream(filePath);
|
|
353
|
-
|
|
353
|
+
if (shouldGzip) {
|
|
354
|
+
const gzip = createGzip();
|
|
355
|
+
await pipeline(nodeStream, csvTransform, gzip, fileOut);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
await pipeline(nodeStream, csvTransform, fileOut);
|
|
359
|
+
}
|
|
354
360
|
return filePath;
|
|
355
361
|
}
|
|
356
362
|
/**
|
|
@@ -2,6 +2,7 @@ import type { FolderDetails, FolderItem, FolderSummary } from "../schemas.js";
|
|
|
2
2
|
import { BaseResource } from "./base.js";
|
|
3
3
|
export declare class FoldersResource extends BaseResource {
|
|
4
4
|
list(projectKey?: string): Promise<FolderSummary[]>;
|
|
5
|
+
resolveId(nameOrId: string, projectKey?: string): Promise<string>;
|
|
5
6
|
get(folderId: string, projectKey?: string): Promise<FolderDetails>;
|
|
6
7
|
contents(folderId: string, opts?: {
|
|
7
8
|
projectKey?: string;
|
|
@@ -21,6 +21,14 @@ export class FoldersResource extends BaseResource {
|
|
|
21
21
|
const raw = await this.client.get(`/public/api/projects/${this.enc(projectKey)}/managedfolders/`);
|
|
22
22
|
return this.client.safeParse(FolderSummaryArraySchema, raw, "folders.list");
|
|
23
23
|
}
|
|
24
|
+
async resolveId(nameOrId, projectKey) {
|
|
25
|
+
const folders = await this.list(projectKey);
|
|
26
|
+
if (folders.some((folder) => folder.id === nameOrId)) {
|
|
27
|
+
return nameOrId;
|
|
28
|
+
}
|
|
29
|
+
const match = folders.find((folder) => folder.name === nameOrId);
|
|
30
|
+
return match?.id ?? nameOrId;
|
|
31
|
+
}
|
|
24
32
|
async get(folderId, projectKey) {
|
|
25
33
|
const fEnc = encodeURIComponent(folderId);
|
|
26
34
|
const raw = await this.client.get(`/public/api/projects/${this.enc(projectKey)}/managedfolders/${fEnc}`);
|
|
@@ -18,7 +18,8 @@ export declare class JobsResource extends BaseResource {
|
|
|
18
18
|
get(jobId: string, projectKey?: string): Promise<Record<string, unknown>>;
|
|
19
19
|
/**
|
|
20
20
|
* Retrieve job log text.
|
|
21
|
-
* Returns the last `maxLogLines` lines (default
|
|
21
|
+
* Returns the last `maxLogLines` lines (default 500) from the tail.
|
|
22
|
+
* Use `0` or `-1` to return the full log without truncation.
|
|
22
23
|
*/
|
|
23
24
|
log(jobId: string, opts?: {
|
|
24
25
|
activity?: string;
|
|
@@ -3,7 +3,7 @@ import { BaseResource, } from "./base.js";
|
|
|
3
3
|
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
4
4
|
const MAX_POLL_INTERVAL_MS = 10_000;
|
|
5
5
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
6
|
-
const DEFAULT_MAX_LOG_LINES =
|
|
6
|
+
const DEFAULT_MAX_LOG_LINES = 500;
|
|
7
7
|
const TERMINAL_STATES = new Set([
|
|
8
8
|
"DONE",
|
|
9
9
|
"FAILED",
|
|
@@ -49,7 +49,8 @@ export class JobsResource extends BaseResource {
|
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
51
|
* Retrieve job log text.
|
|
52
|
-
* Returns the last `maxLogLines` lines (default
|
|
52
|
+
* Returns the last `maxLogLines` lines (default 500) from the tail.
|
|
53
|
+
* Use `0` or `-1` to return the full log without truncation.
|
|
53
54
|
*/
|
|
54
55
|
async log(jobId, opts) {
|
|
55
56
|
const jobEnc = encodeURIComponent(jobId);
|
|
@@ -57,8 +58,11 @@ export class JobsResource extends BaseResource {
|
|
|
57
58
|
const log = await this.client.getText(`/public/api/projects/${this.enc(opts?.projectKey)}/jobs/${jobEnc}/log/${query}`);
|
|
58
59
|
if (!log)
|
|
59
60
|
return "";
|
|
60
|
-
const lines = log.split("\n");
|
|
61
61
|
const limit = opts?.maxLogLines ?? DEFAULT_MAX_LOG_LINES;
|
|
62
|
+
if (limit === 0 || limit === -1) {
|
|
63
|
+
return log;
|
|
64
|
+
}
|
|
65
|
+
const lines = log.split("\n");
|
|
62
66
|
if (lines.length > limit) {
|
|
63
67
|
return lines.slice(-limit).join("\n");
|
|
64
68
|
}
|
|
@@ -7,13 +7,28 @@ export declare class NotebooksResource extends BaseResource {
|
|
|
7
7
|
getJupyter(name: string, projectKey?: string): Promise<JupyterNotebookContent>;
|
|
8
8
|
/** Save (overwrite) a Jupyter notebook's content. */
|
|
9
9
|
saveJupyter(name: string, content: JupyterNotebookContent, projectKey?: string): Promise<void>;
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Delete a Jupyter notebook.
|
|
12
|
+
*
|
|
13
|
+
* DSS public APIs can delete notebooks but do not expose notebook creation, so
|
|
14
|
+
* this can only target notebooks created outside this SDK (for example in the UI).
|
|
15
|
+
*/
|
|
11
16
|
deleteJupyter(name: string, projectKey?: string): Promise<void>;
|
|
12
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Clear all cell outputs from a Jupyter notebook.
|
|
19
|
+
*
|
|
20
|
+
* DSS public APIs do not expose a dedicated clear-outputs endpoint, so this
|
|
21
|
+
* method fetches the notebook, strips outputs locally, and saves it back.
|
|
22
|
+
*/
|
|
13
23
|
clearJupyterOutputs(name: string, projectKey?: string): Promise<void>;
|
|
14
24
|
/** List running kernel sessions for a Jupyter notebook. */
|
|
15
25
|
listJupyterSessions(name: string, projectKey?: string): Promise<NotebookSession[]>;
|
|
16
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Unload (stop) a running Jupyter notebook session.
|
|
28
|
+
*
|
|
29
|
+
* DSS public APIs do not expose notebook or session creation, so this only
|
|
30
|
+
* works for sessions started outside this SDK.
|
|
31
|
+
*/
|
|
17
32
|
unloadJupyter(name: string, sessionId: string, projectKey?: string): Promise<void>;
|
|
18
33
|
/** List all SQL notebooks in a project. */
|
|
19
34
|
listSql(projectKey?: string): Promise<SqlNotebookSummary[]>;
|
|
@@ -18,15 +18,30 @@ export class NotebooksResource extends BaseResource {
|
|
|
18
18
|
const nameEnc = encodeURIComponent(name);
|
|
19
19
|
await this.client.putVoid(`/public/api/projects/${this.enc(projectKey)}/jupyter-notebooks/${nameEnc}`, content);
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Delete a Jupyter notebook.
|
|
23
|
+
*
|
|
24
|
+
* DSS public APIs can delete notebooks but do not expose notebook creation, so
|
|
25
|
+
* this can only target notebooks created outside this SDK (for example in the UI).
|
|
26
|
+
*/
|
|
22
27
|
async deleteJupyter(name, projectKey) {
|
|
23
28
|
const nameEnc = encodeURIComponent(name);
|
|
24
29
|
await this.client.del(`/public/api/projects/${this.enc(projectKey)}/jupyter-notebooks/${nameEnc}`);
|
|
25
30
|
}
|
|
26
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Clear all cell outputs from a Jupyter notebook.
|
|
33
|
+
*
|
|
34
|
+
* DSS public APIs do not expose a dedicated clear-outputs endpoint, so this
|
|
35
|
+
* method fetches the notebook, strips outputs locally, and saves it back.
|
|
36
|
+
*/
|
|
27
37
|
async clearJupyterOutputs(name, projectKey) {
|
|
28
|
-
const
|
|
29
|
-
|
|
38
|
+
const notebook = await this.getJupyter(name, projectKey);
|
|
39
|
+
const clearedCells = notebook.cells.map((cell) => ({
|
|
40
|
+
...cell,
|
|
41
|
+
outputs: [],
|
|
42
|
+
execution_count: null,
|
|
43
|
+
}));
|
|
44
|
+
await this.saveJupyter(name, { ...notebook, cells: clearedCells, }, projectKey);
|
|
30
45
|
}
|
|
31
46
|
/** List running kernel sessions for a Jupyter notebook. */
|
|
32
47
|
async listJupyterSessions(name, projectKey) {
|
|
@@ -34,7 +49,12 @@ export class NotebooksResource extends BaseResource {
|
|
|
34
49
|
const raw = await this.client.get(`/public/api/projects/${this.enc(projectKey)}/jupyter-notebooks/${nameEnc}/sessions`);
|
|
35
50
|
return this.client.safeParse(NotebookSessionArraySchema, raw, "notebooks.sessionsJupyter");
|
|
36
51
|
}
|
|
37
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Unload (stop) a running Jupyter notebook session.
|
|
54
|
+
*
|
|
55
|
+
* DSS public APIs do not expose notebook or session creation, so this only
|
|
56
|
+
* works for sessions started outside this SDK.
|
|
57
|
+
*/
|
|
38
58
|
async unloadJupyter(name, sessionId, projectKey) {
|
|
39
59
|
const nameEnc = encodeURIComponent(name);
|
|
40
60
|
const sidEnc = encodeURIComponent(sessionId);
|
|
@@ -19,9 +19,18 @@ export declare class RecipesResource extends BaseResource {
|
|
|
19
19
|
create(opts: RecipeCreateOptions): Promise<RecipeCreateResult>;
|
|
20
20
|
/**
|
|
21
21
|
* Update a recipe by merging the patch into the current definition.
|
|
22
|
-
* The `recipe` sub-object is
|
|
22
|
+
* The `recipe` sub-object is deep-merged to preserve nested fields.
|
|
23
23
|
*/
|
|
24
24
|
update(recipeName: string, data: Record<string, unknown>, projectKey?: string): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Download a recipe code payload to a local file.
|
|
27
|
+
|
|
28
|
+
* Returns the path to the written file.
|
|
29
|
+
*/
|
|
30
|
+
downloadCode(recipeName: string, opts?: {
|
|
31
|
+
outputPath?: string;
|
|
32
|
+
projectKey?: string;
|
|
33
|
+
}): Promise<string>;
|
|
25
34
|
/** Delete a recipe. */
|
|
26
35
|
delete(recipeName: string, projectKey?: string): Promise<void>;
|
|
27
36
|
/**
|
|
@@ -2,6 +2,7 @@ import { writeFile, } from "node:fs/promises";
|
|
|
2
2
|
import { resolve, } from "node:path";
|
|
3
3
|
import { DataikuError, } from "../errors.js";
|
|
4
4
|
import { RecipeSummaryArraySchema, } from "../schemas.js";
|
|
5
|
+
import { deepMerge, } from "../utils/deep-merge.js";
|
|
5
6
|
import { sanitizeFileName, } from "../utils/sanitize.js";
|
|
6
7
|
import { BaseResource, } from "./base.js";
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
@@ -21,6 +22,22 @@ function asRecord(value) {
|
|
|
21
22
|
return undefined;
|
|
22
23
|
return value;
|
|
23
24
|
}
|
|
25
|
+
function inferRecipeCodeExtension(recipeType) {
|
|
26
|
+
const normalized = typeof recipeType === "string" ? recipeType.trim().toLowerCase() : "";
|
|
27
|
+
if (!normalized)
|
|
28
|
+
return ".txt";
|
|
29
|
+
if (normalized.includes("python") || normalized.includes("pyspark"))
|
|
30
|
+
return ".py";
|
|
31
|
+
if (normalized.includes("sql"))
|
|
32
|
+
return ".sql";
|
|
33
|
+
if (normalized === "r" || normalized.startsWith("r_"))
|
|
34
|
+
return ".R";
|
|
35
|
+
if (normalized.includes("scala"))
|
|
36
|
+
return ".scala";
|
|
37
|
+
if (normalized.includes("shell"))
|
|
38
|
+
return ".sh";
|
|
39
|
+
return ".txt";
|
|
40
|
+
}
|
|
24
41
|
// ---------------------------------------------------------------------------
|
|
25
42
|
// Helpers: retry predicate
|
|
26
43
|
// ---------------------------------------------------------------------------
|
|
@@ -53,7 +70,7 @@ export class RecipesResource extends BaseResource {
|
|
|
53
70
|
* Get a recipe definition (and optionally its payload).
|
|
54
71
|
* Returns the raw API response shape: `{ recipe, payload }`.
|
|
55
72
|
*/
|
|
56
|
-
get(recipeName, opts) {
|
|
73
|
+
async get(recipeName, opts) {
|
|
57
74
|
const enc = this.enc(opts?.projectKey);
|
|
58
75
|
const rnEnc = encodeURIComponent(recipeName);
|
|
59
76
|
const params = new URLSearchParams();
|
|
@@ -64,7 +81,12 @@ export class RecipesResource extends BaseResource {
|
|
|
64
81
|
params.set("payloadMaxLines", String(opts.payloadMaxLines));
|
|
65
82
|
const qs = params.toString();
|
|
66
83
|
const url = `/public/api/projects/${enc}/recipes/${rnEnc}${qs ? `?${qs}` : ""}`;
|
|
67
|
-
|
|
84
|
+
const result = await this.client.get(url);
|
|
85
|
+
const recipe = asRecord(result?.recipe);
|
|
86
|
+
if (!result || !recipe) {
|
|
87
|
+
throw new DataikuError(404, "Not Found", `Recipe "${recipeName}" not found in project "${this.resolveProjectKey(opts?.projectKey)}" (DSS returned empty response).`);
|
|
88
|
+
}
|
|
89
|
+
return { ...result, recipe, };
|
|
68
90
|
}
|
|
69
91
|
/** Create a recipe, with optional output dataset provisioning and join configuration. */
|
|
70
92
|
async create(opts) {
|
|
@@ -242,7 +264,7 @@ export class RecipesResource extends BaseResource {
|
|
|
242
264
|
}
|
|
243
265
|
/**
|
|
244
266
|
* Update a recipe by merging the patch into the current definition.
|
|
245
|
-
* The `recipe` sub-object is
|
|
267
|
+
* The `recipe` sub-object is deep-merged to preserve nested fields.
|
|
246
268
|
*/
|
|
247
269
|
async update(recipeName, data, projectKey) {
|
|
248
270
|
const enc = this.enc(projectKey);
|
|
@@ -252,13 +274,28 @@ export class RecipesResource extends BaseResource {
|
|
|
252
274
|
if (!currentRecipe) {
|
|
253
275
|
throw new Error(`Recipe "${recipeName}" was not found or returned an empty definition.`);
|
|
254
276
|
}
|
|
255
|
-
const mergedRecipe = {
|
|
256
|
-
...currentRecipe,
|
|
257
|
-
...data.recipe,
|
|
258
|
-
};
|
|
277
|
+
const mergedRecipe = deepMerge(currentRecipe, asRecord(data.recipe) ?? {});
|
|
259
278
|
const merged = { ...current, ...data, recipe: mergedRecipe, };
|
|
260
279
|
await this.client.put(`/public/api/projects/${enc}/recipes/${rnEnc}`, merged);
|
|
261
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Download a recipe code payload to a local file.
|
|
283
|
+
|
|
284
|
+
* Returns the path to the written file.
|
|
285
|
+
*/
|
|
286
|
+
async downloadCode(recipeName, opts) {
|
|
287
|
+
const result = await this.get(recipeName, {
|
|
288
|
+
includePayload: true,
|
|
289
|
+
projectKey: opts?.projectKey,
|
|
290
|
+
});
|
|
291
|
+
if (!result.payload) {
|
|
292
|
+
throw new Error(`Recipe "${recipeName}" has no code payload.`);
|
|
293
|
+
}
|
|
294
|
+
const safeRecipeName = sanitizeFileName(recipeName, "recipe");
|
|
295
|
+
const filePath = opts?.outputPath ?? resolve(process.cwd(), `${safeRecipeName}${inferRecipeCodeExtension(result.recipe.type)}`);
|
|
296
|
+
await writeFile(filePath, result.payload, "utf-8");
|
|
297
|
+
return filePath;
|
|
298
|
+
}
|
|
262
299
|
/** Delete a recipe. */
|
|
263
300
|
async delete(recipeName, projectKey) {
|
|
264
301
|
const enc = this.enc(projectKey);
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
+
import { DataikuError, } from "../errors.js";
|
|
1
2
|
import { BaseResource, } from "./base.js";
|
|
3
|
+
const UNSUPPORTED_SQL_DATASET_CONNECTION_DETAIL = "neither of sql nor hdfs type";
|
|
4
|
+
function isUnsupportedSqlDatasetConnectionError(error) {
|
|
5
|
+
if (!(error instanceof DataikuError))
|
|
6
|
+
return false;
|
|
7
|
+
const detail = `${error.statusText}\n${error.body}\n${error.message}`.toLowerCase();
|
|
8
|
+
return detail.includes(UNSUPPORTED_SQL_DATASET_CONNECTION_DETAIL);
|
|
9
|
+
}
|
|
10
|
+
function buildUnsupportedSqlDatasetConnectionMessage(datasetFullName) {
|
|
11
|
+
const subject = datasetFullName
|
|
12
|
+
? `Dataset "${datasetFullName}" uses a connection that DSS does not support for direct SQL queries.`
|
|
13
|
+
: "This query uses a connection that DSS does not support for direct SQL queries.";
|
|
14
|
+
return `${subject} Use --connection with a SQL-compatible connection instead.`;
|
|
15
|
+
}
|
|
2
16
|
export class SqlResource extends BaseResource {
|
|
3
17
|
/**
|
|
4
18
|
* Start a SQL query and return the queryId + schema.
|
|
@@ -6,7 +20,10 @@ export class SqlResource extends BaseResource {
|
|
|
6
20
|
* or `datasetFullName` (run against a dataset's connection).
|
|
7
21
|
*/
|
|
8
22
|
async startQuery(opts) {
|
|
9
|
-
return this.client.post(
|
|
23
|
+
return this.client.post("/public/api/sql/queries/", {
|
|
24
|
+
...opts,
|
|
25
|
+
type: opts.type ?? "sql",
|
|
26
|
+
});
|
|
10
27
|
}
|
|
11
28
|
/**
|
|
12
29
|
* Stream results of a started query as parsed JSON (array of arrays).
|
|
@@ -32,9 +49,19 @@ export class SqlResource extends BaseResource {
|
|
|
32
49
|
* This is the primary method most callers want.
|
|
33
50
|
*/
|
|
34
51
|
async query(opts) {
|
|
35
|
-
const {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
const queryOpts = { ...opts, type: opts.type ?? "sql", };
|
|
53
|
+
try {
|
|
54
|
+
const { queryId, schema, } = await this.startQuery(queryOpts);
|
|
55
|
+
const rows = await this.streamResults(queryId);
|
|
56
|
+
await this.finishStreaming(queryId);
|
|
57
|
+
return { queryId, schema, rows, };
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (!isUnsupportedSqlDatasetConnectionError(error))
|
|
61
|
+
throw error;
|
|
62
|
+
throw new Error(buildUnsupportedSqlDatasetConnectionMessage(queryOpts.datasetFullName), {
|
|
63
|
+
cause: error,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
39
66
|
}
|
|
40
67
|
}
|
|
@@ -7,6 +7,15 @@ export class VariablesResource extends BaseResource {
|
|
|
7
7
|
return this.client.safeParse(ProjectVariablesSchema, raw, "variables.get");
|
|
8
8
|
}
|
|
9
9
|
async set(opts) {
|
|
10
|
+
const enc = this.enc(opts.projectKey);
|
|
11
|
+
if (opts.replace === true) {
|
|
12
|
+
const replaced = {
|
|
13
|
+
standard: opts.standard ?? {},
|
|
14
|
+
local: opts.local ?? {},
|
|
15
|
+
};
|
|
16
|
+
await this.client.putVoid(`/public/api/projects/${enc}/variables/`, replaced);
|
|
17
|
+
return replaced;
|
|
18
|
+
}
|
|
10
19
|
if (opts.standard === undefined && opts.local === undefined) {
|
|
11
20
|
throw new Error("At least one of standard or local must be provided");
|
|
12
21
|
}
|
|
@@ -15,7 +24,6 @@ export class VariablesResource extends BaseResource {
|
|
|
15
24
|
standard: { ...existing.standard, ...opts.standard, },
|
|
16
25
|
local: { ...existing.local, ...opts.local, },
|
|
17
26
|
};
|
|
18
|
-
const enc = this.enc(opts.projectKey);
|
|
19
27
|
await this.client.putVoid(`/public/api/projects/${enc}/variables/`, merged);
|
|
20
28
|
return merged;
|
|
21
29
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dataiku-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Dataiku DSS SDK and CLI for programmatic access to DSS REST APIs",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"workspaces": [
|
|
6
|
+
"workspaces": [
|
|
7
|
+
"packages/*"
|
|
8
|
+
],
|
|
7
9
|
"main": "dist/src/index.js",
|
|
8
10
|
"types": "dist/src/index.d.ts",
|
|
9
11
|
"bin": {
|
|
@@ -298,8 +298,8 @@ export declare const JupyterNotebookSummarySchema: import("@sinclair/typebox").T
|
|
|
298
298
|
language: import("@sinclair/typebox").TString;
|
|
299
299
|
kernelSpec: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
300
300
|
name: import("@sinclair/typebox").TString;
|
|
301
|
-
display_name: import("@sinclair/typebox").TString
|
|
302
|
-
language: import("@sinclair/typebox").TString
|
|
301
|
+
display_name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
302
|
+
language: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
303
303
|
}>>;
|
|
304
304
|
}>;
|
|
305
305
|
export type JupyterNotebookSummary = Static<typeof JupyterNotebookSummarySchema>;
|
|
@@ -438,8 +438,8 @@ export declare const JupyterNotebookSummaryArraySchema: import("@sinclair/typebo
|
|
|
438
438
|
language: import("@sinclair/typebox").TString;
|
|
439
439
|
kernelSpec: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
440
440
|
name: import("@sinclair/typebox").TString;
|
|
441
|
-
display_name: import("@sinclair/typebox").TString
|
|
442
|
-
language: import("@sinclair/typebox").TString
|
|
441
|
+
display_name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
442
|
+
language: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
443
443
|
}>>;
|
|
444
444
|
}>>;
|
|
445
445
|
export declare const SqlNotebookSummaryArraySchema: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
@@ -306,8 +306,8 @@ export const JupyterNotebookSummarySchema = Type.Object({
|
|
|
306
306
|
language: Type.String(),
|
|
307
307
|
kernelSpec: Type.Optional(Type.Object({
|
|
308
308
|
name: Type.String(),
|
|
309
|
-
display_name: Type.String(),
|
|
310
|
-
language: Type.String(),
|
|
309
|
+
display_name: Type.Optional(Type.String()),
|
|
310
|
+
language: Type.Optional(Type.String()),
|
|
311
311
|
}, { additionalProperties: true, })),
|
|
312
312
|
}, { additionalProperties: true, });
|
|
313
313
|
export const JupyterNotebookContentSchema = Type.Object({
|