@diplodoc/cli 4.13.8 → 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.8",
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,7 +31,7 @@
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",
@@ -1,4 +1,5 @@
1
- import axios from 'axios';
1
+ import axios, {AxiosError, AxiosResponse} from 'axios';
2
+ import {green, red} from 'chalk';
2
3
  import {Arguments} from 'yargs';
3
4
  import {ArgvService} from '../../services';
4
5
  import {logger} from '../../utils';
@@ -6,10 +7,14 @@ import {ok} from 'assert';
6
7
  import {dirname, extname, join, resolve} from 'path';
7
8
  import {mkdir} from 'fs/promises';
8
9
  import {getYandexAuth} from './yandex/auth';
9
- import {asyncify, eachLimit, retry} from 'async';
10
+ import {asyncify, eachLimit} from 'async';
10
11
 
11
12
  import {
13
+ AuthError,
12
14
  Defer,
15
+ LimitExceed,
16
+ RequestError,
17
+ TranslateError,
13
18
  TranslateParams,
14
19
  bytes,
15
20
  compose,
@@ -23,24 +28,39 @@ const REQUESTS_LIMIT = 20;
23
28
  const BYTES_LIMIT = 10000;
24
29
  const RETRY_LIMIT = 3;
25
30
 
26
- class TranslatorError extends Error {
27
- path: string;
31
+ type TranslatorParams = {
32
+ input: string;
33
+ output: string;
34
+ sourceLanguage: string;
35
+ targetLanguage: string;
36
+ // yandexCloudTranslateGlossaryPairs: YandexCloudTranslateGlossaryPair[];
37
+ };
28
38
 
29
- constructor(message: string, path: string) {
30
- super(message);
39
+ type RequesterParams = {
40
+ auth: string;
41
+ folderId: string | undefined;
42
+ sourceLanguage: string;
43
+ targetLanguage: string;
44
+ dryRun: boolean;
45
+ };
31
46
 
32
- this.path = path;
33
- }
34
- }
47
+ type Request = {
48
+ (texts: string[]): () => Promise<void>;
49
+ stat: {
50
+ bytes: number;
51
+ chunks: number;
52
+ };
53
+ };
35
54
 
36
- class RequestError extends Error {
37
- code: string;
55
+ type Split = (path: string, texts: string[]) => Promise<string[]>;
38
56
 
39
- constructor(error: Error) {
40
- super(error?.message || String(error));
41
- this.code = 'REQUEST_ERROR';
42
- }
43
- }
57
+ type Cache = Map<string, Defer>;
58
+
59
+ type Translations = {
60
+ translations: {
61
+ text: string;
62
+ }[];
63
+ };
44
64
 
45
65
  export async function handler(args: Arguments<any>) {
46
66
  const params = normalizeParams({
@@ -85,106 +105,130 @@ export async function handler(args: Arguments<any>) {
85
105
  try {
86
106
  await translate(file);
87
107
  } catch (error: any) {
88
- 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
+ }
89
117
  }
90
118
  }),
91
119
  );
92
120
 
93
- 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
+ );
94
125
  }
95
126
  } catch (error: any) {
96
- const message = error.message;
97
-
98
- 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
+ }
99
132
 
100
- logger.error(file, message);
133
+ process.exit(1);
101
134
  }
102
135
  }
103
136
 
104
- type TranslatorParams = {
105
- input: string;
106
- output: string;
107
- sourceLanguage: string;
108
- targetLanguage: string;
109
- // yandexCloudTranslateGlossaryPairs: YandexCloudTranslateGlossaryPair[];
110
- };
137
+ function scheduler(limit: number, interval: number) {
138
+ const scheduled: Defer<void>[] = [];
111
139
 
112
- type RequesterParams = {
113
- auth: string;
114
- folderId: string | undefined;
115
- sourceLanguage: string;
116
- targetLanguage: string;
117
- dryRun: boolean;
118
- };
140
+ let processing = 0;
119
141
 
120
- type Request = {
121
- (texts: string[]): () => Promise<string[]>;
122
- stat: {
123
- bytes: number;
124
- chunks: number;
125
- };
126
- };
142
+ function idle() {
143
+ const defer = new Defer<void>();
127
144
 
128
- type Split = (path: string, texts: string[]) => Promise<string>[];
145
+ scheduled.push(defer);
129
146
 
130
- type Cache = Map<string, Defer>;
147
+ return defer.promise;
148
+ }
131
149
 
132
- type Translations = {
133
- translations: {
134
- text: string;
135
- }[];
136
- };
150
+ async function queue() {
151
+ processing++;
152
+ await wait(interval);
153
+ processing--;
154
+ unqueue();
155
+ }
137
156
 
138
- function requester(params: RequesterParams, cache: Cache) {
139
- const {auth, folderId, sourceLanguage, targetLanguage, dryRun} = params;
140
- const resolve = (text: string, index: number, texts: string[]) => {
141
- const defer = cache.get(texts[index]);
142
- if (defer) {
143
- defer.resolve(text);
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();
144
164
  }
145
- return text;
165
+
166
+ queue();
167
+
168
+ return action();
146
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);
147
175
 
148
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
+
149
184
  request.stat.bytes += bytes(texts);
150
185
  request.stat.chunks++;
151
186
 
152
187
  return async function () {
153
188
  if (dryRun) {
154
- return texts.map(resolve);
189
+ texts.forEach(resolve);
155
190
  }
156
191
 
157
- return axios({
158
- method: 'POST',
159
- url: 'https://translate.api.cloud.yandex.net/translate/v2/translate',
160
- headers: {
161
- 'Content-Type': 'application/json',
162
- Authorization: auth,
163
- },
164
- data: {
165
- folderId,
166
- texts,
167
- sourceLanguageCode: sourceLanguage,
168
- targetLanguageCode: targetLanguage,
169
- format: 'HTML',
170
- },
171
- })
172
- .then(({data, status}) => {
173
- if (status === 200) {
174
- return data;
175
- } else {
176
- throw new Error(data.message);
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
+ },
211
+ }),
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);
177
227
  }
178
- })
179
- .then((result: Translations) => {
180
- return result.translations.map(({text}, index) => {
181
- return resolve(text, index, texts);
182
- });
183
- })
184
- .catch((error) => {
185
- console.error(error);
186
- throw new RequestError(error);
187
- });
228
+ }
229
+
230
+ throw new RequestError(0, error.message, {fatal: true});
231
+ }
188
232
  };
