@toloka-platform/sdk 0.1.0-preview.4 → 0.1.0-preview.6
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 +1 -1
- package/dist/cli/toloka.js +132 -126
- package/dist/cli/toloka.js.jsc +0 -0
- package/dist/cli/toloka.js.jsc.sha256 +1 -1
- package/dist/cli/toloka.js.sha256 +1 -1
- package/docs/cli-v2.md +4 -0
- package/docs/cli.md +82 -6
- package/docs/datasets.md +8 -9
- package/docs/getting-started.md +2 -2
- package/docs/limitations.md +5 -8
- package/docs/troubleshooting.md +0 -11
- package/openapi/gen/api.ts +782 -876
- package/openapi/gen/models/baseModel.ts +9 -0
- package/openapi/gen/models/baseModelId.ts +2 -0
- package/openapi/gen/models/baseModelIdProperty.ts +2 -0
- package/openapi/gen/models/createDatasetBody.ts +2 -0
- package/openapi/gen/models/createDatasetBodyProfile.ts +2 -0
- package/openapi/gen/models/createDatasetUploadBodyFormat.ts +1 -1
- package/openapi/gen/models/createProjectModelBody.ts +8 -0
- package/openapi/gen/models/createProjectModelBodyDescription.ts +2 -0
- package/openapi/gen/models/dataset.ts +2 -0
- package/openapi/gen/models/datasetProfile.ts +3 -0
- package/openapi/gen/models/datasetProfileStamp.ts +6 -0
- package/openapi/gen/models/datasetProfileStampName.ts +2 -0
- package/openapi/gen/models/generationNodeSettingsAudienceAndPricingOneOfOneone.ts +1 -0
- package/openapi/gen/models/index.ts +21 -0
- package/openapi/gen/models/listBaseModels200.ts +5 -0
- package/openapi/gen/models/listModelVersions200.ts +7 -0
- package/openapi/gen/models/listModelVersionsParams.ts +5 -0
- package/openapi/gen/models/listProjectModels200.ts +7 -0
- package/openapi/gen/models/listProjectModelsParams.ts +5 -0
- package/openapi/gen/models/model.ts +16 -0
- package/openapi/gen/models/modelArchivedAt.ts +2 -0
- package/openapi/gen/models/modelDescription.ts +2 -0
- package/openapi/gen/models/modelId.ts +2 -0
- package/openapi/gen/models/modelVersion.ts +9 -0
- package/openapi/gen/models/modelVersionId.ts +2 -0
- package/openapi/gen/models/pipelineGraphSnapshotGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneone.ts +1 -0
- package/openapi/gen/models/runCost.ts +7 -0
- package/openapi/gen/models/validatePipelineGraphBodyGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneone.ts +1 -0
- package/package.json +2 -1
- package/skills/catalog.json +22 -0
- package/skills/{SKILL.md → toloka-build/SKILL.md} +2 -2
- package/skills/{references → toloka-build/references}/workflow-and-cli.md +26 -5
- package/skills/toloka-report-issue/SKILL.md +25 -0
- package/skills/toloka-report-issue/references/report-template.md +34 -0
- package/src/cli/analytics.ts +53 -1
- package/src/cli/data.ts +148 -25
- package/src/cli/file-system.ts +34 -0
- package/src/cli/format-error.ts +19 -0
- package/src/cli/init.ts +149 -194
- package/src/cli/models.ts +188 -0
- package/src/cli/program.ts +202 -22
- package/src/cli/report.ts +144 -0
- package/src/cli/skill.ts +363 -0
- package/src/client.ts +109 -35
- package/src/errors.ts +25 -10
- package/src/pipeline-graph-v1.contract.json +5 -1
- package/src/transport.ts +16 -13
- /package/skills/{references → toloka-build/references}/node-semantics.md +0 -0
package/src/cli/data.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { assert } from "es-toolkit";
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname } from "node:path";
|
|
4
|
-
import type { Dataset, InferredField, PotatoClient } from "../client";
|
|
4
|
+
import type { Dataset, InferredField, PotatoClient, UploadId } from "../client";
|
|
5
5
|
import { PotatoApiError } from "../errors";
|
|
6
6
|
import { generateFieldDeclarationsSource } from "../importer";
|
|
7
7
|
import { readTolokaState } from "../state";
|
|
@@ -14,6 +14,14 @@ import type { ProjectSource } from "./source";
|
|
|
14
14
|
const RAW_UPLOAD_FIELD_NAME = "uploaded";
|
|
15
15
|
const DATASET_ID_PATTERN =
|
|
16
16
|
/^ds\.[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
17
|
+
const FINALIZE_UPLOAD_ATTEMPTS = 3;
|
|
18
|
+
|
|
19
|
+
class DatasetUploadError extends Error {
|
|
20
|
+
constructor(message: string, cause: unknown) {
|
|
21
|
+
super(message, { cause });
|
|
22
|
+
this.name = "DatasetUploadError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
17
25
|
|
|
18
26
|
const canonicalDatasetId = (token: string): string | undefined =>
|
|
19
27
|
DATASET_ID_PATTERN.test(token) ? token.toLowerCase() : undefined;
|
|
@@ -22,6 +30,16 @@ type UploadFieldDeclaration = NonNullable<
|
|
|
22
30
|
Parameters<PotatoClient["datasets"]["startUpload"]>[0]["fields"]
|
|
23
31
|
>[number];
|
|
24
32
|
|
|
33
|
+
type DatasetUploadFormat = NonNullable<
|
|
34
|
+
Parameters<PotatoClient["datasets"]["startUpload"]>[0]["format"]
|
|
35
|
+
>;
|
|
36
|
+
|
|
37
|
+
const DATASET_UPLOAD_CONTENT_TYPES: Record<DatasetUploadFormat, string> = {
|
|
38
|
+
json: "application/json",
|
|
39
|
+
jsonl: "application/x-ndjson",
|
|
40
|
+
csv: "text/csv",
|
|
41
|
+
};
|
|
42
|
+
|
|
25
43
|
export type UploadDatasetOptions = {
|
|
26
44
|
readonly sourceRoot: string;
|
|
27
45
|
readonly datasetName: string;
|
|
@@ -201,29 +219,138 @@ const resolveLiveDataset = async (
|
|
|
201
219
|
return { datasetId: created.id, name: created.name, nameIsUnique: true };
|
|
202
220
|
};
|
|
203
221
|
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
222
|
+
const resolveDatasetUploadFormat = (filename: string): DatasetUploadFormat => {
|
|
223
|
+
const normalizedFilename = filename.toLowerCase();
|
|
224
|
+
|
|
225
|
+
if (normalizedFilename.endsWith(".json")) return "json";
|
|
226
|
+
if (normalizedFilename.endsWith(".jsonl")) return "jsonl";
|
|
227
|
+
if (normalizedFilename.endsWith(".csv")) return "csv";
|
|
228
|
+
|
|
229
|
+
assert(false, `${filename} must have a .json, .jsonl, or .csv extension`);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const readDatasetUpload = async (filePath: string) => {
|
|
233
|
+
const filename = basename(filePath);
|
|
234
|
+
const format = resolveDatasetUploadFormat(filename);
|
|
235
|
+
const payload = await readFile(filePath, "utf8");
|
|
236
|
+
|
|
237
|
+
if (format === "jsonl") {
|
|
238
|
+
for (const { line, lineNumber } of payload
|
|
239
|
+
.split(/\r?\n/u)
|
|
240
|
+
.map((line, index) => ({ line, lineNumber: index + 1 }))
|
|
241
|
+
.filter(({ line }) => line.length > 0)) {
|
|
211
242
|
let value: unknown;
|
|
212
243
|
try {
|
|
213
244
|
value = JSON.parse(line);
|
|
214
245
|
} catch {
|
|
215
|
-
assert(false, `${
|
|
246
|
+
assert(false, `${filename}:${lineNumber} is not valid JSON`);
|
|
216
247
|
}
|
|
217
|
-
assert(isJsonObject(value), `${
|
|
218
|
-
|
|
248
|
+
assert(isJsonObject(value), `${filename}:${lineNumber} must contain a JSON object`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { payload, format, contentType: DATASET_UPLOAD_CONTENT_TYPES[format] };
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
type StartedDatasetUpload = { readonly uploadUrl: string; readonly job: { readonly id: UploadId } };
|
|
256
|
+
|
|
257
|
+
const cancelStartedUpload = async (
|
|
258
|
+
client: PotatoClient,
|
|
259
|
+
uploadId: StartedDatasetUpload["job"]["id"],
|
|
260
|
+
error: unknown,
|
|
261
|
+
): Promise<never> => {
|
|
262
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
await client.datasets.cancelUpload({ uploadId });
|
|
266
|
+
} catch (cancelError) {
|
|
267
|
+
const cancelReason = cancelError instanceof Error ? cancelError.message : String(cancelError);
|
|
268
|
+
throw new AggregateError(
|
|
269
|
+
[error, cancelError],
|
|
270
|
+
`Dataset upload ${uploadId} failed and could not be canceled. Upload error: ${reason}. Cancellation error: ${cancelReason}.`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
throw new DatasetUploadError(
|
|
275
|
+
`Dataset upload ${uploadId} failed and was canceled: ${reason}`,
|
|
276
|
+
error,
|
|
277
|
+
);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const transferAndFinalizeUpload = async ({
|
|
281
|
+
client,
|
|
282
|
+
started,
|
|
283
|
+
payload,
|
|
284
|
+
contentType,
|
|
285
|
+
fetch,
|
|
286
|
+
retryIntervalMs,
|
|
287
|
+
}: {
|
|
288
|
+
client: PotatoClient;
|
|
289
|
+
started: StartedDatasetUpload;
|
|
290
|
+
payload: string;
|
|
291
|
+
contentType: string;
|
|
292
|
+
fetch: typeof globalThis.fetch;
|
|
293
|
+
retryIntervalMs: number;
|
|
294
|
+
}): Promise<void> => {
|
|
295
|
+
let uploadResponse: Response;
|
|
296
|
+
try {
|
|
297
|
+
uploadResponse = await fetch(started.uploadUrl, {
|
|
298
|
+
method: "PUT",
|
|
299
|
+
headers: {
|
|
300
|
+
"Content-Type": contentType,
|
|
301
|
+
"x-ms-blob-type": "BlockBlob",
|
|
302
|
+
},
|
|
303
|
+
body: payload,
|
|
219
304
|
});
|
|
305
|
+
} catch (error) {
|
|
306
|
+
return cancelStartedUpload(client, started.job.id, error);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!uploadResponse.ok) {
|
|
310
|
+
return cancelStartedUpload(
|
|
311
|
+
client,
|
|
312
|
+
started.job.id,
|
|
313
|
+
new DatasetUploadError(
|
|
314
|
+
`Dataset upload failed with HTTP ${uploadResponse.status}.`,
|
|
315
|
+
undefined,
|
|
316
|
+
),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let transientFailures = 0;
|
|
321
|
+
while (true) {
|
|
322
|
+
try {
|
|
323
|
+
await client.datasets.finalizeUpload({ uploadId: started.job.id });
|
|
324
|
+
return;
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (!(error instanceof PotatoApiError && error.status === 503)) {
|
|
327
|
+
return cancelStartedUpload(client, started.job.id, error);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
transientFailures += 1;
|
|
331
|
+
if (transientFailures === FINALIZE_UPLOAD_ATTEMPTS) {
|
|
332
|
+
return cancelStartedUpload(
|
|
333
|
+
client,
|
|
334
|
+
started.job.id,
|
|
335
|
+
new DatasetUploadError(
|
|
336
|
+
`Storage remained unavailable after ${FINALIZE_UPLOAD_ATTEMPTS} finalization attempts.`,
|
|
337
|
+
error,
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
343
|
+
setTimeout(resolve, retryIntervalMs);
|
|
344
|
+
await promise;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
220
347
|
};
|
|
221
348
|
|
|
222
349
|
export async function uploadDataset(
|
|
223
350
|
client: PotatoClient,
|
|
224
351
|
options: UploadDatasetOptions,
|
|
225
352
|
): Promise<UploadDatasetResult> {
|
|
226
|
-
const
|
|
353
|
+
const { payload, format, contentType } = await readDatasetUpload(options.filePath);
|
|
227
354
|
const resolved = await resolveLiveDataset(client, options, true);
|
|
228
355
|
const datasetId = resolved.datasetId;
|
|
229
356
|
|
|
@@ -236,25 +363,21 @@ export async function uploadDataset(
|
|
|
236
363
|
if (!fieldsByName.has(field.name)) fieldsByName.set(field.name, field);
|
|
237
364
|
}
|
|
238
365
|
const fields = [...fieldsByName.values()];
|
|
239
|
-
const payload = JSON.stringify(rows);
|
|
240
|
-
const payloadBytes = new TextEncoder().encode(payload);
|
|
241
366
|
const started = await client.datasets.startUpload({
|
|
242
367
|
datasetId,
|
|
243
368
|
filename: basename(options.filePath),
|
|
244
|
-
format
|
|
245
|
-
declaredSizeBytes:
|
|
369
|
+
format,
|
|
370
|
+
declaredSizeBytes: new TextEncoder().encode(payload).byteLength,
|
|
246
371
|
fields,
|
|
247
372
|
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
373
|
+
await transferAndFinalizeUpload({
|
|
374
|
+
client,
|
|
375
|
+
started,
|
|
376
|
+
payload,
|
|
377
|
+
contentType,
|
|
378
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
379
|
+
retryIntervalMs: options.waitFor?.intervalMs ?? 1000,
|
|
255
380
|
});
|
|
256
|
-
assert(uploadResponse.ok, `Dataset upload failed with HTTP ${uploadResponse.status}.`);
|
|
257
|
-
await client.datasets.finalizeUpload({ uploadId: started.job.id });
|
|
258
381
|
const completed = await client.waitFor(
|
|
259
382
|
{ type: "datasetUpload", uploadId: started.job.id },
|
|
260
383
|
options.waitFor,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { assert } from "es-toolkit";
|
|
2
|
+
import { lstat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export const hasErrorCode = (error: unknown, ...codes: readonly string[]) =>
|
|
6
|
+
error instanceof Error &&
|
|
7
|
+
"code" in error &&
|
|
8
|
+
typeof error.code === "string" &&
|
|
9
|
+
codes.includes(error.code);
|
|
10
|
+
|
|
11
|
+
export const isMissingFileError = (error: unknown) => hasErrorCode(error, "ENOENT");
|
|
12
|
+
|
|
13
|
+
export const entryStats = async (path: string) => {
|
|
14
|
+
try {
|
|
15
|
+
return await lstat(path);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (isMissingFileError(error)) return undefined;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const assertSafeDestination = async (
|
|
23
|
+
targetDirectory: string,
|
|
24
|
+
relativePath: string,
|
|
25
|
+
action = "write",
|
|
26
|
+
) => {
|
|
27
|
+
let currentPath = targetDirectory;
|
|
28
|
+
for (const segment of relativePath.split("/")) {
|
|
29
|
+
currentPath = join(currentPath, segment);
|
|
30
|
+
const entry = await entryStats(currentPath);
|
|
31
|
+
if (entry === undefined) return;
|
|
32
|
+
assert(!entry.isSymbolicLink(), `Refusing to ${action} through symbolic link ${currentPath}.`);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PotatoApiError } from "../errors";
|
|
2
|
+
|
|
3
|
+
export const apiErrorDetails = (error: PotatoApiError): string | undefined => {
|
|
4
|
+
const details =
|
|
5
|
+
typeof error.body === "string"
|
|
6
|
+
? error.body
|
|
7
|
+
: error.body === undefined
|
|
8
|
+
? undefined
|
|
9
|
+
: JSON.stringify(error.body);
|
|
10
|
+
return details === undefined || details.length === 0 ? undefined : details;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const formatCliError = (error: unknown): string => {
|
|
14
|
+
if (error instanceof PotatoApiError) {
|
|
15
|
+
const details = apiErrorDetails(error);
|
|
16
|
+
return details === undefined ? error.message : `${error.message}\n${details}`;
|
|
17
|
+
}
|
|
18
|
+
return error instanceof Error ? error.message : String(error);
|
|
19
|
+
};
|