Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: cms/bin/xtm_translations/utils.py

Issue 29886648: Issue #6942 - Add XTM integration in CMS (Closed)
Patch Set: Created Sept. 20, 2018, 4:24 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # This file is part of the Adblock Plus web scripts,
2 # Copyright (C) 2006-present eyeo GmbH
3 #
4 # Adblock Plus is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License version 3 as
6 # published by the Free Software Foundation.
7 #
8 # Adblock Plus is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
15 from collections import OrderedDict
16 import logging
17 import os
18 import time
19 import json
20
21 from cms.utils import process_page
22 import cms.bin.xtm_translations.constants as cnts
23 from cms.bin.xtm_translations.xtm_api import XTMCloudException
24
25
26 __all__ = [
27 'extract_strings', 'resolve_locales', 'map_locales', 'get_local_files',
28 'resolve_remote_filename', 'run_and_wait', 'resolve_naming_conflicts',
29 'read_token', 'get_files_to_upload', 'log_resulting_jobs', 'clear_files',
30 ]
31
32
33 def log_resulting_jobs(jobs):
34 """Log the jobs created as a result of uploading files/ creating projects.
35
36 Parameters
37 ----------
38 jobs: iterable
39 Of dicts, as returned by XTM.
40
41 """
42 if len(jobs) == 0:
43 logging.info(cnts.InfoMessages.NO_JOBS_CREATED)
44 return
45
46 for job in jobs:
47 logging.info(
48 cnts.InfoMessages.CREATED_JOB.format(
49 job['jobId'], job['fileName'], job['targetLanguage'],
50 ),
51 )
52
53
54 def get_files_to_upload(files, page_strings):
55 """Return the files to upload in the format supported by XTMCloudAPI.
56
57 Parameters
58 ----------
59 files: set
60 Of tuples, with (<local_name>, <remote_name>) format.
61 page_strings: dict
62 Containing the files data.
63
64 Returns
65 -------
66 generator
67 Of (<file_name>, <file_data>) tuples.
68
69 """
70 files_to_upload = []
71 for file in files:
72 page = os.path.splitext(file[0])[0]
73 files_to_upload.append((file[1], json.dumps(page_strings[page])))
74
75 return files_to_upload
76
77
78 def read_token():
79 """Read token from pre-defined environment variable."""
80 token = os.environ.get(cnts.TOKEN['env_var'])
81 if token:
82 return token
83
84 raise Exception(cnts.ErrorMessages.NO_TOKEN_PROVIDED.format(
85 cnts.TOKEN['creation_cmd'],
86 ))
87
88 # !!! QUESTION !!!: Do we want to add the option for the user to enter
89 # the token from the console, pretty much the same way as the password,
90 # when logging in?
91
92
93 def resolve_naming_conflicts(name):
94 """Handle project name conflicts.
95
96 Parameters
97 ----------
98 name: str
99 The name of the project.
100
101 Returns
102 -------
103 str
104 The new name of the project, with the length cut down to
105 cnts.PROJECT_NAME['max_length'] and invalid characters replaced with
106 cnts.PROJECT_NAME['name_wildcard'].
107
108 """
109 # !!!QUESTION!!!: https://gitlab.com/eyeo/websites/cms/issues/4 defined
110 # the naming guidelines when we were going with one project per
111 # issue approach. How do they change now that we have a project per
112 # website, instead of GitLab issue?
113 valid_name = ''.join(
114 [cnts.PROJECT_NAME['name_wildcard']
115 if c in cnts.PROJECT_NAME['invalid_chars'] else c for c in name],
116 )
117 if len(valid_name) > cnts.PROJECT_NAME['max_length']:
118 return valid_name[:cnts.PROJECT_NAME['max_length']]
119 return valid_name
120
121
122 def extract_strings(source):
123 """Extract strings from a website.
124
125 Parameters
126 ----------
127 source: cms.sources.Source
128 The source representing the website.
129
130 Returns
131 -------
132 dict
133 With the extracted strings.
134
135 """
136 logging.info(cnts.InfoMessages.EXTRACTING_STRINGS)
137 page_strings = {}
138
139 defaultlocale = source.read_config().get(
140 cnts.CONFIG['main_section'], cnts.CONFIG['default_locale_option'],
141 )
142
143 def record_string(page, locale, name, value, comment, fixed_strings):
144 if locale != defaultlocale:
145 return
146
147 try:
Vasily Kuznetsov 2018/09/24 16:32:11 We can use a `collections.defaultdict(OrderedDict)
Tudor Avram 2018/09/25 12:26:25 Done.
148 store = page_strings[page]
149 except KeyError:
150 store = page_strings[page] = OrderedDict()
151
152 store[name] = {'message': value}
153
154 if fixed_strings:
155 comment = comment + '\n' if comment else ''
156 comment += ', '.join('{{{0}}}: {1}'.format(*i_s)
157 for i_s in enumerate(fixed_strings, 1))
158 if comment:
159 store[name]['description'] = comment
160
161 for page, page_format in source.list_pages():
162 process_page(source, defaultlocale, page, format=page_format,
163 localized_string_callback=record_string)
164 return page_strings
165
166
167 def resolve_locales(api, source):
168 """Sync a website's locales with the target languages of the API.
169
170 Parameters
171 ----------
172 api: cms.bin.xtm_translations.xtm_api.XTMCloudAPI
173 Handler used to make requests to the API.
174 source: cms.sources.Source
175 Source representing a website.
176
177 """
178 logging.info(cnts.InfoMessages.RESOLVING_LOCALES)
179 local_locales = map_locales(source)
180 project_id = source.read_config().get(
181 cnts.CONFIG['XTM_section'], cnts.CONFIG['project_option'],
182 )
183
184 languages = run_and_wait(
185 api.get_target_languages,
186 XTMCloudException,
187 cnts.UNDER_ANALYSIS_MESSAGE,
188 cnts.InfoMessages.WAITING_FOR_PROJECT,
189 project_id=project_id,
190 )
191
192 enabled_locales = {l.encode('utf-8') for l in languages}
193
194 if len(enabled_locales - local_locales) != 0:
195 raise Exception(cnts.ErrorMessages.LOCALES_NOT_PRESENT.format(
196 enabled_locales - local_locales, project_id,
197 ))
198
199 if not local_locales == enabled_locales:
200 # Add languages to the project
201 langs_to_add = list(local_locales - enabled_locales)
202 logging.info(cnts.InfoMessages.ADDING_LANGUAGES.format(
203 project_id, langs_to_add,
204 ))
205 run_and_wait(
206 api.add_target_languages,
207 XTMCloudException,
208 cnts.UNDER_ANALYSIS_MESSAGE,
209 cnts.InfoMessages.WAITING_FOR_PROJECT,
210 project_id=project_id,
211 target_languages=langs_to_add,
212 )
213
214
215 def map_locales(source):
216 """Map website locale to target languages supported by XTM.
217
218 Parameters
219 ----------
220 source: cms.sources.Source
221 Source representing a website.
222
223 Returns
224 -------
225 set
226 Of the resulting mapped locales.
227
228 """
229 config = source.read_config()
230 defaultlocale = config.get(cnts.CONFIG['main_section'],
231 cnts.CONFIG['default_locale_option'])
232 locales = source.list_locales() - {defaultlocale}
233
234 mapped_locales = set()
235
236 for locale in locales:
237 if locale in cnts.SUPPORTED_LOCALES:
238 mapped_locales.add(locale)
239 else:
240 xtm_locale = '{0}_{1}'.format(locale, locale.upper())
241 if xtm_locale not in cnts.SUPPORTED_LOCALES:
Vasily Kuznetsov 2018/09/24 16:32:11 I think this if statement would feel more natural
Tudor Avram 2018/09/25 12:26:25 Done.
242 logging.warning(cnts.WarningMessages.UNSUPPORTED_LOCALE.format(
243 locale,
244 ))
245 else:
246 mapped_locales.add(xtm_locale)
247
248 return mapped_locales
249
250
251 def get_local_files(page_strings):
252 """List the local files and directories from the page_strings dictionary.
253
254 Parameters
255 ----------
256 page_strings: dir
257 The parsed strings for all the pages.
258
259 Returns
260 -------
261 set
262 With the local files. Each element in the set is a tuple with the
263 format:
264 (<local_file_name>, <remote_file_name>)
265
266 """
267 files = set()
268
269 for page, strings in page_strings.iteritems():
270 if strings:
271 page += '.json'
272 file = (
273 page,
274 page.replace(os.path.sep, cnts.FILENAMES['path_sep_rep']),
275 )
276 if len(file[1]) > cnts.FILENAMES['max_length']:
277 raise Exception(cnts.ErrorMessages.FILENAME_TOO_LONG.format(
278 file[0], cnts.FILENAMES['max_length'],
279 ))
280 files.add(file)
281
282 return files
283
284
285 def resolve_remote_filename(filename, source_dir, locales):
286 """Parse a remote filename and construct a local filesystem name.
287
288 Parameters
289 ----------
290 filename: str
291 The remote filename.
292 source_dir: str
293 The path to the source directory of the file.
294 locales: iterator
295 Of local language directories.
296
297 Returns
298 -------
299 str
300 The full path of the file.
301
302 """
303 path_elements = filename.split(cnts.FILENAMES['path_sep_rep'])
304 if path_elements[0] == '':
305 path_elements = path_elements[1:]
306 if path_elements[0] not in locales:
307 candidate_locale = path_elements[0].split('_')[0]
308 if candidate_locale not in locales:
309 raise Exception(cnts.ErrorMessages.CANT_RESOLVE_REMOTE_LANG.format(
310 path_elements[0],
311 ))
312
313 path_elements[0] = ''.join([
314 candidate_locale,
315 path_elements[0].split('_')[1][len(candidate_locale):],
316 ])
317
318 return os.path.join(source_dir, *path_elements)
319
320
321 def run_and_wait(func, exp, err_msg, user_msg=None, wait=1, max_tries=10,
322 step=1, **kw):
323 """Run a function and, if a specific exception occurs, try again.
324
325 Tries to run the function, and if an exception with a specific message
326 is raised, it sleeps and tries again.
327 Parameters
328 ----------
329 func: function
330 The function to be run.
331 wait: int
332 The amount of time to wait on this specific run
333 exp: Exception
334 The exception we expect to be raised.
335 err_msg: str
336 The message we expect to be in the exception.
337 user_msg: str
338 Message to be displayed to the user if we waited for more than 3 steps.
339 max_tries: int
340 The maximum number of tries until giving up.
341 step: int
342 The try we're at.
343 kw: dict
344 The keyword arguments for the function.
345
346 Returns
347 -------
348 The return result of the function.
349
350 """
351 if step == 3 and user_msg:
352 logging.info(user_msg)
353 try:
354 result = func(**kw)
355 return result
356 except exp as err:
357 if step > max_tries:
358 raise
359 if err_msg in str(err):
360 time.sleep(wait)
361 return run_and_wait(
362 func, exp, err_msg, user_msg,
363 min(wait * 2, cnts.MAX_WAIT_TIME), max_tries, step + 1, **kw
364 )
365 raise
366
367
368 def clear_files(dir_path, required_locales, extension='.json'):
369 """Delete files with an extension from the tree starting with dir_path.
370
371 Parameters
372 ----------
373 dir_path: str
374 Path to the root of the subtree we want to delete the files from.
375 required_locales: iterable
376 Only directories form required_locales will be included
377 extension: str
378 The extension of the files to delete.
379
380 """
381 for root, dirs, files in os.walk(dir_path, topdown=True):
382 if root == dir_path:
383 dirs[:] = [d for d in dirs if d in required_locales]
384 for f in files:
385 if f.lower().endswith(extension.lower()):
386 os.remove(os.path.join(root, f))
387
388
389 def get_locales(path, default):
390 """List the locales available in a website.
391
392 It will exclude the default language from the list.
393
394 Parameters
395 ----------
396 path: str
397 The path to the locales directory.
398 default: str
399 The default language for the website.
400
401 Returns
402 -------
403 iterable
404 Of the available locales.
405
406 """
407 full_contents = os.listdir(path)
408
409 return [
410 d for d in full_contents
411 if os.path.isdir(os.path.join(path, d)) and d != default
412 ]
413
414
415 def write_to_file(data, file_path):
416 """Write data to a given file path.
417
418 If the directory path does not exist, then it will be created by default.
419
420 Parameters
421 ----------
422 data: bytes
423 The data to be written to the file.
424 file_path: str
425 The path of the file we want to write.
426
427 """
428 dirs_path = os.path.join(*os.path.split(file_path)[:-1])
429 if not os.path.isdir(dirs_path):
430 os.makedirs(dirs_path)
431
432 with open(file_path, 'wb') as f:
433 f.write(data)
OLDNEW

Powered by Google App Engine
This is Rietveld