189
233
  };
190
234
 
@@ -234,7 +278,7 @@ function translator(params: TranslatorParams, split: Split) {
234
278
  return;
235
279
  }
236
280
 
237
- const parts = await Promise.all(split(path, units));
281
+ const parts = await split(path, units);
238
282
  const composed = compose(skeleton, parts, {useSource: true});
239
283
 
240
284
  await dumpFile(outputPath, composed);
@@ -242,13 +286,14 @@ function translator(params: TranslatorParams, split: Split) {
242
286
  }
243
287
 
244
288
  function splitter(request: Request, cache: Cache): Split {
245
- return function (path: string, texts: string[]) {
289
+ return async function (path: string, texts: string[]) {
246
290
  const promises: Promise<string>[] = [];
291
+ const requests: Promise<void>[] = [];
247
292
  let buffer: string[] = [];
248
293
  let bufferSize = 0;
249
294
 
250
295
  const release = () => {
251
- backoff(request(buffer));
296
+ requests.push(backoff(request(buffer)));
252
297
  buffer = [];
253
298
  bufferSize = 0;
254
299
  };
@@ -278,16 +323,30 @@ function splitter(request: Request, cache: Cache): Split {
278
323
  release();
279
324
  }
280
325
 
281
- return promises;
326
+ await Promise.all(requests);
327
+
328
+ return Promise.all(promises);
282
329
  };
283
330
  }
284
331
 
285
- function backoff(action: () => Promise<string[]>): Promise<string[]> {
286
- return retry(
287
- {
288
- times: RETRY_LIMIT,
289
- interval: (count: number) => Math.pow(2, count) * 1000,
290
- },
291
- asyncify(action),
292
- );
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
+ }
293
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,12 +142,12 @@ export function resolveSchemas(_path: string) {
141
142
  return null;
142
143
  }
143
144
 
144
- export class Defer {
145
- resolve!: (text: string) => void;
145
+ export class Defer<T = string> {
146
+ resolve!: (text: T) => void;
146
147
 
147
148
  reject!: (error: any) => void;
148
149
 
149
- promise: Promise<string>;
150
+ promise: Promise<T>;
150
151
 
151
152
  constructor() {
152
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,6 +1,8 @@
1
1
  import {readFileSync} from 'fs';
2
2
 
3
3
  const resolveKey = (data: string) => {
4
+ data = data.trim();
5
+
4
6
  switch (true) {
5
7
  case data.startsWith('y0_'):
6
8
  return 'Bearer ' + data;
@@ -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';
@@ -1,42 +0,0 @@
1
- import {readFile} from 'fs/promises';
2
- import {env} from 'process';
3
- import {homedir} from 'os';
4
- import {join} from 'path';
5
-
6
- import {logger} from '../../utils';
7
-
8
- const YANDEX_OAUTH_TOKEN_FILENAME = '.ya_oauth_token';
9
-
10
- async function getYandexOAuthToken() {
11
- const {YANDEX_OAUTH_TOKEN} = env;
12
-
13
- return YANDEX_OAUTH_TOKEN ?? getYandexOAuthTokenFromHomeDir();
14
- }
15
-
16
- async function getYandexOAuthTokenFromHomeDir() {
17
- const error = 'failed reading yandex oauth token';
18
-
19
- const path = join(homedir(), YANDEX_OAUTH_TOKEN_FILENAME);
20
-
21
- let token;
22
-
23
- try {
24
- token = await readFile(path, {encoding: 'utf8'});
25
-
26
- token = token.trim();
27
-
28
- if (!token?.length) {
29
- throw new Error(error);
30
- }
31
- } catch (err) {
32
- logger.error(error);
33
-
34
- throw err;
35
- }
36
-
37
- return token;
38
- }
39
-
40
- export {getYandexOAuthToken};
41
-
42
- export default {getYandexOAuthToken};