dataiku-sdk 3.1.0 → 3.3.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 (149) hide show
  1. package/README.md +253 -38
  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/auth.js +1 -1
  6. package/dist/src/cli/coerce.js +11 -2
  7. package/dist/src/cli/commands/analysis.js +4 -3
  8. package/dist/src/cli/commands/api-service.js +5 -3
  9. package/dist/src/cli/commands/app.js +61 -18
  10. package/dist/src/cli/commands/bundle.js +3 -2
  11. package/dist/src/cli/commands/code-env.js +23 -16
  12. package/dist/src/cli/commands/connection.d.ts +9 -0
  13. package/dist/src/cli/commands/connection.js +267 -1
  14. package/dist/src/cli/commands/dashboard.js +7 -6
  15. package/dist/src/cli/commands/data-collection.d.ts +2 -0
  16. package/dist/src/cli/commands/data-collection.js +164 -0
  17. package/dist/src/cli/commands/data-quality.js +9 -8
  18. package/dist/src/cli/commands/dataset.js +170 -40
  19. package/dist/src/cli/commands/doctor.js +4 -2
  20. package/dist/src/cli/commands/flow-zone.js +9 -8
  21. package/dist/src/cli/commands/folder.js +9 -8
  22. package/dist/src/cli/commands/future.js +4 -3
  23. package/dist/src/cli/commands/group.d.ts +2 -0
  24. package/dist/src/cli/commands/group.js +109 -0
  25. package/dist/src/cli/commands/index.js +151 -78
  26. package/dist/src/cli/commands/insight.js +8 -7
  27. package/dist/src/cli/commands/job.js +4 -3
  28. package/dist/src/cli/commands/knowledge-bank.d.ts +2 -0
  29. package/dist/src/cli/commands/knowledge-bank.js +80 -0
  30. package/dist/src/cli/commands/llm.d.ts +2 -0
  31. package/dist/src/cli/commands/llm.js +93 -0
  32. package/dist/src/cli/commands/macro.d.ts +2 -0
  33. package/dist/src/cli/commands/macro.js +221 -0
  34. package/dist/src/cli/commands/meaning.js +5 -4
  35. package/dist/src/cli/commands/ml-task.js +21 -8
  36. package/dist/src/cli/commands/model-evaluation-store.js +4 -3
  37. package/dist/src/cli/commands/notebook.js +17 -12
  38. package/dist/src/cli/commands/plugin.d.ts +2 -0
  39. package/dist/src/cli/commands/plugin.js +799 -0
  40. package/dist/src/cli/commands/project-folder.d.ts +2 -0
  41. package/dist/src/cli/commands/project-folder.js +197 -0
  42. package/dist/src/cli/commands/project-git.js +14 -54
  43. package/dist/src/cli/commands/project-library.js +7 -6
  44. package/dist/src/cli/commands/project.js +55 -8
  45. package/dist/src/cli/commands/recipe.js +50 -14
  46. package/dist/src/cli/commands/saved-model.d.ts +5 -0
  47. package/dist/src/cli/commands/saved-model.js +392 -4
  48. package/dist/src/cli/commands/scenario.js +147 -9
  49. package/dist/src/cli/commands/sql.js +3 -2
  50. package/dist/src/cli/commands/statistics.js +7 -6
  51. package/dist/src/cli/commands/user.d.ts +8 -0
  52. package/dist/src/cli/commands/user.js +296 -0
  53. package/dist/src/cli/commands/variable.js +2 -1
  54. package/dist/src/cli/commands/webapp.js +2 -1
  55. package/dist/src/cli/commands/wiki.js +7 -6
  56. package/dist/src/cli/contract.d.ts +2 -3
  57. package/dist/src/cli/contract.js +1167 -163
  58. package/dist/src/cli/flags.d.ts +4 -0
  59. package/dist/src/cli/flags.js +50 -2
  60. package/dist/src/cli/helpers/app-successor.d.ts +19 -4
  61. package/dist/src/cli/helpers/app-successor.js +90 -30
  62. package/dist/src/cli/helpers/cleanup.js +3 -1
  63. package/dist/src/cli/helpers/flow-zone.js +1 -2
  64. package/dist/src/cli/output.d.ts +1 -1
  65. package/dist/src/cli/output.js +23 -10
  66. package/dist/src/cli/usage.d.ts +1 -0
  67. package/dist/src/cli/usage.js +47 -1
  68. package/dist/src/cli.js +225 -95
  69. package/dist/src/client.d.ts +86 -6
  70. package/dist/src/client.js +263 -50
  71. package/dist/src/errors.d.ts +3 -0
  72. package/dist/src/errors.js +4 -0
  73. package/dist/src/index.d.ts +16 -7
  74. package/dist/src/index.js +11 -2
  75. package/dist/src/resources/analyses.js +1 -8
  76. package/dist/src/resources/applications.d.ts +8 -6
  77. package/dist/src/resources/applications.js +21 -21
  78. package/dist/src/resources/base.d.ts +4 -0
  79. package/dist/src/resources/base.js +15 -0
  80. package/dist/src/resources/bundles.d.ts +9 -4
  81. package/dist/src/resources/bundles.js +10 -5
  82. package/dist/src/resources/connections.d.ts +78 -1
  83. package/dist/src/resources/connections.js +132 -1
  84. package/dist/src/resources/data-collections.d.ts +118 -0
  85. package/dist/src/resources/data-collections.js +49 -0
  86. package/dist/src/resources/datasets.d.ts +69 -0
  87. package/dist/src/resources/datasets.js +99 -1
  88. package/dist/src/resources/folders.js +2 -1
  89. package/dist/src/resources/futures.d.ts +4 -1
  90. package/dist/src/resources/futures.js +38 -6
  91. package/dist/src/resources/groups.d.ts +47 -0
  92. package/dist/src/resources/groups.js +56 -0
  93. package/dist/src/resources/jobs.d.ts +8 -13
  94. package/dist/src/resources/jobs.js +41 -16
  95. package/dist/src/resources/knowledge-banks.d.ts +58 -0
  96. package/dist/src/resources/knowledge-banks.js +61 -0
  97. package/dist/src/resources/llms.d.ts +92 -0
  98. package/dist/src/resources/llms.js +62 -0
  99. package/dist/src/resources/macros.d.ts +121 -0
  100. package/dist/src/resources/macros.js +231 -0
  101. package/dist/src/resources/ml-tasks.d.ts +37 -2
  102. package/dist/src/resources/ml-tasks.js +100 -20
  103. package/dist/src/resources/model-evaluation-stores.js +1 -8
  104. package/dist/src/resources/notebooks.d.ts +43 -7
  105. package/dist/src/resources/notebooks.js +53 -13
  106. package/dist/src/resources/plugins.d.ts +265 -0
  107. package/dist/src/resources/plugins.js +458 -0
  108. package/dist/src/resources/project-folders.d.ts +71 -0
  109. package/dist/src/resources/project-folders.js +57 -0
  110. package/dist/src/resources/project-git.d.ts +6 -0
  111. package/dist/src/resources/project-git.js +69 -42
  112. package/dist/src/resources/project-library.d.ts +0 -1
  113. package/dist/src/resources/project-library.js +1 -17
  114. package/dist/src/resources/projects.d.ts +19 -3
  115. package/dist/src/resources/projects.js +30 -18
  116. package/dist/src/resources/recipes.d.ts +15 -1
  117. package/dist/src/resources/recipes.js +73 -6
  118. package/dist/src/resources/saved-models.d.ts +134 -1
  119. package/dist/src/resources/saved-models.js +214 -6
  120. package/dist/src/resources/scenarios.d.ts +105 -0
  121. package/dist/src/resources/scenarios.js +232 -106
  122. package/dist/src/resources/sql.js +5 -1
  123. package/dist/src/resources/users.d.ts +129 -0
  124. package/dist/src/resources/users.js +128 -0
  125. package/dist/src/resources/webapps.d.ts +15 -2
  126. package/dist/src/resources/webapps.js +18 -4
  127. package/dist/src/schemas.d.ts +2 -2
  128. package/dist/src/schemas.js +1 -1
  129. package/dist/src/skill.d.ts +13 -4
  130. package/dist/src/skill.js +75 -24
  131. package/dist/src/utils/deep-merge.d.ts +1 -1
  132. package/dist/src/utils/polling.d.ts +20 -0
  133. package/dist/src/utils/polling.js +27 -0
  134. package/dist/src/utils/response-file.d.ts +2 -0
  135. package/dist/src/utils/response-file.js +34 -0
  136. package/dist/src/utils/secret-sanitize.d.ts +54 -0
  137. package/dist/src/utils/secret-sanitize.js +101 -0
  138. package/package.json +8 -5
  139. package/packages/types/dist/index.d.ts +102 -0
  140. package/packages/types/dist/index.js +76 -0
  141. package/plugin.json +1 -1
  142. package/skills/dataiku-dss/SKILL.md +47 -225
  143. package/skills/dataiku-dss/references/app-releases.md +28 -0
  144. package/skills/dataiku-dss/references/authentication.md +39 -0
  145. package/skills/dataiku-dss/references/coding.md +8 -0
  146. package/skills/dataiku-dss/references/discovery.md +30 -0
  147. package/skills/dataiku-dss/references/flow-maps.md +12 -0
  148. package/skills/dataiku-dss/references/mutations.md +33 -0
  149. package/skills/dataiku-dss/references/troubleshooting.md +29 -0
