282287fc4ee960aaeb96f2f6b61f41d379bc761c
[pub/Android/ownCloud.git] / third_party / transifex-client / txclib / commands.py
1 # -*- coding: utf-8 -*-
2 """
3 In this file we have all the top level commands for the transifex client.
4 Since we're using a way to automatically list them and execute them, when
5 adding code to this file you must take care of the following:
6 * Added functions must begin with 'cmd_' followed by the actual name of the
7 command being used in the command line (eg cmd_init)
8 * The description for each function that we display to the user is read from
9 the func_doc attribute which reads the doc string. So, when adding
10 docstring to a new function make sure you add an oneliner which is
11 descriptive and is meant to be seen by the user.
12 * When including libraries, it's best if you include modules instead of
13 functions because that way our function resolution will work faster and the
14 chances of overlapping are minimal
15 * All functions should use the OptionParser and should have a usage and
16 descripition field.
17 """
18 import os
19 import re, shutil
20 import sys
21 from optparse import OptionParser, OptionGroup
22 import ConfigParser
23
24
25 from txclib import utils, project
26 from txclib.utils import parse_json, compile_json, relpath
27 from txclib.config import OrderedRawConfigParser
28 from txclib.exceptions import UnInitializedError
29 from txclib.parsers import delete_parser, help_parser, parse_csv_option, \
30 status_parser, pull_parser, set_parser, push_parser, init_parser
31 from txclib.log import logger
32
33
34 def cmd_init(argv, path_to_tx):
35 "Initialize a new transifex project."
36 parser = init_parser()
37 (options, args) = parser.parse_args(argv)
38 if len(args) > 1:
39 parser.error("Too many arguments were provided. Aborting...")
40 if args:
41 path_to_tx = args[0]
42 else:
43 path_to_tx = os.getcwd()
44
45 if os.path.isdir(os.path.join(path_to_tx,".tx")):
46 logger.info("tx: There is already a tx folder!")
47 reinit = raw_input("Do you want to delete it and reinit the project? [y/N]: ")
48 while (reinit != 'y' and reinit != 'Y' and reinit != 'N' and reinit != 'n' and reinit != ''):
49 reinit = raw_input("Do you want to delete it and reinit the project? [y/N]: ")
50 if not reinit or reinit in ['N', 'n', 'NO', 'no', 'No']:
51 return
52 # Clean the old settings
53 # FIXME: take a backup
54 else:
55 rm_dir = os.path.join(path_to_tx, ".tx")
56 shutil.rmtree(rm_dir)
57
58 logger.info("Creating .tx folder...")
59 os.mkdir(os.path.join(path_to_tx,".tx"))
60
61 # Handle the credentials through transifexrc
62 home = os.path.expanduser("~")
63 txrc = os.path.join(home, ".transifexrc")
64 config = OrderedRawConfigParser()
65
66 default_transifex = "https://www.transifex.com"
67 transifex_host = options.host or raw_input("Transifex instance [%s]: " % default_transifex)
68
69 if not transifex_host:
70 transifex_host = default_transifex
71 if not transifex_host.startswith(('http://', 'https://')):
72 transifex_host = 'https://' + transifex_host
73
74 config_file = os.path.join(path_to_tx, ".tx", "config")
75 if not os.path.exists(config_file):
76 # The path to the config file (.tx/config)
77 logger.info("Creating skeleton...")
78 config = OrderedRawConfigParser()
79 config.add_section('main')
80 config.set('main', 'host', transifex_host)
81 # Touch the file if it doesn't exist
82 logger.info("Creating config file...")
83 fh = open(config_file, 'w')
84 config.write(fh)
85 fh.close()
86
87 prj = project.Project(path_to_tx)
88 prj.getset_host_credentials(transifex_host, user=options.user,
89 password=options.password)
90 prj.save()
91 logger.info("Done.")
92
93
94 def cmd_set(argv, path_to_tx):
95 "Add local or remote files under transifex"
96 parser = set_parser()
97 (options, args) = parser.parse_args(argv)
98
99 # Implement options/args checks
100 # TODO !!!!!!!
101 if options.local:
102 try:
103 expression = args[0]
104 except IndexError:
105 parser.error("Please specify an expression.")
106 if not options.resource:
107 parser.error("Please specify a resource")
108 if not options.source_language:
109 parser.error("Please specify a source language.")
110 if not '<lang>' in expression:
111 parser.error("The expression you have provided is not valid.")
112 if not utils.valid_slug(options.resource):
113 parser.error("Invalid resource slug. The format is <project_slug>"\
114 ".<resource_slug> and the valid characters include [_-\w].")
115 _auto_local(path_to_tx, options.resource,
116 source_language=options.source_language,
117 expression = expression, source_file=options.source_file,
118 execute=options.execute, regex=False)
119 if options.execute:
120 _set_minimum_perc(options.resource, options.minimum_perc, path_to_tx)
121 _set_mode(options.resource, options.mode, path_to_tx)
122 _set_type(options.resource, options.i18n_type, path_to_tx)
123 return
124
125 if options.remote:
126 try:
127 url = args[0]
128 except IndexError:
129 parser.error("Please specify an remote url")
130 _auto_remote(path_to_tx, url)
131 _set_minimum_perc(options.resource, options.minimum_perc, path_to_tx)
132 _set_mode(options.resource, options.mode, path_to_tx)
133 return
134
135 if options.is_source:
136 resource = options.resource
137 if not resource:
138 parser.error("You must specify a resource name with the"
139 " -r|--resource flag.")
140
141 lang = options.language
142 if not lang:
143 parser.error("Please specify a source language.")
144
145 if len(args) != 1:
146 parser.error("Please specify a file.")
147
148 if not utils.valid_slug(resource):
149 parser.error("Invalid resource slug. The format is <project_slug>"\
150 ".<resource_slug> and the valid characters include [_-\w].")
151
152 file = args[0]
153 # Calculate relative path
154 path_to_file = relpath(file, path_to_tx)
155 _set_source_file(path_to_tx, resource, options.language, path_to_file)
156 elif options.resource or options.language:
157 resource = options.resource
158 lang = options.language
159
160 if len(args) != 1:
161 parser.error("Please specify a file")
162
163 # Calculate relative path
164 path_to_file = relpath(args[0], path_to_tx)
165
166 try:
167 _go_to_dir(path_to_tx)
168 except UnInitializedError, e:
169 utils.logger.error(e)
170 return
171
172 if not utils.valid_slug(resource):
173 parser.error("Invalid resource slug. The format is <project_slug>"\
174 ".<resource_slug> and the valid characters include [_-\w].")
175 _set_translation(path_to_tx, resource, lang, path_to_file)
176
177 _set_mode(options.resource, options.mode, path_to_tx)
178 _set_type(options.resource, options.i18n_type, path_to_tx)
179 _set_minimum_perc(options.resource, options.minimum_perc, path_to_tx)
180
181 logger.info("Done.")
182 return
183
184
185 def _auto_local(path_to_tx, resource, source_language, expression, execute=False,
186 source_file=None, regex=False):
187 """Auto configure local project."""
188 # The path everything will be relative to
189 curpath = os.path.abspath(os.curdir)
190
191 # Force expr to be a valid regex expr (escaped) but keep <lang> intact
192 expr_re = utils.regex_from_filefilter(expression, curpath)
193 expr_rec = re.compile(expr_re)
194
195 if not execute:
196 logger.info("Only printing the commands which will be run if the "
197 "--execute switch is specified.")
198
199 # First, let's construct a dictionary of all matching files.
200 # Note: Only the last matching file of a language will be stored.
201 translation_files = {}
202 for root, dirs, files in os.walk(curpath):
203 for f in files:
204 f_path = os.path.abspath(os.path.join(root, f))
205 match = expr_rec.match(f_path)
206 if match:
207 lang = match.group(1)
208 f_path = os.path.abspath(f_path)
209 if lang == source_language and not source_file:
210 source_file = f_path
211 else:
212 translation_files[lang] = f_path
213
214 if not source_file:
215 raise Exception("Could not find a source language file. Please run"
216 " set --source manually and then re-run this command or provide"
217 " the source file with the -s flag.")
218 if execute:
219 logger.info("Updating source for resource %s ( %s -> %s )." % (resource,
220 source_language, relpath(source_file, path_to_tx)))
221 _set_source_file(path_to_tx, resource, source_language,
222 relpath(source_file, path_to_tx))
223 else:
224 logger.info('\ntx set --source -r %(res)s -l %(lang)s %(file)s\n' % {
225 'res': resource,
226 'lang': source_language,
227 'file': relpath(source_file, curpath)})
228
229 prj = project.Project(path_to_tx)
230 root_dir = os.path.abspath(path_to_tx)
231
232 if execute:
233 try:
234 prj.config.get("%s" % resource, "source_file")
235 except ConfigParser.NoSectionError:
236 raise Exception("No resource with slug \"%s\" was found.\nRun 'tx set --auto"
237 "-local -r %s \"expression\"' to do the initial configuration." % resource)
238
239 # Now let's handle the translation files.
240 if execute:
241 logger.info("Updating file expression for resource %s ( %s )." % (resource,
242 expression))
243 # Eval file_filter relative to root dir
244 file_filter = relpath(os.path.join(curpath, expression),
245 path_to_tx)
246 prj.config.set("%s" % resource, "file_filter", file_filter)
247 else:
248 for (lang, f_path) in sorted(translation_files.items()):
249 logger.info('tx set -r %(res)s -l %(lang)s %(file)s' % {
250 'res': resource,
251 'lang': lang,
252 'file': relpath(f_path, curpath)})
253
254 if execute:
255 prj.save()
256
257
258 def _auto_remote(path_to_tx, url):
259 """
260 Initialize a remote release/project/resource to the current directory.
261 """
262 logger.info("Auto configuring local project from remote URL...")
263
264 type, vars = utils.parse_tx_url(url)
265 prj = project.Project(path_to_tx)
266 username, password = prj.getset_host_credentials(vars['hostname'])
267
268 if type == 'project':
269 logger.info("Getting details for project %s" % vars['project'])
270 proj_info = utils.get_details('project_details',
271 username, password,
272 hostname = vars['hostname'], project = vars['project'])
273 resources = [ '.'.join([vars['project'], r['slug']]) for r in proj_info['resources'] ]
274 logger.info("%s resources found. Configuring..." % len(resources))
275 elif type == 'release':
276 logger.info("Getting details for release %s" % vars['release'])
277 rel_info = utils.get_details('release_details',
278 username, password, hostname = vars['hostname'],
279 project = vars['project'], release = vars['release'])
280 resources = []
281 for r in rel_info['resources']:
282 if r.has_key('project'):
283 resources.append('.'.join([r['project']['slug'], r['slug']]))
284 else:
285 resources.append('.'.join([vars['project'], r['slug']]))
286 logger.info("%s resources found. Configuring..." % len(resources))
287 elif type == 'resource':
288 logger.info("Getting details for resource %s" % vars['resource'])
289 resources = [ '.'.join([vars['project'], vars['resource']]) ]
290 else:
291 raise("Url '%s' is not recognized." % url)
292
293 for resource in resources:
294 logger.info("Configuring resource %s." % resource)
295 proj, res = resource.split('.')
296 res_info = utils.get_details('resource_details',
297 username, password, hostname = vars['hostname'],
298 project = proj, resource=res)
299 try:
300 source_lang = res_info['source_language_code']
301 i18n_type = res_info['i18n_type']
302 except KeyError:
303 raise Exception("Remote server seems to be running an unsupported version"
304 " of Transifex. Either update your server software of fallback"
305 " to a previous version of transifex-client.")
306 prj.set_remote_resource(
307 resource=resource,
308 host = vars['hostname'],
309 source_lang = source_lang,
310 i18n_type = i18n_type)
311
312 prj.save()
313
314
315 def cmd_push(argv, path_to_tx):
316 "Push local files to remote server"
317 parser = push_parser()
318 (options, args) = parser.parse_args(argv)
319 force_creation = options.force_creation
320 languages = parse_csv_option(options.languages)
321 resources = parse_csv_option(options.resources)
322 skip = options.skip_errors
323 prj = project.Project(path_to_tx)
324 if not (options.push_source or options.push_translations):
325 parser.error("You need to specify at least one of the -s|--source,"
326 " -t|--translations flags with the push command.")
327
328 prj.push(
329 force=force_creation, resources=resources, languages=languages,
330 skip=skip, source=options.push_source,
331 translations=options.push_translations,
332 no_interactive=options.no_interactive
333 )
334 logger.info("Done.")
335
336
337 def cmd_pull(argv, path_to_tx):
338 "Pull files from remote server to local repository"
339 parser = pull_parser()
340 (options, args) = parser.parse_args(argv)
341 if options.fetchall and options.languages:
342 parser.error("You can't user a language filter along with the"\
343 " -a|--all option")
344 languages = parse_csv_option(options.languages)
345 resources = parse_csv_option(options.resources)
346 skip = options.skip_errors
347 minimum_perc = options.minimum_perc or None
348
349 try:
350 _go_to_dir(path_to_tx)
351 except UnInitializedError, e:
352 utils.logger.error(e)
353 return
354
355 # instantiate the project.Project
356 prj = project.Project(path_to_tx)
357 prj.pull(
358 languages=languages, resources=resources, overwrite=options.overwrite,
359 fetchall=options.fetchall, fetchsource=options.fetchsource,
360 force=options.force, skip=skip, minimum_perc=minimum_perc,
361 mode=options.mode
362 )
363 logger.info("Done.")
364
365
366 def _set_source_file(path_to_tx, resource, lang, path_to_file):
367 """Reusable method to set source file."""
368 proj, res = resource.split('.')
369 if not proj or not res:
370 raise Exception("\"%s.%s\" is not a valid resource identifier. It should"
371 " be in the following format project_slug.resource_slug." %
372 (proj, res))
373 if not lang:
374 raise Exception("You haven't specified a source language.")
375
376 try:
377 _go_to_dir(path_to_tx)
378 except UnInitializedError, e:
379 utils.logger.error(e)
380 return
381
382 if not os.path.exists(path_to_file):
383 raise Exception("tx: File ( %s ) does not exist." %
384 os.path.join(path_to_tx, path_to_file))
385
386 # instantiate the project.Project
387 prj = project.Project(path_to_tx)
388 root_dir = os.path.abspath(path_to_tx)
389
390 if root_dir not in os.path.normpath(os.path.abspath(path_to_file)):
391 raise Exception("File must be under the project root directory.")
392
393 logger.info("Setting source file for resource %s.%s ( %s -> %s )." % (
394 proj, res, lang, path_to_file))
395
396 path_to_file = relpath(path_to_file, root_dir)
397
398 prj = project.Project(path_to_tx)
399
400 # FIXME: Check also if the path to source file already exists.
401 try:
402 try:
403 prj.config.get("%s.%s" % (proj, res), "source_file")
404 except ConfigParser.NoSectionError:
405 prj.config.add_section("%s.%s" % (proj, res))
406 except ConfigParser.NoOptionError:
407 pass
408 finally:
409 prj.config.set("%s.%s" % (proj, res), "source_file",
410 path_to_file)
411 prj.config.set("%s.%s" % (proj, res), "source_lang",
412 lang)
413
414 prj.save()
415
416
417 def _set_translation(path_to_tx, resource, lang, path_to_file):
418 """Reusable method to set translation file."""
419
420 proj, res = resource.split('.')
421 if not project or not resource:
422 raise Exception("\"%s\" is not a valid resource identifier. It should"
423 " be in the following format project_slug.resource_slug." %
424 resource)
425
426 try:
427 _go_to_dir(path_to_tx)
428 except UnInitializedError, e:
429 utils.logger.error(e)
430 return
431
432 # Warn the user if the file doesn't exist
433 if not os.path.exists(path_to_file):
434 logger.info("Warning: File '%s' doesn't exist." % path_to_file)
435
436 # instantiate the project.Project
437 prj = project.Project(path_to_tx)
438 root_dir = os.path.abspath(path_to_tx)
439
440 if root_dir not in os.path.normpath(os.path.abspath(path_to_file)):
441 raise Exception("File must be under the project root directory.")
442
443 if lang == prj.config.get("%s.%s" % (proj, res), "source_lang"):
444 raise Exception("tx: You cannot set translation file for the source language."
445 " Source languages contain the strings which will be translated!")
446
447 logger.info("Updating translations for resource %s ( %s -> %s )." % (resource,
448 lang, path_to_file))
449 path_to_file = relpath(path_to_file, root_dir)
450 prj.config.set("%s.%s" % (proj, res), "trans.%s" % lang,
451 path_to_file)
452
453 prj.save()
454
455
456 def cmd_status(argv, path_to_tx):
457 "Print status of current project"
458 parser = status_parser()
459 (options, args) = parser.parse_args(argv)
460 resources = parse_csv_option(options.resources)
461 prj = project.Project(path_to_tx)
462 resources = prj.get_chosen_resources(resources)
463 resources_num = len(resources)
464 for idx, res in enumerate(resources):
465 p, r = res.split('.')
466 logger.info("%s -> %s (%s of %s)" % (p, r, idx + 1, resources_num))
467 logger.info("Translation Files:")
468 slang = prj.get_resource_option(res, 'source_lang')
469 sfile = prj.get_resource_option(res, 'source_file') or "N/A"
470 lang_map = prj.get_resource_lang_mapping(res)
471 logger.info(" - %s: %s (%s)" % (utils.color_text(slang, "RED"),
472 sfile, utils.color_text("source", "YELLOW")))
473 files = prj.get_resource_files(res)
474 fkeys = files.keys()
475 fkeys.sort()
476 for lang in fkeys:
477 local_lang = lang
478 if lang in lang_map.values():
479 local_lang = lang_map.flip[lang]
480 logger.info(" - %s: %s" % (utils.color_text(local_lang, "RED"),
481 files[lang]))
482 logger.info("")
483
484
485 def cmd_help(argv, path_to_tx):
486 """List all available commands"""
487 parser = help_parser()
488 (options, args) = parser.parse_args(argv)
489 if len(args) > 1:
490 parser.error("Multiple arguments received. Exiting...")
491
492 # Get all commands
493 fns = utils.discover_commands()
494
495 # Print help for specific command
496 if len(args) == 1:
497 try:
498 fns[argv[0]](['--help'], path_to_tx)
499 except KeyError:
500 utils.logger.error("Command %s not found" % argv[0])
501 # or print summary of all commands
502
503 # the code below will only be executed if the KeyError exception is thrown
504 # becuase in all other cases the function called with --help will exit
505 # instead of return here
506 keys = fns.keys()
507 keys.sort()
508
509 logger.info("Transifex command line client.\n")
510 logger.info("Available commands are:")
511 for key in keys:
512 logger.info(" %-15s\t%s" % (key, fns[key].func_doc))
513 logger.info("\nFor more information run %s command --help" % sys.argv[0])
514
515
516 def cmd_delete(argv, path_to_tx):
517 "Delete an accessible resource or translation in a remote server."
518 parser = delete_parser()
519 (options, args) = parser.parse_args(argv)
520 languages = parse_csv_option(options.languages)
521 resources = parse_csv_option(options.resources)
522 skip = options.skip_errors
523 force = options.force_delete
524 prj = project.Project(path_to_tx)
525 prj.delete(resources, languages, skip, force)
526 logger.info("Done.")
527
528
529 def _go_to_dir(path):
530 """Change the current working directory to the directory specified as
531 argument.
532
533 Args:
534 path: The path to chdor to.
535 Raises:
536 UnInitializedError, in case the directory has not been initialized.
537 """
538 if path is None:
539 raise UnInitializedError(
540 "Directory has not been initialzied. "
541 "Did you forget to run 'tx init' first?"
542 )
543 os.chdir(path)
544
545
546 def _set_minimum_perc(resource, value, path_to_tx):
547 """Set the minimum percentage in the .tx/config file."""
548 args = (resource, 'minimum_perc', value, path_to_tx, 'set_min_perc')
549 _set_project_option(*args)
550
551
552 def _set_mode(resource, value, path_to_tx):
553 """Set the mode in the .tx/config file."""
554 args = (resource, 'mode', value, path_to_tx, 'set_default_mode')
555 _set_project_option(*args)
556
557
558 def _set_type(resource, value, path_to_tx):
559 """Set the i18n type in the .tx/config file."""
560 args = (resource, 'type', value, path_to_tx, 'set_i18n_type')
561 _set_project_option(*args)
562
563
564 def _set_project_option(resource, name, value, path_to_tx, func_name):
565 """Save the option to the project config file."""
566 if value is None:
567 return
568 if not resource:
569 logger.debug("Setting the %s for all resources." % name)
570 resources = []
571 else:
572 logger.debug("Setting the %s for resource %s." % (name, resource))
573 resources = [resource, ]
574 prj = project.Project(path_to_tx)
575 getattr(prj, func_name)(resources, value)
576 prj.save()