@diplodoc/cli 4.13.7 → 4.13.9-alpha-1

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "author": "Yandex Data UI Team <data-ui@yandex-team.ru>",
4
4
  "description": "Make documentation using yfm-docs in Markdown and HTML formats",
5
5
  "license": "MIT",
6
- "version": "4.13.7",
6
+ "version": "4.13.9-alpha-1",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git@github.com:diplodoc-platform/cli.git"
@@ -31,16 +31,16 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@aws-sdk/client-s3": "^3.369.0",
34
- "@diplodoc/client": "^2.0.5",
34
+ "@diplodoc/client": "^2.1.1",
35
35
  "@diplodoc/latex-extension": "^1.1.0",
36
36
  "@diplodoc/mermaid-extension": "^1.2.1",
37
37
  "@diplodoc/openapi-extension": "^1.4.13",
38
38
  "@diplodoc/transform": "^4.8.2",
39
- "@diplodoc/translation": "^1.0.6",
39
+ "@diplodoc/translation": "^1.0.7",
40
40
  "@octokit/core": "4.2.4",
41
- "@yandex-cloud/nodejs-sdk": "^2.2.2",
42
41
  "ajv": "^8.11.0",
43
42
  "async": "^3.2.4",
43
+ "axios": "^1.6.7",
44
44
  "chalk": "4.1.2",
45
45
  "glob": "^8.0.3",
46
46
  "highlight.js": "^11.7.0",
@@ -28,7 +28,6 @@ function builder<T>(argv: Argv<T>) {
28
28
  alias: 'o',
29
29
  describe: 'output folder where translated markdown will be stored',
30
30
  type: 'string',
31
- default: process.cwd(),
32
31
  })
