dataiku-sdk 3.2.0 → 3.4.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 +151 -46
- package/dist/build-metadata.json +1 -1
- package/dist/packages/types/src/index.d.ts +102 -0
- package/dist/packages/types/src/index.js +76 -0
- package/dist/src/cli/commands/app.js +57 -15
- package/dist/src/cli/commands/bundle.js +1 -1
- package/dist/src/cli/commands/code-env.js +13 -7
- package/dist/src/cli/commands/code.js +1 -1
- package/dist/src/cli/commands/connection.d.ts +9 -0
- package/dist/src/cli/commands/connection.js +267 -1
- package/dist/src/cli/commands/data-collection.d.ts +2 -0
- package/dist/src/cli/commands/data-collection.js +164 -0
- package/dist/src/cli/commands/dataset.js +162 -33
- package/dist/src/cli/commands/future.js +2 -2
- package/dist/src/cli/commands/group.d.ts +2 -0
- package/dist/src/cli/commands/group.js +109 -0
- package/dist/src/cli/commands/index.js +151 -78
- package/dist/src/cli/commands/job.js +4 -2
- package/dist/src/cli/commands/knowledge-bank.d.ts +2 -0
- package/dist/src/cli/commands/knowledge-bank.js +80 -0
- package/dist/src/cli/commands/llm.d.ts +2 -0
- package/dist/src/cli/commands/llm.js +93 -0
- package/dist/src/cli/commands/macro.d.ts +2 -0
- package/dist/src/cli/commands/macro.js +221 -0
- package/dist/src/cli/commands/ml-task.js +18 -6
- package/dist/src/cli/commands/notebook.js +6 -2
- package/dist/src/cli/commands/plugin.d.ts +2 -0
- package/dist/src/cli/commands/plugin.js +799 -0
- package/dist/src/cli/commands/project-folder.d.ts +2 -0
- package/dist/src/cli/commands/project-folder.js +197 -0
- package/dist/src/cli/commands/project-git.js +15 -55
- package/dist/src/cli/commands/project.js +52 -6
- package/dist/src/cli/commands/recipe.js +36 -1
- package/dist/src/cli/commands/saved-model.d.ts +5 -0
- package/dist/src/cli/commands/saved-model.js +388 -1
- package/dist/src/cli/commands/scenario.js +139 -2
- package/dist/src/cli/commands/user.d.ts +8 -0
- package/dist/src/cli/commands/user.js +296 -0
- package/dist/src/cli/contract.d.ts +1 -2
- package/dist/src/cli/contract.js +1098 -297
- package/dist/src/cli/flags.js +30 -1
- package/dist/src/cli/helpers/app-successor.d.ts +19 -4
- package/dist/src/cli/helpers/app-successor.js +90 -30
- package/dist/src/cli/helpers/cleanup.js +3 -1
- package/dist/src/cli/helpers/flow-zone.js +1 -2
- package/dist/src/cli/output-schemas.d.ts +24 -0
- package/dist/src/cli/output-schemas.js +233 -0
- package/dist/src/cli/output.d.ts +1 -1
- package/dist/src/cli/output.js +23 -10
- package/dist/src/cli/usage.d.ts +1 -0
- package/dist/src/cli/usage.js +47 -1
- package/dist/src/cli.js +147 -41
- package/dist/src/client.d.ts +92 -5
- package/dist/src/client.js +202 -31
- package/dist/src/errors.d.ts +3 -0
- package/dist/src/errors.js +8 -4
- package/dist/src/generated/action-output-schemas.json +1735 -0
- package/dist/src/index.d.ts +16 -7
- package/dist/src/index.js +11 -2
- package/dist/src/resources/analyses.js +1 -8
- package/dist/src/resources/applications.d.ts +8 -6
- package/dist/src/resources/applications.js +21 -21
- package/dist/src/resources/base.d.ts +4 -0
- package/dist/src/resources/base.js +15 -0
- package/dist/src/resources/bundles.d.ts +9 -4
- package/dist/src/resources/bundles.js +10 -5
- package/dist/src/resources/connections.d.ts +78 -1
- package/dist/src/resources/connections.js +132 -1
- package/dist/src/resources/data-collections.d.ts +118 -0
- package/dist/src/resources/data-collections.js +49 -0
- package/dist/src/resources/datasets.d.ts +69 -0
- package/dist/src/resources/datasets.js +136 -5
- package/dist/src/resources/flow-zones.d.ts +6 -2
- package/dist/src/resources/flow-zones.js +6 -3
- package/dist/src/resources/futures.d.ts +6 -1
- package/dist/src/resources/futures.js +68 -22
- package/dist/src/resources/groups.d.ts +47 -0
- package/dist/src/resources/groups.js +56 -0
- package/dist/src/resources/jobs.d.ts +8 -13
- package/dist/src/resources/jobs.js +71 -34
- package/dist/src/resources/knowledge-banks.d.ts +58 -0
- package/dist/src/resources/knowledge-banks.js +61 -0
- package/dist/src/resources/llms.d.ts +92 -0
- package/dist/src/resources/llms.js +62 -0
- package/dist/src/resources/macros.d.ts +121 -0
- package/dist/src/resources/macros.js +231 -0
- package/dist/src/resources/ml-tasks.d.ts +37 -2
- package/dist/src/resources/ml-tasks.js +109 -20
- package/dist/src/resources/model-evaluation-stores.js +1 -8
- package/dist/src/resources/notebooks.d.ts +43 -7
- package/dist/src/resources/notebooks.js +53 -13
- package/dist/src/resources/plugins.d.ts +263 -0
- package/dist/src/resources/plugins.js +455 -0
- package/dist/src/resources/project-folders.d.ts +71 -0
- package/dist/src/resources/project-folders.js +57 -0
- package/dist/src/resources/project-git.d.ts +12 -12
- package/dist/src/resources/project-git.js +84 -82
- package/dist/src/resources/project-library.d.ts +0 -1
- package/dist/src/resources/project-library.js +1 -17
- package/dist/src/resources/projects.d.ts +23 -3
- package/dist/src/resources/projects.js +60 -34
- package/dist/src/resources/recipes.d.ts +15 -1
- package/dist/src/resources/recipes.js +73 -6
- package/dist/src/resources/saved-models.d.ts +134 -1
- package/dist/src/resources/saved-models.js +214 -6
- package/dist/src/resources/scenarios.d.ts +105 -0
- package/dist/src/resources/scenarios.js +232 -106
- package/dist/src/resources/sql.js +5 -1
- package/dist/src/resources/users.d.ts +129 -0
- package/dist/src/resources/users.js +128 -0
- package/dist/src/resources/webapps.d.ts +15 -2
- package/dist/src/resources/webapps.js +18 -4
- package/dist/src/schemas.d.ts +2 -2
- package/dist/src/schemas.js +1 -1
- package/dist/src/utils/deep-merge.d.ts +1 -1
- package/dist/src/utils/git-reference.d.ts +12 -0
- package/dist/src/utils/git-reference.js +38 -0
- package/dist/src/utils/polling.d.ts +20 -0
- package/dist/src/utils/polling.js +27 -0
- package/dist/src/utils/response-file.js +1 -1
- package/dist/src/utils/secret-sanitize.d.ts +54 -0
- package/dist/src/utils/secret-sanitize.js +101 -0
- package/package.json +9 -8
- package/packages/types/dist/index.d.ts +102 -0
- package/packages/types/dist/index.js +76 -0
- package/plugin.json +1 -1
- package/skills/dataiku-dss/SKILL.md +11 -7
- package/skills/dataiku-dss/references/app-releases.md +8 -8
- package/skills/dataiku-dss/references/authentication.md +6 -6
- package/skills/dataiku-dss/references/discovery.md +4 -4
- package/skills/dataiku-dss/references/mutations.md +1 -1
- package/skills/dataiku-dss/references/troubleshooting.md +2 -2
package/dist/src/client.d.ts
CHANGED
|
@@ -8,19 +8,26 @@ import { CodeEnvsResource } from "./resources/code-envs.js";
|
|
|
8
8
|
import { ConnectionsResource } from "./resources/connections.js";
|
|
9
9
|
import { ContinuousActivitiesResource } from "./resources/continuous-activities.js";
|
|
10
10
|
import { DashboardsResource } from "./resources/dashboards.js";
|
|
11
|
+
import { DataCollectionsResource } from "./resources/data-collections.js";
|
|
11
12
|
import { DataQualityResource } from "./resources/data-quality.js";
|
|
12
13
|
import { DatasetsResource } from "./resources/datasets.js";
|
|
13
14
|
import { DiscussionsResource } from "./resources/discussions.js";
|
|
14
15
|
import { FlowZonesResource } from "./resources/flow-zones.js";
|
|
15
16
|
import { FoldersResource } from "./resources/folders.js";
|
|
16
17
|
import { FuturesResource } from "./resources/futures.js";
|
|
18
|
+
import { GroupsResource } from "./resources/groups.js";
|
|
17
19
|
import { InsightsResource } from "./resources/insights.js";
|
|
18
20
|
import { JobsResource } from "./resources/jobs.js";
|
|
21
|
+
import { KnowledgeBanksResource } from "./resources/knowledge-banks.js";
|
|
22
|
+
import { LlmsResource } from "./resources/llms.js";
|
|
23
|
+
import { MacrosResource } from "./resources/macros.js";
|
|
19
24
|
import { MeaningsResource } from "./resources/meanings.js";
|
|
20
25
|
import { MetricsResource } from "./resources/metrics.js";
|
|
21
26
|
import { MlTasksResource } from "./resources/ml-tasks.js";
|
|
22
27
|
import { ModelEvaluationStoresResource } from "./resources/model-evaluation-stores.js";
|
|
23
28
|
import { NotebooksResource } from "./resources/notebooks.js";
|
|
29
|
+
import { PluginsResource } from "./resources/plugins.js";
|
|
30
|
+
import { ProjectFoldersResource } from "./resources/project-folders.js";
|
|
24
31
|
import { ProjectGitResource } from "./resources/project-git.js";
|
|
25
32
|
import { ProjectLibraryResource } from "./resources/project-library.js";
|
|
26
33
|
import { ProjectsResource } from "./resources/projects.js";
|
|
@@ -30,10 +37,52 @@ import { ScenariosResource } from "./resources/scenarios.js";
|
|
|
30
37
|
import { SqlResource } from "./resources/sql.js";
|
|
31
38
|
import { StatisticsResource } from "./resources/statistics.js";
|
|
32
39
|
import { StreamingEndpointsResource } from "./resources/streaming-endpoints.js";
|
|
40
|
+
import { UsersResource } from "./resources/users.js";
|
|
33
41
|
import { VariablesResource } from "./resources/variables.js";
|
|
34
42
|
import { WebappsResource } from "./resources/webapps.js";
|
|
35
43
|
import { WikiResource } from "./resources/wiki.js";
|
|
36
44
|
import { WorkspacesResource } from "./resources/workspaces.js";
|
|
45
|
+
/** One part of a {@link DataikuClient.uploadForm} multipart/form-data body. */
|
|
46
|
+
export interface UploadFormPart {
|
|
47
|
+
/** Form field name. */
|
|
48
|
+
name: string;
|
|
49
|
+
/** Text field value. Mutually exclusive with `blob`/`fileName` file parts. */
|
|
50
|
+
value?: string;
|
|
51
|
+
/** File content. Omit (or null) together with `fileName` for a zero-byte file part. */
|
|
52
|
+
blob?: Blob | null;
|
|
53
|
+
/** Filename for file parts. */
|
|
54
|
+
fileName?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Multipart part modes for {@link DataikuClient.uploadForm}:
|
|
58
|
+
* - `value`: a form text field;
|
|
59
|
+
* - neither `blob` nor `fileName`: a filename-less, zero-length field part —
|
|
60
|
+
* the official Python client's files={"file": (None, None)} shape required
|
|
61
|
+
* by the managed-folder MLflow import (a Blob would add `filename=`, even
|
|
62
|
+
* when empty);
|
|
63
|
+
* - `blob` (optionally with `fileName`): a file part.
|
|
64
|
+
*/
|
|
65
|
+
/** Per-call overrides for {@link DataikuClient.get}. */
|
|
66
|
+
export interface DataikuGetOptions {
|
|
67
|
+
/**
|
|
68
|
+
* TOTAL duration budget in milliseconds for this call, measured from call
|
|
69
|
+
* start: it bounds every retry attempt's fetch, the backoff sleeps between
|
|
70
|
+
* attempts, and the response body read. When the budget is exhausted the
|
|
71
|
+
* request is aborted and a timeout DataikuError is thrown. The client-level
|
|
72
|
+
* requestTimeoutMs still caps each attempt and buffered body read; this
|
|
73
|
+
* budget may shorten those limits but never extends them.
|
|
74
|
+
*/
|
|
75
|
+
timeoutMs?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Issue exactly one transport attempt (no retries). Independent of
|
|
78
|
+
* `timeoutMs`: when both are supplied the total budget above still applies
|
|
79
|
+
* and aborts the attempt; when no budget is supplied the attempt is capped
|
|
80
|
+
* by the client-level `requestTimeoutMs`. Wait loops use this for the one
|
|
81
|
+
* observation whose wait budget is already spent, so it still reaches the
|
|
82
|
+
* server once instead of failing before any transport attempt.
|
|
83
|
+
*/
|
|
84
|
+
noRetry?: boolean;
|
|
85
|
+
}
|
|
37
86
|
export interface DataikuClientTraceEvent {
|
|
38
87
|
type: "trace";
|
|
39
88
|
phase: "request" | "response" | "error";
|
|
@@ -72,9 +121,9 @@ export interface DataikuClientConfig {
|
|
|
72
121
|
/** Max retry attempts for idempotent requests (default 4, capped at 10) */
|
|
73
122
|
retryMaxAttempts?: number;
|
|
74
123
|
/**
|
|
75
|
-
* Maximum bytes
|
|
76
|
-
*
|
|
77
|
-
*
|
|
124
|
+
* Maximum bytes retained from a single response body by text/JSON consumers
|
|
125
|
+
* (default 50 MiB). Oversized successful responses are rejected. Error
|
|
126
|
+
* responses retain a bounded prefix with DataikuError.bodyTruncated=true.
|
|
78
127
|
*/
|
|
79
128
|
maxResponseBodyBytes?: number;
|
|
80
129
|
/** Emit HTTP request/response trace events. Defaults to JSONL on stderr when verbose is true. */
|
|
@@ -139,6 +188,14 @@ export declare class DataikuClient {
|
|
|
139
188
|
private mlTasksResource?;
|
|
140
189
|
private savedModelsResource?;
|
|
141
190
|
private modelEvaluationStoresResource?;
|
|
191
|
+
private projectFoldersResource?;
|
|
192
|
+
private dataCollectionsResource?;
|
|
193
|
+
private llmsResource?;
|
|
194
|
+
private knowledgeBanksResource?;
|
|
195
|
+
private macrosResource?;
|
|
196
|
+
private pluginsResource?;
|
|
197
|
+
private usersResource?;
|
|
198
|
+
private groupsResource?;
|
|
142
199
|
get projects(): ProjectsResource;
|
|
143
200
|
get datasets(): DatasetsResource;
|
|
144
201
|
get dashboards(): DashboardsResource;
|
|
@@ -175,6 +232,14 @@ export declare class DataikuClient {
|
|
|
175
232
|
get mlTasks(): MlTasksResource;
|
|
176
233
|
get savedModels(): SavedModelsResource;
|
|
177
234
|
get modelEvaluationStores(): ModelEvaluationStoresResource;
|
|
235
|
+
get projectFolders(): ProjectFoldersResource;
|
|
236
|
+
get dataCollections(): DataCollectionsResource;
|
|
237
|
+
get llms(): LlmsResource;
|
|
238
|
+
get knowledgeBanks(): KnowledgeBanksResource;
|
|
239
|
+
get macros(): MacrosResource;
|
|
240
|
+
get plugins(): PluginsResource;
|
|
241
|
+
get users(): UsersResource;
|
|
242
|
+
get groups(): GroupsResource;
|
|
178
243
|
constructor(config?: DataikuClientConfig);
|
|
179
244
|
/**
|
|
180
245
|
* Canonical request base URL: the exact prefix every HTTP verb concatenates
|
|
@@ -189,7 +254,7 @@ export declare class DataikuClient {
|
|
|
189
254
|
*/
|
|
190
255
|
getMaxResponseBodyBytes(): number;
|
|
191
256
|
resolveProjectKey(paramValue?: string): string;
|
|
192
|
-
get<T = unknown>(path: string): Promise<T>;
|
|
257
|
+
get<T = unknown>(path: string, options?: DataikuGetOptions): Promise<T>;
|
|
193
258
|
/**
|
|
194
259
|
* GET returning the parsed JSON body plus selected response metadata
|
|
195
260
|
* (documented DSS version headers, server date, request id). Authorization
|
|
@@ -239,10 +304,32 @@ export declare class DataikuClient {
|
|
|
239
304
|
* {@link getText} (byte cap `maxResponseBodyBytes`, request deadline).
|
|
240
305
|
*/
|
|
241
306
|
postText(path: string, body?: unknown): Promise<string>;
|
|
307
|
+
/** Send raw file contents through the shared authenticated transport. */
|
|
308
|
+
postRawBody(path: string, body: string | Uint8Array): Promise<void>;
|
|
309
|
+
/** Git endpoints require Basic API-key authentication, unlike the bearer API. */
|
|
310
|
+
requestGit(method: string, path: string, body?: unknown, deadlineAt?: number, noRetry?: boolean): Promise<Response>;
|
|
242
311
|
postStream(path: string, body?: unknown): Promise<Response>;
|
|
243
312
|
put<T = unknown>(path: string, body: unknown): Promise<T>;
|
|
244
313
|
del(path: string): Promise<void>;
|
|
245
314
|
putVoid(path: string, body: unknown): Promise<void>;
|
|
315
|
+
/**
|
|
316
|
+
* PUT without a request body. A few DSS endpoints (bundle export) reject
|
|
317
|
+
* an empty JSON object body with 400 validation, and the official Python
|
|
318
|
+
* client sends no body at all for them.
|
|
319
|
+
*/
|
|
320
|
+
putVoidNoBody(path: string): Promise<void>;
|
|
321
|
+
/**
|
|
322
|
+
* POST a multipart/form-data body built from explicit parts, with optional
|
|
323
|
+
* query parameters appended to the path, returning the parsed JSON body.
|
|
324
|
+
*
|
|
325
|
+
* A part with `value` becomes a form text field; a part with `blob`
|
|
326
|
+
* (or a `fileName` with no blob) becomes a file part — a `fileName` without
|
|
327
|
+
* blob yields a zero-byte file part, which some DSS import endpoints
|
|
328
|
+
* (e.g. saved-model MLflow version import) require for metadata-only parts.
|
|
329
|
+
* The `Content-Type` header is left to the runtime so the multipart
|
|
330
|
+
* boundary is set correctly.
|
|
331
|
+
*/
|
|
332
|
+
uploadForm<T = unknown>(path: string, parts: UploadFormPart[], query?: URLSearchParams): Promise<T>;
|
|
246
333
|
private uploadResponse;
|
|
247
334
|
upload(path: string, filePath: string, fileName?: string): Promise<void>;
|
|
248
335
|
uploadJson<T>(path: string, filePath: string, fileName?: string): Promise<T>;
|
|
@@ -271,7 +358,7 @@ export declare class DataikuClient {
|
|
|
271
358
|
private requestIdFromHeaders;
|
|
272
359
|
private parseJsonResponse;
|
|
273
360
|
/**
|
|
274
|
-
* Read at most `maxBytes` of a
|
|
361
|
+
* Read at most `maxBytes` of a response body as UTF-8 text.
|
|
275
362
|
*
|
|
276
363
|
* The body is consumed with the request deadline still active (measured
|
|
277
364
|
* from the first read): a stalled body throws a DataikuError instead of
|
package/dist/src/client.js
CHANGED
|
@@ -13,19 +13,26 @@ import { CodeEnvsResource, } from "./resources/code-envs.js";
|
|
|
13
13
|
import { ConnectionsResource, } from "./resources/connections.js";
|
|
14
14
|
import { ContinuousActivitiesResource, } from "./resources/continuous-activities.js";
|
|
15
15
|
import { DashboardsResource, } from "./resources/dashboards.js";
|
|
16
|
+
import { DataCollectionsResource, } from "./resources/data-collections.js";
|
|
16
17
|
import { DataQualityResource, } from "./resources/data-quality.js";
|
|
17
18
|
import { DatasetsResource, } from "./resources/datasets.js";
|
|
18
19
|
import { DiscussionsResource, } from "./resources/discussions.js";
|
|
19
20
|
import { FlowZonesResource, } from "./resources/flow-zones.js";
|
|
20
21
|
import { FoldersResource, } from "./resources/folders.js";
|
|
21
22
|
import { FuturesResource, } from "./resources/futures.js";
|
|
23
|
+
import { GroupsResource, } from "./resources/groups.js";
|
|
22
24
|
import { InsightsResource, } from "./resources/insights.js";
|
|
23
25
|
import { JobsResource, } from "./resources/jobs.js";
|
|
26
|
+
import { KnowledgeBanksResource, } from "./resources/knowledge-banks.js";
|
|
27
|
+
import { LlmsResource, } from "./resources/llms.js";
|
|
28
|
+
import { MacrosResource, } from "./resources/macros.js";
|
|
24
29
|
import { MeaningsResource, } from "./resources/meanings.js";
|
|
25
30
|
import { MetricsResource, } from "./resources/metrics.js";
|
|
26
31
|
import { MlTasksResource, } from "./resources/ml-tasks.js";
|
|
27
32
|
import { ModelEvaluationStoresResource, } from "./resources/model-evaluation-stores.js";
|
|
28
33
|
import { NotebooksResource, } from "./resources/notebooks.js";
|
|
34
|
+
import { PluginsResource, } from "./resources/plugins.js";
|
|
35
|
+
import { ProjectFoldersResource, } from "./resources/project-folders.js";
|
|
29
36
|
import { ProjectGitResource, } from "./resources/project-git.js";
|
|
30
37
|
import { ProjectLibraryResource, } from "./resources/project-library.js";
|
|
31
38
|
import { ProjectsResource, } from "./resources/projects.js";
|
|
@@ -35,6 +42,7 @@ import { ScenariosResource, } from "./resources/scenarios.js";
|
|
|
35
42
|
import { SqlResource, } from "./resources/sql.js";
|
|
36
43
|
import { StatisticsResource, } from "./resources/statistics.js";
|
|
37
44
|
import { StreamingEndpointsResource, } from "./resources/streaming-endpoints.js";
|
|
45
|
+
import { UsersResource, } from "./resources/users.js";
|
|
38
46
|
import { VariablesResource, } from "./resources/variables.js";
|
|
39
47
|
import { WebappsResource, } from "./resources/webapps.js";
|
|
40
48
|
import { WikiResource, } from "./resources/wiki.js";
|
|
@@ -70,6 +78,19 @@ function computeBackoffDelayMs(retryNumber) {
|
|
|
70
78
|
const cap = Math.min(MAX_BACKOFF_DELAY_MS, BASE_DELAY_MS * 2 ** Math.max(0, retryNumber - 1));
|
|
71
79
|
return Math.floor(Math.random() * (cap + 1));
|
|
72
80
|
}
|
|
81
|
+
/** Server-directed waits never bypass the existing automatic backoff cap. */
|
|
82
|
+
function retryAfterDelayMs(value) {
|
|
83
|
+
if (value === null)
|
|
84
|
+
return 0;
|
|
85
|
+
const text = value.trim();
|
|
86
|
+
if (/^\d+$/.test(text))
|
|
87
|
+
return Number(text) * 1_000;
|
|
88
|
+
// Accept HTTP dates, not Date.parse's permissive numeric date shorthand.
|
|
89
|
+
if (!/^[A-Za-z]{3,9},? /.test(text))
|
|
90
|
+
return 0;
|
|
91
|
+
const date = Date.parse(text);
|
|
92
|
+
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : 0;
|
|
93
|
+
}
|
|
73
94
|
function isTransientError(status, body) {
|
|
74
95
|
return classifyDataikuError(status, body).category === "transient";
|
|
75
96
|
}
|
|
@@ -173,6 +194,20 @@ function buildFetchTlsOptions(config) {
|
|
|
173
194
|
}
|
|
174
195
|
return tls;
|
|
175
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Resolve the total-budget request deadline timestamp for a per-call GET
|
|
199
|
+
* budget: `Date.now() + timeoutMs`. Undefined when no budget is supplied, in
|
|
200
|
+
* which case the client-level per-attempt timeout applies unchanged.
|
|
201
|
+
*/
|
|
202
|
+
function resolveGetDeadlineAt(options) {
|
|
203
|
+
const timeoutMs = options?.timeoutMs;
|
|
204
|
+
if (timeoutMs === undefined)
|
|
205
|
+
return undefined;
|
|
206
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1) {
|
|
207
|
+
throw new ClientValidationError("timeoutMs must be a positive integer.");
|
|
208
|
+
}
|
|
209
|
+
return Date.now() + timeoutMs;
|
|
210
|
+
}
|
|
176
211
|
/**
|
|
177
212
|
* True when the URL embeds userinfo (`https://user:password@host`). Embedded
|
|
178
213
|
* credentials are rejected up front so they can never reach the canonical base
|
|
@@ -240,6 +275,14 @@ export class DataikuClient {
|
|
|
240
275
|
mlTasksResource;
|
|
241
276
|
savedModelsResource;
|
|
242
277
|
modelEvaluationStoresResource;
|
|
278
|
+
projectFoldersResource;
|
|
279
|
+
dataCollectionsResource;
|
|
280
|
+
llmsResource;
|
|
281
|
+
knowledgeBanksResource;
|
|
282
|
+
macrosResource;
|
|
283
|
+
pluginsResource;
|
|
284
|
+
usersResource;
|
|
285
|
+
groupsResource;
|
|
243
286
|
get projects() {
|
|
244
287
|
return (this.projectsResource ??= new ProjectsResource(this));
|
|
245
288
|
}
|
|
@@ -348,6 +391,30 @@ export class DataikuClient {
|
|
|
348
391
|
get modelEvaluationStores() {
|
|
349
392
|
return (this.modelEvaluationStoresResource ??= new ModelEvaluationStoresResource(this));
|
|
350
393
|
}
|
|
394
|
+
get projectFolders() {
|
|
395
|
+
return (this.projectFoldersResource ??= new ProjectFoldersResource(this));
|
|
396
|
+
}
|
|
397
|
+
get dataCollections() {
|
|
398
|
+
return (this.dataCollectionsResource ??= new DataCollectionsResource(this));
|
|
399
|
+
}
|
|
400
|
+
get llms() {
|
|
401
|
+
return (this.llmsResource ??= new LlmsResource(this));
|
|
402
|
+
}
|
|
403
|
+
get knowledgeBanks() {
|
|
404
|
+
return (this.knowledgeBanksResource ??= new KnowledgeBanksResource(this));
|
|
405
|
+
}
|
|
406
|
+
get macros() {
|
|
407
|
+
return (this.macrosResource ??= new MacrosResource(this));
|
|
408
|
+
}
|
|
409
|
+
get plugins() {
|
|
410
|
+
return (this.pluginsResource ??= new PluginsResource(this));
|
|
411
|
+
}
|
|
412
|
+
get users() {
|
|
413
|
+
return (this.usersResource ??= new UsersResource(this));
|
|
414
|
+
}
|
|
415
|
+
get groups() {
|
|
416
|
+
return (this.groupsResource ??= new GroupsResource(this));
|
|
417
|
+
}
|
|
351
418
|
constructor(config) {
|
|
352
419
|
const envUrl = process.env["DATAIKU_URL"]?.trim();
|
|
353
420
|
const envApiKey = process.env["DATAIKU_API_KEY"]?.trim();
|
|
@@ -399,12 +466,10 @@ export class DataikuClient {
|
|
|
399
466
|
throw new Error("projectKey is required — pass it as a parameter or set projectKey in DataikuClientConfig");
|
|
400
467
|
}
|
|
401
468
|
/* ---- public: HTTP verbs ---- */
|
|
402
|
-
async get(path) {
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
});
|
|
407
|
-
return this.parseJsonResponse(res);
|
|
469
|
+
async get(path, options) {
|
|
470
|
+
const deadlineAt = resolveGetDeadlineAt(options);
|
|
471
|
+
const res = await this.fetchWithRetry(`${this.baseUrl}${path}`, { method: "GET", headers: this.getHeaders(), }, options?.noRetry === true ? 1 : undefined, deadlineAt);
|
|
472
|
+
return this.parseJsonResponse(res, deadlineAt);
|
|
408
473
|
}
|
|
409
474
|
/**
|
|
410
475
|
* GET returning the parsed JSON body plus selected response metadata
|
|
@@ -496,6 +561,32 @@ export class DataikuClient {
|
|
|
496
561
|
throw this.buildResponseTooLargeError(res, this.maxResponseBodyBytes);
|
|
497
562
|
return text;
|
|
498
563
|
}
|
|
564
|
+
/** Send raw file contents through the shared authenticated transport. */
|
|
565
|
+
async postRawBody(path, body) {
|
|
566
|
+
const payload = typeof body === "string"
|
|
567
|
+
? body
|
|
568
|
+
: body.buffer instanceof ArrayBuffer
|
|
569
|
+
? new Uint8Array(body.buffer, body.byteOffset, body.byteLength)
|
|
570
|
+
: new Uint8Array(body);
|
|
571
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
572
|
+
method: "POST",
|
|
573
|
+
headers: this.getAnyHeaders(),
|
|
574
|
+
body: payload,
|
|
575
|
+
});
|
|
576
|
+
await response.text();
|
|
577
|
+
}
|
|
578
|
+
/** Git endpoints require Basic API-key authentication, unlike the bearer API. */
|
|
579
|
+
async requestGit(method, path, body, deadlineAt, noRetry = false) {
|
|
580
|
+
return this.fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
581
|
+
method,
|
|
582
|
+
headers: {
|
|
583
|
+
Authorization: `Basic ${Buffer.from(`${this.apiKey}:`, "utf8").toString("base64")}`,
|
|
584
|
+
Accept: "application/json",
|
|
585
|
+
"Content-Type": "application/json",
|
|
586
|
+
},
|
|
587
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
588
|
+
}, noRetry ? 1 : undefined, deadlineAt);
|
|
589
|
+
}
|
|
499
590
|
async postStream(path, body) {
|
|
500
591
|
const res = await this.fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
501
592
|
method: "POST",
|
|
@@ -525,6 +616,54 @@ export class DataikuClient {
|
|
|
525
616
|
body: JSON.stringify(body),
|
|
526
617
|
});
|
|
527
618
|
}
|
|
619
|
+
/**
|
|
620
|
+
* PUT without a request body. A few DSS endpoints (bundle export) reject
|
|
621
|
+
* an empty JSON object body with 400 validation, and the official Python
|
|
622
|
+
* client sends no body at all for them.
|
|
623
|
+
*/
|
|
624
|
+
async putVoidNoBody(path) {
|
|
625
|
+
await this.fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
626
|
+
method: "PUT",
|
|
627
|
+
headers: this.getHeaders(),
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* POST a multipart/form-data body built from explicit parts, with optional
|
|
632
|
+
* query parameters appended to the path, returning the parsed JSON body.
|
|
633
|
+
*
|
|
634
|
+
* A part with `value` becomes a form text field; a part with `blob`
|
|
635
|
+
* (or a `fileName` with no blob) becomes a file part — a `fileName` without
|
|
636
|
+
* blob yields a zero-byte file part, which some DSS import endpoints
|
|
637
|
+
* (e.g. saved-model MLflow version import) require for metadata-only parts.
|
|
638
|
+
* The `Content-Type` header is left to the runtime so the multipart
|
|
639
|
+
* boundary is set correctly.
|
|
640
|
+
*/
|
|
641
|
+
async uploadForm(path, parts, query) {
|
|
642
|
+
const formData = new FormData();
|
|
643
|
+
for (const part of parts) {
|
|
644
|
+
if (part.value !== undefined) {
|
|
645
|
+
formData.append(part.name, part.value);
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if ((part.blob ?? null) === null && (part.fileName ?? null) === null) {
|
|
649
|
+
// Filename-less, zero-length field part — the shape the official
|
|
650
|
+
// Python client produces with files={"file": (None, None)} and
|
|
651
|
+
// the one DSS's managed-folder MLflow import requires (a Blob
|
|
652
|
+
// would always add a `filename=` parameter, even when empty).
|
|
653
|
+
formData.append(part.name, "");
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
const blob = part.blob ?? new Blob([]);
|
|
657
|
+
formData.append(part.name, blob, part.fileName);
|
|
658
|
+
}
|
|
659
|
+
const suffix = query && [...query.keys(),].length > 0 ? `?${query.toString()}` : "";
|
|
660
|
+
const res = await this.fetchWithRetry(`${this.baseUrl}${path}${suffix}`, {
|
|
661
|
+
method: "POST",
|
|
662
|
+
headers: { Authorization: `Bearer ${this.apiKey}`, },
|
|
663
|
+
body: formData,
|
|
664
|
+
});
|
|
665
|
+
return this.parseJsonResponse(res);
|
|
666
|
+
}
|
|
528
667
|
async uploadResponse(path, filePath, fileName) {
|
|
529
668
|
const fileBlob = Bun.file(filePath);
|
|
530
669
|
const formData = new FormData();
|
|
@@ -658,8 +797,8 @@ export class DataikuClient {
|
|
|
658
797
|
}
|
|
659
798
|
return undefined;
|
|
660
799
|
}
|
|
661
|
-
async parseJsonResponse(res) {
|
|
662
|
-
const { text, truncated, } = await this.readBoundedBodyText(res, this.maxResponseBodyBytes);
|
|
800
|
+
async parseJsonResponse(res, deadlineAt) {
|
|
801
|
+
const { text, truncated, } = await this.readBoundedBodyText(res, this.maxResponseBodyBytes, deadlineAt);
|
|
663
802
|
if (truncated)
|
|
664
803
|
throw this.buildResponseTooLargeError(res, this.maxResponseBodyBytes);
|
|
665
804
|
// SAFETY: Empty 2xx responses from DSS are surfaced to callers as undefined
|
|
@@ -677,7 +816,7 @@ export class DataikuClient {
|
|
|
677
816
|
}
|
|
678
817
|
/* ---- private: bounded response body reading ---- */
|
|
679
818
|
/**
|
|
680
|
-
* Read at most `maxBytes` of a
|
|
819
|
+
* Read at most `maxBytes` of a response body as UTF-8 text.
|
|
681
820
|
*
|
|
682
821
|
* The body is consumed with the request deadline still active (measured
|
|
683
822
|
* from the first read): a stalled body throws a DataikuError instead of
|
|
@@ -686,10 +825,15 @@ export class DataikuClient {
|
|
|
686
825
|
* text is trimmed to a UTF-8 character boundary so it never decodes beyond
|
|
687
826
|
* the byte cap.
|
|
688
827
|
*/
|
|
689
|
-
async readBoundedBodyText(res, maxBytes) {
|
|
828
|
+
async readBoundedBodyText(res, maxBytes, deadlineAt) {
|
|
690
829
|
const limit = Math.max(0, Math.floor(maxBytes));
|
|
691
830
|
if (!res.body)
|
|
692
831
|
return { text: "", truncated: false, };
|
|
832
|
+
// Total-budget mode: the read shares the call's absolute deadline, so
|
|
833
|
+
// the budget already spent on attempts/backoff is not re-credited here.
|
|
834
|
+
const bodyTimeoutMs = deadlineAt === undefined
|
|
835
|
+
? this.requestTimeoutMs
|
|
836
|
+
: Math.min(this.requestTimeoutMs, Math.max(1, deadlineAt - Date.now()));
|
|
693
837
|
const reader = res.body.getReader();
|
|
694
838
|
const decoder = new TextDecoder();
|
|
695
839
|
const parts = [];
|
|
@@ -724,10 +868,10 @@ export class DataikuClient {
|
|
|
724
868
|
const startedAt = Date.now();
|
|
725
869
|
try {
|
|
726
870
|
while (bytesRead < limit) {
|
|
727
|
-
const remainingMs =
|
|
871
|
+
const remainingMs = bodyTimeoutMs - (Date.now() - startedAt);
|
|
728
872
|
if (remainingMs <= 0)
|
|
729
|
-
throw buildBodyReadTimeoutError(
|
|
730
|
-
const { done, value, } = await readChunkWithDeadline(reader, remainingMs,
|
|
873
|
+
throw buildBodyReadTimeoutError(bodyTimeoutMs);
|
|
874
|
+
const { done, value, } = await readChunkWithDeadline(reader, remainingMs, bodyTimeoutMs);
|
|
731
875
|
if (done)
|
|
732
876
|
break;
|
|
733
877
|
const room = limit - bytesRead;
|
|
@@ -742,10 +886,10 @@ export class DataikuClient {
|
|
|
742
886
|
}
|
|
743
887
|
if (!truncated && bytesRead >= limit) {
|
|
744
888
|
// Buffer filled exactly on a chunk boundary; peek whether more data remains.
|
|
745
|
-
const remainingMs =
|
|
889
|
+
const remainingMs = bodyTimeoutMs - (Date.now() - startedAt);
|
|
746
890
|
if (remainingMs <= 0)
|
|
747
|
-
throw buildBodyReadTimeoutError(
|
|
748
|
-
const { done, } = await readChunkWithDeadline(reader, remainingMs,
|
|
891
|
+
throw buildBodyReadTimeoutError(bodyTimeoutMs);
|
|
892
|
+
const { done, } = await readChunkWithDeadline(reader, remainingMs, bodyTimeoutMs);
|
|
749
893
|
if (!done)
|
|
750
894
|
truncated = true;
|
|
751
895
|
}
|
|
@@ -838,7 +982,7 @@ export class DataikuClient {
|
|
|
838
982
|
return new DataikuError(res.status, RESPONSE_TOO_LARGE_STATUS_TEXT, `Response body exceeded the ${limit}-byte limit; the body was cancelled after ${limit} bytes.`, undefined, this.requestIdFromHeaders(res.headers));
|
|
839
983
|
}
|
|
840
984
|
/* ---- private: retry loop ---- */
|
|
841
|
-
async fetchWithRetry(url, init, retryMaxAttempts) {
|
|
985
|
+
async fetchWithRetry(url, init, retryMaxAttempts, deadlineAt) {
|
|
842
986
|
const method = (init.method ?? "GET").toUpperCase();
|
|
843
987
|
const retryEnabled = shouldRetryMethod(method) || retryMaxAttempts !== undefined;
|
|
844
988
|
const maxAttempts = retryMaxAttempts === undefined
|
|
@@ -850,11 +994,21 @@ export class DataikuClient {
|
|
|
850
994
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
851
995
|
let timedOut = false;
|
|
852
996
|
const startedAt = Date.now();
|
|
997
|
+
// An overall deadline may shorten, but never extend, the attempt cap.
|
|
998
|
+
const remainingMs = deadlineAt === undefined
|
|
999
|
+
? undefined
|
|
1000
|
+
: deadlineAt - startedAt;
|
|
1001
|
+
if (remainingMs !== undefined && remainingMs <= 0) {
|
|
1002
|
+
throw new DataikuError(0, "Request Timeout", `Request deadline exceeded after ${attempt - 1} attempt(s) and ${delaysMs.length} backoff delay(s).`, buildRetryMetadata(method, retryEnabled, maxAttempts, attempt, delaysMs, true));
|
|
1003
|
+
}
|
|
1004
|
+
const attemptTimeoutMs = remainingMs === undefined
|
|
1005
|
+
? this.requestTimeoutMs
|
|
1006
|
+
: Math.min(remainingMs, this.requestTimeoutMs);
|
|
853
1007
|
const controller = new AbortController();
|
|
854
1008
|
const timeout = setTimeout(() => {
|
|
855
1009
|
timedOut = true;
|
|
856
1010
|
controller.abort();
|
|
857
|
-
},
|
|
1011
|
+
}, attemptTimeoutMs);
|
|
858
1012
|
this.logTrace({ phase: "request", method, url, attempt, maxAttempts, });
|
|
859
1013
|
try {
|
|
860
1014
|
const requestInit = {
|
|
@@ -875,30 +1029,47 @@ export class DataikuClient {
|
|
|
875
1029
|
elapsedMs: Date.now() - startedAt,
|
|
876
1030
|
});
|
|
877
1031
|
if (!res.ok) {
|
|
878
|
-
const text = await res.
|
|
1032
|
+
const { text, truncated, } = await this.readBoundedBodyText(res, this.maxResponseBodyBytes, startedAt + attemptTimeoutMs);
|
|
879
1033
|
const canRetry = retryEnabled && attempt < maxAttempts && isTransientError(res.status, text);
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1034
|
+
const serverDelayMs = canRetry ? retryAfterDelayMs(res.headers.get("retry-after")) : 0;
|
|
1035
|
+
// A longer server wait declines automatic retry; never clamp it into an early request.
|
|
1036
|
+
if (canRetry && serverDelayMs <= MAX_BACKOFF_DELAY_MS) {
|
|
1037
|
+
const delayMs = Math.max(computeBackoffDelayMs(attempt), serverDelayMs);
|
|
1038
|
+
// Do not start a retry whose backoff cannot fit in the budget.
|
|
1039
|
+
if (deadlineAt === undefined || Date.now() + delayMs <= deadlineAt) {
|
|
1040
|
+
delaysMs.push(delayMs);
|
|
1041
|
+
await sleep(delayMs);
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
await sleep(Math.max(0, deadlineAt - Date.now()));
|
|
1045
|
+
throw new DataikuError(0, "Request Timeout", `Request deadline exceeded before backoff retry ${attempt + 1}.`, buildRetryMetadata(method, retryEnabled, maxAttempts, attempt, delaysMs, true));
|
|
885
1046
|
}
|
|
886
|
-
throw new DataikuError(res.status, res.statusText, text, buildRetryMetadata(method, retryEnabled, maxAttempts, attempt, delaysMs, false), this.requestIdFromHeaders(res.headers));
|
|
1047
|
+
throw new DataikuError(res.status, res.statusText, text, buildRetryMetadata(method, retryEnabled, maxAttempts, attempt, delaysMs, false), this.requestIdFromHeaders(res.headers), truncated ? { bodyTruncated: true, } : undefined);
|
|
887
1048
|
}
|
|
888
1049
|
return res;
|
|
889
1050
|
}
|
|
890
1051
|
catch (error) {
|
|
891
|
-
if (error instanceof DataikuError)
|
|
892
|
-
|
|
1052
|
+
if (error instanceof DataikuError) {
|
|
1053
|
+
// A bounded error-body read is still part of this transport attempt.
|
|
1054
|
+
// Preserve the same retry/deadline policy as a fetch body abort.
|
|
1055
|
+
if (error.status !== 0 || error.statusText !== "Request Timeout" || error.retry !== undefined) {
|
|
1056
|
+
throw error;
|
|
1057
|
+
}
|
|
1058
|
+
timedOut = true;
|
|
1059
|
+
}
|
|
893
1060
|
const canRetry = retryEnabled && attempt < maxAttempts;
|
|
894
1061
|
if (canRetry) {
|
|
895
1062
|
const delayMs = computeBackoffDelayMs(attempt);
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1063
|
+
if (deadlineAt === undefined || Date.now() + delayMs <= deadlineAt) {
|
|
1064
|
+
delaysMs.push(delayMs);
|
|
1065
|
+
await sleep(delayMs);
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
await sleep(Math.max(0, deadlineAt - Date.now()));
|
|
1069
|
+
throw new DataikuError(0, "Request Timeout", `Request deadline exceeded before backoff retry ${attempt + 1}.`, buildRetryMetadata(method, retryEnabled, maxAttempts, attempt, delaysMs, true));
|
|
899
1070
|
}
|
|
900
1071
|
const detail = timedOut
|
|
901
|
-
? `Request timed out after ${
|
|
1072
|
+
? `Request timed out after ${attemptTimeoutMs}ms`
|
|
902
1073
|
: error instanceof Error
|
|
903
1074
|
? error.message
|
|
904
1075
|
: "Unknown transport error";
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface DataikuRetryMetadata {
|
|
|
18
18
|
export interface DataikuErrorTrustedMetadata {
|
|
19
19
|
target?: string;
|
|
20
20
|
elapsedMs?: number;
|
|
21
|
+
/** Set by the transport when the retained response body is only a bounded prefix. */
|
|
22
|
+
bodyTruncated?: boolean;
|
|
21
23
|
}
|
|
22
24
|
export declare function canonicalStatusText(status: number): string;
|
|
23
25
|
export declare class ClientValidationError extends Error {
|
|
@@ -44,6 +46,7 @@ export declare class DataikuError extends Error {
|
|
|
44
46
|
requestId?: string;
|
|
45
47
|
readonly trustedTarget?: string;
|
|
46
48
|
readonly trustedElapsedMs?: number;
|
|
49
|
+
readonly bodyTruncated?: boolean;
|
|
47
50
|
constructor(status: number, statusText: string, body: string, retry?: DataikuRetryMetadata, requestId?: string, trustedMetadata?: DataikuErrorTrustedMetadata);
|
|
48
51
|
get safeMessage(): string;
|
|
49
52
|
private static extractSummary;
|
package/dist/src/errors.js
CHANGED
|
@@ -92,10 +92,12 @@ function firstHtmlTextLine(body) {
|
|
|
92
92
|
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ")
|
|
93
93
|
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ")
|
|
94
94
|
.replace(/<[^>]+>/g, "\n"));
|
|
95
|
-
|
|
96
|
-
.
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
for (const match of text.matchAll(/[^\n]+/g)) {
|
|
96
|
+
const line = match[0].trim();
|
|
97
|
+
if (line.length > 0)
|
|
98
|
+
return line;
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
99
101
|
}
|
|
100
102
|
function conciseErrorSummary(text) {
|
|
101
103
|
const sanitized = stripServerFilesystemPaths(text);
|
|
@@ -339,6 +341,7 @@ export class DataikuError extends Error {
|
|
|
339
341
|
requestId;
|
|
340
342
|
trustedTarget;
|
|
341
343
|
trustedElapsedMs;
|
|
344
|
+
bodyTruncated;
|
|
342
345
|
constructor(status, statusText, body, retry, requestId, trustedMetadata) {
|
|
343
346
|
const details = DataikuError.buildDetails(status, body, retry);
|
|
344
347
|
super(details.message);
|
|
@@ -353,6 +356,7 @@ export class DataikuError extends Error {
|
|
|
353
356
|
this.requestId = requestId;
|
|
354
357
|
this.trustedTarget = trustedMetadata?.target;
|
|
355
358
|
this.trustedElapsedMs = trustedMetadata?.elapsedMs;
|
|
359
|
+
this.bodyTruncated = trustedMetadata?.bodyTruncated === true ? true : undefined;
|
|
356
360
|
}
|
|
357
361
|
get safeMessage() {
|
|
358
362
|
const retrySummary = DataikuError.formatRetryMetadata(this.retry);
|