dataiku-sdk 2.0.0 → 3.1.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/LICENSE +107 -17
- package/README.md +144 -17
- package/dist/build-metadata.json +1 -1
- package/dist/packages/types/src/index.d.ts +39 -4
- package/dist/packages/types/src/index.js +31 -2
- package/dist/src/cli/commands/api-service.js +62 -16
- package/dist/src/cli/commands/bundle.js +49 -15
- package/dist/src/cli/commands/code-env.js +210 -49
- package/dist/src/cli/commands/code.d.ts +19 -0
- package/dist/src/cli/commands/code.js +87 -15
- package/dist/src/cli/commands/dataset.js +8 -3
- package/dist/src/cli/commands/flow-zone.js +33 -5
- package/dist/src/cli/commands/notebook.js +72 -41
- package/dist/src/cli/commands/project-git.js +8 -5
- package/dist/src/cli/commands/project-library.d.ts +21 -0
- package/dist/src/cli/commands/project-library.js +121 -50
- package/dist/src/cli/commands/project.js +16 -2
- package/dist/src/cli/commands/recipe.js +37 -12
- package/dist/src/cli/commands/sql.d.ts +10 -0
- package/dist/src/cli/commands/sql.js +39 -17
- package/dist/src/cli/commands/webapp.js +67 -13
- package/dist/src/cli/contract.d.ts +15 -0
- package/dist/src/cli/contract.js +870 -107
- package/dist/src/cli/env.d.ts +9 -0
- package/dist/src/cli/env.js +20 -2
- package/dist/src/cli/flags.js +9 -0
- package/dist/src/cli/helpers/cleanup.js +104 -3
- package/dist/src/cli/helpers/flow-zone.d.ts +3 -0
- package/dist/src/cli/helpers/flow-zone.js +33 -3
- package/dist/src/cli/helpers/recipe.d.ts +20 -0
- package/dist/src/cli/helpers/recipe.js +158 -3
- package/dist/src/cli/output.d.ts +2 -0
- package/dist/src/cli/output.js +33 -6
- package/dist/src/cli/runtime.d.ts +35 -1
- package/dist/src/cli/runtime.js +125 -18
- package/dist/src/cli/types.d.ts +7 -0
- package/dist/src/cli.js +135 -28
- package/dist/src/client.d.ts +60 -0
- package/dist/src/client.js +237 -42
- package/dist/src/errors.d.ts +8 -2
- package/dist/src/errors.js +40 -0
- package/dist/src/index.d.ts +4 -3
- package/dist/src/index.js +2 -1
- package/dist/src/resources/api-services.d.ts +48 -6
- package/dist/src/resources/api-services.js +54 -8
- package/dist/src/resources/bundles.d.ts +51 -6
- package/dist/src/resources/bundles.js +39 -9
- package/dist/src/resources/code-envs.d.ts +25 -3
- package/dist/src/resources/code-envs.js +104 -2
- package/dist/src/resources/datasets.d.ts +20 -0
- package/dist/src/resources/datasets.js +232 -52
- package/dist/src/resources/jobs.d.ts +35 -10
- package/dist/src/resources/jobs.js +92 -17
- package/dist/src/resources/notebooks.d.ts +77 -7
- package/dist/src/resources/notebooks.js +143 -19
- package/dist/src/resources/project-git.d.ts +12 -0
- package/dist/src/resources/project-git.js +39 -8
- package/dist/src/resources/project-library.d.ts +122 -7
- package/dist/src/resources/project-library.js +494 -25
- package/dist/src/resources/projects.d.ts +5 -2
- package/dist/src/resources/projects.js +18 -2
- package/dist/src/resources/scenarios.d.ts +30 -0
- package/dist/src/resources/scenarios.js +73 -27
- package/dist/src/resources/sql.d.ts +6 -0
- package/dist/src/resources/sql.js +22 -2
- package/dist/src/resources/webapps.d.ts +35 -4
- package/dist/src/resources/webapps.js +54 -5
- package/dist/src/schemas.d.ts +2 -2
- package/dist/src/schemas.js +1 -1
- package/dist/src/skill.d.ts +9 -0
- package/dist/src/skill.js +37 -4
- package/dist/src/utils/flow-analysis.d.ts +53 -0
- package/dist/src/utils/flow-analysis.js +382 -0
- package/dist/src/utils/project-archive.d.ts +41 -2
- package/dist/src/utils/project-archive.js +159 -16
- package/package.json +5 -5
- package/packages/types/dist/index.d.ts +39 -4
- package/packages/types/dist/index.js +31 -2
- package/plugin.json +2 -2
- package/skills/dataiku-dss/SKILL.md +75 -48
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as nodePath from "node:path";
|
|
3
3
|
import { Readable, Transform, } from "node:stream";
|
|
4
4
|
import { pipeline, } from "node:stream/promises";
|
|
5
|
+
import { StringDecoder, } from "node:string_decoder";
|
|
5
6
|
import { createGzip, } from "node:zlib";
|
|
6
7
|
import { ClientValidationError, DataikuError, } from "../errors.js";
|
|
7
8
|
import { DatasetDetailsSchema, DatasetSchemaSchema, DatasetSummaryArraySchema, } from "../schemas.js";
|
|
@@ -12,7 +13,52 @@ import { resolveAdminManagedStorageConnection, } from "./connections.js";
|
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
// Helpers: TSV → CSV streaming conversion
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Decoded-byte budgets for the streaming TSV parser. Each budget fails closed
|
|
18
|
+
* — the response is cancelled and any partial download removed — so a dataset
|
|
19
|
+
* holding one oversized field, an unterminated quoted row, or a huge response
|
|
20
|
+
* can never accumulate unbounded memory or CPU before row limits apply.
|
|
21
|
+
*/
|
|
22
|
+
const TSV_MAX_FIELD_BYTES = 16 * 1024 * 1024; // 16 MiB per decoded field
|
|
23
|
+
const TSV_MAX_ROW_BYTES = 64 * 1024 * 1024; // 64 MiB per decoded row
|
|
24
|
+
const TSV_MAX_RESPONSE_BYTES = 16 * 1024 * 1024 * 1024; // 16 GiB per decoded response
|
|
25
|
+
function formatLimitBytes(bytes) {
|
|
26
|
+
if (bytes >= 1024 * 1024 * 1024)
|
|
27
|
+
return `${bytes / (1024 * 1024 * 1024)} GiB`;
|
|
28
|
+
if (bytes >= 1024 * 1024)
|
|
29
|
+
return `${bytes / (1024 * 1024)} MiB`;
|
|
30
|
+
return `${bytes} B`;
|
|
31
|
+
}
|
|
32
|
+
function tsvLimitError(what, limit) {
|
|
33
|
+
return new DataikuError(0, "Dataset TSV Limit", `Dataset TSV ${what} exceeded ${formatLimitBytes(limit)}; streaming was aborted to guard against unbounded memory use.`);
|
|
34
|
+
}
|
|
35
|
+
/** Characters that evaluate a spreadsheet cell as a formula. */
|
|
36
|
+
const FORMULA_SIGILS = "=+-@";
|
|
37
|
+
/**
|
|
38
|
+
* True when the first effective character of `field` — the first character
|
|
39
|
+
* that is not leading whitespace/control — is a spreadsheet formula sigil.
|
|
40
|
+
* Spreadsheet applications evaluate such cells even when CSV-quoted, so an
|
|
41
|
+
* untrusted dataset producer could plant formulas that execute on open.
|
|
42
|
+
*/
|
|
43
|
+
function hasFormulaPrefix(field) {
|
|
44
|
+
for (let i = 0; i < field.length; i++) {
|
|
45
|
+
const code = field.charCodeAt(i);
|
|
46
|
+
if (code <= 0x20 || (code >= 0x7f && code <= 0x9f))
|
|
47
|
+
continue;
|
|
48
|
+
return FORMULA_SIGILS.includes(field[i]);
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Escape one TSV field for CSV output. In spreadsheet-safe mode (the default
|
|
54
|
+
* for dataset downloads) a field whose first effective character is a formula
|
|
55
|
+
* sigil gets a leading apostrophe so spreadsheet importers show text instead
|
|
56
|
+
* of executing it; `rawData: true` (CLI `--raw-data`) preserves exact bytes.
|
|
57
|
+
*/
|
|
58
|
+
function csvEscape(field, spreadsheetSafe) {
|
|
59
|
+
if (spreadsheetSafe && hasFormulaPrefix(field)) {
|
|
60
|
+
field = `'${field}`;
|
|
61
|
+
}
|
|
16
62
|
if (field.includes(",")
|
|
17
63
|
|| field.includes('"')
|
|
18
64
|
|| field.includes("\n")
|
|
@@ -22,21 +68,56 @@ function csvEscape(field) {
|
|
|
22
68
|
}
|
|
23
69
|
return field;
|
|
24
70
|
}
|
|
25
|
-
function createTsvStreamState(
|
|
71
|
+
function createTsvStreamState(limits = {
|
|
72
|
+
maxFieldBytes: TSV_MAX_FIELD_BYTES,
|
|
73
|
+
maxRowBytes: TSV_MAX_ROW_BYTES,
|
|
74
|
+
maxResponseBytes: TSV_MAX_RESPONSE_BYTES,
|
|
75
|
+
}) {
|
|
26
76
|
return {
|
|
27
77
|
currentField: "",
|
|
28
78
|
currentRow: [],
|
|
29
79
|
inQuotes: false,
|
|
30
80
|
pendingQuoteInQuotes: false,
|
|
81
|
+
fieldBytes: 0,
|
|
82
|
+
rowBytes: 0,
|
|
83
|
+
responseBytes: 0,
|
|
84
|
+
limits,
|
|
31
85
|
};
|
|
32
86
|
}
|
|
87
|
+
/** Append decoded field data, bounding field/row bytes and failing closed. */
|
|
88
|
+
function appendFieldText(state, text) {
|
|
89
|
+
if (text.length === 0)
|
|
90
|
+
return;
|
|
91
|
+
const bytes = Buffer.byteLength(text, "utf-8");
|
|
92
|
+
state.fieldBytes += bytes;
|
|
93
|
+
state.rowBytes += bytes;
|
|
94
|
+
if (state.fieldBytes > state.limits.maxFieldBytes) {
|
|
95
|
+
throw tsvLimitError("field", state.limits.maxFieldBytes);
|
|
96
|
+
}
|
|
97
|
+
if (state.rowBytes > state.limits.maxRowBytes) {
|
|
98
|
+
throw tsvLimitError("row", state.limits.maxRowBytes);
|
|
99
|
+
}
|
|
100
|
+
state.currentField += text;
|
|
101
|
+
}
|
|
102
|
+
/** Close a field at a tab/newline delimiter and reset the field byte count. */
|
|
103
|
+
function endField(state) {
|
|
104
|
+
state.currentRow.push(state.currentField);
|
|
105
|
+
state.currentField = "";
|
|
106
|
+
state.fieldBytes = 0;
|
|
107
|
+
state.rowBytes += 1; // count the delimiter byte
|
|
108
|
+
}
|
|
33
109
|
function consumeTsvChunk(text, state, onRow) {
|
|
110
|
+
// Response budget: bound total decoded bytes regardless of row structure.
|
|
111
|
+
state.responseBytes += Buffer.byteLength(text, "utf-8");
|
|
112
|
+
if (state.responseBytes > state.limits.maxResponseBytes) {
|
|
113
|
+
throw tsvLimitError("response", state.limits.maxResponseBytes);
|
|
114
|
+
}
|
|
34
115
|
let i = 0;
|
|
35
116
|
if (state.pendingQuoteInQuotes) {
|
|
36
117
|
state.pendingQuoteInQuotes = false;
|
|
37
118
|
const first = text[0];
|
|
38
119
|
if (first === '"') {
|
|
39
|
-
state
|
|
120
|
+
appendFieldText(state, '"');
|
|
40
121
|
i = 1;
|
|
41
122
|
}
|
|
42
123
|
else if (first === "\t" || first === "\n" || first === "\r") {
|
|
@@ -44,72 +125,102 @@ function consumeTsvChunk(text, state, onRow) {
|
|
|
44
125
|
}
|
|
45
126
|
else if (first !== undefined) {
|
|
46
127
|
// Ambiguous terminal quote from previous chunk; keep it as data.
|
|
47
|
-
state
|
|
128
|
+
appendFieldText(state, '"');
|
|
48
129
|
}
|
|
49
130
|
}
|
|
50
|
-
|
|
51
|
-
const ch = text[i];
|
|
131
|
+
while (i < text.length) {
|
|
52
132
|
if (state.inQuotes) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
133
|
+
const quoteIdx = text.indexOf('"', i);
|
|
134
|
+
if (quoteIdx === -1) {
|
|
135
|
+
appendFieldText(state, text.slice(i));
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
if (quoteIdx > i)
|
|
139
|
+
appendFieldText(state, text.slice(i, quoteIdx));
|
|
140
|
+
const next = text[quoteIdx + 1];
|
|
141
|
+
if (next === '"') {
|
|
142
|
+
appendFieldText(state, '"');
|
|
143
|
+
i = quoteIdx + 2;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (next === undefined) {
|
|
147
|
+
state.pendingQuoteInQuotes = true;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
if (next === "\t" || next === "\n" || next === "\r") {
|
|
151
|
+
state.inQuotes = false;
|
|
152
|
+
i = quoteIdx + 1;
|
|
70
153
|
continue;
|
|
71
154
|
}
|
|
72
|
-
|
|
155
|
+
// Quote in the middle of quoted field text — keep it literal.
|
|
156
|
+
appendFieldText(state, '"');
|
|
157
|
+
i = quoteIdx + 1;
|
|
73
158
|
continue;
|
|
74
159
|
}
|
|
75
|
-
|
|
76
|
-
|
|
160
|
+
// Scan ahead in whole runs to the next structural character, so field
|
|
161
|
+
// data is appended in slices instead of one character at a time.
|
|
162
|
+
let special = -1;
|
|
163
|
+
for (let j = i; j < text.length; j++) {
|
|
164
|
+
const ch = text[j];
|
|
165
|
+
if (ch === '"' || ch === "\t" || ch === "\n" || ch === "\r") {
|
|
166
|
+
special = j;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const runEnd = special === -1 ? text.length : special;
|
|
171
|
+
if (runEnd > i) {
|
|
172
|
+
appendFieldText(state, text.slice(i, runEnd));
|
|
173
|
+
i = runEnd;
|
|
174
|
+
}
|
|
175
|
+
if (special === -1)
|
|
176
|
+
break;
|
|
177
|
+
const ch = text[special];
|
|
178
|
+
if (ch === '"') {
|
|
179
|
+
if (state.currentField.length === 0) {
|
|
180
|
+
state.inQuotes = true;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// Quote in the middle of unquoted field text — keep it literal.
|
|
184
|
+
appendFieldText(state, '"');
|
|
185
|
+
}
|
|
186
|
+
i = special + 1;
|
|
77
187
|
continue;
|
|
78
188
|
}
|
|
79
189
|
if (ch === "\t") {
|
|
80
|
-
|
|
81
|
-
|
|
190
|
+
endField(state);
|
|
191
|
+
i = special + 1;
|
|
82
192
|
continue;
|
|
83
193
|
}
|
|
84
194
|
if (ch === "\n") {
|
|
85
|
-
|
|
86
|
-
|
|
195
|
+
endField(state);
|
|
196
|
+
i = special + 1;
|
|
87
197
|
const row = state.currentRow;
|
|
88
198
|
state.currentRow = [];
|
|
199
|
+
state.rowBytes = 0;
|
|
89
200
|
onRow(row);
|
|
90
201
|
continue;
|
|
91
202
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
state.currentField += ch;
|
|
203
|
+
// "\r" — carriage return: skipped, as in the original parser.
|
|
204
|
+
i = special + 1;
|
|
96
205
|
}
|
|
97
206
|
}
|
|
98
207
|
function flushTsvStream(state, onRow) {
|
|
99
208
|
if (state.pendingQuoteInQuotes) {
|
|
100
|
-
state
|
|
209
|
+
appendFieldText(state, '"');
|
|
101
210
|
state.pendingQuoteInQuotes = false;
|
|
102
211
|
}
|
|
103
212
|
if (state.currentField.length === 0 && state.currentRow.length === 0)
|
|
104
213
|
return;
|
|
105
214
|
state.currentRow.push(state.currentField);
|
|
106
215
|
state.currentField = "";
|
|
216
|
+
state.fieldBytes = 0;
|
|
107
217
|
const row = state.currentRow;
|
|
108
218
|
state.currentRow = [];
|
|
219
|
+
state.rowBytes = 0;
|
|
109
220
|
onRow(row);
|
|
110
221
|
}
|
|
111
|
-
function rowToCsv(row) {
|
|
112
|
-
return row.map((field) => csvEscape(field)).join(",");
|
|
222
|
+
function rowToCsv(row, spreadsheetSafe) {
|
|
223
|
+
return row.map((field) => csvEscape(field, spreadsheetSafe)).join(",");
|
|
113
224
|
}
|
|
114
225
|
function isBlankRow(row) {
|
|
115
226
|
return row.length === 1 && row[0].length === 0;
|
|
@@ -154,6 +265,7 @@ async function readChunkWithTimeout(reader, remainingMs, timeoutMs, action) {
|
|
|
154
265
|
}
|
|
155
266
|
async function collectPreviewRows(body, maxDataRows, timeoutMs, onHeader) {
|
|
156
267
|
const state = createTsvStreamState();
|
|
268
|
+
const decoder = new StringDecoder("utf-8");
|
|
157
269
|
let columns;
|
|
158
270
|
const rows = [];
|
|
159
271
|
let done = false;
|
|
@@ -193,12 +305,22 @@ async function collectPreviewRows(body, maxDataRows, timeoutMs, onHeader) {
|
|
|
193
305
|
const result = await readChunkWithTimeout(reader, remainingMs, timeoutMs, "preview");
|
|
194
306
|
if (result.done)
|
|
195
307
|
break;
|
|
196
|
-
consumeTsvChunk(
|
|
308
|
+
consumeTsvChunk(decoder.write(result.value), state, handleRow);
|
|
197
309
|
}
|
|
198
|
-
if (!done)
|
|
310
|
+
if (!done) {
|
|
311
|
+
const tail = decoder.end();
|
|
312
|
+
if (tail !== "")
|
|
313
|
+
consumeTsvChunk(tail, state, handleRow);
|
|
199
314
|
flushTsvStream(state, handleRow);
|
|
315
|
+
}
|
|
200
316
|
return { columns: columns ?? [], rows, truncated, };
|
|
201
317
|
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
// Fail closed on TSV byte-limit overflow: cancel the response instead
|
|
320
|
+
// of retaining partial decoded data.
|
|
321
|
+
void reader.cancel().catch(() => { });
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
202
324
|
finally {
|
|
203
325
|
reader.releaseLock();
|
|
204
326
|
}
|
|
@@ -211,6 +333,7 @@ async function collectPreviewRows(body, maxDataRows, timeoutMs, onHeader) {
|
|
|
211
333
|
*/
|
|
212
334
|
async function collectTsvRowCount(body, stopAfter, timeoutMs) {
|
|
213
335
|
const state = createTsvStreamState();
|
|
336
|
+
const decoder = new StringDecoder("utf-8");
|
|
214
337
|
let headerSeen = false;
|
|
215
338
|
let count = 0;
|
|
216
339
|
let bounded = false;
|
|
@@ -251,18 +374,29 @@ async function collectTsvRowCount(body, stopAfter, timeoutMs) {
|
|
|
251
374
|
const result = await readChunkWithTimeout(reader, remainingMs, timeoutMs, "row count assertion");
|
|
252
375
|
if (result.done)
|
|
253
376
|
break;
|
|
254
|
-
consumeTsvChunk(
|
|
377
|
+
consumeTsvChunk(decoder.write(result.value), state, handleRow);
|
|
255
378
|
}
|
|
256
|
-
if (!bounded)
|
|
379
|
+
if (!bounded) {
|
|
380
|
+
const tail = decoder.end();
|
|
381
|
+
if (tail !== "")
|
|
382
|
+
consumeTsvChunk(tail, state, handleRow);
|
|
257
383
|
flushTsvStream(state, handleRow);
|
|
384
|
+
}
|
|
258
385
|
return { count, bounded, };
|
|
259
386
|
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
// Fail closed on TSV byte-limit overflow: cancel the response instead
|
|
389
|
+
// of continuing to consume unbounded decoded data.
|
|
390
|
+
void reader.cancel().catch(() => { });
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
260
393
|
finally {
|
|
261
394
|
reader.releaseLock();
|
|
262
395
|
}
|
|
263
396
|
}
|
|
264
|
-
function tsvToCsvTransform(maxDataRows, stats, onHeader) {
|
|
397
|
+
function tsvToCsvTransform(maxDataRows, stats, onHeader, spreadsheetSafe = true) {
|
|
265
398
|
const state = createTsvStreamState();
|
|
399
|
+
const decoder = new StringDecoder("utf-8");
|
|
266
400
|
const maxRows = Math.max(1, maxDataRows);
|
|
267
401
|
let headerSeen = false;
|
|
268
402
|
let done = false;
|
|
@@ -276,7 +410,7 @@ function tsvToCsvTransform(maxDataRows, stats, onHeader) {
|
|
|
276
410
|
headerSeen = true;
|
|
277
411
|
columnCount = row.length;
|
|
278
412
|
onHeader?.(row);
|
|
279
|
-
push(`${rowToCsv(row)}\n`);
|
|
413
|
+
push(`${rowToCsv(row, spreadsheetSafe)}\n`);
|
|
280
414
|
return;
|
|
281
415
|
}
|
|
282
416
|
if (isBlankRow(row) && columnCount !== 1)
|
|
@@ -286,7 +420,7 @@ function tsvToCsvTransform(maxDataRows, stats, onHeader) {
|
|
|
286
420
|
done = true;
|
|
287
421
|
return;
|
|
288
422
|
}
|
|
289
|
-
push(`${rowToCsv(row)}\n`);
|
|
423
|
+
push(`${rowToCsv(row, spreadsheetSafe)}\n`);
|
|
290
424
|
stats.rows += 1;
|
|
291
425
|
};
|
|
292
426
|
return new Transform({
|
|
@@ -295,15 +429,35 @@ function tsvToCsvTransform(maxDataRows, stats, onHeader) {
|
|
|
295
429
|
callback();
|
|
296
430
|
return;
|
|
297
431
|
}
|
|
298
|
-
|
|
432
|
+
try {
|
|
433
|
+
consumeTsvChunk(decoder.write(chunk), state, (row) => handleRow(row, (line) => this.push(line)));
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
// Byte-limit overflow: destroy the transform so the pipeline
|
|
437
|
+
// cancels the response and the partial file is removed.
|
|
438
|
+
callback(error instanceof Error ? error : new Error(String(error)));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
299
441
|
if (done)
|
|
300
442
|
this.push(null);
|
|
301
443
|
callback();
|
|
302
444
|
},
|
|
303
445
|
flush(callback) {
|
|
304
|
-
if (
|
|
446
|
+
if (done) {
|
|
447
|
+
callback();
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
const tail = decoder.end();
|
|
452
|
+
if (tail !== "") {
|
|
453
|
+
consumeTsvChunk(tail, state, (row) => handleRow(row, (line) => this.push(line)));
|
|
454
|
+
}
|
|
305
455
|
flushTsvStream(state, (row) => handleRow(row, (line) => this.push(line)));
|
|
306
456
|
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
callback(error instanceof Error ? error : new Error(String(error)));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
307
461
|
callback();
|
|
308
462
|
},
|
|
309
463
|
});
|
|
@@ -495,6 +649,10 @@ export class DatasetsResource extends BaseResource {
|
|
|
495
649
|
*
|
|
496
650
|
* If `validateColumns` is provided, the first TSV row (header) is checked
|
|
497
651
|
* against the column names. Mismatches emit a warning via onValidationWarning.
|
|
652
|
+
*
|
|
653
|
+
* The stream is byte-bounded (16 MiB per field, 64 MiB per row, 16 GiB per
|
|
654
|
+
* response): an oversized field or unterminated quoted row aborts the
|
|
655
|
+
* preview, cancels the response, and fails with a DataikuError.
|
|
498
656
|
*/
|
|
499
657
|
async preview(datasetName, opts) {
|
|
500
658
|
const maxRows = Math.max(1, Math.min(opts?.maxRows ?? 50, 500));
|
|
@@ -529,6 +687,10 @@ export class DatasetsResource extends BaseResource {
|
|
|
529
687
|
* fully scanned. Returns the observed count together with `exact` (false when
|
|
530
688
|
* the probe cap was reached, i.e. the count is a lower bound) and whether the
|
|
531
689
|
* count satisfies the assertion.
|
|
690
|
+
*
|
|
691
|
+
* Streaming is byte-bounded (16 MiB per field, 64 MiB per row, 16 GiB per
|
|
692
|
+
* response); oversized fields abort the count, cancel the response, and
|
|
693
|
+
* fail with a DataikuError.
|
|
532
694
|
*/
|
|
533
695
|
async assertRowCount(datasetName, expected, projectKey) {
|
|
534
696
|
if (!Number.isSafeInteger(expected) || expected < 0) {
|
|
@@ -555,6 +717,16 @@ export class DatasetsResource extends BaseResource {
|
|
|
555
717
|
* (default 100k). Returns the written path, the number of data rows written,
|
|
556
718
|
* whether the dataset had more rows than the cap (truncated), and the limit
|
|
557
719
|
* used — so callers can detect truncation instead of silently getting a sample.
|
|
720
|
+
*
|
|
721
|
+
* Exported cells are spreadsheet-safe by default: headers/values whose
|
|
722
|
+
* first effective character is a spreadsheet formula sigil (`=`, `+`, `-`,
|
|
723
|
+
* `@`, including leading whitespace/control variants) are neutralized with
|
|
724
|
+
* a leading apostrophe so the file never executes formulas on open. Pass
|
|
725
|
+
* `rawData: true` to preserve exact bytes.
|
|
726
|
+
*
|
|
727
|
+
* Streaming is byte-bounded (16 MiB per field, 64 MiB per row, 16 GiB per
|
|
728
|
+
* response); on overflow the response is cancelled, the error is thrown,
|
|
729
|
+
* and any partially written download file is removed.
|
|
558
730
|
*/
|
|
559
731
|
async download(datasetName, opts) {
|
|
560
732
|
const limit = Math.max(1, opts?.limit ?? 100_000);
|
|
@@ -575,15 +747,23 @@ export class DatasetsResource extends BaseResource {
|
|
|
575
747
|
const shouldGzip = filePath.endsWith(".gz");
|
|
576
748
|
const stats = { rows: 0, truncated: false, };
|
|
577
749
|
const nodeStream = Readable.fromWeb(res.body);
|
|
578
|
-
const csvTransform = tsvToCsvTransform(limit, stats, onHeader);
|
|
750
|
+
const csvTransform = tsvToCsvTransform(limit, stats, onHeader, opts?.rawData !== true);
|
|
579
751
|
fs.mkdirSync(nodePath.dirname(filePath), { recursive: true, });
|
|
580
752
|
const fileOut = fs.createWriteStream(filePath);
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
753
|
+
try {
|
|
754
|
+
if (shouldGzip) {
|
|
755
|
+
const gzip = createGzip();
|
|
756
|
+
await pipeline(nodeStream, csvTransform, gzip, fileOut);
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
await pipeline(nodeStream, csvTransform, fileOut);
|
|
760
|
+
}
|
|
584
761
|
}
|
|
585
|
-
|
|
586
|
-
|
|
762
|
+
catch (error) {
|
|
763
|
+
// A failed export (byte-limit overflow, network error) must never
|
|
764
|
+
// leave a partial file that looks like a complete download.
|
|
765
|
+
fs.rmSync(filePath, { force: true, });
|
|
766
|
+
throw error;
|
|
587
767
|
}
|
|
588
768
|
return { path: filePath, rows: stats.rows, truncated: stats.truncated, limit, };
|
|
589
769
|
}
|
|
@@ -14,6 +14,15 @@ export interface JobLogSummary {
|
|
|
14
14
|
lines: string[];
|
|
15
15
|
progress?: JobLogProgress;
|
|
16
16
|
}
|
|
17
|
+
/** Why a requested job log could not be retrieved. */
|
|
18
|
+
export type JobLogUnavailableReason = "not_found" | "error";
|
|
19
|
+
/** Result of a completed job wait, including log-unavailable metadata. */
|
|
20
|
+
export type JobWaitOutcome = JobWaitResult & {
|
|
21
|
+
logSummary?: JobLogSummary;
|
|
22
|
+
logUnavailable?: JobLogUnavailableReason;
|
|
23
|
+
/** True when the job itself no longer exists on the server. */
|
|
24
|
+
removed?: boolean;
|
|
25
|
+
};
|
|
17
26
|
export interface JobBuildTarget {
|
|
18
27
|
id: string;
|
|
19
28
|
type?: JobBuildTargetType;
|
|
@@ -57,17 +66,25 @@ export declare class JobsResource extends BaseResource {
|
|
|
57
66
|
/**
|
|
58
67
|
* Retrieve job log text.
|
|
59
68
|
* Returns the last `maxLogLines` lines (default 500) from the tail.
|
|
60
|
-
* Use `0` or `-1` to return the
|
|
69
|
+
* Use `0` or `-1` to return the log without line truncation.
|
|
70
|
+
*
|
|
71
|
+
* The download is byte-bounded and deadline-covered: at most `maxLogBytes`
|
|
72
|
+
* (default 10 MiB) of the *most recent* log output is retained, so a
|
|
73
|
+
* collaborator-influenced oversized log cannot exhaust client memory while
|
|
74
|
+
* the line limit previously only applied after a full unbounded download.
|
|
75
|
+
* Logs up to the cap are returned in full.
|
|
61
76
|
*/
|
|
62
77
|
log(jobId: string, opts?: {
|
|
63
78
|
activity?: string;
|
|
64
79
|
logId?: string;
|
|
65
80
|
logFilter?: JobLogFilter;
|
|
66
81
|
maxLogLines?: number;
|
|
82
|
+
maxLogBytes?: number;
|
|
67
83
|
projectKey?: string;
|
|
68
84
|
}): Promise<string>;
|
|
69
85
|
logFromUrl(logUrl: string, opts?: {
|
|
70
86
|
maxLogLines?: number;
|
|
87
|
+
maxLogBytes?: number;
|
|
71
88
|
}): Promise<string>;
|
|
72
89
|
/**
|
|
73
90
|
* Start a build job for one or more dataset or managed-folder outputs.
|
|
@@ -87,22 +104,26 @@ export declare class JobsResource extends BaseResource {
|
|
|
87
104
|
* Build one or more dataset or managed-folder outputs and wait for a terminal state.
|
|
88
105
|
* Combines {@link buildOutputs} then {@link wait}.
|
|
89
106
|
*/
|
|
90
|
-
buildAndWaitOutputs(targets: JobBuildTarget[], opts?: JobBuildAndWaitOptions): Promise<
|
|
91
|
-
logSummary?: JobLogSummary;
|
|
92
|
-
}>;
|
|
107
|
+
buildAndWaitOutputs(targets: JobBuildTarget[], opts?: JobBuildAndWaitOptions): Promise<JobWaitOutcome>;
|
|
93
108
|
/**
|
|
94
109
|
* Build a dataset or managed folder and wait for the job to reach a terminal state.
|
|
95
110
|
* Combines {@link build} then {@link wait}.
|
|
96
111
|
*/
|
|
97
|
-
buildAndWait(targetId: string, opts?: JobBuildAndWaitOptions): Promise<
|
|
98
|
-
logSummary?: JobLogSummary;
|
|
99
|
-
}>;
|
|
112
|
+
buildAndWait(targetId: string, opts?: JobBuildAndWaitOptions): Promise<JobWaitOutcome>;
|
|
100
113
|
/**
|
|
101
114
|
* Poll a job until it reaches a terminal state or times out.
|
|
102
115
|
*
|
|
103
116
|
* Adaptive polling doubles the interval every 3 polls when
|
|
104
117
|
* `pollIntervalMs` is not explicitly set.
|
|
105
118
|
*
|
|
119
|
+
* A terminal state observed from the status endpoint is authoritative:
|
|
120
|
+
* when log retrieval fails afterwards (e.g. `not_found` because logs were
|
|
121
|
+
* purged or the job was removed), `wait` still returns the terminal
|
|
122
|
+
* outcome instead of throwing, with `logUnavailable` holding the machine-
|
|
123
|
+
* readable reason and `removed` set once the job itself is probed and
|
|
124
|
+
* confirmed gone. Explicit `includeLogs`/`summary` requests stay honest:
|
|
125
|
+
* `log`/`logSummary` are only present when logs were actually retrieved.
|
|
126
|
+
*
|
|
106
127
|
* On timeout, returns `{ success: false, ... }` rather than throwing.
|
|
107
128
|
*/
|
|
108
129
|
wait(jobId: string, opts?: {
|
|
@@ -115,9 +136,13 @@ export declare class JobsResource extends BaseResource {
|
|
|
115
136
|
summary?: boolean;
|
|
116
137
|
timeoutMs?: number;
|
|
117
138
|
projectKey?: string;
|
|
118
|
-
}): Promise<
|
|
119
|
-
|
|
120
|
-
|
|
139
|
+
}): Promise<JobWaitOutcome>;
|
|
140
|
+
/**
|
|
141
|
+
* Probe whether a job still exists. Returns `true` when the server reports
|
|
142
|
+
* it as not found, `false` when it is still visible, and `undefined` when
|
|
143
|
+
* the probe outcome is ambiguous.
|
|
144
|
+
*/
|
|
145
|
+
private probeJobRemoved;
|
|
121
146
|
/** Request a job abort. */
|
|
122
147
|
abort(jobId: string, projectKey?: string): Promise<void>;
|
|
123
148
|
}
|