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
|
@@ -1,7 +1,152 @@
|
|
|
1
|
+
import { DataikuError, } from "../errors.js";
|
|
1
2
|
import { ScenarioDetailsSchema, ScenarioStatusSchema, ScenarioSummaryArraySchema, } from "../schemas.js";
|
|
2
3
|
import { deepMerge, } from "../utils/deep-merge.js";
|
|
3
4
|
import { BaseResource, } from "./base.js";
|
|
4
5
|
import { computeNextPollDelayMs, } from "./jobs.js";
|
|
6
|
+
export const SCENARIO_CANONICAL_EDITABLE_FIELDS = [
|
|
7
|
+
"params.steps",
|
|
8
|
+
"params.triggers",
|
|
9
|
+
"params.reporters",
|
|
10
|
+
"params.customScript",
|
|
11
|
+
"active",
|
|
12
|
+
"name",
|
|
13
|
+
];
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function jsonValueEqual(left, right) {
|
|
18
|
+
if (Object.is(left, right))
|
|
19
|
+
return true;
|
|
20
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
21
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
|
|
22
|
+
return false;
|
|
23
|
+
return left.every((value, index) => jsonValueEqual(value, right[index]));
|
|
24
|
+
}
|
|
25
|
+
if (!isRecord(left) || !isRecord(right))
|
|
26
|
+
return false;
|
|
27
|
+
const leftKeys = Object.keys(left);
|
|
28
|
+
const rightKeys = Object.keys(right);
|
|
29
|
+
if (leftKeys.length !== rightKeys.length)
|
|
30
|
+
return false;
|
|
31
|
+
return leftKeys.every((key) => Object.hasOwn(right, key) && jsonValueEqual(left[key], right[key]));
|
|
32
|
+
}
|
|
33
|
+
function jsonValueContains(actual, expected) {
|
|
34
|
+
if (jsonValueEqual(actual, expected))
|
|
35
|
+
return true;
|
|
36
|
+
if (Array.isArray(actual) || Array.isArray(expected)) {
|
|
37
|
+
if (!Array.isArray(actual) || !Array.isArray(expected) || actual.length !== expected.length) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return expected.every((value, index) => jsonValueContains(actual[index], value));
|
|
41
|
+
}
|
|
42
|
+
if (!isRecord(actual) || !isRecord(expected))
|
|
43
|
+
return false;
|
|
44
|
+
return Object.entries(expected).every(([key, value,]) => Object.hasOwn(actual, key) && jsonValueContains(actual[key], value));
|
|
45
|
+
}
|
|
46
|
+
function collectPatchPaths(value, prefix = "", paths = []) {
|
|
47
|
+
if (!isRecord(value)) {
|
|
48
|
+
if (prefix)
|
|
49
|
+
paths.push(prefix);
|
|
50
|
+
return paths;
|
|
51
|
+
}
|
|
52
|
+
const entries = Object.entries(value);
|
|
53
|
+
if (entries.length === 0) {
|
|
54
|
+
if (prefix)
|
|
55
|
+
paths.push(prefix);
|
|
56
|
+
return paths;
|
|
57
|
+
}
|
|
58
|
+
for (const [key, child,] of entries) {
|
|
59
|
+
collectPatchPaths(child, prefix ? `${prefix}.${key}` : key, paths);
|
|
60
|
+
}
|
|
61
|
+
return paths;
|
|
62
|
+
}
|
|
63
|
+
function valueAtPath(value, path) {
|
|
64
|
+
let current = value;
|
|
65
|
+
for (const part of path.split(".")) {
|
|
66
|
+
if (!isRecord(current))
|
|
67
|
+
return undefined;
|
|
68
|
+
current = current[part];
|
|
69
|
+
}
|
|
70
|
+
return current;
|
|
71
|
+
}
|
|
72
|
+
function scenarioFieldChanges(before, after, patch) {
|
|
73
|
+
const changes = [];
|
|
74
|
+
const unchangedPaths = [];
|
|
75
|
+
for (const path of collectPatchPaths(patch)) {
|
|
76
|
+
const beforeValue = valueAtPath(before, path);
|
|
77
|
+
const afterValue = valueAtPath(after, path);
|
|
78
|
+
if (jsonValueEqual(beforeValue, afterValue)) {
|
|
79
|
+
unchangedPaths.push(path);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
changes.push({ path, before: beforeValue, after: afterValue, });
|
|
83
|
+
}
|
|
84
|
+
return { changes, unchangedPaths, };
|
|
85
|
+
}
|
|
86
|
+
function scenarioFieldMismatches(actual, expected, patch) {
|
|
87
|
+
const mismatches = [];
|
|
88
|
+
for (const path of collectPatchPaths(patch)) {
|
|
89
|
+
const actualValue = valueAtPath(actual, path);
|
|
90
|
+
const expectedValue = valueAtPath(expected, path);
|
|
91
|
+
if (!jsonValueContains(actualValue, expectedValue)) {
|
|
92
|
+
mismatches.push({ path, expected: expectedValue, actual: actualValue, });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return mismatches;
|
|
96
|
+
}
|
|
97
|
+
export function normalizeScenarioUpdateData(data) {
|
|
98
|
+
const normalizedData = { ...data, };
|
|
99
|
+
const normalization = [];
|
|
100
|
+
const rawParams = data.rawParams;
|
|
101
|
+
if (!isRecord(rawParams) || !isRecord(rawParams.params)) {
|
|
102
|
+
return { normalizedData, normalization, };
|
|
103
|
+
}
|
|
104
|
+
const canonicalParams = isRecord(data.params) ? data.params : undefined;
|
|
105
|
+
const mergedParams = canonicalParams === undefined
|
|
106
|
+
? rawParams.params
|
|
107
|
+
: deepMerge(rawParams.params, canonicalParams);
|
|
108
|
+
const promoted = canonicalParams === undefined || !jsonValueEqual(mergedParams, canonicalParams);
|
|
109
|
+
if (promoted)
|
|
110
|
+
normalizedData.params = mergedParams;
|
|
111
|
+
const rawParamsWithoutParams = { ...rawParams, };
|
|
112
|
+
delete rawParamsWithoutParams.params;
|
|
113
|
+
if (Object.keys(rawParamsWithoutParams).length === 0)
|
|
114
|
+
delete normalizedData.rawParams;
|
|
115
|
+
else
|
|
116
|
+
normalizedData.rawParams = rawParamsWithoutParams;
|
|
117
|
+
normalization.push({
|
|
118
|
+
from: "rawParams.params",
|
|
119
|
+
to: "params",
|
|
120
|
+
action: promoted ? "promoted" : "ignored",
|
|
121
|
+
message: promoted
|
|
122
|
+
? "rawParams.params is a DSS echo; the editable scenario definition uses params."
|
|
123
|
+
: "rawParams.params was ignored because canonical params already supplied the same editable fields.",
|
|
124
|
+
});
|
|
125
|
+
return { normalizedData, normalization, };
|
|
126
|
+
}
|
|
127
|
+
export function scenarioUpdatePreview(current, data) {
|
|
128
|
+
const { normalizedData, normalization, } = normalizeScenarioUpdateData(data);
|
|
129
|
+
const next = deepMerge(current, normalizedData);
|
|
130
|
+
const { changes, unchangedPaths, } = scenarioFieldChanges(current, next, normalizedData);
|
|
131
|
+
return {
|
|
132
|
+
canonicalEditableFields: SCENARIO_CANONICAL_EDITABLE_FIELDS,
|
|
133
|
+
normalization,
|
|
134
|
+
normalizedData,
|
|
135
|
+
current,
|
|
136
|
+
next,
|
|
137
|
+
changes,
|
|
138
|
+
unchangedPaths,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function scenarioUpdateVerificationError(mismatches, normalization) {
|
|
142
|
+
const mismatchPaths = mismatches.map((mismatch) => mismatch.path).join(", ");
|
|
143
|
+
return new DataikuError(400, "Scenario Update Verification Failed", JSON.stringify({
|
|
144
|
+
message: `Scenario update did not persist requested fields after refetch: ${mismatchPaths}`,
|
|
145
|
+
mismatches,
|
|
146
|
+
canonicalEditableFields: SCENARIO_CANONICAL_EDITABLE_FIELDS,
|
|
147
|
+
...(normalization.length > 0 ? { normalization, } : {}),
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
5
150
|
export class ScenariosResource extends BaseResource {
|
|
6
151
|
/** List all scenarios in a project. */
|
|
7
152
|
async list(projectKey) {
|
|
@@ -42,13 +187,27 @@ export class ScenariosResource extends BaseResource {
|
|
|
42
187
|
const raw = await this.client.get(`/public/api/projects/${this.enc(projectKey)}/scenarios/${scEnc}/light/`);
|
|
43
188
|
return this.client.safeParse(ScenarioStatusSchema, raw, "scenarios.status");
|
|
44
189
|
}
|
|
45
|
-
/** Merge-update a scenario's definition. */
|
|
190
|
+
/** Merge-update a scenario's definition, then refetch and verify requested fields persisted. */
|
|
46
191
|
async update(scenarioId, data, projectKey) {
|
|
47
192
|
const scEnc = encodeURIComponent(scenarioId);
|
|
48
193
|
const pkEnc = this.enc(projectKey);
|
|
49
194
|
const current = await this.client.get(`/public/api/projects/${pkEnc}/scenarios/${scEnc}/`);
|
|
50
|
-
const
|
|
51
|
-
await this.client.put(`/public/api/projects/${pkEnc}/scenarios/${scEnc}/`,
|
|
195
|
+
const preview = scenarioUpdatePreview(current, data);
|
|
196
|
+
await this.client.put(`/public/api/projects/${pkEnc}/scenarios/${scEnc}/`, preview.next);
|
|
197
|
+
const after = await this.client.get(`/public/api/projects/${pkEnc}/scenarios/${scEnc}/`);
|
|
198
|
+
const mismatches = scenarioFieldMismatches(after, preview.next, preview.normalizedData);
|
|
199
|
+
if (mismatches.length > 0) {
|
|
200
|
+
throw scenarioUpdateVerificationError(mismatches, preview.normalization);
|
|
201
|
+
}
|
|
202
|
+
const verified = scenarioFieldChanges(current, after, preview.normalizedData);
|
|
203
|
+
return {
|
|
204
|
+
...preview,
|
|
205
|
+
after,
|
|
206
|
+
changes: verified.changes,
|
|
207
|
+
unchangedPaths: verified.unchangedPaths,
|
|
208
|
+
verified: true,
|
|
209
|
+
mismatches: [],
|
|
210
|
+
};
|
|
52
211
|
}
|
|
53
212
|
/** Delete a scenario. */
|
|
54
213
|
async delete(scenarioId, projectKey) {
|
|
@@ -24,6 +24,82 @@ function asString(value) {
|
|
|
24
24
|
const trimmed = value.trim();
|
|
25
25
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
26
26
|
}
|
|
27
|
+
function firstStringField(record, fields) {
|
|
28
|
+
for (const field of fields) {
|
|
29
|
+
const value = record[field];
|
|
30
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
31
|
+
return value.trim();
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
function isLikelySqlErrorDetail(detail) {
|
|
36
|
+
const lower = detail.toLowerCase();
|
|
37
|
+
return /\b[A-Z_]+(?:_ERROR|_NOT_FOUND|_MISMATCH|_DENIED|_EXCEEDED)\b/.test(detail)
|
|
38
|
+
|| lower.includes("athena")
|
|
39
|
+
|| lower.includes("sql")
|
|
40
|
+
|| lower.includes("query")
|
|
41
|
+
|| lower.includes("column")
|
|
42
|
+
|| lower.includes("table")
|
|
43
|
+
|| lower.includes("line ");
|
|
44
|
+
}
|
|
45
|
+
function sqlErrorDetailFromBody(body) {
|
|
46
|
+
const trimmed = body.trim();
|
|
47
|
+
if (!trimmed)
|
|
48
|
+
return undefined;
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(trimmed);
|
|
51
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
52
|
+
const record = parsed;
|
|
53
|
+
const nested = asRecord(record.details) ?? asRecord(record.error);
|
|
54
|
+
const nestedDetail = nested
|
|
55
|
+
? firstStringField(nested, ["message", "errorMessage", "error", "reason", "cause",])
|
|
56
|
+
: undefined;
|
|
57
|
+
if (nestedDetail && isLikelySqlErrorDetail(nestedDetail))
|
|
58
|
+
return nestedDetail;
|
|
59
|
+
const direct = firstStringField(record, [
|
|
60
|
+
"message",
|
|
61
|
+
"detailedMessage",
|
|
62
|
+
"errorMessage",
|
|
63
|
+
"error",
|
|
64
|
+
"reason",
|
|
65
|
+
"cause",
|
|
66
|
+
]);
|
|
67
|
+
if (direct && isLikelySqlErrorDetail(direct))
|
|
68
|
+
return direct;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Fall back to regex extraction from raw DSS/Athena text below.
|
|
73
|
+
}
|
|
74
|
+
const match = trimmed.match(/\b[A-Z_]+(?:_ERROR|_NOT_FOUND|_MISMATCH|_DENIED|_EXCEEDED)\b[:\s-]+[^\n\r]+/);
|
|
75
|
+
return match?.[0] ?? (isLikelySqlErrorDetail(trimmed) ? trimmed.slice(0, 500) : undefined);
|
|
76
|
+
}
|
|
77
|
+
function withSqlErrorContext(error) {
|
|
78
|
+
if (error instanceof DataikuError) {
|
|
79
|
+
const detail = sqlErrorDetailFromBody(error.body);
|
|
80
|
+
if (detail) {
|
|
81
|
+
let body = error.body;
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(error.body);
|
|
84
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
85
|
+
body = JSON.stringify({
|
|
86
|
+
...parsed,
|
|
87
|
+
message: `SQL query failed: ${detail}`,
|
|
88
|
+
sqlError: detail,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
body = `SQL query failed: ${detail}\n${error.body}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
body = `SQL query failed: ${detail}\n${error.body}`;
|
|
97
|
+
}
|
|
98
|
+
throw new DataikuError(error.status, error.statusText, body, error.retry);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
27
103
|
function splitDatasetIdentifier(datasetFullName, fallbackProjectKey) {
|
|
28
104
|
const trimmed = datasetFullName.trim();
|
|
29
105
|
const dotIndex = trimmed.indexOf(".");
|
|
@@ -79,7 +155,7 @@ export class SqlResource extends BaseResource {
|
|
|
79
155
|
const { queryId, schema, } = await this.startQuery(opts);
|
|
80
156
|
const rows = await this.streamResults(queryId);
|
|
81
157
|
await this.finishStreaming(queryId);
|
|
82
|
-
return { queryId, schema, rows, };
|
|
158
|
+
return { queryId, schema, columns: schema, rows, };
|
|
83
159
|
}
|
|
84
160
|
async resolveDatasetQueryFallback(opts) {
|
|
85
161
|
const datasetFullName = opts.datasetFullName;
|
|
@@ -119,14 +195,19 @@ export class SqlResource extends BaseResource {
|
|
|
119
195
|
}
|
|
120
196
|
catch (error) {
|
|
121
197
|
if (!isUnsupportedSqlDatasetConnectionError(error))
|
|
122
|
-
|
|
198
|
+
withSqlErrorContext(error);
|
|
123
199
|
const retryOpts = await this.resolveDatasetQueryFallback(queryOpts);
|
|
124
200
|
if (!retryOpts) {
|
|
125
201
|
throw new Error(buildUnsupportedSqlDatasetConnectionMessage(queryOpts.datasetFullName), {
|
|
126
202
|
cause: error,
|
|
127
203
|
});
|
|
128
204
|
}
|
|
129
|
-
|
|
205
|
+
try {
|
|
206
|
+
return await this.executeQuery(retryOpts);
|
|
207
|
+
}
|
|
208
|
+
catch (retryError) {
|
|
209
|
+
withSqlErrorContext(retryError);
|
|
210
|
+
}
|
|
130
211
|
}
|
|
131
212
|
}
|
|
132
213
|
}
|
package/dist/src/skill.d.ts
CHANGED
|
@@ -24,8 +24,8 @@ export interface DetectedAgent {
|
|
|
24
24
|
}
|
|
25
25
|
export declare function detectAgents(): DetectedAgent[];
|
|
26
26
|
/**
|
|
27
|
-
* Walk upward from startDir looking for
|
|
28
|
-
*
|
|
27
|
+
* Walk upward from startDir looking for strong project markers.
|
|
28
|
+
* Agent config directories are install targets, not workspace roots.
|
|
29
29
|
*/
|
|
30
30
|
export declare function findWorkspaceRoot(startDir: string): string;
|
|
31
31
|
export interface InstallResult {
|
package/dist/src/skill.js
CHANGED
|
@@ -208,10 +208,10 @@ export function detectAgents() {
|
|
|
208
208
|
// ---------------------------------------------------------------------------
|
|
209
209
|
// Workspace root detection
|
|
210
210
|
// ---------------------------------------------------------------------------
|
|
211
|
-
const WORKSPACE_MARKERS = [".git",
|
|
211
|
+
const WORKSPACE_MARKERS = [".git",];
|
|
212
212
|
/**
|
|
213
|
-
* Walk upward from startDir looking for
|
|
214
|
-
*
|
|
213
|
+
* Walk upward from startDir looking for strong project markers.
|
|
214
|
+
* Agent config directories are install targets, not workspace roots.
|
|
215
215
|
*/
|
|
216
216
|
export function findWorkspaceRoot(startDir) {
|
|
217
217
|
let dir = startDir;
|
package/package.json
CHANGED
|
@@ -611,6 +611,10 @@ export declare const SqlQueryResponseSchema: import("@sinclair/typebox").TObject
|
|
|
611
611
|
name: import("@sinclair/typebox").TString;
|
|
612
612
|
type: import("@sinclair/typebox").TString;
|
|
613
613
|
}>>;
|
|
614
|
+
columns: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
615
|
+
name: import("@sinclair/typebox").TString;
|
|
616
|
+
type: import("@sinclair/typebox").TString;
|
|
617
|
+
}>>>;
|
|
614
618
|
rows: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnknown>>;
|
|
615
619
|
}>;
|
|
616
620
|
export type SqlQueryResponse = Static<typeof SqlQueryResponseSchema>;
|
|
@@ -601,6 +601,7 @@ export const SqlQueryResultSchema = Type.Object({
|
|
|
601
601
|
export const SqlQueryResponseSchema = Type.Object({
|
|
602
602
|
queryId: Type.String(),
|
|
603
603
|
schema: Type.Array(SqlQuerySchemaSchema),
|
|
604
|
+
columns: Type.Optional(Type.Array(SqlQuerySchemaSchema)),
|
|
604
605
|
rows: Type.Array(Type.Array(Type.Unknown())),
|
|
605
606
|
});
|
|
606
607
|
// ---------------------------------------------------------------------------
|