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.
Files changed (132) hide show
  1. package/README.md +151 -46
  2. package/dist/build-metadata.json +1 -1
  3. package/dist/packages/types/src/index.d.ts +102 -0
  4. package/dist/packages/types/src/index.js +76 -0
  5. package/dist/src/cli/commands/app.js +57 -15
  6. package/dist/src/cli/commands/bundle.js +1 -1
  7. package/dist/src/cli/commands/code-env.js +13 -7
  8. package/dist/src/cli/commands/code.js +1 -1
  9. package/dist/src/cli/commands/connection.d.ts +9 -0
  10. package/dist/src/cli/commands/connection.js +267 -1
  11. package/dist/src/cli/commands/data-collection.d.ts +2 -0
  12. package/dist/src/cli/commands/data-collection.js +164 -0
  13. package/dist/src/cli/commands/dataset.js +162 -33
  14. package/dist/src/cli/commands/future.js +2 -2
  15. package/dist/src/cli/commands/group.d.ts +2 -0
  16. package/dist/src/cli/commands/group.js +109 -0
  17. package/dist/src/cli/commands/index.js +151 -78
  18. package/dist/src/cli/commands/job.js +4 -2
  19. package/dist/src/cli/commands/knowledge-bank.d.ts +2 -0
  20. package/dist/src/cli/commands/knowledge-bank.js +80 -0
  21. package/dist/src/cli/commands/llm.d.ts +2 -0
  22. package/dist/src/cli/commands/llm.js +93 -0
  23. package/dist/src/cli/commands/macro.d.ts +2 -0
  24. package/dist/src/cli/commands/macro.js +221 -0
  25. package/dist/src/cli/commands/ml-task.js +18 -6
  26. package/dist/src/cli/commands/notebook.js +6 -2
  27. package/dist/src/cli/commands/plugin.d.ts +2 -0
  28. package/dist/src/cli/commands/plugin.js +799 -0
  29. package/dist/src/cli/commands/project-folder.d.ts +2 -0
  30. package/dist/src/cli/commands/project-folder.js +197 -0
  31. package/dist/src/cli/commands/project-git.js +15 -55
  32. package/dist/src/cli/commands/project.js +52 -6
  33. package/dist/src/cli/commands/recipe.js +36 -1
  34. package/dist/src/cli/commands/saved-model.d.ts +5 -0
  35. package/dist/src/cli/commands/saved-model.js +388 -1
  36. package/dist/src/cli/commands/scenario.js +139 -2
  37. package/dist/src/cli/commands/user.d.ts +8 -0
  38. package/dist/src/cli/commands/user.js +296 -0
  39. package/dist/src/cli/contract.d.ts +1 -2
  40. package/dist/src/cli/contract.js +1098 -297
  41. package/dist/src/cli/flags.js +30 -1
  42. package/dist/src/cli/helpers/app-successor.d.ts +19 -4
  43. package/dist/src/cli/helpers/app-successor.js +90 -30
  44. package/dist/src/cli/helpers/cleanup.js +3 -1
  45. package/dist/src/cli/helpers/flow-zone.js +1 -2
  46. package/dist/src/cli/output-schemas.d.ts +24 -0
  47. package/dist/src/cli/output-schemas.js +233 -0
  48. package/dist/src/cli/output.d.ts +1 -1
  49. package/dist/src/cli/output.js +23 -10
  50. package/dist/src/cli/usage.d.ts +1 -0
  51. package/dist/src/cli/usage.js +47 -1
  52. package/dist/src/cli.js +147 -41
  53. package/dist/src/client.d.ts +92 -5
  54. package/dist/src/client.js +202 -31
  55. package/dist/src/errors.d.ts +3 -0
  56. package/dist/src/errors.js +8 -4
  57. package/dist/src/generated/action-output-schemas.json +1735 -0
  58. package/dist/src/index.d.ts +16 -7
  59. package/dist/src/index.js +11 -2
  60. package/dist/src/resources/analyses.js +1 -8
  61. package/dist/src/resources/applications.d.ts +8 -6
  62. package/dist/src/resources/applications.js +21 -21
  63. package/dist/src/resources/base.d.ts +4 -0
  64. package/dist/src/resources/base.js +15 -0
  65. package/dist/src/resources/bundles.d.ts +9 -4
  66. package/dist/src/resources/bundles.js +10 -5
  67. package/dist/src/resources/connections.d.ts +78 -1
  68. package/dist/src/resources/connections.js +132 -1
  69. package/dist/src/resources/data-collections.d.ts +118 -0
  70. package/dist/src/resources/data-collections.js +49 -0
  71. package/dist/src/resources/datasets.d.ts +69 -0
  72. package/dist/src/resources/datasets.js +136 -5
  73. package/dist/src/resources/flow-zones.d.ts +6 -2
  74. package/dist/src/resources/flow-zones.js +6 -3
  75. package/dist/src/resources/futures.d.ts +6 -1
  76. package/dist/src/resources/futures.js +68 -22
  77. package/dist/src/resources/groups.d.ts +47 -0
  78. package/dist/src/resources/groups.js +56 -0
  79. package/dist/src/resources/jobs.d.ts +8 -13
  80. package/dist/src/resources/jobs.js +71 -34
  81. package/dist/src/resources/knowledge-banks.d.ts +58 -0
  82. package/dist/src/resources/knowledge-banks.js +61 -0
  83. package/dist/src/resources/llms.d.ts +92 -0
  84. package/dist/src/resources/llms.js +62 -0
  85. package/dist/src/resources/macros.d.ts +121 -0
  86. package/dist/src/resources/macros.js +231 -0
  87. package/dist/src/resources/ml-tasks.d.ts +37 -2
  88. package/dist/src/resources/ml-tasks.js +109 -20
  89. package/dist/src/resources/model-evaluation-stores.js +1 -8
  90. package/dist/src/resources/notebooks.d.ts +43 -7
  91. package/dist/src/resources/notebooks.js +53 -13
  92. package/dist/src/resources/plugins.d.ts +263 -0
  93. package/dist/src/resources/plugins.js +455 -0
  94. package/dist/src/resources/project-folders.d.ts +71 -0
  95. package/dist/src/resources/project-folders.js +57 -0
  96. package/dist/src/resources/project-git.d.ts +12 -12
  97. package/dist/src/resources/project-git.js +84 -82
  98. package/dist/src/resources/project-library.d.ts +0 -1
  99. package/dist/src/resources/project-library.js +1 -17
  100. package/dist/src/resources/projects.d.ts +23 -3
  101. package/dist/src/resources/projects.js +60 -34
  102. package/dist/src/resources/recipes.d.ts +15 -1
  103. package/dist/src/resources/recipes.js +73 -6
  104. package/dist/src/resources/saved-models.d.ts +134 -1
  105. package/dist/src/resources/saved-models.js +214 -6
  106. package/dist/src/resources/scenarios.d.ts +105 -0
  107. package/dist/src/resources/scenarios.js +232 -106
  108. package/dist/src/resources/sql.js +5 -1
  109. package/dist/src/resources/users.d.ts +129 -0
  110. package/dist/src/resources/users.js +128 -0
  111. package/dist/src/resources/webapps.d.ts +15 -2
  112. package/dist/src/resources/webapps.js +18 -4
  113. package/dist/src/schemas.d.ts +2 -2
  114. package/dist/src/schemas.js +1 -1
  115. package/dist/src/utils/deep-merge.d.ts +1 -1
  116. package/dist/src/utils/git-reference.d.ts +12 -0
  117. package/dist/src/utils/git-reference.js +38 -0
  118. package/dist/src/utils/polling.d.ts +20 -0
  119. package/dist/src/utils/polling.js +27 -0
  120. package/dist/src/utils/response-file.js +1 -1
  121. package/dist/src/utils/secret-sanitize.d.ts +54 -0
  122. package/dist/src/utils/secret-sanitize.js +101 -0
  123. package/package.json +9 -8
  124. package/packages/types/dist/index.d.ts +102 -0
  125. package/packages/types/dist/index.js +76 -0
  126. package/plugin.json +1 -1
  127. package/skills/dataiku-dss/SKILL.md +11 -7
  128. package/skills/dataiku-dss/references/app-releases.md +8 -8
  129. package/skills/dataiku-dss/references/authentication.md +6 -6
  130. package/skills/dataiku-dss/references/discovery.md +4 -4
  131. package/skills/dataiku-dss/references/mutations.md +1 -1
  132. package/skills/dataiku-dss/references/troubleshooting.md +2 -2
