dataiku-sdk 3.7.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/build-metadata.json +1 -1
- package/dist/packages/types/src/index.d.ts +40 -4
- package/dist/packages/types/src/index.js +22 -1
- package/dist/src/cli/coerce.d.ts +1 -0
- package/dist/src/cli/coerce.js +1 -1
- package/dist/src/cli/command-syntax.json +156 -0
- package/dist/src/cli/commands/dataset.js +44 -8
- package/dist/src/cli/commands/flow-zone.js +241 -46
- package/dist/src/cli/commands/job.js +52 -9
- package/dist/src/cli/commands/project.js +4 -3
- package/dist/src/cli/commands/recipe.d.ts +4 -0
- package/dist/src/cli/commands/recipe.js +38 -1
- package/dist/src/cli/commands/wiki.js +56 -0
- package/dist/src/cli/contract.js +13 -8
- package/dist/src/cli/doctor.js +2 -0
- package/dist/src/cli/flags.d.ts +7 -0
- package/dist/src/cli/flags.js +28 -2
- package/dist/src/cli/helpers/dataset.d.ts +12 -0
- package/dist/src/cli/helpers/dataset.js +36 -0
- package/dist/src/cli/helpers/flow-zone.d.ts +21 -0
- package/dist/src/cli/helpers/flow-zone.js +144 -4
- package/dist/src/cli/output-schemas.js +27 -3
- package/dist/src/cli/output.js +6 -0
- package/dist/src/cli/plans.js +58 -2
- package/dist/src/cli/project-map-output.d.ts +24 -0
- package/dist/src/cli/project-map-output.js +21 -0
- package/dist/src/cli/read-shapes.d.ts +14 -0
- package/dist/src/cli/read-shapes.js +114 -0
- package/dist/src/cli/result-shapes.d.ts +6 -0
- package/dist/src/cli/result-shapes.js +235 -0
- package/dist/src/cli/types.d.ts +2 -0
- package/dist/src/cli/version.d.ts +2 -2
- package/dist/src/cli/version.js +2 -2
- package/dist/src/cli.js +23 -20
- package/dist/src/errors.d.ts +11 -1
- package/dist/src/errors.js +18 -3
- package/dist/src/generated/action-output-schemas.json +224 -35
- package/dist/src/resources/dataset-create.d.ts +19 -0
- package/dist/src/resources/dataset-create.js +69 -0
- package/dist/src/resources/datasets.d.ts +21 -0
- package/dist/src/resources/datasets.js +61 -1
- package/dist/src/resources/flow-zones.d.ts +29 -3
- package/dist/src/resources/flow-zones.js +53 -3
- package/dist/src/resources/jobs.d.ts +36 -1
- package/dist/src/resources/jobs.js +111 -5
- package/dist/src/resources/projects.d.ts +9 -0
- package/dist/src/resources/projects.js +10 -3
- package/dist/src/resources/recipe-create.d.ts +17 -3
- package/dist/src/resources/recipe-create.js +149 -33
- package/dist/src/resources/recipes.js +76 -23
- package/dist/src/resources/sql.js +52 -13
- package/dist/src/resources/wiki.d.ts +47 -1
- package/dist/src/resources/wiki.js +89 -0
- package/dist/src/utils/flow-analysis.js +4 -0
- package/dist/src/utils/secret-sanitize.d.ts +11 -2
- package/dist/src/utils/secret-sanitize.js +23 -6
- package/package.json +4 -4
- package/plugin.json +1 -1
- package/skills/dataiku-dss/references/discovery.md +2 -2
- package/skills/dataiku-dss/references/flow-maps.md +2 -2
- package/skills/dataiku-dss/references/troubleshooting.md +3 -3
|
@@ -55,6 +55,18 @@ export interface DatasetManagedCreateResult {
|
|
|
55
55
|
projectKey: string;
|
|
56
56
|
creationSettings: Record<string, unknown>;
|
|
57
57
|
}
|
|
58
|
+
/** Outcome of {@link DatasetsResource.detectSqlSchema}. */
|
|
59
|
+
export interface DatasetSqlSchemaDetection {
|
|
60
|
+
/** True when DSS suggested a schema and it was stored on the dataset. */
|
|
61
|
+
detected: boolean;
|
|
62
|
+
/** Stored columns (empty when detection failed). */
|
|
63
|
+
columns: Array<{
|
|
64
|
+
name: string;
|
|
65
|
+
type: string;
|
|
66
|
+
}>;
|
|
67
|
+
/** Why detection failed. */
|
|
68
|
+
error?: string;
|
|
69
|
+
}
|
|
58
70
|
export interface DatasetCloneOptions {
|
|
59
71
|
projectKey?: string;
|
|
60
72
|
path?: string;
|
|
@@ -106,6 +118,15 @@ export declare class DatasetsResource extends BaseResource {
|
|
|
106
118
|
schema(datasetName: string, projectKey?: string): Promise<DatasetSchema>;
|
|
107
119
|
/** Replace dataset schema columns directly through the schema endpoint. */
|
|
108
120
|
updateSchema(datasetName: string, columns: DatasetSchemaColumnInput[], projectKey?: string): Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Detect the schema of a SQL dataset (table or query mode) with DSS and
|
|
123
|
+
* store it: `POST /actions/testAndDetectSettings/externalSQL` (what
|
|
124
|
+
* dataikuapi's `autodetect_settings` calls), then `PUT /schema` with the
|
|
125
|
+
* suggested schema. DSS reports connection, table, and query problems in the
|
|
126
|
+
* response body (HTTP 200), so a failed detection is returned with
|
|
127
|
+
* `detected: false` and the reason instead of thrown.
|
|
128
|
+
*/
|
|
129
|
+
detectSqlSchema(datasetName: string, projectKey?: string): Promise<DatasetSqlSchemaDetection>;
|
|
109
130
|
/**
|
|
110
131
|
* Get the raw dataset schema object exactly as the DSS schema endpoint
|
|
111
132
|
* returns it, without schema validation — used for deterministic comparison.
|
|
@@ -4,6 +4,7 @@ import { StringDecoder, } from "node:string_decoder";
|
|
|
4
4
|
import { ClientValidationError, DataikuError, } from "../errors.js";
|
|
5
5
|
import { DatasetDetailsSchema, DatasetSchemaSchema, DatasetSummaryArraySchema, } from "../schemas.js";
|
|
6
6
|
import { deepMerge, } from "../utils/deep-merge.js";
|
|
7
|
+
import { isRecord, } from "../utils/records.js";
|
|
7
8
|
import { sanitizeFileName, } from "../utils/sanitize.js";
|
|
8
9
|
import { BaseResource, } from "./base.js";
|
|
9
10
|
import { resolveAdminManagedStorageConnection, } from "./connections.js";
|
|
@@ -519,6 +520,28 @@ function cloneDatasetParams(params) {
|
|
|
519
520
|
}
|
|
520
521
|
return cloned;
|
|
521
522
|
}
|
|
523
|
+
/** Explain an empty detection result from the first error DSS put in the response body. */
|
|
524
|
+
function describeSqlDetectionFailure(raw) {
|
|
525
|
+
if (!isRecord(raw))
|
|
526
|
+
return "DSS returned no schema detection result.";
|
|
527
|
+
for (const [key, value,] of Object.entries(raw)) {
|
|
528
|
+
if (!key.endsWith("Error"))
|
|
529
|
+
continue;
|
|
530
|
+
if (typeof value === "string" && value !== "")
|
|
531
|
+
return value;
|
|
532
|
+
if (isRecord(value)) {
|
|
533
|
+
const message = value["detailedMessage"] ?? value["message"];
|
|
534
|
+
if (typeof message === "string" && message !== "")
|
|
535
|
+
return message;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const failedChecks = Object.entries(raw)
|
|
539
|
+
.filter(([key, value,]) => key.endsWith("OK") && value === false)
|
|
540
|
+
.map(([key,]) => key);
|
|
541
|
+
return failedChecks.length > 0
|
|
542
|
+
? `DSS detected no columns (failed checks: ${failedChecks.join(", ")}).`
|
|
543
|
+
: "DSS detected no columns.";
|
|
544
|
+
}
|
|
522
545
|
function parseUploadedFiles(raw, datasetName) {
|
|
523
546
|
if (!Array.isArray(raw)) {
|
|
524
547
|
throw new ClientValidationError(`DSS returned an invalid uploaded-files list for dataset ${datasetName}.`, "validation_failed", "Expected an array from the UploadedFiles dataset file endpoint.");
|
|
@@ -592,6 +615,40 @@ export class DatasetsResource extends BaseResource {
|
|
|
592
615
|
const dsEnc = encodeURIComponent(datasetName);
|
|
593
616
|
await this.client.put(`/public/api/projects/${this.enc(projectKey)}/datasets/${dsEnc}/schema`, { columns, });
|
|
594
617
|
}
|
|
618
|
+
/**
|
|
619
|
+
* Detect the schema of a SQL dataset (table or query mode) with DSS and
|
|
620
|
+
* store it: `POST /actions/testAndDetectSettings/externalSQL` (what
|
|
621
|
+
* dataikuapi's `autodetect_settings` calls), then `PUT /schema` with the
|
|
622
|
+
* suggested schema. DSS reports connection, table, and query problems in the
|
|
623
|
+
* response body (HTTP 200), so a failed detection is returned with
|
|
624
|
+
* `detected: false` and the reason instead of thrown.
|
|
625
|
+
*/
|
|
626
|
+
async detectSqlSchema(datasetName, projectKey) {
|
|
627
|
+
const base = `/public/api/projects/${this.enc(projectKey)}/datasets/${encodeURIComponent(datasetName)}`;
|
|
628
|
+
try {
|
|
629
|
+
const raw = await this.client.post(`${base}/actions/testAndDetectSettings/externalSQL`);
|
|
630
|
+
const detection = isRecord(raw) ? raw["schemaDetection"] : undefined;
|
|
631
|
+
const newSchema = isRecord(detection) ? detection["newSchema"] : undefined;
|
|
632
|
+
const rawColumns = isRecord(newSchema) && Array.isArray(newSchema["columns"])
|
|
633
|
+
? newSchema["columns"]
|
|
634
|
+
: [];
|
|
635
|
+
const columns = rawColumns.flatMap((column) => isRecord(column) && typeof column["name"] === "string"
|
|
636
|
+
&& typeof column["type"] === "string"
|
|
637
|
+
? [{ name: column["name"], type: column["type"], },]
|
|
638
|
+
: []);
|
|
639
|
+
if (!isRecord(newSchema) || columns.length === 0) {
|
|
640
|
+
return { detected: false, columns: [], error: describeSqlDetectionFailure(raw), };
|
|
641
|
+
}
|
|
642
|
+
await this.client.put(`${base}/schema`, newSchema);
|
|
643
|
+
return { detected: true, columns, };
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
if (error instanceof DataikuError) {
|
|
647
|
+
return { detected: false, columns: [], error: error.message.split("\n")[0] ?? "", };
|
|
648
|
+
}
|
|
649
|
+
throw error;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
595
652
|
/**
|
|
596
653
|
* Get the raw dataset schema object exactly as the DSS schema endpoint
|
|
597
654
|
* returns it, without schema validation — used for deterministic comparison.
|
|
@@ -859,7 +916,7 @@ export class DatasetsResource extends BaseResource {
|
|
|
859
916
|
const enc = encodeURIComponent(pk);
|
|
860
917
|
const explicitType = opts.dsType;
|
|
861
918
|
let dsType = explicitType
|
|
862
|
-
?? (opts.table ? DEFAULT_DATABASE_DATASET_TYPE : DEFAULT_FILESYSTEM_DATASET_TYPE);
|
|
919
|
+
?? (opts.table || opts.query ? DEFAULT_DATABASE_DATASET_TYPE : DEFAULT_FILESYSTEM_DATASET_TYPE);
|
|
863
920
|
let body = buildDatasetCreateBody({
|
|
864
921
|
projectKey: pk,
|
|
865
922
|
datasetName: opts.datasetName,
|
|
@@ -868,6 +925,7 @@ export class DatasetsResource extends BaseResource {
|
|
|
868
925
|
table: opts.table,
|
|
869
926
|
dbSchema: opts.dbSchema,
|
|
870
927
|
catalog: opts.catalog,
|
|
928
|
+
query: opts.query,
|
|
871
929
|
formatType: opts.formatType,
|
|
872
930
|
formatParams: opts.formatParams,
|
|
873
931
|
managed: opts.managed,
|
|
@@ -899,6 +957,7 @@ export class DatasetsResource extends BaseResource {
|
|
|
899
957
|
table: opts.table,
|
|
900
958
|
dbSchema: opts.dbSchema,
|
|
901
959
|
catalog: opts.catalog,
|
|
960
|
+
query: opts.query,
|
|
902
961
|
formatType: opts.formatType,
|
|
903
962
|
formatParams: opts.formatParams,
|
|
904
963
|
managed: opts.managed,
|
|
@@ -923,6 +982,7 @@ export class DatasetsResource extends BaseResource {
|
|
|
923
982
|
table: opts.table,
|
|
924
983
|
dbSchema: opts.dbSchema,
|
|
925
984
|
catalog: opts.catalog,
|
|
985
|
+
query: opts.query,
|
|
926
986
|
formatType: opts.formatType,
|
|
927
987
|
formatParams: opts.formatParams,
|
|
928
988
|
managed: opts.managed,
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import type { DataikuGetOptions } from "../client.js";
|
|
2
|
-
import type { FlowZone, FlowZoneCreateOptions, FlowZoneObjectType, FlowZoneUpdateOptions } from "../schemas.js";
|
|
2
|
+
import type { FlowZone, FlowZoneCreateOptions, FlowZoneObjectType, FlowZonePosition, FlowZoneUpdateOptions } from "../schemas.js";
|
|
3
3
|
import { BaseResource } from "./base.js";
|
|
4
4
|
export type FlowZoneItemInput = {
|
|
5
5
|
objectId: string;
|
|
6
6
|
objectType: FlowZoneObjectType;
|
|
7
7
|
projectKey?: string;
|
|
8
8
|
};
|
|
9
|
+
export type FlowZoneRecreateOptions = {
|
|
10
|
+
position: FlowZonePosition;
|
|
11
|
+
name?: string;
|
|
12
|
+
color?: string;
|
|
13
|
+
projectKey?: string;
|
|
14
|
+
};
|
|
15
|
+
export type FlowZoneRecreateResult = {
|
|
16
|
+
zone: FlowZone;
|
|
17
|
+
replacedZoneId: string;
|
|
18
|
+
};
|
|
9
19
|
export declare class FlowZonesResource extends BaseResource {
|
|
10
20
|
/**
|
|
11
21
|
* List all flow zones in a project. Pass `timeoutMs` to bound the whole
|
|
@@ -14,10 +24,26 @@ export declare class FlowZonesResource extends BaseResource {
|
|
|
14
24
|
list(projectKey?: string, options?: DataikuGetOptions): Promise<FlowZone[]>;
|
|
15
25
|
/** Get one flow zone by id. */
|
|
16
26
|
get(zoneId: string, projectKey?: string): Promise<FlowZone>;
|
|
17
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Create a flow zone. DSS stores `position` only while the project's manual
|
|
29
|
+
* zone positioning is off; with it on, DSS auto-places the zone.
|
|
30
|
+
*/
|
|
18
31
|
create(opts: FlowZoneCreateOptions): Promise<FlowZone>;
|
|
19
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Update flow zone name and color. DSS's public API ignores `position` on
|
|
34
|
+
* this endpoint; use {@link recreate} to move an existing zone.
|
|
35
|
+
*/
|
|
20
36
|
update(zoneId: string, opts: FlowZoneUpdateOptions): Promise<FlowZone>;
|
|
37
|
+
/**
|
|
38
|
+
* Replace a zone with a new one at `position`: DSS honors a zone position
|
|
39
|
+
* only on creation, and only while the project's manual zone positioning is
|
|
40
|
+
* off (otherwise it auto-places the new zone). The replacement receives the
|
|
41
|
+
* old zone's name, color, metadata (tags, custom fields, checklists, ...),
|
|
42
|
+
* items, and shared items; the old zone is deleted once it is empty. The zone
|
|
43
|
+
* id changes, so anything referencing the old id (for example scenario zone
|
|
44
|
+
* builds) must be updated.
|
|
45
|
+
*/
|
|
46
|
+
recreate(zoneId: string, opts: FlowZoneRecreateOptions): Promise<FlowZoneRecreateResult>;
|
|
21
47
|
/** Delete a flow zone. DSS moves its items back to the default zone. */
|
|
22
48
|
delete(zoneId: string, projectKey?: string): Promise<void>;
|
|
23
49
|
/** Move items into a flow zone. */
|
|
@@ -23,7 +23,10 @@ export class FlowZonesResource extends BaseResource {
|
|
|
23
23
|
const raw = await this.client.get(`/public/api/projects/${this.enc(projectKey)}/flow/zones/${encodeURIComponent(zoneId)}`);
|
|
24
24
|
return this.client.safeParse(FlowZoneSchema, raw, "flowZones.get");
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Create a flow zone. DSS stores `position` only while the project's manual
|
|
28
|
+
* zone positioning is off; with it on, DSS auto-places the zone.
|
|
29
|
+
*/
|
|
27
30
|
async create(opts) {
|
|
28
31
|
const raw = await this.client.post(`/public/api/projects/${this.enc(opts.projectKey)}/flow/zones`, {
|
|
29
32
|
name: opts.name,
|
|
@@ -32,17 +35,64 @@ export class FlowZonesResource extends BaseResource {
|
|
|
32
35
|
});
|
|
33
36
|
return this.client.safeParse(FlowZoneSchema, raw, "flowZones.create");
|
|
34
37
|
}
|
|
35
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Update flow zone name and color. DSS's public API ignores `position` on
|
|
40
|
+
* this endpoint; use {@link recreate} to move an existing zone.
|
|
41
|
+
*/
|
|
36
42
|
async update(zoneId, opts) {
|
|
37
43
|
const current = await this.get(zoneId, opts.projectKey);
|
|
38
44
|
const merged = deepMerge(current, {
|
|
39
45
|
...(opts.name !== undefined ? { name: opts.name, } : {}),
|
|
40
46
|
...(opts.color !== undefined ? { color: opts.color, } : {}),
|
|
41
|
-
...(opts.position !== undefined ? { position: opts.position, } : {}),
|
|
42
47
|
});
|
|
43
48
|
await this.client.putVoid(`/public/api/projects/${this.enc(opts.projectKey)}/flow/zones/${encodeURIComponent(zoneId)}`, merged);
|
|
44
49
|
return this.get(zoneId, opts.projectKey);
|
|
45
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Replace a zone with a new one at `position`: DSS honors a zone position
|
|
53
|
+
* only on creation, and only while the project's manual zone positioning is
|
|
54
|
+
* off (otherwise it auto-places the new zone). The replacement receives the
|
|
55
|
+
* old zone's name, color, metadata (tags, custom fields, checklists, ...),
|
|
56
|
+
* items, and shared items; the old zone is deleted once it is empty. The zone
|
|
57
|
+
* id changes, so anything referencing the old id (for example scenario zone
|
|
58
|
+
* builds) must be updated.
|
|
59
|
+
*/
|
|
60
|
+
async recreate(zoneId, opts) {
|
|
61
|
+
const base = `/public/api/projects/${this.enc(opts.projectKey)}/flow/zones`;
|
|
62
|
+
const old = await this.get(zoneId, opts.projectKey);
|
|
63
|
+
const created = await this.create({
|
|
64
|
+
name: opts.name ?? old.name,
|
|
65
|
+
color: opts.color ?? old.color,
|
|
66
|
+
position: opts.position,
|
|
67
|
+
projectKey: opts.projectKey,
|
|
68
|
+
});
|
|
69
|
+
const completed = ["create",];
|
|
70
|
+
try {
|
|
71
|
+
const { id: _id, items, shared, position: _position, ...metadata } = old;
|
|
72
|
+
await this.client.putVoid(`${base}/${encodeURIComponent(created.id)}`, {
|
|
73
|
+
...created,
|
|
74
|
+
...metadata,
|
|
75
|
+
id: created.id,
|
|
76
|
+
name: created.name,
|
|
77
|
+
color: created.color,
|
|
78
|
+
position: created.position,
|
|
79
|
+
});
|
|
80
|
+
completed.push("metadata");
|
|
81
|
+
if (items && items.length > 0) {
|
|
82
|
+
await this.moveItems(created.id, items, opts.projectKey);
|
|
83
|
+
}
|
|
84
|
+
completed.push("items");
|
|
85
|
+
for (const item of shared ?? []) {
|
|
86
|
+
await this.client.post(`${base}/${encodeURIComponent(created.id)}/shared`, normalizeZoneItem(item));
|
|
87
|
+
}
|
|
88
|
+
completed.push("shared");
|
|
89
|
+
await this.delete(old.id, opts.projectKey);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
throw new ClientValidationError(`Recreating flow zone ${old.id} at a new position stopped after ${completed.at(-1)}; both zones may now exist.`, "ambiguous_outcome", "Inspect both zones with dss flow-zone list, then move remaining items and delete the zone you no longer need.", { replacedZoneId: old.id, zoneId: created.id, completed, }, { cause: error, });
|
|
93
|
+
}
|
|
94
|
+
return { zone: await this.get(created.id, opts.projectKey), replacedZoneId: old.id, };
|
|
95
|
+
}
|
|
46
96
|
/** Delete a flow zone. DSS moves its items back to the default zone. */
|
|
47
97
|
async delete(zoneId, projectKey) {
|
|
48
98
|
await this.client.del(`/public/api/projects/${this.enc(projectKey)}/flow/zones/${encodeURIComponent(zoneId)}`);
|
|
@@ -23,12 +23,30 @@ export interface JobLogSummary {
|
|
|
23
23
|
}
|
|
24
24
|
/** Why a requested job log could not be retrieved. */
|
|
25
25
|
export type JobLogUnavailableReason = "not_found" | "error";
|
|
26
|
+
/**
|
|
27
|
+
* A requested build target that no flow object can ever produce: a managed
|
|
28
|
+
* dataset with no recipe writing it.
|
|
29
|
+
*/
|
|
30
|
+
export interface JobUnbuildableTarget {
|
|
31
|
+
type: "DATASET";
|
|
32
|
+
id: string;
|
|
33
|
+
projectKey: string;
|
|
34
|
+
reason: "no_producing_recipe";
|
|
35
|
+
}
|
|
36
|
+
/** Why a job that ended DONE is reported as failed because it built nothing. */
|
|
37
|
+
export interface JobNothingToBuildFailure {
|
|
38
|
+
code: "nothing_to_build";
|
|
39
|
+
message: string;
|
|
40
|
+
targets: JobUnbuildableTarget[];
|
|
41
|
+
}
|
|
26
42
|
/** Result of a completed job wait, including log-unavailable metadata. */
|
|
27
43
|
export type JobWaitOutcome = JobWaitResult & {
|
|
28
44
|
logSummary?: JobLogSummary;
|
|
29
45
|
logUnavailable?: JobLogUnavailableReason;
|
|
30
46
|
/** True when the job itself no longer exists on the server. */
|
|
31
47
|
removed?: boolean;
|
|
48
|
+
/** Set (with `success: false`) when a DONE job ran no activity and a target has no producer. */
|
|
49
|
+
failure?: JobNothingToBuildFailure;
|
|
32
50
|
};
|
|
33
51
|
export interface JobBuildTarget {
|
|
34
52
|
id: string;
|
|
@@ -62,7 +80,8 @@ export declare class JobsResource extends BaseResource {
|
|
|
62
80
|
/**
|
|
63
81
|
* Retrieve job log text.
|
|
64
82
|
* Returns the last `maxLogLines` lines (default 500) from the tail.
|
|
65
|
-
* Use `0` or `-1` to return the log without line truncation.
|
|
83
|
+
* Use `0` or `-1` to return the log without line truncation. Credential-like
|
|
84
|
+
* JSON pairs (`"jobTicketSecret":"..."`) are redacted.
|
|
66
85
|
*
|
|
67
86
|
* The download is byte-bounded and deadline-covered: at most `maxLogBytes`
|
|
68
87
|
* (default 10 MiB) of the *most recent* log output is retained, so a
|
|
@@ -120,6 +139,12 @@ export declare class JobsResource extends BaseResource {
|
|
|
120
139
|
* confirmed gone. Explicit `includeLogs`/`summary` requests stay honest:
|
|
121
140
|
* `log`/`logSummary` are only present when logs were actually retrieved.
|
|
122
141
|
*
|
|
142
|
+
* A DONE job that ran no activity (`progress.total === 0`) is also what DSS
|
|
143
|
+
* reports for an already-up-to-date build, so it stays successful unless an
|
|
144
|
+
* output is a managed dataset that no recipe writes: then nothing can ever
|
|
145
|
+
* build it, and the outcome is `success: false` with
|
|
146
|
+
* `failure.code === "nothing_to_build"`.
|
|
147
|
+
*
|
|
123
148
|
* On timeout, returns `{ success: false, ... }` rather than throwing.
|
|
124
149
|
*/
|
|
125
150
|
wait(jobId: string, opts?: {
|
|
@@ -133,6 +158,16 @@ export declare class JobsResource extends BaseResource {
|
|
|
133
158
|
timeoutMs?: number;
|
|
134
159
|
projectKey?: string;
|
|
135
160
|
}): Promise<JobWaitOutcome>;
|
|
161
|
+
/**
|
|
162
|
+
* Dataset outputs of a job that ran no activity which nothing can produce.
|
|
163
|
+
* DSS ends such a job DONE exactly like an already-up-to-date build, so the
|
|
164
|
+
* job status cannot tell them apart; the flow graph can: a target is
|
|
165
|
+
* unbuildable when it is a managed dataset (DSS owns its storage, so a
|
|
166
|
+
* recipe is the only way to fill it) that no recipe writes. Unmanaged
|
|
167
|
+
* datasets (uploads, external tables) are sources and legitimately build
|
|
168
|
+
* nothing. Lookups that fail leave the DONE outcome untouched.
|
|
169
|
+
*/
|
|
170
|
+
private unbuildableTargets;
|
|
136
171
|
/**
|
|
137
172
|
* Probe whether a job still exists. Returns `true` when the server reports
|
|
138
173
|
* it as not found, `false` when it is still visible, and `undefined` when
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ClientValidationError, DataikuError, } from "../errors.js";
|
|
2
2
|
import { JobSummaryArraySchema, } from "../schemas.js";
|
|
3
3
|
import { computeNextPollDelayMs, DEFAULT_POLL_INTERVAL_MS, DEFAULT_TIMEOUT_MS, isRequestDeadlineError, } from "../utils/polling.js";
|
|
4
|
+
import { asRecord, } from "../utils/records.js";
|
|
5
|
+
import { redactCredentialPairs, } from "../utils/secret-sanitize.js";
|
|
4
6
|
import { BaseResource, } from "./base.js";
|
|
5
7
|
const DEFAULT_MAX_LOG_LINES = 500;
|
|
6
8
|
/**
|
|
@@ -103,6 +105,41 @@ function dssRecordMessage(line) {
|
|
|
103
105
|
function isErrorContent(line) {
|
|
104
106
|
return ERROR_KEYWORDS.test(line) || ERROR_CLASS_NAME.test(line) || PY_FILE_FRAME.test(line);
|
|
105
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Remove embedded JSON object payloads (`{"...": ...}`) from a DSS record
|
|
110
|
+
* line. Structured payloads such as `SQL status {"statusWarnLevel":"ERROR",...}`
|
|
111
|
+
* carry level/class words as data, not as a failure of the record itself.
|
|
112
|
+
* An unbalanced payload (truncated line) is dropped to the end of the line.
|
|
113
|
+
*/
|
|
114
|
+
function stripJsonPayloads(line) {
|
|
115
|
+
let out = "";
|
|
116
|
+
let from = 0;
|
|
117
|
+
for (let start = line.indexOf('{"'); start !== -1; start = line.indexOf('{"', from)) {
|
|
118
|
+
out += line.slice(from, start);
|
|
119
|
+
let depth = 0;
|
|
120
|
+
let inString = false;
|
|
121
|
+
let end = line.length;
|
|
122
|
+
for (let i = start; i < line.length; i++) {
|
|
123
|
+
const ch = line[i];
|
|
124
|
+
if (inString) {
|
|
125
|
+
if (ch === "\\")
|
|
126
|
+
i++;
|
|
127
|
+
else if (ch === '"')
|
|
128
|
+
inString = false;
|
|
129
|
+
}
|
|
130
|
+
else if (ch === '"')
|
|
131
|
+
inString = true;
|
|
132
|
+
else if (ch === "{")
|
|
133
|
+
depth++;
|
|
134
|
+
else if (ch === "}" && --depth === 0) {
|
|
135
|
+
end = i + 1;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
from = end;
|
|
140
|
+
}
|
|
141
|
+
return out + line.slice(from);
|
|
142
|
+
}
|
|
106
143
|
function lineMatchesLogFilter(line, filter) {
|
|
107
144
|
const normalized = line.toLowerCase();
|
|
108
145
|
switch (filter) {
|
|
@@ -159,7 +196,7 @@ function filterJobLogToErrors(lines) {
|
|
|
159
196
|
pyTraceback = false;
|
|
160
197
|
}
|
|
161
198
|
if (isHeader) {
|
|
162
|
-
recordMatches = isErrorContent(line) || PY_FILE_FRAME.test(message);
|
|
199
|
+
recordMatches = isErrorContent(stripJsonPayloads(line)) || PY_FILE_FRAME.test(message);
|
|
163
200
|
if (recordMatches && (PY_TRACEBACK_HEAD.test(message) || PY_FILE_FRAME.test(message))) {
|
|
164
201
|
pyTraceback = true;
|
|
165
202
|
}
|
|
@@ -175,7 +212,7 @@ function filterJobLogToErrors(lines) {
|
|
|
175
212
|
if (recordMatches
|
|
176
213
|
&& !JVM_STACK_FRAME.test(line)
|
|
177
214
|
&& !JVM_MORE_FRAMES.test(line)
|
|
178
|
-
&& isErrorContent(line))
|
|
215
|
+
&& isErrorContent(stripJsonPayloads(line)))
|
|
179
216
|
kept.push(line);
|
|
180
217
|
}
|
|
181
218
|
return kept.join("\n");
|
|
@@ -263,6 +300,10 @@ function summarizeJobLog(state, log, maxLines, elapsedMs) {
|
|
|
263
300
|
...(progress ? { progress, } : {}),
|
|
264
301
|
};
|
|
265
302
|
}
|
|
303
|
+
function nothingToBuildMessage(targets) {
|
|
304
|
+
const names = targets.map((target) => `${target.projectKey}.${target.id}`).join(", ");
|
|
305
|
+
return `The job ran no activity: ${names} ${targets.length === 1 ? "is a managed dataset" : "are managed datasets"} that no recipe produces, so nothing was built. Create a recipe that writes ${targets.length === 1 ? "it" : "them"} or build the recipe's output instead.`;
|
|
306
|
+
}
|
|
266
307
|
export class JobsResource extends BaseResource {
|
|
267
308
|
/** List jobs in a project. */
|
|
268
309
|
async list(projectKey) {
|
|
@@ -278,7 +319,8 @@ export class JobsResource extends BaseResource {
|
|
|
278
319
|
/**
|
|
279
320
|
* Retrieve job log text.
|
|
280
321
|
* Returns the last `maxLogLines` lines (default 500) from the tail.
|
|
281
|
-
* Use `0` or `-1` to return the log without line truncation.
|
|
322
|
+
* Use `0` or `-1` to return the log without line truncation. Credential-like
|
|
323
|
+
* JSON pairs (`"jobTicketSecret":"..."`) are redacted.
|
|
282
324
|
*
|
|
283
325
|
* The download is byte-bounded and deadline-covered: at most `maxLogBytes`
|
|
284
326
|
* (default 10 MiB) of the *most recent* log output is retained, so a
|
|
@@ -293,7 +335,9 @@ export class JobsResource extends BaseResource {
|
|
|
293
335
|
const path = `/public/api/projects/${this.enc(opts?.projectKey)}/jobs/${jobEnc}/log/${query}`;
|
|
294
336
|
const maxLogBytes = Math.max(1, opts?.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES);
|
|
295
337
|
const { text, } = await this.client.getTextTailLimited(path, maxLogBytes);
|
|
296
|
-
|
|
338
|
+
// Single download choke point (logFromUrl and wait/summary go through here):
|
|
339
|
+
// DSS logs embed the job ticket secret in start_session JSON.
|
|
340
|
+
return limitJobLog(filterJobLog(redactCredentialPairs(text), opts?.logFilter), opts?.maxLogLines);
|
|
297
341
|
}
|
|
298
342
|
async logFromUrl(logUrl, opts) {
|
|
299
343
|
const parsed = new URL(logUrl, "http://dss.local");
|
|
@@ -375,6 +419,12 @@ export class JobsResource extends BaseResource {
|
|
|
375
419
|
* confirmed gone. Explicit `includeLogs`/`summary` requests stay honest:
|
|
376
420
|
* `log`/`logSummary` are only present when logs were actually retrieved.
|
|
377
421
|
*
|
|
422
|
+
* A DONE job that ran no activity (`progress.total === 0`) is also what DSS
|
|
423
|
+
* reports for an already-up-to-date build, so it stays successful unless an
|
|
424
|
+
* output is a managed dataset that no recipe writes: then nothing can ever
|
|
425
|
+
* build it, and the outcome is `success: false` with
|
|
426
|
+
* `failure.code === "nothing_to_build"`.
|
|
427
|
+
*
|
|
378
428
|
* On timeout, returns `{ success: false, ... }` rather than throwing.
|
|
379
429
|
*/
|
|
380
430
|
async wait(jobId, opts) {
|
|
@@ -492,8 +542,18 @@ export class JobsResource extends BaseResource {
|
|
|
492
542
|
}
|
|
493
543
|
}
|
|
494
544
|
}
|
|
545
|
+
const unbuildable = success && gs.total === 0
|
|
546
|
+
? await this.unbuildableTargets(def.outputs, def.projectKey ?? opts?.projectKey)
|
|
547
|
+
: [];
|
|
548
|
+
const failure = unbuildable.length > 0
|
|
549
|
+
? {
|
|
550
|
+
code: "nothing_to_build",
|
|
551
|
+
message: nothingToBuildMessage(unbuildable),
|
|
552
|
+
targets: unbuildable,
|
|
553
|
+
}
|
|
554
|
+
: undefined;
|
|
495
555
|
return {
|
|
496
|
-
success,
|
|
556
|
+
success: success && failure === undefined,
|
|
497
557
|
jobId: def.id ?? jobId,
|
|
498
558
|
state,
|
|
499
559
|
type: def.type ?? "unknown",
|
|
@@ -505,6 +565,7 @@ export class JobsResource extends BaseResource {
|
|
|
505
565
|
running: gs.running ?? 0,
|
|
506
566
|
total: gs.total ?? null,
|
|
507
567
|
},
|
|
568
|
+
...(failure !== undefined ? { failure, } : {}),
|
|
508
569
|
...(log !== undefined ? { log, } : {}),
|
|
509
570
|
...(logSummary !== undefined ? { logSummary, } : {}),
|
|
510
571
|
...(logUnavailable !== undefined ? { logUnavailable, } : {}),
|
|
@@ -523,6 +584,51 @@ export class JobsResource extends BaseResource {
|
|
|
523
584
|
await sleep(Math.min(nextDelayMs, timeout - elapsedMs));
|
|
524
585
|
}
|
|
525
586
|
}
|
|
587
|
+
/**
|
|
588
|
+
* Dataset outputs of a job that ran no activity which nothing can produce.
|
|
589
|
+
* DSS ends such a job DONE exactly like an already-up-to-date build, so the
|
|
590
|
+
* job status cannot tell them apart; the flow graph can: a target is
|
|
591
|
+
* unbuildable when it is a managed dataset (DSS owns its storage, so a
|
|
592
|
+
* recipe is the only way to fill it) that no recipe writes. Unmanaged
|
|
593
|
+
* datasets (uploads, external tables) are sources and legitimately build
|
|
594
|
+
* nothing. Lookups that fail leave the DONE outcome untouched.
|
|
595
|
+
*/
|
|
596
|
+
async unbuildableTargets(outputs, fallbackProjectKey) {
|
|
597
|
+
if (!Array.isArray(outputs))
|
|
598
|
+
return [];
|
|
599
|
+
const writtenByProject = new Map();
|
|
600
|
+
const unbuildable = [];
|
|
601
|
+
for (const output of outputs) {
|
|
602
|
+
const record = asRecord(output);
|
|
603
|
+
if (record?.type !== "DATASET" || typeof record.targetDataset !== "string")
|
|
604
|
+
continue;
|
|
605
|
+
const id = record.targetDataset;
|
|
606
|
+
const projectKey = typeof record.targetDatasetProjectKey === "string"
|
|
607
|
+
? record.targetDatasetProjectKey
|
|
608
|
+
: fallbackProjectKey;
|
|
609
|
+
if (projectKey === undefined)
|
|
610
|
+
continue;
|
|
611
|
+
try {
|
|
612
|
+
const details = await this.client.datasets.get(id, projectKey);
|
|
613
|
+
if (details.managed !== true)
|
|
614
|
+
continue;
|
|
615
|
+
let written = writtenByProject.get(projectKey);
|
|
616
|
+
if (written === undefined) {
|
|
617
|
+
const { graph, } = await this.client.projects.flowTopology(projectKey);
|
|
618
|
+
written = new Set(graph.edges.filter((edge) => edge.relation === "writes").map((edge) => edge.to));
|
|
619
|
+
writtenByProject.set(projectKey, written);
|
|
620
|
+
}
|
|
621
|
+
if (!written.has(id)) {
|
|
622
|
+
unbuildable.push({ type: "DATASET", id, projectKey, reason: "no_producing_recipe", });
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
if (!(error instanceof DataikuError || error instanceof ClientValidationError))
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return unbuildable;
|
|
631
|
+
}
|
|
526
632
|
/**
|
|
527
633
|
* Probe whether a job still exists. Returns `true` when the server reports
|
|
528
634
|
* it as not found, `false` when it is still visible, and `undefined` when
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FlowMapOptions, ProjectDetails, ProjectMetadata, ProjectSummary, ProjectTags } from "../schemas.js";
|
|
2
2
|
import type { AnalyzedFlowMap, FlowMapRendering } from "../utils/flow-analysis.js";
|
|
3
|
+
import type { NormalizedFlowMap } from "../utils/flow-map.js";
|
|
3
4
|
import { BaseResource } from "./base.js";
|
|
4
5
|
interface FlowMapTruncationSummary {
|
|
5
6
|
truncated: boolean;
|
|
@@ -150,6 +151,14 @@ export declare class ProjectsResource extends BaseResource {
|
|
|
150
151
|
setTags(projectKey: string | undefined, tags: ProjectTags): Promise<void>;
|
|
151
152
|
/** Get the raw flow graph for a project. */
|
|
152
153
|
flow(projectKey?: string): Promise<unknown>;
|
|
154
|
+
/**
|
|
155
|
+
* Normalized flow topology (nodes and edges, no metadata lookups) plus its
|
|
156
|
+
* stable fingerprint, excluding visual metadata.
|
|
157
|
+
*/
|
|
158
|
+
flowTopology(projectKey?: string): Promise<{
|
|
159
|
+
graph: NormalizedFlowMap;
|
|
160
|
+
topologyFingerprint: string;
|
|
161
|
+
}>;
|
|
153
162
|
/** Compute a stable fingerprint of flow nodes and edges, excluding visual metadata. */
|
|
154
163
|
topologyFingerprint(projectKey?: string): Promise<string>;
|
|
155
164
|
/**
|
|
@@ -340,11 +340,18 @@ export class ProjectsResource extends BaseResource {
|
|
|
340
340
|
const enc = this.enc(projectKey);
|
|
341
341
|
return this.client.get(`/public/api/projects/${enc}/flow/graph/`);
|
|
342
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Normalized flow topology (nodes and edges, no metadata lookups) plus its
|
|
345
|
+
* stable fingerprint, excluding visual metadata.
|
|
346
|
+
*/
|
|
347
|
+
async flowTopology(projectKey) {
|
|
348
|
+
const pk = this.resolveProjectKey(projectKey);
|
|
349
|
+
const graph = normalizeFlowGraph(await this.flow(pk), pk);
|
|
350
|
+
return { graph, topologyFingerprint: flowTopologyFingerprint(graph), };
|
|
351
|
+
}
|
|
343
352
|
/** Compute a stable fingerprint of flow nodes and edges, excluding visual metadata. */
|
|
344
353
|
async topologyFingerprint(projectKey) {
|
|
345
|
-
|
|
346
|
-
const rawGraph = await this.flow(pk);
|
|
347
|
-
return flowTopologyFingerprint(normalizeFlowGraph(rawGraph, pk));
|
|
354
|
+
return (await this.flowTopology(projectKey)).topologyFingerprint;
|
|
348
355
|
}
|
|
349
356
|
/**
|
|
350
357
|
* Build a normalized, optionally truncated flow map for a project.
|
|
@@ -33,12 +33,26 @@ export declare function groupingPayloadConfig(opts: RecipeCreateOptions): Groupi
|
|
|
33
33
|
* aggregate flags (and `orderColumn` for first/last).
|
|
34
34
|
*/
|
|
35
35
|
export declare function applyGroupingPayload(payload: Record<string, unknown>, config: GroupingPayloadConfig): Record<string, unknown>;
|
|
36
|
+
/** Prepare option groups, in the default step order. */
|
|
37
|
+
export declare const PREPARE_STEP_KINDS: readonly ["rename", "fillEmpty", "formula", "filter", "dropColumns", "keepColumns"];
|
|
38
|
+
export type PrepareStepKind = (typeof PREPARE_STEP_KINDS)[number];
|
|
36
39
|
/**
|
|
37
|
-
* Prepare (shaker) steps from create options,
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
+
* Prepare (shaker) steps from create options: rename, fill empty, formula
|
|
41
|
+
* column, row filter, drop columns, keep columns. They run in `stepOrder`
|
|
42
|
+
* (flag order on the CLI), defaulting to that sequence, so a step sees the
|
|
43
|
+
* columns the steps before it produced. Empty when no prepare option was given.
|
|
40
44
|
*/
|
|
41
45
|
export declare function prepareSteps(opts: RecipeCreateOptions): Record<string, unknown>[];
|
|
46
|
+
export interface RecipeCreateWarning {
|
|
47
|
+
code: string;
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Warnings about the prepare options that DSS cannot report itself: a
|
|
52
|
+
* `--formula`/`--filter` that runs after a `--rename` but still names a
|
|
53
|
+
* column that rename removed (it would fail or read an empty column).
|
|
54
|
+
*/
|
|
55
|
+
export declare function recipeCreateWarnings(opts: RecipeCreateOptions): RecipeCreateWarning[];
|
|
42
56
|
/**
|
|
43
57
|
* Pure request construction for POST /recipes/ (no DSS calls). Shared with
|
|
44
58
|
* `recipe create --plan` so the plan equals the request the command sends.
|