dataiku-sdk 0.5.1 → 0.6.1
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/packages/types/src/index.d.ts +4 -0
- package/dist/packages/types/src/index.js +1 -0
- package/dist/src/cli.js +1353 -128
- package/dist/src/errors.js +12 -0
- package/dist/src/index.d.ts +5 -5
- package/dist/src/index.js +2 -2
- package/dist/src/resources/connections.d.ts +10 -0
- package/dist/src/resources/connections.js +16 -0
- package/dist/src/resources/datasets.d.ts +36 -0
- package/dist/src/resources/datasets.js +80 -0
- package/dist/src/resources/jobs.d.ts +66 -19
- package/dist/src/resources/jobs.js +180 -33
- package/dist/src/resources/recipes.d.ts +80 -1
- package/dist/src/resources/recipes.js +349 -0
- package/dist/src/resources/scenarios.d.ts +38 -2
- package/dist/src/resources/scenarios.js +162 -3
- package/dist/src/resources/sql.js +84 -3
- package/dist/src/skill.d.ts +2 -2
- package/dist/src/skill.js +3 -3
- package/package.json +1 -1
- package/packages/types/dist/index.d.ts +4 -0
- package/packages/types/dist/index.js +1 -0
package/dist/src/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createHash, } from "node:crypto";
|
|
2
3
|
import { readFileSync, } from "node:fs";
|
|
3
|
-
import { writeFile, } from "node:fs/promises";
|
|
4
|
-
import { dirname, resolve, } from "node:path";
|
|
4
|
+
import { mkdir, writeFile, } from "node:fs/promises";
|
|
5
|
+
import { dirname, join, resolve, } from "node:path";
|
|
5
6
|
import { createInterface, } from "node:readline";
|
|
6
7
|
import { Writable, } from "node:stream";
|
|
7
8
|
import { fileURLToPath, } from "node:url";
|
|
@@ -9,29 +10,72 @@ import { validateCredentials, } from "./auth.js";
|
|
|
9
10
|
import { DataikuClient, } from "./client.js";
|
|
10
11
|
import { deleteCredentials, getCredentialsPath, loadCredentials, maskApiKey, saveCredentials, } from "./config.js";
|
|
11
12
|
import { DataikuError, dataikuErrorCode, } from "./errors.js";
|
|
13
|
+
import { buildDatasetCloneSettings, } from "./resources/datasets.js";
|
|
14
|
+
import { parseJobLogProgress, } from "./resources/jobs.js";
|
|
15
|
+
import { scenarioUpdatePreview, } from "./resources/scenarios.js";
|
|
12
16
|
import { AGENTS, detectAgents, findWorkspaceRoot, installSkill, } from "./skill.js";
|
|
13
17
|
import { appendCleanupLedgerEntry, readCleanupLedger, } from "./utils/cleanup-ledger.js";
|
|
14
18
|
import { deepMerge, } from "./utils/deep-merge.js";
|
|
19
|
+
import { sanitizeFileName, } from "./utils/sanitize.js";
|
|
15
20
|
// ---------------------------------------------------------------------------
|
|
16
21
|
// Utility helpers
|
|
17
22
|
// ---------------------------------------------------------------------------
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
dir = dirname(dir);
|
|
28
|
-
}
|
|
23
|
+
function findPackageRoot() {
|
|
24
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
for (let i = 0; i < 5; i++) {
|
|
26
|
+
try {
|
|
27
|
+
readFileSync(resolve(dir, "package.json"), "utf-8");
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
dir = dirname(dir);
|
|
29
32
|
}
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
function packageVersion(packageRoot) {
|
|
37
|
+
if (!packageRoot)
|
|
30
38
|
return "unknown";
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf-8")).version;
|
|
31
41
|
}
|
|
32
42
|
catch {
|
|
33
43
|
return "unknown";
|
|
34
44
|
}
|
|
45
|
+
}
|
|
46
|
+
function gitDirectory(packageRoot) {
|
|
47
|
+
try {
|
|
48
|
+
const gitFile = readFileSync(resolve(packageRoot, ".git"), "utf-8").trim();
|
|
49
|
+
if (gitFile.startsWith("gitdir:")) {
|
|
50
|
+
return resolve(packageRoot, gitFile.slice("gitdir:".length).trim());
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Normal checkouts have a .git directory, not a .git file.
|
|
55
|
+
}
|
|
56
|
+
return resolve(packageRoot, ".git");
|
|
57
|
+
}
|
|
58
|
+
function gitRevision(packageRoot) {
|
|
59
|
+
if (!packageRoot)
|
|
60
|
+
return undefined;
|
|
61
|
+
try {
|
|
62
|
+
const gitDir = gitDirectory(packageRoot);
|
|
63
|
+
const head = readFileSync(resolve(gitDir, "HEAD"), "utf-8").trim();
|
|
64
|
+
if (!head.startsWith("ref:"))
|
|
65
|
+
return head.slice(0, 7);
|
|
66
|
+
const ref = head.slice("ref:".length).trim();
|
|
67
|
+
const full = readFileSync(resolve(gitDir, ref), "utf-8").trim();
|
|
68
|
+
return full.slice(0, 7);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const PACKAGE_ROOT = findPackageRoot();
|
|
75
|
+
const CLI_VERSION = packageVersion(PACKAGE_ROOT);
|
|
76
|
+
const CLI_VERSION_LABEL = (() => {
|
|
77
|
+
const revision = gitRevision(PACKAGE_ROOT);
|
|
78
|
+
return revision ? `${CLI_VERSION}+g${revision}` : CLI_VERSION;
|
|
35
79
|
})();
|
|
36
80
|
function num(v) {
|
|
37
81
|
if (typeof v !== "string")
|
|
@@ -43,18 +87,166 @@ function jobBuildTargetType(v) {
|
|
|
43
87
|
if (v === undefined)
|
|
44
88
|
return "DATASET";
|
|
45
89
|
if (typeof v !== "string") {
|
|
46
|
-
throw new UsageError("Invalid
|
|
90
|
+
throw new UsageError("Invalid job target type. Use dataset or managed-folder.", "invalid_enum");
|
|
47
91
|
}
|
|
48
92
|
const normalized = v.trim().toUpperCase().replace(/-/g, "_");
|
|
49
93
|
if (normalized === "DATASET" || normalized === "MANAGED_FOLDER")
|
|
50
94
|
return normalized;
|
|
51
|
-
throw new UsageError("Invalid
|
|
95
|
+
throw new UsageError("Invalid job target type. Use dataset or managed-folder.", "invalid_enum");
|
|
96
|
+
}
|
|
97
|
+
function jobBuildTargetTypeFromFlags(flags) {
|
|
98
|
+
return jobBuildTargetType(flags["target-type"] ?? flags["type"]);
|
|
99
|
+
}
|
|
100
|
+
function maxLogLinesFromFlags(flags) {
|
|
101
|
+
return num(flags["max-log-lines"] ?? flags["max-lines"]);
|
|
102
|
+
}
|
|
103
|
+
function jobLogFilterFromFlag(v) {
|
|
104
|
+
if (v === undefined)
|
|
105
|
+
return undefined;
|
|
106
|
+
if (typeof v !== "string") {
|
|
107
|
+
throw new UsageError("Invalid --log-filter value. Use stdout, stderr, user, or errors.", "invalid_enum");
|
|
108
|
+
}
|
|
109
|
+
const normalized = v.trim().toLowerCase();
|
|
110
|
+
if (normalized === "stdout" || normalized === "stderr" || normalized === "user"
|
|
111
|
+
|| normalized === "errors") {
|
|
112
|
+
return normalized;
|
|
113
|
+
}
|
|
114
|
+
throw new UsageError("Invalid --log-filter value. Use stdout, stderr, user, or errors.", "invalid_enum");
|
|
115
|
+
}
|
|
116
|
+
function recipeBackupPath(recipeName, backupDir) {
|
|
117
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
118
|
+
return join(backupDir, `${sanitizeFileName(recipeName, "recipe")}-${stamp}.recipe-backup.json`);
|
|
119
|
+
}
|
|
120
|
+
function sha256Hex(value) {
|
|
121
|
+
return createHash("sha256").update(value).digest("hex");
|
|
122
|
+
}
|
|
123
|
+
function normalizeLineEndings(value) {
|
|
124
|
+
return value.replace(/\r\n/g, "\n");
|
|
125
|
+
}
|
|
126
|
+
function stableJson(value) {
|
|
127
|
+
if (value === undefined)
|
|
128
|
+
return "undefined";
|
|
129
|
+
if (value === null || typeof value !== "object")
|
|
130
|
+
return JSON.stringify(value);
|
|
131
|
+
if (Array.isArray(value))
|
|
132
|
+
return `[${value.map((item) => stableJson(item)).join(",")}]`;
|
|
133
|
+
const entries = Object.entries(value).sort(([a,], [b,]) => a.localeCompare(b));
|
|
134
|
+
return `{${entries.map(([key, item,]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
|
|
135
|
+
}
|
|
136
|
+
function stableHash(value) {
|
|
137
|
+
return sha256Hex(stableJson(value));
|
|
138
|
+
}
|
|
139
|
+
function recipeCodeEnv(recipe) {
|
|
140
|
+
const params = recipe.params;
|
|
141
|
+
if (!params || typeof params !== "object" || Array.isArray(params))
|
|
142
|
+
return undefined;
|
|
143
|
+
return params.envSelection;
|
|
144
|
+
}
|
|
145
|
+
function recipeGraph(recipe) {
|
|
146
|
+
return {
|
|
147
|
+
inputs: recipe.inputs,
|
|
148
|
+
outputs: recipe.outputs,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function recipeBackupDocument(recipeName, projectKey, current) {
|
|
152
|
+
return {
|
|
153
|
+
resource: "recipe",
|
|
154
|
+
recipeName,
|
|
155
|
+
projectKey,
|
|
156
|
+
createdAt: new Date().toISOString(),
|
|
157
|
+
versionTag: current.recipe.versionTag,
|
|
158
|
+
payloadHash: sha256Hex(current.payload ?? ""),
|
|
159
|
+
graphHash: stableHash(recipeGraph(current.recipe)),
|
|
160
|
+
normalizedPayloadHash: sha256Hex(normalizeLineEndings(current.payload ?? "")),
|
|
161
|
+
codeEnvHash: stableHash(recipeCodeEnv(current.recipe)),
|
|
162
|
+
codeEnv: recipeCodeEnv(current.recipe),
|
|
163
|
+
recipe: current.recipe,
|
|
164
|
+
payload: current.payload ?? "",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function readRecipeBackup(backupPath) {
|
|
168
|
+
const raw = readFileSync(backupPath, "utf-8");
|
|
169
|
+
try {
|
|
170
|
+
const parsed = JSON.parse(raw);
|
|
171
|
+
if (parsed && typeof parsed === "object" && parsed.resource === "recipe")
|
|
172
|
+
return parsed;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Backward-compatible payload-only backups are handled below.
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
resource: "recipe",
|
|
179
|
+
recipeName: "unknown",
|
|
180
|
+
payloadHash: sha256Hex(raw),
|
|
181
|
+
payload: raw,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function recipeRunShouldWait(flags) {
|
|
185
|
+
if (flags["wait"] === true && flags["no-wait"] === true) {
|
|
186
|
+
throw new UsageError("--wait and --no-wait are mutually exclusive.", "invalid_enum");
|
|
187
|
+
}
|
|
188
|
+
const waitImplied = flags["include-logs"] === true
|
|
189
|
+
|| flags["summary"] === true
|
|
190
|
+
|| flags["timeout"] !== undefined
|
|
191
|
+
|| flags["poll-interval"] !== undefined;
|
|
192
|
+
if (flags["no-wait"] === true && waitImplied) {
|
|
193
|
+
throw new UsageError("--include-logs, --summary, --timeout, and --poll-interval require waiting; remove --no-wait.", "invalid_enum");
|
|
194
|
+
}
|
|
195
|
+
return flags["no-wait"] !== true && (flags["wait"] === true || waitImplied);
|
|
52
196
|
}
|
|
53
197
|
function splitCsvFlag(v) {
|
|
54
198
|
if (typeof v !== "string")
|
|
55
199
|
return [];
|
|
56
200
|
return v.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
57
201
|
}
|
|
202
|
+
function recipeInputDatasetsFromFlags(flags) {
|
|
203
|
+
const inputs = splitCsvFlag(flags["input"]);
|
|
204
|
+
return inputs.length > 0 ? inputs : undefined;
|
|
205
|
+
}
|
|
206
|
+
function rewritePairsFromFlags(flags, flagName) {
|
|
207
|
+
const rewrites = {};
|
|
208
|
+
for (const spec of splitCsvFlag(flags[flagName])) {
|
|
209
|
+
const idx = spec.indexOf("=");
|
|
210
|
+
if (idx <= 0 || idx === spec.length - 1) {
|
|
211
|
+
throw new UsageError(`--${flagName} values must use FROM=TO.`, "invalid_enum");
|
|
212
|
+
}
|
|
213
|
+
const from = spec.slice(0, idx).trim();
|
|
214
|
+
const to = spec.slice(idx + 1).trim();
|
|
215
|
+
if (!from || !to)
|
|
216
|
+
throw new UsageError(`--${flagName} values must use FROM=TO.`, "invalid_enum");
|
|
217
|
+
rewrites[from] = to;
|
|
218
|
+
}
|
|
219
|
+
return rewrites;
|
|
220
|
+
}
|
|
221
|
+
function plainRecord(value) {
|
|
222
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
223
|
+
? value
|
|
224
|
+
: undefined;
|
|
225
|
+
}
|
|
226
|
+
function datasetSourceSummary(details) {
|
|
227
|
+
const params = details.params ?? {};
|
|
228
|
+
return {
|
|
229
|
+
resource: "dataset",
|
|
230
|
+
name: details.name,
|
|
231
|
+
projectKey: details.projectKey,
|
|
232
|
+
type: details.type,
|
|
233
|
+
managed: details.managed,
|
|
234
|
+
connection: params.connection,
|
|
235
|
+
catalog: params.catalog,
|
|
236
|
+
schema: params.schema,
|
|
237
|
+
table: params.table,
|
|
238
|
+
path: params.path,
|
|
239
|
+
folderSmartId: params.folderSmartId,
|
|
240
|
+
formatType: details.formatType,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function requiredStringFlag(flags, name, usage) {
|
|
244
|
+
const value = flags[name];
|
|
245
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
246
|
+
throw new UsageError(`--${name} is required. Usage: ${usage}`, "missing_required_flag");
|
|
247
|
+
}
|
|
248
|
+
return value.trim();
|
|
249
|
+
}
|
|
58
250
|
function flowZoneId(value) {
|
|
59
251
|
const trimmed = value.trim();
|
|
60
252
|
if (!trimmed)
|
|
@@ -125,27 +317,298 @@ function flowZoneMoveItems(flags) {
|
|
|
125
317
|
}
|
|
126
318
|
return items;
|
|
127
319
|
}
|
|
128
|
-
function
|
|
320
|
+
function flowZoneItems(zone) {
|
|
321
|
+
return [...(zone.items ?? []), ...(zone.shared ?? []),];
|
|
322
|
+
}
|
|
323
|
+
function flowZoneContains(zone, object) {
|
|
324
|
+
return flowZoneItems(zone).some((item) => item.objectId === object.objectId
|
|
325
|
+
&& item.objectType === object.objectType
|
|
326
|
+
&& (object.projectKey === undefined || item.projectKey === object.projectKey));
|
|
327
|
+
}
|
|
328
|
+
function flowZoneSummary(zone, object) {
|
|
329
|
+
const items = flowZoneItems(zone);
|
|
330
|
+
return {
|
|
331
|
+
id: zone.id,
|
|
332
|
+
name: zone.name,
|
|
333
|
+
itemCount: items.length,
|
|
334
|
+
...(object ? { containsMatchingObject: flowZoneContains(zone, object), } : {}),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function flowZoneDetailSummary(zone) {
|
|
338
|
+
return {
|
|
339
|
+
...flowZoneSummary(zone),
|
|
340
|
+
items: flowZoneItems(zone),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
async function resolveFlowZoneIdFromFlags(client, flags, projectKey) {
|
|
344
|
+
const zoneId = typeof flags["zone-id"] === "string" ? flags["zone-id"].trim() : "";
|
|
345
|
+
if (zoneId)
|
|
346
|
+
return zoneId;
|
|
347
|
+
const zone = typeof flags["zone"] === "string" ? flags["zone"].trim() : "";
|
|
348
|
+
if (!zone)
|
|
349
|
+
return undefined;
|
|
350
|
+
const zones = await client.flowZones.list(projectKey);
|
|
351
|
+
const match = zones.find((candidate) => candidate.id === zone || candidate.name === zone);
|
|
352
|
+
if (!match)
|
|
353
|
+
throw new UsageError(`Flow zone not found: ${zone}`, "invalid_enum");
|
|
354
|
+
return match.id;
|
|
355
|
+
}
|
|
356
|
+
async function moveCreatedItemsToZone(client, flags, items, projectKey) {
|
|
357
|
+
const zoneId = await resolveFlowZoneIdFromFlags(client, flags, projectKey);
|
|
358
|
+
if (!zoneId || items.length === 0)
|
|
359
|
+
return {};
|
|
360
|
+
await client.flowZones.moveItems(zoneId, items, projectKey);
|
|
361
|
+
return { zoneId, moved: items, };
|
|
362
|
+
}
|
|
363
|
+
function nestedValue(value, path) {
|
|
364
|
+
let current = value;
|
|
365
|
+
for (const key of path) {
|
|
366
|
+
const record = plainRecord(current);
|
|
367
|
+
if (!record)
|
|
368
|
+
return undefined;
|
|
369
|
+
current = record[key];
|
|
370
|
+
}
|
|
371
|
+
return current;
|
|
372
|
+
}
|
|
373
|
+
function stringPath(value, path) {
|
|
374
|
+
const item = nestedValue(value, path);
|
|
375
|
+
return typeof item === "string" && item.length > 0 ? item : undefined;
|
|
376
|
+
}
|
|
377
|
+
function numberPath(value, path) {
|
|
378
|
+
const item = nestedValue(value, path);
|
|
379
|
+
return typeof item === "number" && Number.isFinite(item) ? item : undefined;
|
|
380
|
+
}
|
|
381
|
+
function firstNumberPath(value, paths) {
|
|
382
|
+
for (const path of paths) {
|
|
383
|
+
const item = numberPath(value, path);
|
|
384
|
+
if (item !== undefined)
|
|
385
|
+
return item;
|
|
386
|
+
}
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
function jobSummaryId(job, fallback) {
|
|
390
|
+
return stringPath(job, ["baseStatus", "def", "id",])
|
|
391
|
+
?? stringPath(job, ["def", "id",])
|
|
392
|
+
?? stringPath(job, ["id",])
|
|
393
|
+
?? fallback
|
|
394
|
+
?? "unknown";
|
|
395
|
+
}
|
|
396
|
+
function jobSummaryType(job) {
|
|
397
|
+
return stringPath(job, ["baseStatus", "def", "type",])
|
|
398
|
+
?? stringPath(job, ["def", "type",])
|
|
399
|
+
?? stringPath(job, ["type",])
|
|
400
|
+
?? "unknown";
|
|
401
|
+
}
|
|
402
|
+
function jobSummaryState(job) {
|
|
403
|
+
return stringPath(job, ["baseStatus", "state",])
|
|
404
|
+
?? stringPath(job, ["state",])
|
|
405
|
+
?? "unknown";
|
|
406
|
+
}
|
|
407
|
+
function filteredJobList(jobs, flags) {
|
|
408
|
+
const state = typeof flags["state"] === "string" ? flags["state"].trim().toUpperCase() : "";
|
|
409
|
+
const contains = typeof flags["contains"] === "string"
|
|
410
|
+
? flags["contains"].trim().toLowerCase()
|
|
411
|
+
: "";
|
|
412
|
+
const output = typeof flags["output"] === "string" ? flags["output"].trim().toLowerCase() : "";
|
|
413
|
+
let result = jobs.filter((job) => {
|
|
414
|
+
if (state && jobSummaryState(job).toUpperCase() !== state)
|
|
415
|
+
return false;
|
|
416
|
+
const text = JSON.stringify(job).toLowerCase();
|
|
417
|
+
if (contains && !text.includes(contains))
|
|
418
|
+
return false;
|
|
419
|
+
if (output && !text.includes(output))
|
|
420
|
+
return false;
|
|
421
|
+
return true;
|
|
422
|
+
});
|
|
423
|
+
const limit = flags["latest"] === true ? 1 : num(flags["limit"]);
|
|
424
|
+
if (limit !== undefined)
|
|
425
|
+
result = result.slice(0, Math.max(0, limit));
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
function maxNumber(values) {
|
|
429
|
+
return values.length === 0 ? 0 : Math.max(...values);
|
|
430
|
+
}
|
|
431
|
+
function collectWarningCounts(value, inActivity, counts) {
|
|
432
|
+
if (Array.isArray(value)) {
|
|
433
|
+
for (const item of value)
|
|
434
|
+
collectWarningCounts(item, inActivity, counts);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const record = plainRecord(value);
|
|
438
|
+
if (!record)
|
|
439
|
+
return;
|
|
440
|
+
for (const [key, item,] of Object.entries(record)) {
|
|
441
|
+
const lower = key.toLowerCase();
|
|
442
|
+
const nextInActivity = inActivity || lower.includes("activit");
|
|
443
|
+
if (lower.includes("warn")) {
|
|
444
|
+
const target = nextInActivity ? counts.activity : counts.dss;
|
|
445
|
+
if (typeof item === "number" && Number.isFinite(item))
|
|
446
|
+
target.push(item);
|
|
447
|
+
else if (Array.isArray(item))
|
|
448
|
+
target.push(item.length);
|
|
449
|
+
}
|
|
450
|
+
collectWarningCounts(item, nextInActivity, counts);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function jobWarningSummary(details, log) {
|
|
454
|
+
const counts = { dss: [], activity: [], };
|
|
455
|
+
collectWarningCounts(details, false, counts);
|
|
456
|
+
const warningLines = log
|
|
457
|
+
? log.split(/\r?\n/).map((line) => line.trim()).filter((line) => /\bwarn(?:ing)?\b/i.test(line))
|
|
458
|
+
: [];
|
|
459
|
+
return {
|
|
460
|
+
dssSummaryWarningCount: maxNumber(counts.dss),
|
|
461
|
+
activityWarningCount: maxNumber(counts.activity),
|
|
462
|
+
logWarnLineCount: warningLines.length,
|
|
463
|
+
sampledWarningMessages: warningLines.slice(0, 5),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function jobDurationMs(details) {
|
|
467
|
+
const started = firstNumberPath(details, [
|
|
468
|
+
["baseStatus", "startTime",],
|
|
469
|
+
["baseStatus", "start",],
|
|
470
|
+
["startTime",],
|
|
471
|
+
["start",],
|
|
472
|
+
]);
|
|
473
|
+
const ended = firstNumberPath(details, [
|
|
474
|
+
["baseStatus", "endTime",],
|
|
475
|
+
["baseStatus", "end",],
|
|
476
|
+
["endTime",],
|
|
477
|
+
["end",],
|
|
478
|
+
]);
|
|
479
|
+
return started !== undefined && ended !== undefined && ended >= started
|
|
480
|
+
? ended - started
|
|
481
|
+
: undefined;
|
|
482
|
+
}
|
|
483
|
+
async function jobInspectionSummary(client, jobId, flags) {
|
|
484
|
+
const projectKey = flags["project-key"];
|
|
485
|
+
const details = await client.jobs.get(jobId, projectKey);
|
|
486
|
+
let log;
|
|
487
|
+
let logError;
|
|
488
|
+
try {
|
|
489
|
+
log = await client.jobs.log(jobId, {
|
|
490
|
+
activity: flags["activity"],
|
|
491
|
+
logId: flags["log-id"],
|
|
492
|
+
maxLogLines: maxLogLinesFromFlags(flags),
|
|
493
|
+
projectKey,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
logError = error instanceof Error ? error.message : String(error);
|
|
498
|
+
}
|
|
499
|
+
const durationMs = jobDurationMs(details);
|
|
500
|
+
const progress = log ? parseJobLogProgress(log, durationMs) : undefined;
|
|
501
|
+
const logLines = log
|
|
502
|
+
? log.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0)
|
|
503
|
+
: [];
|
|
504
|
+
const maxSummaryLines = Math.max(1, maxLogLinesFromFlags(flags) ?? 20);
|
|
505
|
+
const outputs = nestedValue(details, ["baseStatus", "def", "outputs",])
|
|
506
|
+
?? nestedValue(details, ["def", "outputs",])
|
|
507
|
+
?? details.outputs;
|
|
508
|
+
return {
|
|
509
|
+
resource: "job",
|
|
510
|
+
jobId: jobSummaryId(details, jobId),
|
|
511
|
+
state: jobSummaryState(details),
|
|
512
|
+
type: jobSummaryType(details),
|
|
513
|
+
...(durationMs !== undefined ? { durationMs, } : {}),
|
|
514
|
+
...(outputs !== undefined ? { outputs, } : {}),
|
|
515
|
+
warnings: jobWarningSummary(details, log),
|
|
516
|
+
...(progress
|
|
517
|
+
? {
|
|
518
|
+
progress,
|
|
519
|
+
latestUsefulProgressLine: progress.lastProgressLine,
|
|
520
|
+
doneLine: progress.doneLine,
|
|
521
|
+
}
|
|
522
|
+
: {}),
|
|
523
|
+
logSummary: {
|
|
524
|
+
lineCount: logLines.length,
|
|
525
|
+
lines: logLines.slice(-maxSummaryLines),
|
|
526
|
+
...(logError ? { error: logError, } : {}),
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function stripUtf8Bom(text) {
|
|
531
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
532
|
+
}
|
|
533
|
+
function parseJsonValue(text, source) {
|
|
534
|
+
try {
|
|
535
|
+
return JSON.parse(stripUtf8Bom(text));
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
539
|
+
throw new UsageError(`Invalid JSON in ${source}: ${message}`, "validation_failed");
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function expectJsonObject(value, source) {
|
|
543
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
544
|
+
return value;
|
|
545
|
+
}
|
|
546
|
+
throw new UsageError(`Expected JSON object in ${source}.`, "validation_failed");
|
|
547
|
+
}
|
|
548
|
+
function parseJsonObject(text, source) {
|
|
549
|
+
return expectJsonObject(parseJsonValue(text, source), source);
|
|
550
|
+
}
|
|
551
|
+
function json(v, source = "JSON flag") {
|
|
129
552
|
if (typeof v !== "string")
|
|
130
553
|
return undefined;
|
|
131
|
-
return
|
|
554
|
+
return parseJsonObject(v, source);
|
|
132
555
|
}
|
|
133
|
-
const SQL_QUERY_USAGE = "dss sql query [SQL | --sql QUERY | --sql-file PATH | --sql - | --stdin] (--connection CONN | --dataset FULL_NAME) [--database DB] [--project-key KEY]";
|
|
556
|
+
const SQL_QUERY_USAGE = "dss sql query [SQL | --sql QUERY | --sql-file PATH | --sql - | --stdin] (--connection CONN | --dataset FULL_NAME) [--database DB] [--output PATH|--output-file PATH] [--request-timeout MS] [--project-key KEY]";
|
|
134
557
|
function readStdinText() {
|
|
135
558
|
return readFileSync(0, "utf-8");
|
|
136
559
|
}
|
|
137
560
|
function jsonInput(flags) {
|
|
138
|
-
if (flags["stdin"] === true)
|
|
139
|
-
return
|
|
140
|
-
}
|
|
561
|
+
if (flags["stdin"] === true)
|
|
562
|
+
return parseJsonObject(readStdinText(), "stdin");
|
|
141
563
|
if (typeof flags["data-file"] === "string") {
|
|
142
|
-
return
|
|
564
|
+
return parseJsonObject(readFileSync(flags["data-file"], "utf-8"), flags["data-file"]);
|
|
143
565
|
}
|
|
144
|
-
if (typeof flags["data"] === "string")
|
|
145
|
-
return
|
|
566
|
+
if (typeof flags["data"] === "string")
|
|
567
|
+
return parseJsonObject(flags["data"], "--data");
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
function unknownJsonInput(flags) {
|
|
571
|
+
if (flags["stdin"] === true)
|
|
572
|
+
return parseJsonValue(readStdinText(), "stdin");
|
|
573
|
+
if (typeof flags["data-file"] === "string") {
|
|
574
|
+
return parseJsonValue(readFileSync(flags["data-file"], "utf-8"), flags["data-file"]);
|
|
146
575
|
}
|
|
576
|
+
if (typeof flags["data"] === "string")
|
|
577
|
+
return parseJsonValue(flags["data"], "--data");
|
|
147
578
|
return undefined;
|
|
148
579
|
}
|
|
580
|
+
function schemaColumnsInput(flags, usage) {
|
|
581
|
+
const input = unknownJsonInput(flags);
|
|
582
|
+
if (input === undefined) {
|
|
583
|
+
throw new UsageError(`--data, --data-file, or --stdin is required. Usage: ${usage}`);
|
|
584
|
+
}
|
|
585
|
+
const columns = Array.isArray(input)
|
|
586
|
+
? input
|
|
587
|
+
: input && typeof input === "object" && Array.isArray(input.columns)
|
|
588
|
+
? input.columns
|
|
589
|
+
: undefined;
|
|
590
|
+
if (!columns) {
|
|
591
|
+
throw new UsageError("Schema input must be an array of columns or an object with a columns array.");
|
|
592
|
+
}
|
|
593
|
+
return columns.map((column, index) => {
|
|
594
|
+
if (!column || typeof column !== "object" || Array.isArray(column)) {
|
|
595
|
+
throw new UsageError(`Schema column at index ${index} must be an object.`);
|
|
596
|
+
}
|
|
597
|
+
const record = column;
|
|
598
|
+
if (typeof record.name !== "string" || record.name.length === 0) {
|
|
599
|
+
throw new UsageError(`Schema column at index ${index} is missing string field "name".`);
|
|
600
|
+
}
|
|
601
|
+
if (typeof record.type !== "string" || record.type.length === 0) {
|
|
602
|
+
throw new UsageError(`Schema column "${record.name}" is missing string field "type".`);
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
...record,
|
|
606
|
+
name: record.name,
|
|
607
|
+
type: record.type,
|
|
608
|
+
...(typeof record.comment === "string" ? { comment: record.comment, } : {}),
|
|
609
|
+
};
|
|
610
|
+
});
|
|
611
|
+
}
|
|
149
612
|
function textInput(flags) {
|
|
150
613
|
if (typeof flags["content"] === "string")
|
|
151
614
|
return flags["content"];
|
|
@@ -155,7 +618,7 @@ function textInput(flags) {
|
|
|
155
618
|
}
|
|
156
619
|
function requiredJsonInput(flags, message) {
|
|
157
620
|
const data = jsonInput(flags);
|
|
158
|
-
if (
|
|
621
|
+
if (data === undefined)
|
|
159
622
|
throw new UsageError(message);
|
|
160
623
|
return data;
|
|
161
624
|
}
|
|
@@ -289,6 +752,28 @@ function formatLineDiff(remoteName, localPath, remoteContent, localContent) {
|
|
|
289
752
|
function writeCommandResult(result) {
|
|
290
753
|
process.stdout.write(`${JSON.stringify(result ?? { ok: true, }, null, 2)}\n`);
|
|
291
754
|
}
|
|
755
|
+
function transientBodyWithTargetContext(body, target, elapsedMs) {
|
|
756
|
+
try {
|
|
757
|
+
const parsed = JSON.parse(body);
|
|
758
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
759
|
+
const record = parsed;
|
|
760
|
+
const message = typeof record.message === "string" && record.message.length > 0
|
|
761
|
+
? `Target: ${target}\nElapsed: ${elapsedMs}ms\n${record.message}`
|
|
762
|
+
: `Target: ${target}\nElapsed: ${elapsedMs}ms`;
|
|
763
|
+
return JSON.stringify({ ...record, message, target, elapsedMs, });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
// Non-JSON DSS bodies are wrapped as text below.
|
|
768
|
+
}
|
|
769
|
+
return `Target: ${target}\nElapsed: ${elapsedMs}ms\n${body}`;
|
|
770
|
+
}
|
|
771
|
+
function addTransientTargetContext(error, target, elapsedMs) {
|
|
772
|
+
if (error instanceof DataikuError && error.category === "transient") {
|
|
773
|
+
throw new DataikuError(error.status, error.statusText, transientBodyWithTargetContext(error.body, target, elapsedMs), error.retry);
|
|
774
|
+
}
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
292
777
|
function isFailedWaitResult(result) {
|
|
293
778
|
if (result === null || typeof result !== "object" || Array.isArray(result))
|
|
294
779
|
return false;
|
|
@@ -299,7 +784,11 @@ function isFailedWaitResult(result) {
|
|
|
299
784
|
&& (typeof record.state === "string" || typeof record.outcome === "string");
|
|
300
785
|
}
|
|
301
786
|
function commandFailureExitCode(result) {
|
|
302
|
-
|
|
787
|
+
if (isFailedWaitResult(result))
|
|
788
|
+
return 4;
|
|
789
|
+
if (result && typeof result === "object" && result.unchanged === false)
|
|
790
|
+
return 4;
|
|
791
|
+
return undefined;
|
|
303
792
|
}
|
|
304
793
|
function isNotFoundError(error) {
|
|
305
794
|
if (error instanceof DataikuError)
|
|
@@ -331,6 +820,7 @@ function planResult(resource, action, options) {
|
|
|
331
820
|
...(options.method ? { method: options.method, } : {}),
|
|
332
821
|
...(options.endpoint ? { endpoint: options.endpoint, } : {}),
|
|
333
822
|
...(options.payload !== undefined ? { payload: options.payload, } : {}),
|
|
823
|
+
...(options.localWrites !== undefined ? { localWrites: options.localWrites, } : {}),
|
|
334
824
|
...(options.wait !== undefined ? { wait: options.wait, } : {}),
|
|
335
825
|
idempotency: options.idempotency,
|
|
336
826
|
async: options.asyncKind,
|
|
@@ -360,7 +850,7 @@ function resultRecord(result) {
|
|
|
360
850
|
: {};
|
|
361
851
|
}
|
|
362
852
|
function cleanupLedgerEntry(resource, action, args, flags, result, projectKey) {
|
|
363
|
-
if (!(action.startsWith("create") || action === "upload"))
|
|
853
|
+
if (!(action.startsWith("create") || action === "clone" || action === "upload"))
|
|
364
854
|
return undefined;
|
|
365
855
|
const record = resultRecord(result);
|
|
366
856
|
if (record.skipped !== undefined)
|
|
@@ -380,6 +870,16 @@ function cleanupLedgerEntry(resource, action, args, flags, result, projectKey) {
|
|
|
380
870
|
cleanup: { argv: ["dataset", "delete", name, "--if-exists", ...withProject,], },
|
|
381
871
|
};
|
|
382
872
|
}
|
|
873
|
+
case "dataset.clone": {
|
|
874
|
+
const name = stringField(record, ["target", "created", "name",]) ?? args[1];
|
|
875
|
+
if (!name)
|
|
876
|
+
return undefined;
|
|
877
|
+
return {
|
|
878
|
+
...base,
|
|
879
|
+
name,
|
|
880
|
+
cleanup: { argv: ["dataset", "delete", name, "--if-exists", ...withProject,], },
|
|
881
|
+
};
|
|
882
|
+
}
|
|
383
883
|
case "recipe.create": {
|
|
384
884
|
const name = stringField(record, ["created", "recipeName", "name",])
|
|
385
885
|
?? flags["name"];
|
|
@@ -391,6 +891,17 @@ function cleanupLedgerEntry(resource, action, args, flags, result, projectKey) {
|
|
|
391
891
|
cleanup: { argv: ["recipe", "delete", name, "--if-exists", ...withProject,], },
|
|
392
892
|
};
|
|
393
893
|
}
|
|
894
|
+
case "recipe.clone": {
|
|
895
|
+
const name = stringField(record, ["recipeName", "target", "created", "name",])
|
|
896
|
+
?? flags["name"];
|
|
897
|
+
if (!name)
|
|
898
|
+
return undefined;
|
|
899
|
+
return {
|
|
900
|
+
...base,
|
|
901
|
+
name,
|
|
902
|
+
cleanup: { argv: ["recipe", "delete", name, "--if-exists", ...withProject,], },
|
|
903
|
+
};
|
|
904
|
+
}
|
|
394
905
|
case "scenario.create": {
|
|
395
906
|
const id = args[0];
|
|
396
907
|
return {
|
|
@@ -504,9 +1015,11 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
504
1015
|
"global",
|
|
505
1016
|
"list-agents",
|
|
506
1017
|
"include-raw",
|
|
1018
|
+
"raw",
|
|
507
1019
|
"include-payload",
|
|
508
1020
|
"no-payload",
|
|
509
1021
|
"include-logs",
|
|
1022
|
+
"summary",
|
|
510
1023
|
"replace",
|
|
511
1024
|
"dry-run",
|
|
512
1025
|
"plan",
|
|
@@ -521,7 +1034,12 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
521
1034
|
"report-json",
|
|
522
1035
|
"no-wait",
|
|
523
1036
|
"force-rebuild",
|
|
1037
|
+
"latest",
|
|
1038
|
+
"copy-output-settings",
|
|
524
1039
|
"continue-on-error",
|
|
1040
|
+
"no-backup",
|
|
1041
|
+
"payload-only",
|
|
1042
|
+
"allow-same-path",
|
|
525
1043
|
]);
|
|
526
1044
|
const SHORT_FLAGS = {
|
|
527
1045
|
h: "help",
|
|
@@ -536,17 +1054,22 @@ const FLAG_ALIASES = {
|
|
|
536
1054
|
"skip-tls-verify": "insecure",
|
|
537
1055
|
"extra-ca-certs": "ca-cert",
|
|
538
1056
|
explain: "plan",
|
|
1057
|
+
"zone-name": "zone",
|
|
539
1058
|
};
|
|
540
1059
|
const VALUE_FLAGS = new Set([
|
|
541
1060
|
"activity",
|
|
542
1061
|
"agent",
|
|
543
1062
|
"api-key",
|
|
544
1063
|
"build-mode",
|
|
1064
|
+
"backup-dir",
|
|
1065
|
+
"backup",
|
|
545
1066
|
"ca-cert",
|
|
1067
|
+
"catalog",
|
|
546
1068
|
"cell-id",
|
|
547
1069
|
"allow-types",
|
|
548
1070
|
"color",
|
|
549
1071
|
"connection",
|
|
1072
|
+
"contains",
|
|
550
1073
|
"content",
|
|
551
1074
|
"content-type",
|
|
552
1075
|
"data",
|
|
@@ -560,6 +1083,7 @@ const VALUE_FLAGS = new Set([
|
|
|
560
1083
|
"install-core-packages",
|
|
561
1084
|
"folder",
|
|
562
1085
|
"input",
|
|
1086
|
+
"from",
|
|
563
1087
|
"knowledge-bank",
|
|
564
1088
|
"labeling-task",
|
|
565
1089
|
"lang",
|
|
@@ -568,17 +1092,23 @@ const VALUE_FLAGS = new Set([
|
|
|
568
1092
|
"local",
|
|
569
1093
|
"max-edges",
|
|
570
1094
|
"max-lines",
|
|
1095
|
+
"max-log-lines",
|
|
571
1096
|
"listed",
|
|
572
1097
|
"max-nodes",
|
|
573
1098
|
"max-rows",
|
|
1099
|
+
"limit",
|
|
574
1100
|
"max-timestamp",
|
|
575
1101
|
"only-monitored",
|
|
576
1102
|
"min-timestamp",
|
|
577
1103
|
"mode",
|
|
1104
|
+
"log-filter",
|
|
1105
|
+
"log-id",
|
|
578
1106
|
"model-evaluation-store",
|
|
579
1107
|
"name",
|
|
580
1108
|
"object",
|
|
1109
|
+
"metastore-table",
|
|
581
1110
|
"output",
|
|
1111
|
+
"output-file",
|
|
582
1112
|
"output-connection",
|
|
583
1113
|
"output-folder",
|
|
584
1114
|
"page",
|
|
@@ -592,18 +1122,41 @@ const VALUE_FLAGS = new Set([
|
|
|
592
1122
|
"results-per-page",
|
|
593
1123
|
"record-cleanup",
|
|
594
1124
|
"rule-id",
|
|
1125
|
+
"retries",
|
|
595
1126
|
"poll-interval",
|
|
596
1127
|
"python-interpreter",
|
|
1128
|
+
"replace-input",
|
|
1129
|
+
"replace-output",
|
|
1130
|
+
"replace-payload-text",
|
|
597
1131
|
"retain",
|
|
598
1132
|
"saved-model",
|
|
599
1133
|
"sql",
|
|
1134
|
+
"schema",
|
|
600
1135
|
"sql-file",
|
|
601
1136
|
"standard",
|
|
1137
|
+
"state",
|
|
602
1138
|
"streaming-endpoint",
|
|
603
1139
|
"target",
|
|
1140
|
+
"target-type",
|
|
604
1141
|
"timeout",
|
|
1142
|
+
"table",
|
|
605
1143
|
"type",
|
|
606
1144
|
"url",
|
|
1145
|
+
"until",
|
|
1146
|
+
"to",
|
|
1147
|
+
"zone",
|
|
1148
|
+
"zone-id",
|
|
1149
|
+
]);
|
|
1150
|
+
const REPEATABLE_VALUE_FLAGS = new Set([
|
|
1151
|
+
"dataset",
|
|
1152
|
+
"folder",
|
|
1153
|
+
"input",
|
|
1154
|
+
"object",
|
|
1155
|
+
"package",
|
|
1156
|
+
"recipe",
|
|
1157
|
+
"replace-input",
|
|
1158
|
+
"replace-output",
|
|
1159
|
+
"replace-payload-text",
|
|
607
1160
|
]);
|
|
608
1161
|
const KNOWN_LONG_FLAGS = new Set([
|
|
609
1162
|
...BOOLEAN_FLAGS,
|
|
@@ -627,6 +1180,14 @@ function requireFlagValue(flagLabel, next) {
|
|
|
627
1180
|
}
|
|
628
1181
|
return next;
|
|
629
1182
|
}
|
|
1183
|
+
function setParsedFlagValue(flags, flagName, value) {
|
|
1184
|
+
const current = flags[flagName];
|
|
1185
|
+
if (REPEATABLE_VALUE_FLAGS.has(flagName) && typeof current === "string" && current.length > 0) {
|
|
1186
|
+
flags[flagName] = `${current},${value}`;
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
flags[flagName] = value;
|
|
1190
|
+
}
|
|
630
1191
|
function parseArgs(argv) {
|
|
631
1192
|
const positional = [];
|
|
632
1193
|
const flags = {};
|
|
@@ -642,7 +1203,7 @@ function parseArgs(argv) {
|
|
|
642
1203
|
if (eqIdx !== -1) {
|
|
643
1204
|
const raw = arg.slice(2, eqIdx);
|
|
644
1205
|
const flagName = normalizeLongFlag(raw);
|
|
645
|
-
flags
|
|
1206
|
+
setParsedFlagValue(flags, flagName, arg.slice(eqIdx + 1));
|
|
646
1207
|
}
|
|
647
1208
|
else {
|
|
648
1209
|
const rawFlagName = arg.slice(2);
|
|
@@ -652,7 +1213,7 @@ function parseArgs(argv) {
|
|
|
652
1213
|
}
|
|
653
1214
|
else {
|
|
654
1215
|
const next = requireFlagValue(`--${rawFlagName}`, argv[i + 1]);
|
|
655
|
-
flags
|
|
1216
|
+
setParsedFlagValue(flags, flagName, next);
|
|
656
1217
|
i++;
|
|
657
1218
|
}
|
|
658
1219
|
}
|
|
@@ -665,7 +1226,7 @@ function parseArgs(argv) {
|
|
|
665
1226
|
}
|
|
666
1227
|
else {
|
|
667
1228
|
const next = requireFlagValue(`-${arg[1]}`, argv[i + 1]);
|
|
668
|
-
flags
|
|
1229
|
+
setParsedFlagValue(flags, long, next);
|
|
669
1230
|
i++;
|
|
670
1231
|
}
|
|
671
1232
|
}
|
|
@@ -1374,10 +1935,45 @@ const commands = {
|
|
|
1374
1935
|
},
|
|
1375
1936
|
"flow-zone": {
|
|
1376
1937
|
list: {
|
|
1377
|
-
handler: (c, _a, f) =>
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1938
|
+
handler: async (c, _a, f) => {
|
|
1939
|
+
const zones = await c.flowZones.list(f["project-key"]);
|
|
1940
|
+
if (f["summary"] !== true)
|
|
1941
|
+
return zones;
|
|
1942
|
+
const objects = flowZoneMoveItems(f);
|
|
1943
|
+
const object = objects.length === 1 ? objects[0] : undefined;
|
|
1944
|
+
return zones.map((zone) => flowZoneSummary(zone, object));
|
|
1945
|
+
},
|
|
1946
|
+
usage: "dss flow-zone list [--summary] [--object TYPE:ID] [--project-key KEY]",
|
|
1947
|
+
description: "List flow zones in a project, optionally as compact summaries.",
|
|
1948
|
+
examples: ["dss flow-zone list", "dss flow-zone list --summary --object RECIPE:compute_orders",],
|
|
1949
|
+
},
|
|
1950
|
+
find: {
|
|
1951
|
+
handler: async (c, a, f) => {
|
|
1952
|
+
const zones = await c.flowZones.list(f["project-key"]);
|
|
1953
|
+
const objects = flowZoneMoveItems(f);
|
|
1954
|
+
const query = a[0]?.trim();
|
|
1955
|
+
if (query && objects.length === 0) {
|
|
1956
|
+
const normalized = query.toLowerCase();
|
|
1957
|
+
return zones
|
|
1958
|
+
.filter((zone) => zone.id.toLowerCase().includes(normalized)
|
|
1959
|
+
|| zone.name.toLowerCase().includes(normalized))
|
|
1960
|
+
.map((zone) => flowZoneDetailSummary(zone));
|
|
1961
|
+
}
|
|
1962
|
+
if (objects.length !== 1) {
|
|
1963
|
+
throw new UsageError("Exactly one zone name/id or object is required. Use <name>, --object TYPE:ID, --dataset DS, or --recipe R.");
|
|
1964
|
+
}
|
|
1965
|
+
const object = objects[0];
|
|
1966
|
+
return zones
|
|
1967
|
+
.filter((zone) => flowZoneContains(zone, object))
|
|
1968
|
+
.map((zone) => flowZoneSummary(zone, object));
|
|
1969
|
+
},
|
|
1970
|
+
usage: "dss flow-zone find [name-or-id] [--object TYPE:ID | --dataset DS | --recipe R | --folder F] [--project-key KEY]",
|
|
1971
|
+
description: "Find flow zones by name/id or by contained flow object.",
|
|
1972
|
+
examples: [
|
|
1973
|
+
"dss flow-zone find ATH_SNW_MAP_FRG49",
|
|
1974
|
+
"dss flow-zone find --object RECIPE:compute_orders",
|
|
1975
|
+
"dss flow-zone find --dataset orders",
|
|
1976
|
+
],
|
|
1381
1977
|
},
|
|
1382
1978
|
get: {
|
|
1383
1979
|
handler: (c, a, f) => {
|
|
@@ -1472,7 +2068,11 @@ const commands = {
|
|
|
1472
2068
|
},
|
|
1473
2069
|
move: {
|
|
1474
2070
|
handler: async (c, a, f) => {
|
|
1475
|
-
|
|
2071
|
+
const pk = f["project-key"];
|
|
2072
|
+
const zoneId = a[0] ? flowZoneId(a[0]) : await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
2073
|
+
if (!zoneId) {
|
|
2074
|
+
throw new UsageError("A zone id or --zone/--zone-id is required. Usage: dss flow-zone move <id> [--dataset DS] [--recipe R] [--folder F] [--object TYPE:ID]");
|
|
2075
|
+
}
|
|
1476
2076
|
const items = flowZoneMoveItems(f);
|
|
1477
2077
|
if (items.length === 0) {
|
|
1478
2078
|
throw new UsageError("At least one object is required. Use --dataset, --recipe, --folder, or --object TYPE:ID.");
|
|
@@ -1482,17 +2082,17 @@ const commands = {
|
|
|
1482
2082
|
dryRun: true,
|
|
1483
2083
|
action: "move",
|
|
1484
2084
|
resource: "flow-zone",
|
|
1485
|
-
id:
|
|
2085
|
+
id: zoneId,
|
|
1486
2086
|
items,
|
|
1487
2087
|
};
|
|
1488
2088
|
}
|
|
1489
|
-
return c.flowZones.moveItems(
|
|
2089
|
+
return c.flowZones.moveItems(zoneId, items, pk);
|
|
1490
2090
|
},
|
|
1491
|
-
usage: "dss flow-zone move
|
|
1492
|
-
description: "Move datasets, recipes, managed folders, or other flow objects into a zone.",
|
|
2091
|
+
usage: "dss flow-zone move [id] [--zone ZONE|--zone-id ID] [--dataset DS[,DS2]] [--recipe R] [--folder F] [--object TYPE:ID] [--dry-run] [--project-key KEY]",
|
|
2092
|
+
description: "Move datasets, recipes, managed folders, or other flow objects into a zone by id or --zone name.",
|
|
1493
2093
|
examples: [
|
|
1494
2094
|
"dss flow-zone move ZONE_ID --dataset orders --dry-run",
|
|
1495
|
-
"dss flow-zone move
|
|
2095
|
+
"dss flow-zone move --zone ATH_SNW_MAP_FRG49 --dataset raw_orders,clean_orders --recipe prepare_orders",
|
|
1496
2096
|
"dss flow-zone move ZONE_ID --folder FOLDER_ID",
|
|
1497
2097
|
"dss flow-zone move ZONE_ID --object SAVED_MODEL:model_id",
|
|
1498
2098
|
],
|
|
@@ -1532,6 +2132,50 @@ const commands = {
|
|
|
1532
2132
|
description: "Show the column schema of a dataset.",
|
|
1533
2133
|
examples: ["dss dataset schema orders",],
|
|
1534
2134
|
},
|
|
2135
|
+
source: {
|
|
2136
|
+
handler: async (c, a, f) => {
|
|
2137
|
+
requireArgs(a, 1, "dss dataset source <name>");
|
|
2138
|
+
return datasetSourceSummary(await c.datasets.get(a[0], f["project-key"]));
|
|
2139
|
+
},
|
|
2140
|
+
usage: "dss dataset source <name> [--project-key KEY]",
|
|
2141
|
+
description: "Show backing connection, catalog/schema/table, path, and format for a dataset.",
|
|
2142
|
+
examples: ["dss dataset source orders",],
|
|
2143
|
+
},
|
|
2144
|
+
"refresh-schema": {
|
|
2145
|
+
handler: async (c, a, f) => {
|
|
2146
|
+
const usage = "dss dataset refresh-schema <name> [--data JSON | --data-file PATH | --stdin] [--dry-run] [--project-key KEY]";
|
|
2147
|
+
requireArgs(a, 1, usage);
|
|
2148
|
+
const columns = schemaColumnsInput(f, usage);
|
|
2149
|
+
const pk = f["project-key"];
|
|
2150
|
+
if (f["dry-run"] === true) {
|
|
2151
|
+
const current = await c.datasets.schema(a[0], pk);
|
|
2152
|
+
return {
|
|
2153
|
+
dryRun: true,
|
|
2154
|
+
action: "refresh-schema",
|
|
2155
|
+
resource: "dataset",
|
|
2156
|
+
name: a[0],
|
|
2157
|
+
current,
|
|
2158
|
+
next: { columns, },
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
await c.datasets.updateSchema(a[0], columns, pk);
|
|
2162
|
+
return { updated: a[0], resource: "dataset", schema: { columns, }, };
|
|
2163
|
+
},
|
|
2164
|
+
usage: "dss dataset refresh-schema <name> [--data JSON | --data-file PATH | --stdin] [--dry-run] [--project-key KEY]",
|
|
2165
|
+
description: "Replace a dataset schema through the DSS schema endpoint.",
|
|
2166
|
+
examples: [
|
|
2167
|
+
`dss dataset refresh-schema orders --data '{"columns":[{"name":"id","type":"bigint"}]}' --dry-run`,
|
|
2168
|
+
],
|
|
2169
|
+
},
|
|
2170
|
+
"validate-build": {
|
|
2171
|
+
handler: (c, a, f) => {
|
|
2172
|
+
requireArgs(a, 1, "dss dataset validate-build <name>");
|
|
2173
|
+
return c.datasets.validateBuildSettings(a[0], f["project-key"]);
|
|
2174
|
+
},
|
|
2175
|
+
usage: "dss dataset validate-build <name> [--project-key KEY]",
|
|
2176
|
+
description: "Check common dataset settings that can make file-backed builds fail.",
|
|
2177
|
+
examples: ["dss dataset validate-build orders",],
|
|
2178
|
+
},
|
|
1535
2179
|
preview: {
|
|
1536
2180
|
handler: (c, a, f) => {
|
|
1537
2181
|
requireArgs(a, 1, "dss dataset preview <name>");
|
|
@@ -1581,6 +2225,7 @@ const commands = {
|
|
|
1581
2225
|
dsType,
|
|
1582
2226
|
projectKey: pk,
|
|
1583
2227
|
};
|
|
2228
|
+
const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
1584
2229
|
if (f["if-not-exists"] === true || f["dry-run"] === true) {
|
|
1585
2230
|
const list = await c.datasets.list(pk);
|
|
1586
2231
|
const existing = list.find((d) => d.name === name);
|
|
@@ -1595,17 +2240,57 @@ const commands = {
|
|
|
1595
2240
|
name,
|
|
1596
2241
|
payload,
|
|
1597
2242
|
...(existing ? { current: existing, } : {}),
|
|
2243
|
+
...(zoneId ? { zoneId, zoneMove: [{ objectId: name, objectType: "DATASET", },], } : {}),
|
|
1598
2244
|
};
|
|
1599
2245
|
}
|
|
1600
2246
|
}
|
|
1601
2247
|
await c.datasets.create(payload);
|
|
1602
|
-
|
|
2248
|
+
const moved = await moveCreatedItemsToZone(c, f, [{ objectId: name, objectType: "DATASET", },], pk);
|
|
2249
|
+
return { created: name, resource: "dataset", ...moved, };
|
|
1603
2250
|
},
|
|
1604
|
-
usage: "dss dataset create --name NAME --connection CONN --type TYPE [--if-not-exists] [--dry-run] [--project-key KEY]",
|
|
2251
|
+
usage: "dss dataset create --name NAME --connection CONN --type TYPE [--zone ZONE|--zone-id ID] [--if-not-exists] [--dry-run] [--project-key KEY]",
|
|
1605
2252
|
description: "Create a new dataset.",
|
|
1606
2253
|
examples: [
|
|
1607
2254
|
"dss dataset create --name orders --connection filesystem --type Filesystem",
|
|
1608
|
-
"dss dataset create --name orders --connection filesystem --type Filesystem --dry-run",
|
|
2255
|
+
"dss dataset create --name orders --connection filesystem --type Filesystem --zone Experiments --dry-run",
|
|
2256
|
+
],
|
|
2257
|
+
},
|
|
2258
|
+
clone: {
|
|
2259
|
+
handler: async (c, a, f) => {
|
|
2260
|
+
const usage = "dss dataset clone <source> <target> [--path PATH] [--table TABLE] [--metastore-table TABLE] [--allow-same-path] [--zone ZONE|--zone-id ID] [--dry-run] [--project-key KEY]";
|
|
2261
|
+
requireArgs(a, 2, usage);
|
|
2262
|
+
const pk = f["project-key"];
|
|
2263
|
+
const opts = {
|
|
2264
|
+
projectKey: pk,
|
|
2265
|
+
path: f["path"],
|
|
2266
|
+
table: f["table"],
|
|
2267
|
+
metastoreTableName: f["metastore-table"],
|
|
2268
|
+
allowSamePath: f["allow-same-path"] === true,
|
|
2269
|
+
};
|
|
2270
|
+
const current = await c.datasets.get(a[0], pk);
|
|
2271
|
+
const next = buildDatasetCloneSettings(current, a[1], pk ?? c.resolveProjectKey(pk), opts);
|
|
2272
|
+
const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
2273
|
+
if (f["dry-run"] === true) {
|
|
2274
|
+
return {
|
|
2275
|
+
dryRun: true,
|
|
2276
|
+
action: "clone",
|
|
2277
|
+
resource: "dataset",
|
|
2278
|
+
source: a[0],
|
|
2279
|
+
target: a[1],
|
|
2280
|
+
current,
|
|
2281
|
+
next,
|
|
2282
|
+
...(zoneId ? { zoneId, zoneMove: [{ objectId: a[1], objectType: "DATASET", },], } : {}),
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
const cloned = await c.datasets.clone(a[0], a[1], opts);
|
|
2286
|
+
const moved = await moveCreatedItemsToZone(c, f, [{ objectId: a[1], objectType: "DATASET", },], pk);
|
|
2287
|
+
return { ...cloned, resource: "dataset", ...moved, };
|
|
2288
|
+
},
|
|
2289
|
+
usage: "dss dataset clone <source> <target> [--path PATH] [--table TABLE] [--metastore-table TABLE] [--allow-same-path] [--zone ZONE|--zone-id ID] [--dry-run] [--project-key KEY]",
|
|
2290
|
+
description: "Clone dataset settings into a new dataset, with storage/table overrides.",
|
|
2291
|
+
examples: [
|
|
2292
|
+
"dss dataset clone source_ds experiment_ds --path /dataiku/TEST/experiment_ds --dry-run",
|
|
2293
|
+
"dss dataset clone source_ds experiment_ds --allow-same-path",
|
|
1609
2294
|
],
|
|
1610
2295
|
},
|
|
1611
2296
|
delete: {
|
|
@@ -1674,6 +2359,60 @@ const commands = {
|
|
|
1674
2359
|
"dss recipe get compute_orders --include-payload",
|
|
1675
2360
|
],
|
|
1676
2361
|
},
|
|
2362
|
+
"validate-graph": {
|
|
2363
|
+
handler: (c, a, f) => {
|
|
2364
|
+
requireArgs(a, 1, "dss recipe validate-graph <name>");
|
|
2365
|
+
return c.recipes.validateGraph(a[0], {
|
|
2366
|
+
projectKey: f["project-key"],
|
|
2367
|
+
});
|
|
2368
|
+
},
|
|
2369
|
+
usage: "dss recipe validate-graph <name> [--project-key KEY]",
|
|
2370
|
+
description: "Validate declared recipe input/output graph references before building.",
|
|
2371
|
+
examples: ["dss recipe validate-graph compute_orders",],
|
|
2372
|
+
},
|
|
2373
|
+
run: {
|
|
2374
|
+
handler: async (c, a, f) => {
|
|
2375
|
+
requireArgs(a, 1, "dss recipe run <name>");
|
|
2376
|
+
const pk = f["project-key"];
|
|
2377
|
+
const wait = recipeRunShouldWait(f);
|
|
2378
|
+
const options = {
|
|
2379
|
+
buildMode: f["build-mode"],
|
|
2380
|
+
includeLogs: f["include-logs"] === true,
|
|
2381
|
+
logFilter: jobLogFilterFromFlag(f["log-filter"]),
|
|
2382
|
+
maxLogLines: maxLogLinesFromFlags(f),
|
|
2383
|
+
partition: f["partition"],
|
|
2384
|
+
pollIntervalMs: num(f["poll-interval"]),
|
|
2385
|
+
projectKey: pk,
|
|
2386
|
+
timeoutMs: num(f["timeout"]),
|
|
2387
|
+
summary: f["summary"] === true,
|
|
2388
|
+
wait,
|
|
2389
|
+
};
|
|
2390
|
+
if (f["dry-run"] === true) {
|
|
2391
|
+
const outputs = await c.recipes.resolveRunOutputs(a[0], {
|
|
2392
|
+
partition: options.partition,
|
|
2393
|
+
projectKey: pk,
|
|
2394
|
+
});
|
|
2395
|
+
return {
|
|
2396
|
+
dryRun: true,
|
|
2397
|
+
action: "run",
|
|
2398
|
+
resource: "recipe",
|
|
2399
|
+
recipe: a[0],
|
|
2400
|
+
outputs,
|
|
2401
|
+
...options,
|
|
2402
|
+
endpoint: encodedProjectEndpoint(c, pk, "/jobs/"),
|
|
2403
|
+
method: "POST",
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
return c.recipes.run(a[0], options);
|
|
2407
|
+
},
|
|
2408
|
+
usage: "dss recipe run <name> [--wait|--no-wait] [--build-mode MODE] [--include-logs] [--log-filter stdout|stderr|user|errors] [--summary] [--max-log-lines N] [--timeout MS] [--poll-interval MS] [--partition PARTITION] [--dry-run] [--project-key KEY]",
|
|
2409
|
+
description: "Run a recipe by resolving its outputs and submitting the correct dataset or managed-folder build job.",
|
|
2410
|
+
examples: [
|
|
2411
|
+
"dss recipe run compute_orders --wait",
|
|
2412
|
+
"dss recipe run compute_exports --include-logs --log-filter stdout --summary --timeout 600000",
|
|
2413
|
+
"dss recipe run compute_exports --dry-run",
|
|
2414
|
+
],
|
|
2415
|
+
},
|
|
1677
2416
|
delete: {
|
|
1678
2417
|
handler: async (c, a, f) => {
|
|
1679
2418
|
requireArgs(a, 1, "dss recipe delete <name>");
|
|
@@ -1742,15 +2481,20 @@ const commands = {
|
|
|
1742
2481
|
}
|
|
1743
2482
|
const name = f["name"];
|
|
1744
2483
|
const pk = f["project-key"];
|
|
2484
|
+
const inputDatasets = recipeInputDatasetsFromFlags(f);
|
|
1745
2485
|
const payload = {
|
|
1746
2486
|
type,
|
|
1747
2487
|
name,
|
|
1748
|
-
inputDatasets
|
|
2488
|
+
inputDatasets,
|
|
1749
2489
|
outputDataset,
|
|
1750
2490
|
outputFolder,
|
|
1751
2491
|
outputConnection: f["output-connection"],
|
|
1752
2492
|
projectKey: pk,
|
|
1753
2493
|
};
|
|
2494
|
+
const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
2495
|
+
const zoneMove = zoneId && name
|
|
2496
|
+
? [{ objectId: name, objectType: "RECIPE", },]
|
|
2497
|
+
: undefined;
|
|
1754
2498
|
if ((f["if-not-exists"] === true || f["dry-run"] === true) && name) {
|
|
1755
2499
|
const list = await c.recipes.list(pk);
|
|
1756
2500
|
const existing = list.find((r) => r.name === name);
|
|
@@ -1764,24 +2508,106 @@ const commands = {
|
|
|
1764
2508
|
resource: "recipe",
|
|
1765
2509
|
name,
|
|
1766
2510
|
payload,
|
|
2511
|
+
...(zoneId ? { zoneId, zoneMove, } : {}),
|
|
1767
2512
|
...(existing ? { current: existing, } : {}),
|
|
1768
2513
|
};
|
|
1769
2514
|
}
|
|
1770
2515
|
}
|
|
1771
2516
|
if (f["dry-run"] === true) {
|
|
1772
|
-
return {
|
|
2517
|
+
return {
|
|
2518
|
+
dryRun: true,
|
|
2519
|
+
action: "create",
|
|
2520
|
+
resource: "recipe",
|
|
2521
|
+
payload,
|
|
2522
|
+
...(zoneId ? { zoneId, zoneMove, } : {}),
|
|
2523
|
+
};
|
|
1773
2524
|
}
|
|
1774
2525
|
const created = await c.recipes.create(payload);
|
|
1775
|
-
|
|
2526
|
+
const createdName = created.recipeName;
|
|
2527
|
+
const moved = await moveCreatedItemsToZone(c, f, [{
|
|
2528
|
+
objectId: createdName,
|
|
2529
|
+
objectType: "RECIPE",
|
|
2530
|
+
},], pk);
|
|
2531
|
+
return { created: createdName, resource: "recipe", ...created, ...moved, };
|
|
1776
2532
|
},
|
|
1777
|
-
usage: "dss recipe create --type TYPE --input DS (--output DS | --output-folder FOLDER_ID) [--name NAME] [--output-connection CONN] [--if-not-exists] [--dry-run] [--project-key KEY]",
|
|
1778
|
-
description: "Create a recipe with
|
|
2533
|
+
usage: "dss recipe create --type TYPE --input DS[,DS2] (--output DS | --output-folder FOLDER_ID) [--name NAME] [--output-connection CONN] [--zone ZONE|--zone-id ID] [--if-not-exists] [--dry-run] [--project-key KEY]",
|
|
2534
|
+
description: "Create a recipe with one or more inputs and a dataset or managed-folder output.",
|
|
1779
2535
|
examples: [
|
|
1780
|
-
"dss recipe create --type python --input
|
|
1781
|
-
"dss recipe create --type python --input orders --output orders_clean --
|
|
2536
|
+
"dss recipe create --type python --input raw_orders,lookup --output orders_clean",
|
|
2537
|
+
"dss recipe create --type python --input orders --input customers --output orders_clean --zone Experiments",
|
|
1782
2538
|
"dss recipe create --type python --input orders --output-folder LT7TUHJ8 --output-connection filesystem --dry-run",
|
|
1783
2539
|
],
|
|
1784
2540
|
},
|
|
2541
|
+
clone: {
|
|
2542
|
+
handler: async (c, a, f) => {
|
|
2543
|
+
const usage = "dss recipe clone [source|--from SOURCE] (--name NAME|--to NAME) [--replace-input FROM=TO] [--replace-output FROM=TO] [--replace-payload-text FROM=TO] [--output DATASET] [--copy-output-settings] [--path PATH] [--metastore-table TABLE] [--zone ZONE|--zone-id ID] [--dry-run] [--project-key KEY]";
|
|
2544
|
+
const fromFlag = typeof f["from"] === "string" ? f["from"].trim() : "";
|
|
2545
|
+
const sourceName = a[0] ?? fromFlag;
|
|
2546
|
+
if (!sourceName) {
|
|
2547
|
+
throw new UsageError(`Source recipe is required. Usage: ${usage}`, "missing_required_flag");
|
|
2548
|
+
}
|
|
2549
|
+
if (a[0] && fromFlag && a[0] !== fromFlag) {
|
|
2550
|
+
throw new UsageError("Positional source and --from must match when both are provided.", "invalid_enum");
|
|
2551
|
+
}
|
|
2552
|
+
const pk = f["project-key"];
|
|
2553
|
+
const toFlag = typeof f["to"] === "string" ? f["to"].trim() : "";
|
|
2554
|
+
const nameFlag = typeof f["name"] === "string" ? f["name"].trim() : "";
|
|
2555
|
+
const name = toFlag || nameFlag;
|
|
2556
|
+
if (!name) {
|
|
2557
|
+
throw new UsageError(`--name or --to is required. Usage: ${usage}`, "missing_required_flag");
|
|
2558
|
+
}
|
|
2559
|
+
const inputRewrites = rewritePairsFromFlags(f, "replace-input");
|
|
2560
|
+
const outputRewrites = rewritePairsFromFlags(f, "replace-output");
|
|
2561
|
+
const payloadTextRewrites = rewritePairsFromFlags(f, "replace-payload-text");
|
|
2562
|
+
const opts = {
|
|
2563
|
+
projectKey: pk,
|
|
2564
|
+
name,
|
|
2565
|
+
outputDataset: f["output"],
|
|
2566
|
+
outputRewrites,
|
|
2567
|
+
inputRewrites,
|
|
2568
|
+
payloadTextRewrites,
|
|
2569
|
+
copyOutputSettings: f["copy-output-settings"] === true,
|
|
2570
|
+
outputPath: f["path"],
|
|
2571
|
+
metastoreTableName: f["metastore-table"],
|
|
2572
|
+
};
|
|
2573
|
+
const source = await c.recipes.get(sourceName, { includePayload: true, projectKey: pk, });
|
|
2574
|
+
const outputItems = Object.values((source.recipe.outputs ?? {})).flatMap((role) => role.items ?? []).filter((item) => typeof item.ref === "string");
|
|
2575
|
+
const plannedOutputRewrites = { ...outputRewrites, };
|
|
2576
|
+
if (opts.outputDataset !== undefined && outputItems.length === 1) {
|
|
2577
|
+
plannedOutputRewrites[outputItems[0].ref] = opts.outputDataset;
|
|
2578
|
+
}
|
|
2579
|
+
if (opts.copyOutputSettings === true
|
|
2580
|
+
&& Object.keys(plannedOutputRewrites).length > 1
|
|
2581
|
+
&& (opts.outputPath !== undefined || opts.metastoreTableName !== undefined)) {
|
|
2582
|
+
throw new UsageError("Cannot reuse --path or --metastore-table for multiple cloned output datasets.", "invalid_enum");
|
|
2583
|
+
}
|
|
2584
|
+
const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
2585
|
+
if (f["dry-run"] === true) {
|
|
2586
|
+
return {
|
|
2587
|
+
dryRun: true,
|
|
2588
|
+
action: "clone",
|
|
2589
|
+
resource: "recipe",
|
|
2590
|
+
source: sourceName,
|
|
2591
|
+
target: name,
|
|
2592
|
+
inputRewrites,
|
|
2593
|
+
outputRewrites: plannedOutputRewrites,
|
|
2594
|
+
copyOutputSettings: opts.copyOutputSettings,
|
|
2595
|
+
payloadTextRewrites,
|
|
2596
|
+
current: source,
|
|
2597
|
+
...(zoneId ? { zoneId, zoneMove: [{ objectId: name, objectType: "RECIPE", },], } : {}),
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
const cloned = await c.recipes.clone(sourceName, opts);
|
|
2601
|
+
const moved = await moveCreatedItemsToZone(c, f, [{ objectId: name, objectType: "RECIPE", },], pk);
|
|
2602
|
+
return { ...cloned, resource: "recipe", ...moved, };
|
|
2603
|
+
},
|
|
2604
|
+
usage: "dss recipe clone [source|--from SOURCE] (--name NAME|--to NAME) [--replace-input FROM=TO] [--replace-output FROM=TO] [--replace-payload-text FROM=TO] [--output DATASET] [--copy-output-settings] [--path PATH] [--metastore-table TABLE] [--zone ZONE|--zone-id ID] [--dry-run] [--project-key KEY]",
|
|
2605
|
+
description: "Clone a recipe graph/settings/payload into a separate experiment recipe.",
|
|
2606
|
+
examples: [
|
|
2607
|
+
"dss recipe clone compute_orders --name compute_orders_opt --output orders_opt --copy-output-settings --dry-run",
|
|
2608
|
+
"dss recipe clone compute_orders --name compute_orders_opt --output orders_opt --zone Experiments",
|
|
2609
|
+
],
|
|
2610
|
+
},
|
|
1785
2611
|
diff: {
|
|
1786
2612
|
handler: async (c, a, f) => {
|
|
1787
2613
|
requireArgs(a, 1, "dss recipe diff <name> --file PATH");
|
|
@@ -1846,13 +2672,24 @@ const commands = {
|
|
|
1846
2672
|
}
|
|
1847
2673
|
return payload;
|
|
1848
2674
|
},
|
|
1849
|
-
usage: "dss recipe get-payload <name> [--output PATH] [--project-key KEY]",
|
|
1850
|
-
description: "Print the recipe code payload to stdout.",
|
|
2675
|
+
usage: "dss recipe get-payload <name> [--raw] [--output PATH] [--project-key KEY]",
|
|
2676
|
+
description: "Print the recipe code payload to stdout; use --raw for pipeable code bytes.",
|
|
1851
2677
|
examples: [
|
|
1852
|
-
"dss recipe get-payload compute_orders",
|
|
2678
|
+
"dss recipe get-payload compute_orders --raw",
|
|
1853
2679
|
"dss recipe get-payload compute_orders -o code.py",
|
|
1854
2680
|
],
|
|
1855
2681
|
},
|
|
2682
|
+
cat: {
|
|
2683
|
+
handler: (c, a, f) => {
|
|
2684
|
+
requireArgs(a, 1, "dss recipe cat <name> [--raw]");
|
|
2685
|
+
return c.recipes.getPayload(a[0], {
|
|
2686
|
+
projectKey: f["project-key"],
|
|
2687
|
+
});
|
|
2688
|
+
},
|
|
2689
|
+
usage: "dss recipe cat <name> [--raw] [--project-key KEY]",
|
|
2690
|
+
description: "Print a recipe code payload; combine with --raw for shell pipes and diffs.",
|
|
2691
|
+
examples: ["dss recipe cat compute_orders --raw",],
|
|
2692
|
+
},
|
|
1856
2693
|
"set-payload": {
|
|
1857
2694
|
handler: async (c, a, f) => {
|
|
1858
2695
|
requireArgs(a, 1, "dss recipe set-payload <name> --file PATH");
|
|
@@ -1860,11 +2697,17 @@ const commands = {
|
|
|
1860
2697
|
if (!filePath)
|
|
1861
2698
|
throw new UsageError("--file is required.");
|
|
1862
2699
|
const content = readFileSync(filePath, "utf-8");
|
|
2700
|
+
const pk = f["project-key"];
|
|
2701
|
+
const shouldBackup = f["no-backup"] !== true;
|
|
2702
|
+
const backupDir = shouldBackup
|
|
2703
|
+
? f["backup-dir"] ?? join(process.cwd(), ".dss-backups", "recipes")
|
|
2704
|
+
: undefined;
|
|
2705
|
+
const backupPath = backupDir ? recipeBackupPath(a[0], backupDir) : undefined;
|
|
2706
|
+
const current = await c.recipes.get(a[0], {
|
|
2707
|
+
projectKey: pk,
|
|
2708
|
+
includePayload: true,
|
|
2709
|
+
});
|
|
1863
2710
|
if (f["dry-run"] === true) {
|
|
1864
|
-
const current = await c.recipes.get(a[0], {
|
|
1865
|
-
projectKey: f["project-key"],
|
|
1866
|
-
includePayload: true,
|
|
1867
|
-
});
|
|
1868
2711
|
return {
|
|
1869
2712
|
dryRun: true,
|
|
1870
2713
|
action: "set-payload",
|
|
@@ -1873,24 +2716,140 @@ const commands = {
|
|
|
1873
2716
|
file: filePath,
|
|
1874
2717
|
current,
|
|
1875
2718
|
next: { ...current, payload: content, },
|
|
2719
|
+
...(backupPath ? { backupPath, backup: recipeBackupDocument(a[0], pk, current), } : {}),
|
|
1876
2720
|
};
|
|
1877
2721
|
}
|
|
1878
|
-
|
|
2722
|
+
if (backupDir && backupPath) {
|
|
2723
|
+
await mkdir(backupDir, { recursive: true, });
|
|
2724
|
+
await writeFile(backupPath, `${JSON.stringify(recipeBackupDocument(a[0], pk, current), null, 2)}\n`, "utf-8");
|
|
2725
|
+
}
|
|
2726
|
+
await c.recipes.replace(a[0], { ...current, payload: content, }, pk);
|
|
2727
|
+
return {
|
|
2728
|
+
updated: a[0],
|
|
2729
|
+
resource: "recipe",
|
|
2730
|
+
file: filePath,
|
|
2731
|
+
backupCreated: backupPath !== undefined,
|
|
2732
|
+
...(backupPath ? { backupPath, } : {}),
|
|
2733
|
+
};
|
|
2734
|
+
},
|
|
2735
|
+
usage: "dss recipe set-payload <name> --file PATH [--backup-dir DIR|--no-backup] [--dry-run] [--project-key KEY]",
|
|
2736
|
+
description: "Upload recipe code from a local file, backing up payload, graph, settings, and version metadata by default.",
|
|
2737
|
+
examples: [
|
|
2738
|
+
"dss recipe set-payload compute_orders --file code.py --dry-run",
|
|
2739
|
+
"dss recipe set-payload compute_orders --file code.py --backup-dir ./backups",
|
|
2740
|
+
"dss recipe set-payload compute_orders --file code.py --no-backup",
|
|
2741
|
+
],
|
|
2742
|
+
},
|
|
2743
|
+
restore: {
|
|
2744
|
+
handler: async (c, a, f) => {
|
|
2745
|
+
const usage = "dss recipe restore <name> --backup FILE [--payload-only] [--dry-run] [--project-key KEY]";
|
|
2746
|
+
requireArgs(a, 1, usage);
|
|
2747
|
+
const backupPath = requiredStringFlag(f, "backup", usage);
|
|
2748
|
+
const backup = readRecipeBackup(backupPath);
|
|
2749
|
+
const payload = typeof backup.payload === "string" ? backup.payload : "";
|
|
2750
|
+
const pk = f["project-key"];
|
|
2751
|
+
const current = await c.recipes.get(a[0], { includePayload: true, projectKey: pk, });
|
|
2752
|
+
const backupRecipe = backup.recipe && typeof backup.recipe === "object" && !Array.isArray(backup.recipe)
|
|
2753
|
+
? backup.recipe
|
|
2754
|
+
: undefined;
|
|
2755
|
+
const restoredRecipe = backupRecipe
|
|
2756
|
+
? { ...backupRecipe, name: a[0], ...(pk ? { projectKey: pk, } : {}), }
|
|
2757
|
+
: undefined;
|
|
2758
|
+
const next = f["payload-only"] === true || !restoredRecipe
|
|
2759
|
+
? { ...current, payload, }
|
|
2760
|
+
: { ...current, recipe: restoredRecipe, payload, };
|
|
2761
|
+
if (f["dry-run"] === true) {
|
|
2762
|
+
return {
|
|
2763
|
+
dryRun: true,
|
|
2764
|
+
action: "restore",
|
|
2765
|
+
resource: "recipe",
|
|
2766
|
+
name: a[0],
|
|
2767
|
+
backupPath,
|
|
2768
|
+
current,
|
|
2769
|
+
next,
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
await c.recipes.replace(a[0], next, pk);
|
|
2773
|
+
return {
|
|
2774
|
+
restored: a[0],
|
|
2775
|
+
resource: "recipe",
|
|
2776
|
+
backupPath,
|
|
2777
|
+
payloadOnly: f["payload-only"] === true,
|
|
2778
|
+
};
|
|
2779
|
+
},
|
|
2780
|
+
usage: "dss recipe restore <name> --backup FILE [--payload-only] [--dry-run] [--project-key KEY]",
|
|
2781
|
+
description: "Restore a recipe from a set-payload backup.",
|
|
2782
|
+
examples: [
|
|
2783
|
+
"dss recipe restore compute_orders --backup .dss-backups/recipes/backup.recipe-backup.json --dry-run",
|
|
2784
|
+
],
|
|
2785
|
+
},
|
|
2786
|
+
"assert-unchanged": {
|
|
2787
|
+
handler: async (c, a, f) => {
|
|
2788
|
+
const usage = "dss recipe assert-unchanged <name> --since BACKUP [--project-key KEY]";
|
|
2789
|
+
requireArgs(a, 1, usage);
|
|
2790
|
+
const backupPath = requiredStringFlag(f, "since", usage);
|
|
2791
|
+
const backup = readRecipeBackup(backupPath);
|
|
2792
|
+
const current = await c.recipes.get(a[0], {
|
|
2793
|
+
includePayload: true,
|
|
1879
2794
|
projectKey: f["project-key"],
|
|
1880
2795
|
});
|
|
1881
|
-
|
|
2796
|
+
const payloadHash = sha256Hex(current.payload ?? "");
|
|
2797
|
+
const normalizedPayloadHash = sha256Hex(normalizeLineEndings(current.payload ?? ""));
|
|
2798
|
+
const expectedPayloadHash = typeof backup.payloadHash === "string"
|
|
2799
|
+
? backup.payloadHash
|
|
2800
|
+
: undefined;
|
|
2801
|
+
const expectedNormalizedPayloadHash = typeof backup.normalizedPayloadHash === "string"
|
|
2802
|
+
? backup.normalizedPayloadHash
|
|
2803
|
+
: typeof backup.payload === "string"
|
|
2804
|
+
? sha256Hex(normalizeLineEndings(backup.payload))
|
|
2805
|
+
: undefined;
|
|
2806
|
+
const checks = [
|
|
2807
|
+
{
|
|
2808
|
+
name: "payload",
|
|
2809
|
+
expected: expectedPayloadHash,
|
|
2810
|
+
actual: payloadHash,
|
|
2811
|
+
unchanged: expectedPayloadHash === payloadHash
|
|
2812
|
+
|| (expectedNormalizedPayloadHash !== undefined
|
|
2813
|
+
&& expectedNormalizedPayloadHash === normalizedPayloadHash),
|
|
2814
|
+
normalizedExpected: expectedNormalizedPayloadHash,
|
|
2815
|
+
normalizedActual: normalizedPayloadHash,
|
|
2816
|
+
},
|
|
2817
|
+
{
|
|
2818
|
+
name: "graph",
|
|
2819
|
+
expected: backup.graphHash,
|
|
2820
|
+
actual: stableHash(recipeGraph(current.recipe)),
|
|
2821
|
+
unchanged: backup.graphHash === stableHash(recipeGraph(current.recipe)),
|
|
2822
|
+
},
|
|
2823
|
+
{
|
|
2824
|
+
name: "codeEnv",
|
|
2825
|
+
expected: backup.codeEnvHash,
|
|
2826
|
+
actual: stableHash(recipeCodeEnv(current.recipe)),
|
|
2827
|
+
unchanged: backup.codeEnvHash === stableHash(recipeCodeEnv(current.recipe)),
|
|
2828
|
+
},
|
|
2829
|
+
].filter((check) => typeof check.expected === "string");
|
|
2830
|
+
const failures = checks.filter((check) => !check.unchanged);
|
|
2831
|
+
return {
|
|
2832
|
+
unchanged: failures.length === 0,
|
|
2833
|
+
resource: "recipe",
|
|
2834
|
+
name: a[0],
|
|
2835
|
+
backupPath,
|
|
2836
|
+
checks,
|
|
2837
|
+
failures,
|
|
2838
|
+
};
|
|
1882
2839
|
},
|
|
1883
|
-
usage: "dss recipe
|
|
1884
|
-
description: "
|
|
1885
|
-
examples: [
|
|
2840
|
+
usage: "dss recipe assert-unchanged <name> --since BACKUP [--project-key KEY]",
|
|
2841
|
+
description: "Compare current recipe payload, graph, and code env against a backup.",
|
|
2842
|
+
examples: [
|
|
2843
|
+
"dss recipe assert-unchanged compute_orders --since .dss-backups/recipes/backup.recipe-backup.json",
|
|
2844
|
+
],
|
|
1886
2845
|
},
|
|
1887
2846
|
},
|
|
1888
2847
|
job: {
|
|
1889
2848
|
list: {
|
|
1890
|
-
handler: (c, _a, f) => c.jobs.list(f["project-key"]),
|
|
1891
|
-
usage: "dss job list [--project-key KEY]",
|
|
1892
|
-
description: "List recent jobs.",
|
|
1893
|
-
examples: ["dss job list",],
|
|
2849
|
+
handler: async (c, _a, f) => filteredJobList(await c.jobs.list(f["project-key"]), f),
|
|
2850
|
+
usage: "dss job list [--state STATE] [--contains TEXT] [--output ID] [--latest] [--limit N] [--project-key KEY]",
|
|
2851
|
+
description: "List recent jobs, optionally filtered for automation.",
|
|
2852
|
+
examples: ["dss job list --state DONE --latest", "dss job list --contains WLM225S --limit 10",],
|
|
1894
2853
|
},
|
|
1895
2854
|
get: {
|
|
1896
2855
|
handler: (c, a, f) => {
|
|
@@ -1901,17 +2860,42 @@ const commands = {
|
|
|
1901
2860
|
description: "Get job details.",
|
|
1902
2861
|
examples: ["dss job get JOB_ID",],
|
|
1903
2862
|
},
|
|
2863
|
+
summary: {
|
|
2864
|
+
handler: (c, a, f) => {
|
|
2865
|
+
requireArgs(a, 1, "dss job summary <id>");
|
|
2866
|
+
return jobInspectionSummary(c, a[0], f);
|
|
2867
|
+
},
|
|
2868
|
+
usage: "dss job summary <id> [--activity ACTIVITY_ID] [--log-id LOG_ID] [--max-lines N|--max-log-lines N] [--project-key KEY]",
|
|
2869
|
+
description: "Summarize job state, outputs, warnings, progress, and useful terminal log lines.",
|
|
2870
|
+
examples: ["dss job summary JOB_ID --max-log-lines 200",],
|
|
2871
|
+
},
|
|
1904
2872
|
log: {
|
|
1905
2873
|
handler: (c, a, f) => {
|
|
1906
2874
|
requireArgs(a, 1, "dss job log <id>");
|
|
1907
2875
|
return c.jobs.log(a[0], {
|
|
1908
2876
|
activity: f["activity"],
|
|
1909
|
-
|
|
2877
|
+
logId: f["log-id"],
|
|
2878
|
+
maxLogLines: maxLogLinesFromFlags(f),
|
|
2879
|
+
projectKey: f["project-key"],
|
|
1910
2880
|
});
|
|
1911
2881
|
},
|
|
1912
|
-
usage: "dss job log <id> [--activity
|
|
1913
|
-
description: "Get log output for
|
|
1914
|
-
examples: [
|
|
2882
|
+
usage: "dss job log <id> [--activity ACTIVITY_ID] [--log-id LOG_ID] [--max-lines N|--max-log-lines N] [--project-key KEY]",
|
|
2883
|
+
description: "Get public API job log output. --log-id is accepted for UI parity but DSS API-key auth cannot select browser-only cat-activity-log files.",
|
|
2884
|
+
examples: [
|
|
2885
|
+
"dss job log JOB_ID",
|
|
2886
|
+
"dss job log JOB_ID --activity main --max-log-lines 200",
|
|
2887
|
+
],
|
|
2888
|
+
},
|
|
2889
|
+
"log-url": {
|
|
2890
|
+
handler: (c, a, f) => {
|
|
2891
|
+
requireArgs(a, 1, "dss job log-url <url>");
|
|
2892
|
+
return c.jobs.logFromUrl(a[0], { maxLogLines: maxLogLinesFromFlags(f), });
|
|
2893
|
+
},
|
|
2894
|
+
usage: "dss job log-url <url> [--max-lines N|--max-log-lines N]",
|
|
2895
|
+
description: "Fetch a DSS cat-activity-log URL pasted from the UI.",
|
|
2896
|
+
examples: [
|
|
2897
|
+
'dss job log-url "https://dss/dip/api/flow/jobs/cat-activity-log?projectKey=TEST&jobId=JOB&activityId=A&logId=L"',
|
|
2898
|
+
],
|
|
1915
2899
|
},
|
|
1916
2900
|
build: {
|
|
1917
2901
|
handler: async (c, a, f) => {
|
|
@@ -1919,8 +2903,9 @@ const commands = {
|
|
|
1919
2903
|
const pk = f["project-key"];
|
|
1920
2904
|
const options = {
|
|
1921
2905
|
buildMode: f["build-mode"],
|
|
2906
|
+
partition: f["partition"],
|
|
1922
2907
|
pollIntervalMs: num(f["poll-interval"]),
|
|
1923
|
-
targetType:
|
|
2908
|
+
targetType: jobBuildTargetTypeFromFlags(f),
|
|
1924
2909
|
timeoutMs: num(f["timeout"]),
|
|
1925
2910
|
};
|
|
1926
2911
|
if (f["dry-run"] === true) {
|
|
@@ -1939,12 +2924,12 @@ const commands = {
|
|
|
1939
2924
|
}
|
|
1940
2925
|
return c.jobs.build(a[0], { ...options, projectKey: pk, });
|
|
1941
2926
|
},
|
|
1942
|
-
usage: "dss job build <target> [--type DATASET|MANAGED_FOLDER] [--build-mode MODE] [--wait] [--timeout MS] [--poll-interval MS] [--dry-run] [--project-key KEY]",
|
|
2927
|
+
usage: "dss job build <target> [--target-type dataset|managed-folder] [--type DATASET|MANAGED_FOLDER] [--build-mode MODE] [--wait] [--timeout MS] [--poll-interval MS] [--partition PARTITION] [--dry-run] [--project-key KEY]",
|
|
1943
2928
|
description: "Start a dataset or managed-folder build, optionally waiting for completion.",
|
|
1944
2929
|
examples: [
|
|
1945
2930
|
"dss job build orders",
|
|
1946
2931
|
"dss job build orders --build-mode RECURSIVE_BUILD --wait",
|
|
1947
|
-
"dss job build LT7TUHJ8 --type
|
|
2932
|
+
"dss job build LT7TUHJ8 --target-type managed-folder --dry-run",
|
|
1948
2933
|
],
|
|
1949
2934
|
},
|
|
1950
2935
|
"build-and-wait": {
|
|
@@ -1954,9 +2939,13 @@ const commands = {
|
|
|
1954
2939
|
const options = {
|
|
1955
2940
|
buildMode: f["build-mode"],
|
|
1956
2941
|
includeLogs: f["include-logs"] === true,
|
|
2942
|
+
logFilter: jobLogFilterFromFlag(f["log-filter"]),
|
|
2943
|
+
maxLogLines: maxLogLinesFromFlags(f),
|
|
2944
|
+
partition: f["partition"],
|
|
1957
2945
|
pollIntervalMs: num(f["poll-interval"]),
|
|
1958
2946
|
timeoutMs: num(f["timeout"]),
|
|
1959
|
-
|
|
2947
|
+
summary: f["summary"] === true,
|
|
2948
|
+
targetType: jobBuildTargetTypeFromFlags(f),
|
|
1960
2949
|
};
|
|
1961
2950
|
if (f["dry-run"] === true) {
|
|
1962
2951
|
return {
|
|
@@ -1971,13 +2960,13 @@ const commands = {
|
|
|
1971
2960
|
}
|
|
1972
2961
|
return c.jobs.buildAndWait(a[0], { ...options, projectKey: pk, });
|
|
1973
2962
|
},
|
|
1974
|
-
usage: "dss job build-and-wait <target> [--type DATASET|MANAGED_FOLDER] [--build-mode MODE] [--include-logs] [--timeout MS] [--poll-interval MS] [--dry-run] [--project-key KEY]",
|
|
2963
|
+
usage: "dss job build-and-wait <target> [--target-type dataset|managed-folder] [--type DATASET|MANAGED_FOLDER] [--build-mode MODE] [--include-logs] [--log-filter stdout|stderr|user|errors] [--summary] [--max-log-lines N] [--timeout MS] [--poll-interval MS] [--partition PARTITION] [--dry-run] [--project-key KEY]",
|
|
1975
2964
|
description: "Build a dataset or managed folder and wait for completion.",
|
|
1976
2965
|
examples: [
|
|
1977
2966
|
"dss job build-and-wait orders",
|
|
1978
|
-
"dss job build-and-wait orders --include-logs",
|
|
2967
|
+
"dss job build-and-wait orders --include-logs --log-filter stdout --summary",
|
|
1979
2968
|
"dss job build-and-wait orders --timeout 300000",
|
|
1980
|
-
"dss job build-and-wait LT7TUHJ8 --type
|
|
2969
|
+
"dss job build-and-wait LT7TUHJ8 --target-type managed-folder --dry-run",
|
|
1981
2970
|
],
|
|
1982
2971
|
},
|
|
1983
2972
|
wait: {
|
|
@@ -1985,13 +2974,58 @@ const commands = {
|
|
|
1985
2974
|
requireArgs(a, 1, "dss job wait <id>");
|
|
1986
2975
|
return c.jobs.wait(a[0], {
|
|
1987
2976
|
includeLogs: f["include-logs"] === true,
|
|
2977
|
+
logFilter: jobLogFilterFromFlag(f["log-filter"]),
|
|
2978
|
+
maxLogLines: maxLogLinesFromFlags(f),
|
|
1988
2979
|
pollIntervalMs: num(f["poll-interval"]),
|
|
1989
2980
|
timeoutMs: num(f["timeout"]),
|
|
2981
|
+
summary: f["summary"] === true,
|
|
2982
|
+
projectKey: f["project-key"],
|
|
1990
2983
|
});
|
|
1991
2984
|
},
|
|
1992
|
-
usage: "dss job wait <id> [--include-logs] [--timeout MS] [--poll-interval MS]",
|
|
2985
|
+
usage: "dss job wait <id> [--include-logs] [--log-filter stdout|stderr|user|errors] [--summary] [--max-log-lines N] [--timeout MS] [--poll-interval MS]",
|
|
1993
2986
|
description: "Wait for an existing job to complete.",
|
|
1994
|
-
examples: [
|
|
2987
|
+
examples: [
|
|
2988
|
+
"dss job wait JOB_ID",
|
|
2989
|
+
"dss job wait JOB_ID --include-logs --log-filter stdout --summary --timeout 60000",
|
|
2990
|
+
],
|
|
2991
|
+
},
|
|
2992
|
+
monitor: {
|
|
2993
|
+
handler: async (c, a, f) => {
|
|
2994
|
+
requireArgs(a, 1, "dss job monitor <id...>");
|
|
2995
|
+
const options = {
|
|
2996
|
+
includeLogs: f["include-logs"] === true,
|
|
2997
|
+
logFilter: jobLogFilterFromFlag(f["log-filter"]),
|
|
2998
|
+
maxLogLines: maxLogLinesFromFlags(f),
|
|
2999
|
+
pollIntervalMs: num(f["poll-interval"]),
|
|
3000
|
+
timeoutMs: num(f["timeout"]),
|
|
3001
|
+
summary: f["summary"] !== false,
|
|
3002
|
+
projectKey: f["project-key"],
|
|
3003
|
+
};
|
|
3004
|
+
const jobs = await Promise.all(a.map((jobId) => c.jobs.wait(jobId, options)));
|
|
3005
|
+
return a.length === 1 ? jobs[0] : { jobs, until: f["until"] ?? "all-done", };
|
|
3006
|
+
},
|
|
3007
|
+
usage: "dss job monitor <id...> [--summary] [--include-logs] [--log-filter stdout|stderr|user|errors] [--max-log-lines N] [--timeout MS] [--poll-interval MS] [--until all-done] [--project-key KEY]",
|
|
3008
|
+
description: "Monitor one or more existing jobs and summarize progress counters from logs.",
|
|
3009
|
+
examples: ["dss job monitor JOB_ID --summary", "dss job monitor JOB1 JOB2 --until all-done",],
|
|
3010
|
+
},
|
|
3011
|
+
watch: {
|
|
3012
|
+
handler: async (c, a, f) => {
|
|
3013
|
+
requireArgs(a, 1, "dss job watch <id...>");
|
|
3014
|
+
const options = {
|
|
3015
|
+
includeLogs: f["include-logs"] === true,
|
|
3016
|
+
logFilter: jobLogFilterFromFlag(f["log-filter"]),
|
|
3017
|
+
maxLogLines: maxLogLinesFromFlags(f),
|
|
3018
|
+
pollIntervalMs: num(f["poll-interval"]),
|
|
3019
|
+
timeoutMs: num(f["timeout"]),
|
|
3020
|
+
summary: true,
|
|
3021
|
+
projectKey: f["project-key"],
|
|
3022
|
+
};
|
|
3023
|
+
const jobs = await Promise.all(a.map((jobId) => c.jobs.wait(jobId, options)));
|
|
3024
|
+
return a.length === 1 ? jobs[0] : { jobs, until: f["until"] ?? "all-done", };
|
|
3025
|
+
},
|
|
3026
|
+
usage: "dss job watch <id...> [--include-logs] [--log-filter stdout|stderr|user|errors] [--max-log-lines N] [--timeout MS] [--poll-interval MS] [--until all-done] [--project-key KEY]",
|
|
3027
|
+
description: "Watch one or more existing jobs with progress extraction enabled.",
|
|
3028
|
+
examples: ["dss job watch JOB_ID", "dss job watch JOB1 JOB2 --until all-done",],
|
|
1995
3029
|
},
|
|
1996
3030
|
abort: {
|
|
1997
3031
|
handler: async (c, a, f) => {
|
|
@@ -2028,7 +3062,7 @@ const commands = {
|
|
|
2028
3062
|
return c.scenarios.get(a[0], { projectKey: f["project-key"], });
|
|
2029
3063
|
},
|
|
2030
3064
|
usage: "dss scenario get <id> [--project-key KEY]",
|
|
2031
|
-
description: "Get scenario definition.",
|
|
3065
|
+
description: "Get raw scenario definition. For step-based scenario edits, patch params.steps; rawParams.params is DSS echo data.",
|
|
2032
3066
|
examples: ["dss scenario get my_scenario",],
|
|
2033
3067
|
},
|
|
2034
3068
|
run: {
|
|
@@ -2159,21 +3193,46 @@ const commands = {
|
|
|
2159
3193
|
handler: async (c, a, f) => {
|
|
2160
3194
|
requireArgs(a, 1, "dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin]");
|
|
2161
3195
|
const data = jsonInput(f);
|
|
2162
|
-
if (
|
|
3196
|
+
if (data === undefined) {
|
|
2163
3197
|
throw new UsageError("--data, --data-file, or --stdin is required. Usage: dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin]");
|
|
2164
3198
|
}
|
|
2165
3199
|
const pk = f["project-key"];
|
|
2166
3200
|
if (f["dry-run"] === true) {
|
|
2167
3201
|
const current = await c.scenarios.get(a[0], { projectKey: pk, });
|
|
2168
|
-
const
|
|
2169
|
-
return {
|
|
3202
|
+
const preview = scenarioUpdatePreview(current, data);
|
|
3203
|
+
return {
|
|
3204
|
+
dryRun: true,
|
|
3205
|
+
action: "update",
|
|
3206
|
+
resource: "scenario",
|
|
3207
|
+
id: a[0],
|
|
3208
|
+
canonicalEditableFields: preview.canonicalEditableFields,
|
|
3209
|
+
normalization: preview.normalization,
|
|
3210
|
+
normalizedData: preview.normalizedData,
|
|
3211
|
+
changes: preview.changes,
|
|
3212
|
+
unchangedPaths: preview.unchangedPaths,
|
|
3213
|
+
current: preview.current,
|
|
3214
|
+
next: preview.next,
|
|
3215
|
+
};
|
|
2170
3216
|
}
|
|
2171
|
-
await c.scenarios.update(a[0], data, pk);
|
|
2172
|
-
return {
|
|
3217
|
+
const result = await c.scenarios.update(a[0], data, pk);
|
|
3218
|
+
return {
|
|
3219
|
+
updated: a[0],
|
|
3220
|
+
resource: "scenario",
|
|
3221
|
+
verified: result.verified,
|
|
3222
|
+
changed: result.changes.length > 0,
|
|
3223
|
+
canonicalEditableFields: result.canonicalEditableFields,
|
|
3224
|
+
normalization: result.normalization,
|
|
3225
|
+
...(result.normalization.length > 0 ? { normalizedData: result.normalizedData, } : {}),
|
|
3226
|
+
changes: result.changes,
|
|
3227
|
+
unchangedPaths: result.unchangedPaths,
|
|
3228
|
+
};
|
|
2173
3229
|
},
|
|
2174
3230
|
usage: "dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin] [--dry-run] [--project-key KEY]",
|
|
2175
|
-
description: "Update scenario settings via JSON merge.",
|
|
2176
|
-
examples: [
|
|
3231
|
+
description: "Update scenario settings via JSON merge; edit step-based scenario steps at params.steps, not rawParams.params.steps.",
|
|
3232
|
+
examples: [
|
|
3233
|
+
'dss scenario update my_scenario --data \'{"params":{"steps":[]}}\' --dry-run',
|
|
3234
|
+
"dss scenario update my_scenario --data-file settings.json --dry-run",
|
|
3235
|
+
],
|
|
2177
3236
|
},
|
|
2178
3237
|
},
|
|
2179
3238
|
folder: {
|
|
@@ -2297,13 +3356,24 @@ const commands = {
|
|
|
2297
3356
|
contents: {
|
|
2298
3357
|
handler: async (c, a, f) => {
|
|
2299
3358
|
requireArgs(a, 1, "dss folder contents <name-or-id>");
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
3359
|
+
const startedAt = Date.now();
|
|
3360
|
+
let folderId = a[0];
|
|
3361
|
+
try {
|
|
3362
|
+
folderId = await resolveFolderId(c, a[0], f);
|
|
3363
|
+
return await c.folders.contents(folderId, {
|
|
3364
|
+
projectKey: f["project-key"],
|
|
3365
|
+
});
|
|
3366
|
+
}
|
|
3367
|
+
catch (error) {
|
|
3368
|
+
addTransientTargetContext(error, `folder:${folderId}`, Date.now() - startedAt);
|
|
3369
|
+
}
|
|
2303
3370
|
},
|
|
2304
|
-
usage: "dss folder contents <name-or-id> [--project-key KEY]",
|
|
3371
|
+
usage: "dss folder contents <name-or-id> [--retries N] [--request-timeout MS] [--project-key KEY]",
|
|
2305
3372
|
description: "List files in a managed folder.",
|
|
2306
|
-
examples: [
|
|
3373
|
+
examples: [
|
|
3374
|
+
"dss folder contents my_folder",
|
|
3375
|
+
"dss folder contents my_folder --retries 8 --request-timeout 60000",
|
|
3376
|
+
],
|
|
2307
3377
|
},
|
|
2308
3378
|
download: {
|
|
2309
3379
|
handler: async (c, a, f) => {
|
|
@@ -2425,6 +3495,40 @@ const commands = {
|
|
|
2425
3495
|
description: "List connections with inferred types and metadata.",
|
|
2426
3496
|
examples: ["dss connection infer", "dss connection infer --mode rich",],
|
|
2427
3497
|
},
|
|
3498
|
+
schemas: {
|
|
3499
|
+
handler: (c, _a, f) => {
|
|
3500
|
+
const connection = f["connection"];
|
|
3501
|
+
if (!connection) {
|
|
3502
|
+
throw new UsageError("--connection is required. Usage: dss connection schemas --connection CONN");
|
|
3503
|
+
}
|
|
3504
|
+
return c.connections.schemas({
|
|
3505
|
+
connection,
|
|
3506
|
+
projectKey: f["project-key"],
|
|
3507
|
+
});
|
|
3508
|
+
},
|
|
3509
|
+
usage: "dss connection schemas --connection CONN [--project-key KEY]",
|
|
3510
|
+
description: "List schemas in a SQL connection.",
|
|
3511
|
+
examples: ["dss connection schemas --connection ATHENA_CONN --project-key MYPROJ",],
|
|
3512
|
+
},
|
|
3513
|
+
tables: {
|
|
3514
|
+
handler: (c, _a, f) => {
|
|
3515
|
+
const connection = f["connection"];
|
|
3516
|
+
if (!connection) {
|
|
3517
|
+
throw new UsageError("--connection is required. Usage: dss connection tables --connection CONN");
|
|
3518
|
+
}
|
|
3519
|
+
return c.connections.tables({
|
|
3520
|
+
connection,
|
|
3521
|
+
catalog: f["catalog"],
|
|
3522
|
+
schema: f["schema"],
|
|
3523
|
+
projectKey: f["project-key"],
|
|
3524
|
+
});
|
|
3525
|
+
},
|
|
3526
|
+
usage: "dss connection tables --connection CONN [--catalog CATALOG] [--schema SCHEMA] [--project-key KEY]",
|
|
3527
|
+
description: "List importable tables in a SQL connection, optionally scoped by catalog and schema.",
|
|
3528
|
+
examples: [
|
|
3529
|
+
"dss connection tables --connection ATHENA_CONN --schema analytics --project-key MYPROJ",
|
|
3530
|
+
],
|
|
3531
|
+
},
|
|
2428
3532
|
},
|
|
2429
3533
|
"code-env": {
|
|
2430
3534
|
list: {
|
|
@@ -2644,20 +3748,35 @@ const commands = {
|
|
|
2644
3748
|
},
|
|
2645
3749
|
sql: {
|
|
2646
3750
|
query: {
|
|
2647
|
-
handler: (c, a, f) => {
|
|
3751
|
+
handler: async (c, a, f) => {
|
|
2648
3752
|
const query = resolveSqlInput(a, f);
|
|
2649
3753
|
const connection = f["connection"];
|
|
2650
3754
|
const datasetFullName = f["dataset"];
|
|
2651
3755
|
if ((connection ? 1 : 0) + (datasetFullName ? 1 : 0) !== 1) {
|
|
2652
3756
|
throw new UsageError(`Pass exactly one of --connection or --dataset. Usage: ${SQL_QUERY_USAGE}`);
|
|
2653
3757
|
}
|
|
2654
|
-
|
|
3758
|
+
const result = await c.sql.query({
|
|
2655
3759
|
query,
|
|
2656
3760
|
connection,
|
|
2657
3761
|
datasetFullName,
|
|
2658
3762
|
database: f["database"],
|
|
2659
3763
|
projectKey: f["project-key"],
|
|
2660
3764
|
});
|
|
3765
|
+
const outputFile = f["output"]
|
|
3766
|
+
?? f["output-file"];
|
|
3767
|
+
if (!outputFile)
|
|
3768
|
+
return result;
|
|
3769
|
+
const outputPath = resolve(outputFile);
|
|
3770
|
+
await mkdir(dirname(outputPath), { recursive: true, });
|
|
3771
|
+
await writeFile(outputPath, `${JSON.stringify(result, null, 2)}\n`, "utf-8");
|
|
3772
|
+
return {
|
|
3773
|
+
queryId: result.queryId,
|
|
3774
|
+
schema: result.schema,
|
|
3775
|
+
columns: result.columns ?? result.schema,
|
|
3776
|
+
rowCount: result.rows.length,
|
|
3777
|
+
outputPath,
|
|
3778
|
+
written: outputPath,
|
|
3779
|
+
};
|
|
2661
3780
|
},
|
|
2662
3781
|
usage: SQL_QUERY_USAGE,
|
|
2663
3782
|
description: "Run a SQL query against a DSS connection or dataset.",
|
|
@@ -2665,6 +3784,7 @@ const commands = {
|
|
|
2665
3784
|
"dss sql query 'SELECT * FROM orders LIMIT 10' --connection my_pg",
|
|
2666
3785
|
"dss sql query --sql-file query.sql --connection my_pg",
|
|
2667
3786
|
"echo 'SELECT 1' | dss sql query --stdin --dataset MYPROJ.orders",
|
|
3787
|
+
"dss sql query --sql-file query.sql --connection my_pg --output results.json --request-timeout 120000",
|
|
2668
3788
|
],
|
|
2669
3789
|
},
|
|
2670
3790
|
},
|
|
@@ -2920,7 +4040,7 @@ function printTopLevelHelp() {
|
|
|
2920
4040
|
" --url URL Dataiku DSS base URL (env: DATAIKU_URL)",
|
|
2921
4041
|
" --api-key KEY API key (env: DATAIKU_API_KEY)",
|
|
2922
4042
|
" --project-key KEY Default project key (env: DATAIKU_PROJECT_KEY)",
|
|
2923
|
-
" --timeout MS Operation timeout (build-and-wait, run-and-wait)",
|
|
4043
|
+
" --timeout MS Operation timeout (build-and-wait, run-and-wait, recipe run)",
|
|
2924
4044
|
" --request-timeout MS HTTP request timeout in ms (default: 30000)",
|
|
2925
4045
|
" --dry-run Preview destructive actions without executing",
|
|
2926
4046
|
" --if-not-exists Skip create if resource already exists",
|
|
@@ -2997,6 +4117,8 @@ function requireArgs(args, count, usage) {
|
|
|
2997
4117
|
// .env auto-loading
|
|
2998
4118
|
// ---------------------------------------------------------------------------
|
|
2999
4119
|
function loadEnvFile() {
|
|
4120
|
+
if (process.env.DATAIKU_DISABLE_ENV === "1")
|
|
4121
|
+
return;
|
|
3000
4122
|
const dirs = [
|
|
3001
4123
|
resolve(dirname(fileURLToPath(import.meta.url)), ".."),
|
|
3002
4124
|
process.cwd(),
|
|
@@ -3168,6 +4290,19 @@ async function probeDoctorPermission(probe) {
|
|
|
3168
4290
|
return { status: permissionStatusForError(error), details: errorDetails(error), };
|
|
3169
4291
|
}
|
|
3170
4292
|
}
|
|
4293
|
+
async function probeReadOnlyPrerequisiteForMutation(probe, readAction) {
|
|
4294
|
+
const readProbe = await probeDoctorPermission(probe);
|
|
4295
|
+
if (readProbe.status !== "yes")
|
|
4296
|
+
return readProbe;
|
|
4297
|
+
return {
|
|
4298
|
+
status: "unknown",
|
|
4299
|
+
details: {
|
|
4300
|
+
reason: "mutation capability was not verified because doctor capabilities are read-only",
|
|
4301
|
+
readAction,
|
|
4302
|
+
readStatus: "yes",
|
|
4303
|
+
},
|
|
4304
|
+
};
|
|
4305
|
+
}
|
|
3171
4306
|
function missingProjectPermission() {
|
|
3172
4307
|
return {
|
|
3173
4308
|
status: "unknown",
|
|
@@ -3273,21 +4408,21 @@ async function doctorCapabilities(client, projectKey, accessibleProjects, flags)
|
|
|
3273
4408
|
? probeDoctorPermission(() => client.projects.get(probeProjectKey))
|
|
3274
4409
|
: Promise.resolve(missingProjectPermission()),
|
|
3275
4410
|
canMutateProject: () => probeProjectKey
|
|
3276
|
-
?
|
|
4411
|
+
? probeReadOnlyPrerequisiteForMutation(() => client.variables.get(probeProjectKey), "variables.get")
|
|
3277
4412
|
: Promise.resolve(missingProjectPermission()),
|
|
3278
4413
|
canCreateFolder: () => probeProjectKey
|
|
3279
|
-
?
|
|
4414
|
+
? probeReadOnlyPrerequisiteForMutation(() => client.folders.list(probeProjectKey), "folders.list")
|
|
3280
4415
|
: Promise.resolve(missingProjectPermission()),
|
|
3281
4416
|
canRunJobs: () => probeProjectKey
|
|
3282
|
-
?
|
|
4417
|
+
? probeReadOnlyPrerequisiteForMutation(() => client.jobs.list(probeProjectKey), "jobs.list")
|
|
3283
4418
|
: Promise.resolve(missingProjectPermission()),
|
|
3284
4419
|
canCreateScenario: () => probeProjectKey
|
|
3285
|
-
?
|
|
4420
|
+
? probeReadOnlyPrerequisiteForMutation(() => client.scenarios.list(probeProjectKey), "scenarios.list")
|
|
3286
4421
|
: Promise.resolve(missingProjectPermission()),
|
|
3287
4422
|
canSaveJupyter: () => probeProjectKey
|
|
3288
|
-
?
|
|
4423
|
+
? probeReadOnlyPrerequisiteForMutation(() => client.notebooks.listJupyter(probeProjectKey), "notebooks.listJupyter")
|
|
3289
4424
|
: Promise.resolve(missingProjectPermission()),
|
|
3290
|
-
canMutateConnection: () =>
|
|
4425
|
+
canMutateConnection: () => probeReadOnlyPrerequisiteForMutation(() => client.connections.list(), "connections.list"),
|
|
3291
4426
|
};
|
|
3292
4427
|
const permissions = {};
|
|
3293
4428
|
const permissionDetails = {};
|
|
@@ -3347,12 +4482,14 @@ async function runDoctor(flags) {
|
|
|
3347
4482
|
let accessibleProjects;
|
|
3348
4483
|
if (credentialsOk) {
|
|
3349
4484
|
const requestTimeoutMs = num(flags["request-timeout"]);
|
|
4485
|
+
const retryMaxAttempts = num(flags["retries"]);
|
|
3350
4486
|
const client = new DataikuClient({
|
|
3351
4487
|
url,
|
|
3352
4488
|
apiKey,
|
|
3353
4489
|
projectKey,
|
|
3354
4490
|
verbose: flags["verbose"] === true,
|
|
3355
4491
|
requestTimeoutMs,
|
|
4492
|
+
retryMaxAttempts,
|
|
3356
4493
|
tlsRejectUnauthorized,
|
|
3357
4494
|
caCertPath,
|
|
3358
4495
|
});
|
|
@@ -3400,13 +4537,14 @@ async function runDoctor(flags) {
|
|
|
3400
4537
|
const result = { ok: checks.every((check) => check.ok), checks, context, };
|
|
3401
4538
|
if (flags["capabilities"] === true && credentialsOk) {
|
|
3402
4539
|
const requestTimeoutMs = num(flags["request-timeout"]);
|
|
4540
|
+
const retryMaxAttempts = num(flags["retries"]) ?? 1;
|
|
3403
4541
|
const client = new DataikuClient({
|
|
3404
4542
|
url,
|
|
3405
4543
|
apiKey,
|
|
3406
4544
|
projectKey,
|
|
3407
4545
|
verbose: flags["verbose"] === true,
|
|
3408
4546
|
requestTimeoutMs,
|
|
3409
|
-
retryMaxAttempts
|
|
4547
|
+
retryMaxAttempts,
|
|
3410
4548
|
tlsRejectUnauthorized,
|
|
3411
4549
|
caCertPath,
|
|
3412
4550
|
});
|
|
@@ -3428,19 +4566,21 @@ async function runFixtures(flags) {
|
|
|
3428
4566
|
}
|
|
3429
4567
|
currentCommandContext.projectKey = projectKey;
|
|
3430
4568
|
const requestTimeoutMs = num(flags["request-timeout"]);
|
|
4569
|
+
const retryMaxAttempts = num(flags["retries"]) ?? 1;
|
|
3431
4570
|
const client = new DataikuClient({
|
|
3432
4571
|
url,
|
|
3433
4572
|
apiKey,
|
|
3434
4573
|
projectKey,
|
|
3435
4574
|
verbose: flags["verbose"] === true,
|
|
3436
4575
|
requestTimeoutMs,
|
|
3437
|
-
retryMaxAttempts
|
|
4576
|
+
retryMaxAttempts,
|
|
3438
4577
|
tlsRejectUnauthorized,
|
|
3439
4578
|
caCertPath,
|
|
3440
4579
|
});
|
|
3441
4580
|
return discoverFixtureReport(client, projectKey, flags);
|
|
3442
4581
|
}
|
|
3443
4582
|
const READ_ACTIONS = new Set([
|
|
4583
|
+
"cat",
|
|
3444
4584
|
"contents",
|
|
3445
4585
|
"diff",
|
|
3446
4586
|
"download",
|
|
@@ -3461,13 +4601,18 @@ const READ_ACTIONS = new Set([
|
|
|
3461
4601
|
"list-jupyter",
|
|
3462
4602
|
"list-sql",
|
|
3463
4603
|
"log",
|
|
4604
|
+
"log-url",
|
|
3464
4605
|
"map",
|
|
3465
4606
|
"metadata",
|
|
3466
4607
|
"peek",
|
|
4608
|
+
"source",
|
|
4609
|
+
"summary",
|
|
3467
4610
|
"wait",
|
|
4611
|
+
"watch",
|
|
3468
4612
|
"preview",
|
|
3469
4613
|
"query",
|
|
3470
4614
|
"schema",
|
|
4615
|
+
"schemas",
|
|
3471
4616
|
"sessions-jupyter",
|
|
3472
4617
|
"status",
|
|
3473
4618
|
"rules",
|
|
@@ -3492,7 +4637,14 @@ const PROJECT_SCOPED_RESOURCES = new Set([
|
|
|
3492
4637
|
"wiki",
|
|
3493
4638
|
]);
|
|
3494
4639
|
const GLOBAL_AGENT_FLAGS = ["help", "json", "report-json", "verbose",];
|
|
3495
|
-
const AUTHENTICATED_AGENT_FLAGS = [
|
|
4640
|
+
const AUTHENTICATED_AGENT_FLAGS = [
|
|
4641
|
+
"url",
|
|
4642
|
+
"api-key",
|
|
4643
|
+
"request-timeout",
|
|
4644
|
+
"retries",
|
|
4645
|
+
"insecure",
|
|
4646
|
+
"ca-cert",
|
|
4647
|
+
];
|
|
3496
4648
|
const COMMANDS_USAGE = "dss commands [--json]";
|
|
3497
4649
|
const COMMANDS_DESCRIPTION = "Print the machine-readable command registry for agent planning.";
|
|
3498
4650
|
const COMMANDS_EXAMPLES = ["dss commands", "dss commands --json",];
|
|
@@ -3586,7 +4738,7 @@ function inferSideEffect(resource, action) {
|
|
|
3586
4738
|
return "write";
|
|
3587
4739
|
if (READ_ACTIONS.has(action))
|
|
3588
4740
|
return "read";
|
|
3589
|
-
if (/^(create|update|delete|set|save|upload|run|build|abort|move|clear|unload|install|login|logout)/
|
|
4741
|
+
if (/^(create|clone|restore|update|delete|set|save|upload|run|build|abort|move|refresh|clear|unload|install|login|logout)/
|
|
3590
4742
|
.test(action)) {
|
|
3591
4743
|
return "write";
|
|
3592
4744
|
}
|
|
@@ -3606,12 +4758,14 @@ function inferRequiresProject(resource, action, usage) {
|
|
|
3606
4758
|
}
|
|
3607
4759
|
const ARRAY_OUTPUT_ACTIONS = new Set([
|
|
3608
4760
|
"history",
|
|
4761
|
+
"find",
|
|
3609
4762
|
"infer",
|
|
3610
4763
|
"last-results",
|
|
3611
4764
|
"list",
|
|
3612
4765
|
"list-jupyter",
|
|
3613
4766
|
"list-sql",
|
|
3614
4767
|
"rules",
|
|
4768
|
+
"schemas",
|
|
3615
4769
|
"sessions-jupyter",
|
|
3616
4770
|
"usages",
|
|
3617
4771
|
]);
|
|
@@ -3620,7 +4774,9 @@ const STRING_OUTPUT_ACTIONS = new Set([
|
|
|
3620
4774
|
"download",
|
|
3621
4775
|
"download-code",
|
|
3622
4776
|
"get-payload",
|
|
4777
|
+
"cat",
|
|
3623
4778
|
"log",
|
|
4779
|
+
"log-url",
|
|
3624
4780
|
"preview",
|
|
3625
4781
|
]);
|
|
3626
4782
|
function inferOutputShape(resource, action) {
|
|
@@ -3661,7 +4817,7 @@ function inferExitCodes(asyncKind) {
|
|
|
3661
4817
|
};
|
|
3662
4818
|
}
|
|
3663
4819
|
function cleanupCommandFromDeleteUsage(resource, action) {
|
|
3664
|
-
if (!action.startsWith("create"))
|
|
4820
|
+
if (!(action.startsWith("create") || action === "clone"))
|
|
3665
4821
|
return undefined;
|
|
3666
4822
|
const deleteAction = action === "create-rule" ? "delete-rule" : "delete";
|
|
3667
4823
|
const deleteUsage = commands[resource]?.[deleteAction]?.usage;
|
|
@@ -3684,7 +4840,10 @@ function inferDestructiveLevel(sideEffect, action) {
|
|
|
3684
4840
|
return "reversible";
|
|
3685
4841
|
}
|
|
3686
4842
|
function inferAsyncKind(resource, action) {
|
|
3687
|
-
if (resource === "job" && ["build", "build-and-wait", "wait",].includes(action))
|
|
4843
|
+
if (resource === "job" && ["build", "build-and-wait", "wait", "monitor", "watch",].includes(action)) {
|
|
4844
|
+
return "job";
|
|
4845
|
+
}
|
|
4846
|
+
if (resource === "recipe" && action === "run")
|
|
3688
4847
|
return "job";
|
|
3689
4848
|
if (resource === "future" && ["get", "peek", "wait", "abort",].includes(action))
|
|
3690
4849
|
return "future";
|
|
@@ -3705,7 +4864,7 @@ function inferIdempotency(sideEffect, action, usage) {
|
|
|
3705
4864
|
return "none";
|
|
3706
4865
|
}
|
|
3707
4866
|
function inferCleanupHint(resource, action) {
|
|
3708
|
-
if (!action.startsWith("create"))
|
|
4867
|
+
if (!(action.startsWith("create") || action === "clone"))
|
|
3709
4868
|
return undefined;
|
|
3710
4869
|
if (resource === "code-env")
|
|
3711
4870
|
return "Delete with `dss code-env delete <lang> <name> --if-exists`.";
|
|
@@ -3762,7 +4921,8 @@ function buildRegistryEntry(resource, action, meta) {
|
|
|
3762
4921
|
outputShape: inferOutputShape(resource, action),
|
|
3763
4922
|
inputContract,
|
|
3764
4923
|
destructive,
|
|
3765
|
-
producesLocalFile: meta.usage.includes("--output PATH")
|
|
4924
|
+
producesLocalFile: meta.usage.includes("--output PATH")
|
|
4925
|
+
|| meta.usage.includes("--output-file PATH"),
|
|
3766
4926
|
mutatesDss,
|
|
3767
4927
|
async: asyncKind,
|
|
3768
4928
|
idempotency: inferIdempotency(sideEffect, action, meta.usage),
|
|
@@ -3853,7 +5013,7 @@ function requiredPlanFlag(flags, name, usage) {
|
|
|
3853
5013
|
}
|
|
3854
5014
|
function optionalJsonFlag(flags, name) {
|
|
3855
5015
|
const value = flags[name];
|
|
3856
|
-
return typeof value === "string" ?
|
|
5016
|
+
return typeof value === "string" ? parseJsonObject(value, `--${name}`) : undefined;
|
|
3857
5017
|
}
|
|
3858
5018
|
function requiredPlanJsonInput(flags, usage) {
|
|
3859
5019
|
return requiredJsonInput(flags, `--data, --data-file, or --stdin is required. Usage: ${usage}`);
|
|
@@ -3874,9 +5034,20 @@ function querySuffix(params) {
|
|
|
3874
5034
|
return raw ? `?${raw}` : "";
|
|
3875
5035
|
}
|
|
3876
5036
|
function jobBuildPayload(target, projectKey, flags) {
|
|
3877
|
-
const targetType =
|
|
5037
|
+
const targetType = jobBuildTargetTypeFromFlags(flags);
|
|
5038
|
+
const partition = flags["partition"];
|
|
5039
|
+
const output = { projectKey, id: target, type: targetType, };
|
|
5040
|
+
if (targetType === "DATASET") {
|
|
5041
|
+
if (partition !== undefined)
|
|
5042
|
+
output.partition = partition;
|
|
5043
|
+
}
|
|
5044
|
+
else {
|
|
5045
|
+
output.targetManagedFolderProjectKey = projectKey;
|
|
5046
|
+
output.targetManagedFolder = target;
|
|
5047
|
+
output.targetPartition = partition ?? "NP";
|
|
5048
|
+
}
|
|
3878
5049
|
const payload = {
|
|
3879
|
-
outputs: [
|
|
5050
|
+
outputs: [output,],
|
|
3880
5051
|
type: flags["build-mode"] ?? "NON_RECURSIVE_FORCED_BUILD",
|
|
3881
5052
|
};
|
|
3882
5053
|
if (flags["force-rebuild"] === true && targetType === "DATASET") {
|
|
@@ -4062,9 +5233,9 @@ function commandPlanShape(resource, action, args, flags, entry, projectKey) {
|
|
|
4062
5233
|
case "flow-zone.move":
|
|
4063
5234
|
return {
|
|
4064
5235
|
method: "POST",
|
|
4065
|
-
endpoint: projectEndpoint(`/flow/zones/${encodeURIComponent(id)}/
|
|
5236
|
+
endpoint: projectEndpoint(`/flow/zones/${encodeURIComponent(id)}/add-items`),
|
|
4066
5237
|
identifiers: { id, },
|
|
4067
|
-
payload:
|
|
5238
|
+
payload: flowZoneMoveItems(flags),
|
|
4068
5239
|
};
|
|
4069
5240
|
case "dataset.create": {
|
|
4070
5241
|
const name = requiredPlanFlag(flags, "name", entry.usage);
|
|
@@ -4083,6 +5254,15 @@ function commandPlanShape(resource, action, args, flags, entry, projectKey) {
|
|
|
4083
5254
|
endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}`),
|
|
4084
5255
|
identifiers: { name: id, },
|
|
4085
5256
|
};
|
|
5257
|
+
case "dataset.refresh-schema": {
|
|
5258
|
+
const columns = schemaColumnsInput(flags, entry.usage);
|
|
5259
|
+
return {
|
|
5260
|
+
method: "PUT",
|
|
5261
|
+
endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}/schema`),
|
|
5262
|
+
identifiers: { name: id, },
|
|
5263
|
+
payload: { columns, },
|
|
5264
|
+
};
|
|
5265
|
+
}
|
|
4086
5266
|
case "dataset.update":
|
|
4087
5267
|
return {
|
|
4088
5268
|
method: "PUT",
|
|
@@ -4124,6 +5304,19 @@ function commandPlanShape(resource, action, args, flags, entry, projectKey) {
|
|
|
4124
5304
|
},
|
|
4125
5305
|
};
|
|
4126
5306
|
}
|
|
5307
|
+
case "recipe.run":
|
|
5308
|
+
return {
|
|
5309
|
+
method: "POST",
|
|
5310
|
+
endpoint: projectEndpoint("/jobs/"),
|
|
5311
|
+
identifiers: { recipe: id, },
|
|
5312
|
+
payload: {
|
|
5313
|
+
recipe: id,
|
|
5314
|
+
outputResolution: "dynamic",
|
|
5315
|
+
projectKey,
|
|
5316
|
+
partition: flags["partition"],
|
|
5317
|
+
},
|
|
5318
|
+
wait: recipeRunShouldWait(flags),
|
|
5319
|
+
};
|
|
4127
5320
|
case "recipe.update":
|
|
4128
5321
|
return {
|
|
4129
5322
|
method: "PUT",
|
|
@@ -4131,13 +5324,27 @@ function commandPlanShape(resource, action, args, flags, entry, projectKey) {
|
|
|
4131
5324
|
identifiers: { name: id, },
|
|
4132
5325
|
payload: requiredPlanJsonInput(flags, entry.usage),
|
|
4133
5326
|
};
|
|
4134
|
-
case "recipe.set-payload":
|
|
5327
|
+
case "recipe.set-payload": {
|
|
5328
|
+
const file = requiredPlanFlag(flags, "file", entry.usage);
|
|
5329
|
+
const backupDir = flags["no-backup"] === true
|
|
5330
|
+
? undefined
|
|
5331
|
+
: flags["backup-dir"]
|
|
5332
|
+
?? join(process.cwd(), ".dss-backups", "recipes");
|
|
5333
|
+
const backupPath = backupDir ? recipeBackupPath(id, backupDir) : undefined;
|
|
4135
5334
|
return {
|
|
4136
5335
|
method: "PUT",
|
|
4137
|
-
endpoint: projectEndpoint(`/recipes/${encodeURIComponent(id)}
|
|
5336
|
+
endpoint: projectEndpoint(`/recipes/${encodeURIComponent(id)}`),
|
|
4138
5337
|
identifiers: { name: id, },
|
|
4139
|
-
payload: {
|
|
5338
|
+
payload: {
|
|
5339
|
+
file,
|
|
5340
|
+
content: textInput(flags),
|
|
5341
|
+
...(backupPath ? { backupPath, } : {}),
|
|
5342
|
+
},
|
|
5343
|
+
...(backupPath
|
|
5344
|
+
? { localWrites: [{ path: backupPath, source: "remote recipe backup", before: "PUT", },], }
|
|
5345
|
+
: {}),
|
|
4140
5346
|
};
|
|
5347
|
+
}
|
|
4141
5348
|
case "job.build":
|
|
4142
5349
|
case "job.build-and-wait":
|
|
4143
5350
|
return {
|
|
@@ -4404,12 +5611,14 @@ async function runCleanup(flags) {
|
|
|
4404
5611
|
throw new UsageError("Missing API key. Set DATAIKU_API_KEY, pass --api-key, or run: dss auth login");
|
|
4405
5612
|
}
|
|
4406
5613
|
const requestTimeoutMs = num(flags["request-timeout"]);
|
|
5614
|
+
const retryMaxAttempts = num(flags["retries"]);
|
|
4407
5615
|
const client = new DataikuClient({
|
|
4408
5616
|
url,
|
|
4409
5617
|
apiKey,
|
|
4410
5618
|
projectKey,
|
|
4411
5619
|
verbose: flags["verbose"] === true,
|
|
4412
5620
|
requestTimeoutMs,
|
|
5621
|
+
retryMaxAttempts,
|
|
4413
5622
|
tlsRejectUnauthorized,
|
|
4414
5623
|
caCertPath,
|
|
4415
5624
|
});
|
|
@@ -4479,17 +5688,26 @@ function promptSecret(label) {
|
|
|
4479
5688
|
// Credential resolution
|
|
4480
5689
|
// ---------------------------------------------------------------------------
|
|
4481
5690
|
function resolveCredentials(flags) {
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
5691
|
+
const hasUrlFlag = Object.hasOwn(flags, "url");
|
|
5692
|
+
const hasApiKeyFlag = Object.hasOwn(flags, "api-key");
|
|
5693
|
+
const hasProjectKeyFlag = Object.hasOwn(flags, "project-key");
|
|
5694
|
+
let url = hasUrlFlag ? flags["url"] : undefined;
|
|
5695
|
+
let apiKey = hasApiKeyFlag ? flags["api-key"] : undefined;
|
|
5696
|
+
let projectKey = hasProjectKeyFlag ? flags["project-key"] : undefined;
|
|
4485
5697
|
const saved = loadCredentials();
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
5698
|
+
if (!hasUrlFlag)
|
|
5699
|
+
url ??= process.env.DATAIKU_URL;
|
|
5700
|
+
if (!hasApiKeyFlag)
|
|
5701
|
+
apiKey ??= process.env.DATAIKU_API_KEY;
|
|
5702
|
+
if (!hasProjectKeyFlag)
|
|
5703
|
+
projectKey ??= process.env.DATAIKU_PROJECT_KEY;
|
|
4489
5704
|
if (saved) {
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
5705
|
+
if (!hasUrlFlag)
|
|
5706
|
+
url ??= saved.url;
|
|
5707
|
+
if (!hasApiKeyFlag)
|
|
5708
|
+
apiKey ??= saved.apiKey;
|
|
5709
|
+
if (!hasProjectKeyFlag)
|
|
5710
|
+
projectKey ??= saved.projectKey;
|
|
4493
5711
|
}
|
|
4494
5712
|
return {
|
|
4495
5713
|
url: url ?? "",
|
|
@@ -4624,7 +5842,7 @@ async function main() {
|
|
|
4624
5842
|
const { positional, flags, } = parseArgs(process.argv.slice(2));
|
|
4625
5843
|
// --version
|
|
4626
5844
|
if (flags["version"] === true) {
|
|
4627
|
-
process.stdout.write(`${
|
|
5845
|
+
process.stdout.write(`${CLI_VERSION_LABEL}\n`);
|
|
4628
5846
|
process.exit(0);
|
|
4629
5847
|
}
|
|
4630
5848
|
// Top-level help
|
|
@@ -4940,12 +6158,14 @@ async function main() {
|
|
|
4940
6158
|
throw new UsageError("Missing API key. Set DATAIKU_API_KEY, pass --api-key, or run: dss auth login");
|
|
4941
6159
|
}
|
|
4942
6160
|
const requestTimeoutMs = num(flags["request-timeout"]);
|
|
6161
|
+
const retryMaxAttempts = num(flags["retries"]);
|
|
4943
6162
|
const client = new DataikuClient({
|
|
4944
6163
|
url,
|
|
4945
6164
|
apiKey,
|
|
4946
6165
|
projectKey,
|
|
4947
6166
|
verbose: flags["verbose"] === true,
|
|
4948
6167
|
requestTimeoutMs,
|
|
6168
|
+
retryMaxAttempts,
|
|
4949
6169
|
tlsRejectUnauthorized,
|
|
4950
6170
|
caCertPath,
|
|
4951
6171
|
});
|
|
@@ -4960,7 +6180,12 @@ async function main() {
|
|
|
4960
6180
|
if (entry)
|
|
4961
6181
|
await appendCleanupLedgerEntry(flags["record-cleanup"], entry);
|
|
4962
6182
|
}
|
|
4963
|
-
|
|
6183
|
+
if (flags["raw"] === true && typeof result === "string") {
|
|
6184
|
+
process.stdout.write(result);
|
|
6185
|
+
}
|
|
6186
|
+
else {
|
|
6187
|
+
writeCommandResult(result);
|
|
6188
|
+
}
|
|
4964
6189
|
const failureExitCode = commandFailureExitCode(result);
|
|
4965
6190
|
if (failureExitCode !== undefined)
|
|
4966
6191
|
process.exit(failureExitCode);
|