@@ -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,43 @@ 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
+ }
37
77
  export interface DataikuClientTraceEvent {
38
78
  type: "trace";
39
79
  phase: "request" | "response" | "error";
@@ -67,14 +107,14 @@ export interface DataikuClientConfig {
67
107
  apiKey: string;
68
108
  /** Default project key — used when a resource method omits projectKey */
69
109
  projectKey?: string;
70
- /** Per-request timeout in milliseconds (default 30 000) */
110
+ /** Request/header timeout and buffered-body budget; idle timeout per raw stream read (default 30 000 ms). */
71
111
  requestTimeoutMs?: number;
72
112
  /** Max retry attempts for idempotent requests (default 4, capped at 10) */
73
113
  retryMaxAttempts?: number;
74
114
  /**
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.
115
+ * Maximum bytes retained from a single response body by text/JSON consumers
116
+ * (default 50 MiB). Oversized successful responses are rejected. Error
117
+ * responses retain a bounded prefix with DataikuError.bodyTruncated=true.
78
118
  */
79
119
  maxResponseBodyBytes?: number;
80
120
  /** Emit HTTP request/response trace events. Defaults to JSONL on stderr when verbose is true. */
@@ -139,6 +179,14 @@ export declare class DataikuClient {
139
179
  private mlTasksResource?;
140
180
  private savedModelsResource?;
141
181
  private modelEvaluationStoresResource?;
182
+ private projectFoldersResource?;
183
+ private dataCollectionsResource?;
184
+ private llmsResource?;
185
+ private knowledgeBanksResource?;
186
+ private macrosResource?;
187
+ private pluginsResource?;
188
+ private usersResource?;
189
+ private groupsResource?;
142
190
  get projects(): ProjectsResource;
143
191
  get datasets(): DatasetsResource;
144
192
  get dashboards(): DashboardsResource;
@@ -175,6 +223,14 @@ export declare class DataikuClient {
175
223
  get mlTasks(): MlTasksResource;
176
224
  get savedModels(): SavedModelsResource;
177
225
  get modelEvaluationStores(): ModelEvaluationStoresResource;
226
+ get projectFolders(): ProjectFoldersResource;
227
+ get dataCollections(): DataCollectionsResource;
228
+ get llms(): LlmsResource;
229
+ get knowledgeBanks(): KnowledgeBanksResource;
230
+ get macros(): MacrosResource;
231
+ get plugins(): PluginsResource;
232
+ get users(): UsersResource;
233
+ get groups(): GroupsResource;
178
234
  constructor(config?: DataikuClientConfig);
179
235
  /**
180
236
  * Canonical request base URL: the exact prefix every HTTP verb concatenates
@@ -189,7 +245,7 @@ export declare class DataikuClient {
189
245
  */
190
246
  getMaxResponseBodyBytes(): number;
191
247
  resolveProjectKey(paramValue?: string): string;
192
- get<T = unknown>(path: string): Promise<T>;
248
+ get<T = unknown>(path: string, options?: DataikuGetOptions): Promise<T>;
193
249
  /**
194
250
  * GET returning the parsed JSON body plus selected response metadata
195
251
  * (documented DSS version headers, server date, request id). Authorization
@@ -239,14 +295,38 @@ export declare class DataikuClient {
239
295
  * {@link getText} (byte cap `maxResponseBodyBytes`, request deadline).
240
296
  */
241
297
  postText(path: string, body?: unknown): Promise<string>;
298
+ /** Send raw file contents through the shared authenticated transport. */
299
+ postRawBody(path: string, body: string | Uint8Array): Promise<void>;
300
+ /** Git endpoints require Basic API-key authentication, unlike the bearer API. */
301
+ requestGit(method: string, path: string, body?: unknown, deadlineAt?: number): Promise<Response>;
242
302
  postStream(path: string, body?: unknown): Promise<Response>;
243
303
  put<T = unknown>(path: string, body: unknown): Promise<T>;
244
304
  del(path: string): Promise<void>;
245
305
  putVoid(path: string, body: unknown): Promise<void>;
306
+ /**
307
+ * PUT without a request body. A few DSS endpoints (bundle export) reject
308
+ * an empty JSON object body with 400 validation, and the official Python
309
+ * client sends no body at all for them.
310
+ */
311
+ putVoidNoBody(path: string): Promise<void>;
312
+ /**
313
+ * POST a multipart/form-data body built from explicit parts, with optional
314
+ * query parameters appended to the path, returning the parsed JSON body.
315
+ *
316
+ * A part with `value` becomes a form text field; a part with `blob`
317
+ * (or a `fileName` with no blob) becomes a file part — a `fileName` without
318
+ * blob yields a zero-byte file part, which some DSS import endpoints
319
+ * (e.g. saved-model MLflow version import) require for metadata-only parts.
320
+ * The `Content-Type` header is left to the runtime so the multipart
321
+ * boundary is set correctly.
322
+ */
323
+ uploadForm<T = unknown>(path: string, parts: UploadFormPart[], query?: URLSearchParams): Promise<T>;
246
324
  private uploadResponse;
247
325
  upload(path: string, filePath: string, fileName?: string): Promise<void>;
248
326
  uploadJson<T>(path: string, filePath: string, fileName?: string): Promise<T>;
249
327
  stream(path: string): Promise<Response>;
328
+ /** Bound each pending read, not total transfer time; preserve large downloads and backpressure. */
329
+ private withBodyDeadline;
250
330
  private getHeaders;
251
331
  private getAnyHeaders;
252
332
  private responseMeta;
@@ -269,7 +349,7 @@ export declare class DataikuClient {
269
349
  private requestIdFromHeaders;
270
350
  private parseJsonResponse;
271
351
  /**
272
- * Read at most `maxBytes` of a 2xx response body as UTF-8 text.
352
+ * Read at most `maxBytes` of a response body as UTF-8 text.
273
353
  *
274
354
  * The body is consumed with the request deadline still active (measured
275
355
  * 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";
@@ -76,15 +84,6 @@ function isTransientError(status, body) {
76
84
  function shouldRetryMethod(method) {
77
85
  return method.toUpperCase() === "GET";
78
86
  }
79
- function concatBytes(chunks, total) {
80
- const out = new Uint8Array(total);
81
- let offset = 0;
82
- for (const chunk of chunks) {
83
- out.set(chunk, offset);
84
- offset += chunk.byteLength;
85
- }
86
- return out;
87
- }
88
87
  /**
89
88
  * Drop a trailing incomplete UTF-8 multibyte sequence so the bytes end on a
90
89
  * character boundary. Guarantees the decoded string re-encodes to no more bytes
@@ -133,7 +132,7 @@ function buildBodyReadTimeoutError(timeoutMs) {
133
132
  * Read the next chunk of a response body under a hard deadline.
134
133
  * When `remainingMs` elapses, the stream is cancelled and the caller's
135
134
  * promise rejects with a DataikuError, so a stalled body cannot hang the
136
- * buffered consumers after headers already arrived.
135
+ * consumers after headers already arrived.
137
136
  */
138
137
  async function readChunkWithDeadline(reader, remainingMs, timeoutMs) {
139
138
  const { promise, resolve, reject, } = Promise.withResolvers();
@@ -182,6 +181,20 @@ function buildFetchTlsOptions(config) {
182
181
  }
183
182
  return tls;
184
183
  }
184
+ /**
185
+ * Resolve the total-budget request deadline timestamp for a per-call GET
186
+ * budget: `Date.now() + timeoutMs`. Undefined when no budget is supplied, in
187
+ * which case the client-level per-attempt timeout applies unchanged.
188
+ */
189
+ function resolveGetDeadlineAt(options) {
190
+ const timeoutMs = options?.timeoutMs;
191
+ if (timeoutMs === undefined)
192
+ return undefined;
193
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1) {
194
+ throw new ClientValidationError("timeoutMs must be a positive integer.");
195
+ }
196
+ return Date.now() + timeoutMs;
197
+ }
185
198
  /**
186
199
  * True when the URL embeds userinfo (`https://user:password@host`). Embedded
187
200
  * credentials are rejected up front so they can never reach the canonical base
@@ -249,6 +262,14 @@ export class DataikuClient {
249
262
  mlTasksResource;
250
263
  savedModelsResource;
251
264
  modelEvaluationStoresResource;
265
+ projectFoldersResource;
266
+ dataCollectionsResource;
267
+ llmsResource;
268
+ knowledgeBanksResource;
269
+ macrosResource;
270
+ pluginsResource;
271
+ usersResource;
272
+ groupsResource;
252
273
  get projects() {
253
274
  return (this.projectsResource ??= new ProjectsResource(this));
254
275
  }
@@ -357,6 +378,30 @@ export class DataikuClient {
357
378
  get modelEvaluationStores() {
358
379
  return (this.modelEvaluationStoresResource ??= new ModelEvaluationStoresResource(this));
359
380
  }
381
+ get projectFolders() {
382
+ return (this.projectFoldersResource ??= new ProjectFoldersResource(this));
383
+ }
384
+ get dataCollections() {
385
+ return (this.dataCollectionsResource ??= new DataCollectionsResource(this));
386
+ }
387
+ get llms() {
388
+ return (this.llmsResource ??= new LlmsResource(this));
389
+ }
390
+ get knowledgeBanks() {
391
+ return (this.knowledgeBanksResource ??= new KnowledgeBanksResource(this));
392
+ }
393
+ get macros() {
394
+ return (this.macrosResource ??= new MacrosResource(this));
395
+ }
396
+ get plugins() {
397
+ return (this.pluginsResource ??= new PluginsResource(this));
398
+ }
399
+ get users() {
400
+ return (this.usersResource ??= new UsersResource(this));
401
+ }
402
+ get groups() {
403
+ return (this.groupsResource ??= new GroupsResource(this));
404
+ }
360
405
  constructor(config) {
361
406
  const envUrl = process.env["DATAIKU_URL"]?.trim();
362
407
  const envApiKey = process.env["DATAIKU_API_KEY"]?.trim();
@@ -408,12 +453,10 @@ export class DataikuClient {
408
453
  throw new Error("projectKey is required — pass it as a parameter or set projectKey in DataikuClientConfig");
409
454
  }
410
455
  /* ---- public: HTTP verbs ---- */
411
- async get(path) {
412
- const res = await this.fetchWithRetry(`${this.baseUrl}${path}`, {
413
- method: "GET",
414
- headers: this.getHeaders(),
415
- });
416
- return this.parseJsonResponse(res);
456
+ async get(path, options) {
457
+ const deadlineAt = resolveGetDeadlineAt(options);
458
+ const res = await this.fetchWithRetry(`${this.baseUrl}${path}`, { method: "GET", headers: this.getHeaders(), }, undefined, deadlineAt);
459
+ return this.parseJsonResponse(res, deadlineAt);
417
460
  }
418
461
  /**
419
462
  * GET returning the parsed JSON body plus selected response metadata
@@ -505,12 +548,39 @@ export class DataikuClient {
505
548
  throw this.buildResponseTooLargeError(res, this.maxResponseBodyBytes);
506
549
  return text;
507
550
  }
508
- async postStream(path, body) {
551
+ /** Send raw file contents through the shared authenticated transport. */
552
+ async postRawBody(path, body) {
553
+ const payload = typeof body === "string"
554
+ ? body
555
+ : body.buffer instanceof ArrayBuffer
556
+ ? new Uint8Array(body.buffer, body.byteOffset, body.byteLength)
557
+ : new Uint8Array(body);
558
+ const response = await this.fetchWithRetry(`${this.baseUrl}${path}`, {
559
+ method: "POST",
560
+ headers: this.getAnyHeaders(),
561
+ body: payload,
562
+ });
563
+ await response.text();
564
+ }
565
+ /** Git endpoints require Basic API-key authentication, unlike the bearer API. */
566
+ async requestGit(method, path, body, deadlineAt) {
509
567
  return this.fetchWithRetry(`${this.baseUrl}${path}`, {
568
+ method,
569
+ headers: {
570
+ Authorization: `Basic ${Buffer.from(`${this.apiKey}:`, "utf8").toString("base64")}`,
571
+ Accept: "application/json",
572
+ "Content-Type": "application/json",
573
+ },
574
+ body: body === undefined ? undefined : JSON.stringify(body),
575
+ }, undefined, deadlineAt);
576
+ }
577
+ async postStream(path, body) {
578
+ const res = await this.fetchWithRetry(`${this.baseUrl}${path}`, {
510
579
  method: "POST",
511
580
  headers: { ...this.getAnyHeaders(), "Content-Type": "application/json", },
512
581
  body: body !== undefined ? JSON.stringify(body) : undefined,
513
582
  });
583
+ return this.withBodyDeadline(res);
514
584
  }
515
585
  async put(path, body) {
516
586
  const res = await this.fetchWithRetry(`${this.baseUrl}${path}`, {
@@ -533,6 +603,54 @@ export class DataikuClient {
533
603
  body: JSON.stringify(body),
534
604
  });
535
605
  }
606
+ /**
607
+ * PUT without a request body. A few DSS endpoints (bundle export) reject
608
+ * an empty JSON object body with 400 validation, and the official Python
609
+ * client sends no body at all for them.
610
+ */
611
+ async putVoidNoBody(path) {
612
+ await this.fetchWithRetry(`${this.baseUrl}${path}`, {
613
+ method: "PUT",
614
+ headers: this.getHeaders(),
615
+ });
616
+ }
617
+ /**
618
+ * POST a multipart/form-data body built from explicit parts, with optional
619
+ * query parameters appended to the path, returning the parsed JSON body.
620
+ *
621
+ * A part with `value` becomes a form text field; a part with `blob`
622
+ * (or a `fileName` with no blob) becomes a file part — a `fileName` without
623
+ * blob yields a zero-byte file part, which some DSS import endpoints
624
+ * (e.g. saved-model MLflow version import) require for metadata-only parts.
625
+ * The `Content-Type` header is left to the runtime so the multipart
626
+ * boundary is set correctly.
627
+ */
628
+ async uploadForm(path, parts, query) {
629
+ const formData = new FormData();
630
+ for (const part of parts) {
631
+ if (part.value !== undefined) {
632
+ formData.append(part.name, part.value);
633
+ continue;
634
+ }
635
+ if ((part.blob ?? null) === null && (part.fileName ?? null) === null) {
636
+ // Filename-less, zero-length field part — the shape the official
637
+ // Python client produces with files={"file": (None, None)} and
638
+ // the one DSS's managed-folder MLflow import requires (a Blob
639
+ // would always add a `filename=` parameter, even when empty).
640
+ formData.append(part.name, "");
641
+ continue;
642
+ }
643
+ const blob = part.blob ?? new Blob([]);
644
+ formData.append(part.name, blob, part.fileName);
645
+ }
646
+ const suffix = query && [...query.keys(),].length > 0 ? `?${query.toString()}` : "";
647
+ const res = await this.fetchWithRetry(`${this.baseUrl}${path}${suffix}`, {
648
+ method: "POST",
649
+ headers: { Authorization: `Bearer ${this.apiKey}`, },
650
+ body: formData,
651
+ });
652
+ return this.parseJsonResponse(res);
653
+ }
536
654
  async uploadResponse(path, filePath, fileName) {
537
655
  const fileBlob = Bun.file(filePath);
538
656
  const formData = new FormData();
@@ -551,10 +669,51 @@ export class DataikuClient {
551
669
  return this.parseJsonResponse(response);
552
670
  }
553
671
  async stream(path) {
554
- return this.fetchWithRetry(`${this.baseUrl}${path}`, {
672
+ const res = await this.fetchWithRetry(`${this.baseUrl}${path}`, {
555
673
  method: "GET",
556
674
  headers: this.getAnyHeaders(),
557
675
  });
676
+ return this.withBodyDeadline(res);
677
+ }
678
+ /** Bound each pending read, not total transfer time; preserve large downloads and backpressure. */
679
+ withBodyDeadline(res) {
680
+ if (!res.body)
681
+ return res;
682
+ const reader = res.body.getReader();
683
+ const timeoutMs = this.requestTimeoutMs;
684
+ const body = new ReadableStream({
685
+ async pull(controller) {
686
+ try {
687
+ const { done, value, } = await readChunkWithDeadline(reader, timeoutMs, timeoutMs);
688
+ if (done) {
689
+ controller.close();
690
+ reader.releaseLock();
691
+ }
692
+ else {
693
+ controller.enqueue(value);
694
+ }
695
+ }
696
+ catch (error) {
697
+ controller.error(error);
698
+ void reader.cancel(error).catch(() => { });
699
+ }
700
+ },
701
+ cancel(reason) {
702
+ void reader.cancel(reason).catch(() => { });
703
+ },
704
+ });
705
+ const response = new Response(body, {
706
+ status: res.status,
707
+ statusText: res.statusText,
708
+ headers: res.headers,
709
+ });
710
+ // ResponseInit has no fields for these fetch response properties.
711
+ Object.defineProperties(response, {
712
+ url: { value: res.url, },
713
+ redirected: { value: res.redirected, },
714
+ type: { value: res.type, },
715
+ });
716
+ return response;
558
717
  }
559
718
  /* ---- private: headers ---- */
560
719
  getHeaders() {
@@ -625,8 +784,8 @@ export class DataikuClient {
625
784
  }
626
785
  return undefined;
627
786
  }
628
- async parseJsonResponse(res) {
629
- const { text, truncated, } = await this.readBoundedBodyText(res, this.maxResponseBodyBytes);
787
+ async parseJsonResponse(res, deadlineAt) {
788
+ const { text, truncated, } = await this.readBoundedBodyText(res, this.maxResponseBodyBytes, deadlineAt);
630
789
  if (truncated)
631
790
  throw this.buildResponseTooLargeError(res, this.maxResponseBodyBytes);
632
791
  // SAFETY: Empty 2xx responses from DSS are surfaced to callers as undefined
@@ -644,7 +803,7 @@ export class DataikuClient {
644
803
  }
645
804
  /* ---- private: bounded response body reading ---- */
646
805
  /**
647
- * Read at most `maxBytes` of a 2xx response body as UTF-8 text.
806
+ * Read at most `maxBytes` of a response body as UTF-8 text.
648
807
  *
649
808
  * The body is consumed with the request deadline still active (measured
650
809
  * from the first read): a stalled body throws a DataikuError instead of
@@ -653,39 +812,71 @@ export class DataikuClient {
653
812
  * text is trimmed to a UTF-8 character boundary so it never decodes beyond
654
813
  * the byte cap.
655
814
  */
656
- async readBoundedBodyText(res, maxBytes) {
815
+ async readBoundedBodyText(res, maxBytes, deadlineAt) {
657
816
  const limit = Math.max(0, Math.floor(maxBytes));
658
817
  if (!res.body)
659
818
  return { text: "", truncated: false, };
819
+ // Total-budget mode: the read shares the call's absolute deadline, so
820
+ // the budget already spent on attempts/backoff is not re-credited here.
821
+ const bodyTimeoutMs = deadlineAt === undefined
822
+ ? this.requestTimeoutMs
823
+ : Math.min(this.requestTimeoutMs, Math.max(1, deadlineAt - Date.now()));
660
824
  const reader = res.body.getReader();
661
- const chunks = [];
825
+ const decoder = new TextDecoder();
826
+ const parts = [];
827
+ // Keep the final UTF-8 sequence undecoded until truncation is known.
828
+ // Four bytes also cover malformed sequences handled by trimToUtf8Boundary.
829
+ const tail = new Uint8Array(4);
830
+ let tailLength = 0;
831
+ const append = (chunk) => {
832
+ if (chunk.byteLength >= tail.length) {
833
+ if (tailLength > 0) {
834
+ parts.push(decoder.decode(tail.subarray(0, tailLength), { stream: true, }));
835
+ }
836
+ const end = chunk.byteLength - tail.length;
837
+ if (end > 0)
838
+ parts.push(decoder.decode(chunk.subarray(0, end), { stream: true, }));
839
+ tail.set(chunk.subarray(end));
840
+ tailLength = tail.length;
841
+ }
842
+ else {
843
+ const excess = Math.max(0, tailLength + chunk.byteLength - tail.length);
844
+ if (excess > 0) {
845
+ parts.push(decoder.decode(tail.subarray(0, excess), { stream: true, }));
846
+ tail.copyWithin(0, excess, tailLength);
847
+ tailLength -= excess;
848
+ }
849
+ tail.set(chunk, tailLength);
850
+ tailLength += chunk.byteLength;
851
+ }
852
+ };
662
853
  let bytesRead = 0;
663
854
  let truncated = false;
664
855
  const startedAt = Date.now();
665
856
  try {
666
857
  while (bytesRead < limit) {
667
- const remainingMs = this.requestTimeoutMs - (Date.now() - startedAt);
858
+ const remainingMs = bodyTimeoutMs - (Date.now() - startedAt);
668
859
  if (remainingMs <= 0)
669
- throw buildBodyReadTimeoutError(this.requestTimeoutMs);
670
- const { done, value, } = await readChunkWithDeadline(reader, remainingMs, this.requestTimeoutMs);
860
+ throw buildBodyReadTimeoutError(bodyTimeoutMs);
861
+ const { done, value, } = await readChunkWithDeadline(reader, remainingMs, bodyTimeoutMs);
671
862
  if (done)
672
863
  break;
673
864
  const room = limit - bytesRead;
674
865
  if (value.byteLength > room) {
675
- chunks.push(value.slice(0, room));
866
+ append(value.subarray(0, room));
676
867
  bytesRead += room;
677
868
  truncated = true;
678
869
  break;
679
870
  }
680
- chunks.push(value);
871
+ append(value);
681
872
  bytesRead += value.byteLength;
682
873
  }
683
874
  if (!truncated && bytesRead >= limit) {
684
875
  // Buffer filled exactly on a chunk boundary; peek whether more data remains.
685
- const remainingMs = this.requestTimeoutMs - (Date.now() - startedAt);
876
+ const remainingMs = bodyTimeoutMs - (Date.now() - startedAt);
686
877
  if (remainingMs <= 0)
687
- throw buildBodyReadTimeoutError(this.requestTimeoutMs);
688
- const { done, } = await readChunkWithDeadline(reader, remainingMs, this.requestTimeoutMs);
878
+ throw buildBodyReadTimeoutError(bodyTimeoutMs);
879
+ const { done, } = await readChunkWithDeadline(reader, remainingMs, bodyTimeoutMs);
689
880
  if (!done)
690
881
  truncated = true;
691
882
  }
@@ -693,12 +884,9 @@ export class DataikuClient {
693
884
  finally {
694
885
  void reader.cancel().catch(() => { });
695
886
  }
696
- const collected = concatBytes(chunks, bytesRead);
697
- // On truncation, drop any trailing partial UTF-8 character so the decoded
698
- // text never re-encodes beyond the byte cap (a stream-flush would otherwise
699
- // emit a 3-byte replacement char for a split multibyte sequence).
700
- const usable = truncated ? trimToUtf8Boundary(collected) : collected;
701
- return { text: new TextDecoder().decode(usable), truncated, };
887
+ const remaining = tail.subarray(0, tailLength);
888
+ parts.push(decoder.decode(truncated ? trimToUtf8Boundary(remaining) : remaining));
889
+ return { text: parts.join(""), truncated, };
702
890
  }
703
891
  /**
704
892
  * Read a 2xx response body keeping only the last `maxBytes` bytes.
@@ -781,7 +969,7 @@ export class DataikuClient {
781
969
  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));
782
970
  }
783
971
  /* ---- private: retry loop ---- */
784
- async fetchWithRetry(url, init, retryMaxAttempts) {
972
+ async fetchWithRetry(url, init, retryMaxAttempts, deadlineAt) {
785
973
  const method = (init.method ?? "GET").toUpperCase();
786
974
  const retryEnabled = shouldRetryMethod(method) || retryMaxAttempts !== undefined;
787
975
  const maxAttempts = retryMaxAttempts === undefined
@@ -793,11 +981,21 @@ export class DataikuClient {
793
981
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
794
982
  let timedOut = false;
795
983
  const startedAt = Date.now();
984
+ // An overall deadline may shorten, but never extend, the attempt cap.
985
+ const remainingMs = deadlineAt === undefined
986
+ ? undefined
987
+ : deadlineAt - startedAt;
988
+ if (remainingMs !== undefined && remainingMs <= 0) {
989
+ 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));
990
+ }
991
+ const attemptTimeoutMs = remainingMs === undefined
992
+ ? this.requestTimeoutMs
993
+ : Math.min(remainingMs, this.requestTimeoutMs);
796
994
  const controller = new AbortController();
797
995
  const timeout = setTimeout(() => {
798
996
  timedOut = true;
799
997
  controller.abort();
800
- }, this.requestTimeoutMs);
998
+ }, attemptTimeoutMs);
801
999
  this.logTrace({ phase: "request", method, url, attempt, maxAttempts, });
802
1000
  try {
803
1001
  const requestInit = {
@@ -818,30 +1016,45 @@ export class DataikuClient {
818
1016
  elapsedMs: Date.now() - startedAt,
819
1017
  });
820
1018
  if (!res.ok) {
821
- const text = await res.text();
1019
+ const { text, truncated, } = await this.readBoundedBodyText(res, this.maxResponseBodyBytes, startedAt + attemptTimeoutMs);
822
1020
  const canRetry = retryEnabled && attempt < maxAttempts && isTransientError(res.status, text);
823
1021
  if (canRetry) {
824
1022
  const delayMs = computeBackoffDelayMs(attempt);
825
- delaysMs.push(delayMs);
826
- await sleep(delayMs);
827
- continue;
1023
+ // Do not start a retry whose backoff cannot fit in the budget.
1024
+ if (deadlineAt === undefined || Date.now() + delayMs <= deadlineAt) {
1025
+ delaysMs.push(delayMs);
1026
+ await sleep(delayMs);
1027
+ continue;
1028
+ }
1029
+ await sleep(Math.max(0, deadlineAt - Date.now()));
1030
+ throw new DataikuError(0, "Request Timeout", `Request deadline exceeded before backoff retry ${attempt + 1}.`, buildRetryMetadata(method, retryEnabled, maxAttempts, attempt, delaysMs, true));
828
1031
  }
829
- throw new DataikuError(res.status, res.statusText, text, buildRetryMetadata(method, retryEnabled, maxAttempts, attempt, delaysMs, false), this.requestIdFromHeaders(res.headers));
1032
+ throw new DataikuError(res.status, res.statusText, text, buildRetryMetadata(method, retryEnabled, maxAttempts, attempt, delaysMs, false), this.requestIdFromHeaders(res.headers), truncated ? { bodyTruncated: true, } : undefined);
830
1033
  }
831
1034
  return res;
832
1035
  }
833
1036
  catch (error) {
834
- if (error instanceof DataikuError)
835
- throw error;
1037
+ if (error instanceof DataikuError) {
1038
+ // A bounded error-body read is still part of this transport attempt.
1039
+ // Preserve the same retry/deadline policy as a fetch body abort.
1040
+ if (error.status !== 0 || error.statusText !== "Request Timeout" || error.retry !== undefined) {
1041
+ throw error;
1042
+ }
1043
+ timedOut = true;
1044
+ }
836
1045
  const canRetry = retryEnabled && attempt < maxAttempts;
837
1046
  if (canRetry) {
838
1047
  const delayMs = computeBackoffDelayMs(attempt);
839
- delaysMs.push(delayMs);
840
- await sleep(delayMs);
841
- continue;
1048
+ if (deadlineAt === undefined || Date.now() + delayMs <= deadlineAt) {
1049
+ delaysMs.push(delayMs);
1050
+ await sleep(delayMs);
1051
+ continue;
1052
+ }
1053
+ await sleep(Math.max(0, deadlineAt - Date.now()));
1054
+ throw new DataikuError(0, "Request Timeout", `Request deadline exceeded before backoff retry ${attempt + 1}.`, buildRetryMetadata(method, retryEnabled, maxAttempts, attempt, delaysMs, true));
842
1055
  }
843
1056
  const detail = timedOut
844
- ? `Request timed out after ${this.requestTimeoutMs}ms`
1057
+ ? `Request timed out after ${attemptTimeoutMs}ms`
845
1058
  : error instanceof Error
846
1059
  ? error.message
847
1060
  : "Unknown transport error";