@@ -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 buffered from a single response body by the text/JSON
76
- * consumer methods (default 50 MiB). Responses larger than this are
77
- * rejected with a DataikuError after the stream is cancelled.
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 2xx response body as UTF-8 text.
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
@@ -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 res = await this.fetchWithRetry(`${this.baseUrl}${path}`, {
404
- method: "GET",
405
- headers: this.getHeaders(),
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 2xx response body as UTF-8 text.
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 = this.requestTimeoutMs - (Date.now() - startedAt);
871
+ const remainingMs = bodyTimeoutMs - (Date.now() - startedAt);
728
872
  if (remainingMs <= 0)
729
- throw buildBodyReadTimeoutError(this.requestTimeoutMs);
730
- const { done, value, } = await readChunkWithDeadline(reader, remainingMs, this.requestTimeoutMs);
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 = this.requestTimeoutMs - (Date.now() - startedAt);
889
+ const remainingMs = bodyTimeoutMs - (Date.now() - startedAt);
746
890
  if (remainingMs <= 0)
747
- throw buildBodyReadTimeoutError(this.requestTimeoutMs);
748
- const { done, } = await readChunkWithDeadline(reader, remainingMs, this.requestTimeoutMs);
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
- }, this.requestTimeoutMs);
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.text();
1032
+ const { text, truncated, } = await this.readBoundedBodyText(res, this.maxResponseBodyBytes, startedAt + attemptTimeoutMs);
879
1033
  const canRetry = retryEnabled && attempt < maxAttempts && isTransientError(res.status, text);
880
- if (canRetry) {
881
- const delayMs = computeBackoffDelayMs(attempt);
882
- delaysMs.push(delayMs);
883
- await sleep(delayMs);
884
- continue;
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
- throw error;
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
- delaysMs.push(delayMs);
897
- await sleep(delayMs);
898
- continue;
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 ${this.requestTimeoutMs}ms`
1072
+ ? `Request timed out after ${attemptTimeoutMs}ms`
902
1073
  : error instanceof Error
903
1074
  ? error.message
904
1075
  : "Unknown transport error";
@@ -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;
@@ -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
- return text
96
- .split(/\r?\n/g)
97
- .map((line) => line.trim())
98
- .find((line) => line.length > 0);
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);