33
32
  .option('use-source', {
34
33
  describe: 'for debug',
@@ -29,7 +29,6 @@ function builder<T>(argv: Argv<T>) {
29
29
  alias: 'o',
30
30
  describe: 'output folder where translated markdown will be stored',
31
31
  type: 'string',
32
- default: process.cwd(),
33
32
  })
34
33
  .option('source', {
35
34
  alias: ['sll', 'source-language-locale'],
@@ -1,26 +1,25 @@
1
+ import axios, {AxiosError, AxiosResponse} from 'axios';
2
+ import {green, red} from 'chalk';
1
3
  import {Arguments} from 'yargs';
2
4
  import {ArgvService} from '../../services';
3
5
  import {logger} from '../../utils';
4
6
  import {ok} from 'assert';
5
- import {dirname, extname, resolve} from 'path';
7
+ import {dirname, extname, join, resolve} from 'path';
6
8
  import {mkdir} from 'fs/promises';
7
- import {AuthInfo, getYandexAuth} from './yandex/auth';
8
- import {asyncify, eachLimit, retry} from 'async';
9
- import {Session} from '@yandex-cloud/nodejs-sdk/dist/session';
10
- import {TranslationServiceClient} from '@yandex-cloud/nodejs-sdk/dist/generated/yandex/cloud/service_clients';
11
- import {
12
- TranslateRequest_Format as Format,
13
- TranslateRequest,
14
- } from '@yandex-cloud/nodejs-sdk/dist/generated/yandex/cloud/ai/translate/v2/translation_service';
9
+ import {getYandexAuth} from './yandex/auth';
10
+ import {asyncify, eachLimit} from 'async';
15
11
 
16
12
  import {
13
+ AuthError,
17
14
  Defer,
15
+ LimitExceed,
16
+ RequestError,
17
+ TranslateError,
18
18
  TranslateParams,
19
19
  bytes,
20
20
  compose,
21
21
  dumpFile,
22
22
  extract,
23
- flat,
24
23
  loadFile,
25
24
  normalizeParams,
26
25
  } from './utils';
@@ -29,24 +28,39 @@ const REQUESTS_LIMIT = 20;
29
28
  const BYTES_LIMIT = 10000;
30
29
  const RETRY_LIMIT = 3;
31
30
 
32
- class TranslatorError extends Error {
33
- path: string;
31
+ type TranslatorParams = {
32
+ input: string;
33
+ output: string;
34
+ sourceLanguage: string;
35
+ targetLanguage: string;
36
+ // yandexCloudTranslateGlossaryPairs: YandexCloudTranslateGlossaryPair[];
37
+ };
34
38
 
35
- constructor(message: string, path: string) {
36
- super(message);
39
+ type RequesterParams = {
40
+ auth: string;
41
+ folderId: string | undefined;
42
+ sourceLanguage: string;
43
+ targetLanguage: string;
44
+ dryRun: boolean;
45
+ };
37
46
 
38
- this.path = path;
39
- }
40
- }
47
+ type Request = {
48
+ (texts: string[]): () => Promise<void>;
49
+ stat: {
50
+ bytes: number;
51
+ chunks: number;
52
+ };
53
+ };
41
54
 
42
- class RequestError extends Error {
43
- code: string;
55
+ type Split = (path: string, texts: string[]) => Promise<string[]>;
44
56
 
45
- constructor(error: Error) {
46
- super(error?.message || String(error));
47
- this.code = 'REQUEST_ERROR';
48
- }
49
- }
57
+ type Cache = Map<string, Defer>;
58
+
59
+ type Translations = {
60
+ translations: {
61
+ text: string;
62
+ }[];
63
+ };
50
64
 
51
65
  export async function handler(args: Arguments<any>) {
52
66
  const params = normalizeParams({
@@ -91,95 +105,130 @@ export async function handler(args: Arguments<any>) {
91
105
  try {
92
106
  await translate(file);
93
107
  } catch (error: any) {
94
- logger.error(file, error.message);
108
+ if (error instanceof TranslateError) {
109
+ logger.error(file, `${error.message}`, error.code);
110
+
111
+ if (error.fatal) {
112
+ process.exit(1);
113
+ }
114
+ } else {
115
+ logger.error(file, error.message);
116
+ }
95
117
  }
96
118
  }),
97
119
  );
98
120
 
99
- console.log('PROCESSED', `bytes: ${request.stat.bytes} chunks: ${request.stat.chunks}`);
121
+ console.log(
122
+ green('PROCESSED'),
123
+ `bytes: ${request.stat.bytes} chunks: ${request.stat.chunks}`,
124
+ );
100
125
  }
101
126
  } catch (error: any) {
102
- const message = error.message;
103
-
104
- const file = error instanceof TranslatorError ? error.path : '';
127
+ if (error instanceof TranslateError) {
128
+ console.error(red(error.code), error.message);
129
+ } else {
130
+ console.error(error);
131
+ }
105
132
 
106
- logger.error(file, message);
133
+ process.exit(1);
107
134
  }
108
135
  }
109
136
 
110
- type TranslatorParams = {
111
- input: string;
112
- output: string;
113
- sourceLanguage: string;
114
- targetLanguage: string;
115
- // yandexCloudTranslateGlossaryPairs: YandexCloudTranslateGlossaryPair[];
116
- };
137
+ function scheduler(limit: number, interval: number) {
138
+ const scheduled: Defer<void>[] = [];
117
139
 
118
- type RequesterParams = {
119
- auth: AuthInfo;
120
- folderId: string | undefined;
121
- sourceLanguage: string;
122
- targetLanguage: string;
123
- dryRun: boolean;
124
- };
140
+ let processing = 0;
125
141
 
126
- type Request = {
127
- (texts: string[]): () => Promise<string[]>;
128
- stat: {
129
- bytes: number;
130
- chunks: number;
131
- };
132
- };
142
+ function idle() {
143
+ const defer = new Defer<void>();
133
144
 
134
- type Split = (path: string, texts: string[]) => Promise<string[]>[];
145
+ scheduled.push(defer);
135
146
 
136
- type Cache = Map<string, Defer>;
147
+ return defer.promise;
148
+ }
137
149
 
138
- function requester(params: RequesterParams, cache: Cache) {
139
- const {auth, folderId, sourceLanguage, targetLanguage, dryRun} = params;
140
- const session = new Session(auth);
141
- const client = session.client(TranslationServiceClient);
142
- const resolve = (text: string, index: number, texts: string[]) => {
143
- const defer = cache.get(texts[index]);
144
- if (defer) {
145
- defer.resolve([text]);
150
+ async function queue() {
151
+ processing++;
152
+ await wait(interval);
153
+ processing--;
154
+ unqueue();
155
+ }
156
+
157
+ async function unqueue() {
158
+ scheduled.shift()?.resolve();
159
+ }
160
+
161
+ return async function <R>(action: Function): Promise<R> {
162
+ if (processing >= limit) {
163
+ await idle();
146
164
  }
147
- return text;
165
+
166
+ queue();
167
+
168
+ return action();
148
169
  };
170
+ }
171
+
172
+ function requester(params: RequesterParams, cache: Cache) {
173
+ const {auth, folderId, sourceLanguage, targetLanguage, dryRun} = params;
174
+ const schedule = scheduler(REQUESTS_LIMIT, 1000);
149
175
 
150
176
  const request = function request(texts: string[]) {
177
+ const resolve = (text: string, index: number) => {
178
+ const defer = cache.get(texts[index]);
179
+ if (defer) {
180
+ defer.resolve(text);
181
+ }
182
+ };
183
+
151
184
  request.stat.bytes += bytes(texts);
152
185
  request.stat.chunks++;
153
186
 
154
187
  return async function () {
155
188
  if (dryRun) {
156
- return texts.map(resolve);
189
+ texts.forEach(resolve);
157
190
  }
158
191
 
159
- return client
160
- .translate(
161
- TranslateRequest.fromPartial({
162
- texts,
163
- folderId,
164
- sourceLanguageCode: sourceLanguage,
165
- targetLanguageCode: targetLanguage,
166
- // glossaryConfig: {
167
- // glossaryData: {
168
- // glossaryPairs: yandexCloudTranslateGlossaryPairs,
169
- // },
170
- // },
171
- format: Format.HTML,
192
+ try {
193
+ const {data} = await schedule<AxiosResponse<Translations>>(() =>
194
+ axios({
195
+ method: 'POST',
196
+ url: 'https://translate.api.cloud.yandex.net/translate/v2/translate',
197
+ timeout: 5000,
198
+ maxRedirects: 0,
199
+ headers: {
200
+ Authorization: auth,
201
+ 'Content-Type': 'application/json',
202
+ 'User-Agent': 'github.com/diplodoc-platform/cli',
203
+ },
204
+ data: {
205
+ folderId,
206
+ texts,
207
+ sourceLanguageCode: sourceLanguage,
208
+ targetLanguageCode: targetLanguage,
209
+ format: 'HTML',
210
+ },
172
211
  }),
173
- )
174
- .then((results) => {
175
- return results.translations.map(({text}, index) => {
176
- return resolve(text, index, texts);
177
- });
178
- })
179
- .catch((error) => {
180
- console.error(error);
181
- throw new RequestError(error);
182
- });
212
+ );
213
+
214
+ return data.translations.map(({text}) => text).forEach(resolve);
215
+ } catch (error: any) {
216
+ if (error instanceof AxiosError) {
217
+ const {response} = error;
218
+ const {status, statusText, data} = response as AxiosResponse;
219
+
220
+ switch (true) {
221
+ case LimitExceed.is(data.message):
222
+ throw new LimitExceed(data.message);
223
+ case AuthError.is(data.message):
224
+ throw new AuthError(data.message);
225
+ default:
226
+ throw new RequestError(status, statusText, data);
227
+ }
228
+ }
229
+
230
+ throw new RequestError(0, error.message, {fatal: true});
231
+ }
183
232
  };
184
233
  };
185
234
 
@@ -193,6 +242,8 @@ function requester(params: RequesterParams, cache: Cache) {
193
242
 
194
243
  function translator(params: TranslatorParams, split: Split) {
195
244
  const {input, output, sourceLanguage, targetLanguage} = params;
245
+ const inputRoot = resolve(input);
246
+ const outputRoot = resolve(output);
196
247
 
197
248
  return async (path: string) => {
198
249
  const ext = extname(path);
@@ -200,8 +251,8 @@ function translator(params: TranslatorParams, split: Split) {
200
251
  return;
201
252
  }
202
253
 
203
- const inputPath = resolve(input, path);
204
- const outputPath = resolve(output, path);
254
+ const inputPath = join(inputRoot, path);
255
+ const outputPath = join(outputRoot, path.replace(sourceLanguage, targetLanguage));
205
256
  const content = await loadFile(inputPath);
206
257
 
207
258
  await mkdir(dirname(outputPath), {recursive: true});
@@ -227,7 +278,7 @@ function translator(params: TranslatorParams, split: Split) {
227
278
  return;
228
279
  }
229
280
 
230
- const parts = flat(await Promise.all(split(path, units)));
281
+ const parts = await split(path, units);
231
282
  const composed = compose(skeleton, parts, {useSource: true});
232
283
 
233
284
  await dumpFile(outputPath, composed);
@@ -235,50 +286,67 @@ function translator(params: TranslatorParams, split: Split) {
235
286
  }
236
287
 
237
288
  function splitter(request: Request, cache: Cache): Split {
238
- return function (path: string, texts: string[]) {
239
- const promises: Promise<string[]>[] = [];
289
+ return async function (path: string, texts: string[]) {
290
+ const promises: Promise<string>[] = [];
291
+ const requests: Promise<void>[] = [];
240
292
  let buffer: string[] = [];
241
293
  let bufferSize = 0;
242
294
 
243
295
  const release = () => {
244
- promises.push(backoff(request(buffer)));
296
+ requests.push(backoff(request(buffer)));
245
297
  buffer = [];
246
298
  bufferSize = 0;
247
299
  };
248
300
 
249
301
  for (const text of texts) {
250
- const defer = cache.get(text);
251
-
252
- if (defer) {
253
- promises.push(defer.promise);
254
- } else if (text.length >= BYTES_LIMIT) {
302
+ if (text.length >= BYTES_LIMIT) {
255
303
  logger.warn(path, 'Skip document part for translation. Part is too big.');
256
- promises.push(Promise.resolve([text]));
304
+ promises.push(Promise.resolve(text));
257
305
  } else {
258
- if (bufferSize + text.length > BYTES_LIMIT) {
259
- release();
306
+ const defer = cache.get(text) || new Defer();
307
+ promises.push(defer.promise);
308
+
309
+ if (!cache.get(text)) {
310
+ if (bufferSize + text.length > BYTES_LIMIT) {
311
+ release();
312
+ }
313
+
314
+ buffer.push(text);
315
+ bufferSize += text.length;
260
316
  }
261
317
 
262
- buffer.push(text);
263
- bufferSize += text.length;
264
- cache.set(text, new Defer());
318
+ cache.set(text, defer);
265
319
  }
266
320
  }
267
321
 
268
322
  if (bufferSize) {
269
- promises.push(backoff(request(buffer)));
323
+ release();
270
324
  }
271
325
 
272
- return promises;
326
+ await Promise.all(requests);
327
+
328
+ return Promise.all(promises);
273
329
  };
274
330
  }
275
331
 
276
- function backoff(action: () => Promise<string[]>): Promise<string[]> {
277
- return retry(
278
- {
279
- times: RETRY_LIMIT,
280
- interval: (count: number) => Math.pow(2, count) * 1000,
281
- },
282
- asyncify(action),
283
- );
332
+ function wait(interval: number) {
333
+ const defer = new Defer<void>();
334
+ setTimeout(() => defer.resolve(), interval);
335
+ return defer.promise;
336
+ }
337
+
338
+ async function backoff(action: () => Promise<void>): Promise<void> {
339
+ let retry = 0;
340
+
341
+ while (++retry < RETRY_LIMIT) {
342
+ try {
343
+ await action();
344
+ } catch (error: any) {
345
+ if (RequestError.canRetry(error)) {
346
+ await wait(Math.pow(2, retry) * 1000);
347
+ } else {
348
+ throw error;
349
+ }
350
+ }
351
+ }
284
352
  }
@@ -0,0 +1,97 @@
1
+ export class TranslateError extends Error {
2
+ code: string;
3
+
4
+ fatal: boolean;
5
+
6
+ constructor(message: string, code: string, fatal = false) {
7
+ super(message);
8
+
9
+ this.code = code;
10
+ this.fatal = fatal;
11
+ }
12
+ }
13
+
14
+ export class RequestError extends TranslateError {
15
+ static canRetry(error: any) {
16
+ if (error instanceof RequestError) {
17
+ switch (true) {
18
+ case error.status === 429:
19
+ return true;
20
+ case error.status === 500:
21
+ return true;
22
+ case error.status === 503:
23
+ return true;
24
+ case error.status === 504:
25
+ return true;
26
+ default:
27
+ return false;
28
+ }
29
+ }
30
+
31
+ return false;
32
+ }
33
+
34
+ status: number;
35
+
36
+ constructor(
37
+ status: number,
38
+ statusText: string,
39
+ info: {code?: number; message?: string; fatal?: boolean} = {},
40
+ ) {
41
+ super(`${statusText}\n${info.message || ''}`, 'REQUEST_ERROR', info.fatal);
42
+
43
+ this.status = status;
44
+ }
45
+ }
46
+
47
+ const INACTIVE_CLOUD = /^The cloud .*? is inactive/;
48
+ const WRONG_APIKEY = /^Unknown api key/;
49
+ const WRONG_TOKEN = /^The token is invalid/;
50
+
51
+ export class AuthError extends TranslateError {
52
+ static is(message: string) {
53
+ return Boolean(AuthError.reason(message));
54
+ }
55
+
56
+ static reason(message: string) {
57
+ switch (true) {
58
+ case INACTIVE_CLOUD.test(message):
59
+ return 'INACTIVE_CLOUD';
60
+ case WRONG_APIKEY.test(message):
61
+ return 'WRONG_APIKEY';
62
+ case WRONG_TOKEN.test(message):
63
+ return 'WRONG_TOKEN';
64
+ default:
65
+ return null;
66
+ }
67
+ }
68
+
69
+ constructor(message: string) {
70
+ super(message, AuthError.reason(message) || 'AUTH_ERROR', true);
71
+ }
72
+ }
73
+
74
+ const LIMIT_EXCEED_RX = /^limit on units was exceeded. (.*)$/;
75
+
76
+ export class LimitExceed extends TranslateError {
77
+ static is(message: string) {
78
+ return Boolean(LIMIT_EXCEED_RX.test(message));
79
+ }
80
+
81
+ constructor(message: string) {
82
+ const [, desc] = LIMIT_EXCEED_RX.exec(message) || [];
83
+ super(desc, 'TRANSLATE_LIMIT_EXCEED', true);
84
+ }
85
+ }
86
+
87
+ export class ExtractError extends TranslateError {
88
+ constructor(error: Error) {
89
+ super('EXTRACT_ERROR', error?.message || String(error));
90
+ }
91
+ }
92
+
93
+ export class ComposeError extends TranslateError {
94
+ constructor(error: Error) {
95
+ super('COMPOSE_ERROR', error?.message || String(error));
96
+ }
97
+ }
@@ -5,6 +5,7 @@ import glob from 'glob';
5
5
 
6
6
  export {dumpFile, loadFile} from './fs';
7
7
  export {extract, compose} from './translate';
8
+ export {TranslateError, LimitExceed, RequestError, AuthError} from './errors';
8
9
 
9
10
  type TranslateArgs = {
10
11
  input: string;
@@ -141,16 +142,12 @@ export function resolveSchemas(_path: string) {
141
142
  return null;
142
143
  }
143
144
 
144
- export function flat<T>(parts: T[][]) {
145
- return ([] as T[]).concat(...parts);
146
- }
147
-
148
- export class Defer {
149
- resolve!: (text: string[]) => void;
145
+ export class Defer<T = string> {
146
+ resolve!: (text: T) => void;
150
147
 
151
148
  reject!: (error: any) => void;
152
149
 
153
- promise: Promise<string[]>;
150
+ promise: Promise<T>;
154
151
 
155
152
  constructor() {
156
153
  this.promise = new Promise((resolve, reject) => {
@@ -1,23 +1,6 @@
1
1
  import type {ComposeOptions, ExtractOptions} from '@diplodoc/translation';
2
2
  import {compose as _compose, extract as _extract} from '@diplodoc/translation';
3
-
4
- class ExtractError extends Error {
5
- code: string;
6
-
7
- constructor(error: Error) {
8
- super(error?.message || String(error));
9
- this.code = 'EXTRACT_ERROR';
10
- }
11
- }
12
-
13
- class ComposeError extends Error {
14
- code: string;
15
-
16
- constructor(error: Error) {
17
- super(error?.message || String(error));
18
- this.code = 'COMPOSE_ERROR';
19
- }
20
- }
3
+ import {ComposeError, ExtractError} from './errors';
21
4
 
22
5
  type Content = Parameters<typeof _extract>[0];
23
6
 
@@ -1,41 +1,32 @@
1
1
  import {readFileSync} from 'fs';
2
2
 
3
- type ServiceAccauntInfo = {
4
- id: string;
5
- service_account_id: string;
6
- public_key: string;
7
- private_key: string;
8
- };
3
+ const resolveKey = (data: string) => {
4
+ data = data.trim();
9
5
 
10
- export type AuthInfo = ReturnType<typeof getYandexAuth>;
6
+ switch (true) {
7
+ case data.startsWith('y0_'):
8
+ return 'Bearer ' + data;
9
+ case data.startsWith('t1.'):
10
+ return 'Bearer ' + data;
11
+ case data.startsWith('AQVN'):
12
+ return 'Api-Key ' + data;
13
+ default:
14
+ return null;
15
+ }
16
+ };
11
17
 
12
18
  export function getYandexAuth(path: string) {
13
- if (path.startsWith('y0_')) {
14
- return {
15
- oauthToken: path,
16
- };
17
- }
19
+ let auth = resolveKey(path);
18
20
 
19
- const data = readFileSync(path, 'utf8');
20
- try {
21
- const json = JSON.parse(data);
21
+ if (auth !== null) {
22
+ return auth;
23
+ }
22
24
 
23
- if (isServeseAccount(json)) {
24
- return {
25
- serviceAccountJson: {
26
- serviceAccountId: json.service_account_id,
27
- accessKeyId: json.id,
28
- privateKey: json.private_key,
29
- },
30
- };
31
- }
32
- } catch {}
25
+ auth = resolveKey(readFileSync(path, 'utf8'));
33
26
 
34
- return {
35
- oauthToken: data,
36
- };
37
- }
27
+ if (auth === null) {
28
+ throw new Error('No Auth');
29
+ }
38
30
 
39
- function isServeseAccount(json: any): json is ServiceAccauntInfo {
40
- return 'private_key' in json;
31
+ return auth;
41
32
  }
@@ -32,8 +32,8 @@ export const logger = {
32
32
 
33
33
  log.warn(`file: ${pathToFile} ${extraMessage}`);
34
34
  },
35
- error: function (pathToFile: string, extraMessage: string) {
36
- const message = `${red('ERROR')} file: ${pathToFile} error: ${extraMessage}`;
35
+ error: function (pathToFile: string, extraMessage: string, reason?: string) {
36
+ const message = `${red(reason || 'ERROR')} file: ${pathToFile} error: ${extraMessage}`;
37
37
 
38
38
  writeLog(message, true);
39
39
 
@@ -1 +0,0 @@
1
- export * from './yandex-oauth';