282287fc4ee960aaeb96f2f6b61f41d379bc761c
1 # -*- coding: utf-8 -*-
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
21 from optparse
import OptionParser
, OptionGroup
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
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
)
39 parser
.error("Too many arguments were provided. Aborting...")
43 path_to_tx
= os
.getcwd()
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']:
52 # Clean the old settings
53 # FIXME: take a backup
55 rm_dir
= os
.path
.join(path_to_tx
, ".tx")
58 logger
.info("Creating .tx folder...")
59 os
.mkdir(os
.path
.join(path_to_tx
,".tx"))
61 # Handle the credentials through transifexrc
62 home
= os
.path
.expanduser("~")
63 txrc
= os
.path
.join(home
, ".transifexrc")
64 config
= OrderedRawConfigParser()
66 default_transifex
= "https://www.transifex.com"
67 transifex_host
= options
.host
or raw_input("Transifex instance [%s]: " % default_transifex
)
69 if not transifex_host
:
70 transifex_host
= default_transifex
71 if not transifex_host
.startswith(('http://', 'https://')):
72 transifex_host
= 'https://' + transifex_host
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')
87 prj
= project
.Project(path_to_tx
)
88 prj
.getset_host_credentials(transifex_host
, user
=options
.user
,
89 password
=options
.password
)
94 def cmd_set(argv
, path_to_tx
):
95 "Add local or remote files under transifex"
97 (options
, args
) = parser
.parse_args(argv
)
99 # Implement options/args checks
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)
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
)
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
)
135 if options
.is_source
:
136 resource
= options
.resource
138 parser
.error("You must specify a resource name with the"
139 " -r|--resource flag.")
141 lang
= options
.language
143 parser
.error("Please specify a source language.")
146 parser
.error("Please specify a file.")
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].")
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
161 parser
.error("Please specify a file")
163 # Calculate relative path
164 path_to_file
= relpath(args
[0], path_to_tx
)
167 _go_to_dir(path_to_tx
)
168 except UnInitializedError
, e
:
169 utils
.logger
.error(e
)
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
)
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
)
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
)
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
)
196 logger
.info("Only printing the commands which will be run if the "
197 "--execute switch is specified.")
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
):
204 f_path
= os
.path
.abspath(os
.path
.join(root
, f
))
205 match
= expr_rec
.match(f_path
)
207 lang
= match
.group(1)
208 f_path
= os
.path
.abspath(f_path
)
209 if lang
== source_language
and not source_file
:
212 translation_files
[lang
] = f_path
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.")
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
))
224 logger
.info('\ntx set --source -r %(res)s -l %(lang)s %(file)s\n' %
{
226 'lang': source_language
,
227 'file': relpath(source_file
, curpath
)})
229 prj
= project
.Project(path_to_tx
)
230 root_dir
= os
.path
.abspath(path_to_tx
)
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
)
239 # Now let's handle the translation files.
241 logger
.info("Updating file expression for resource %s ( %s )." %
(resource
,
243 # Eval file_filter relative to root dir
244 file_filter
= relpath(os
.path
.join(curpath
, expression
),
246 prj
.config
.set("%s" % resource
, "file_filter", file_filter
)
248 for (lang
, f_path
) in sorted(translation_files
.items()):
249 logger
.info('tx set -r %(res)s -l %(lang)s %(file)s' %
{
252 'file': relpath(f_path
, curpath
)})
258 def _auto_remote(path_to_tx
, url
):
260 Initialize a remote release/project/resource to the current directory.
262 logger
.info("Auto configuring local project from remote URL...")
264 type, vars = utils
.parse_tx_url(url
)
265 prj
= project
.Project(path_to_tx
)
266 username
, password
= prj
.getset_host_credentials(vars['hostname'])
268 if type == 'project':
269 logger
.info("Getting details for project %s" %
vars['project'])
270 proj_info
= utils
.get_details('project_details',
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'])
281 for r
in rel_info
['resources']:
282 if r
.has_key('project'):
283 resources
.append('.'.join([r
['project']['slug'], r
['slug']]))
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']]) ]
291 raise("Url '%s' is not recognized." % url
)
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
)
300 source_lang
= res_info
['source_language_code']
301 i18n_type
= res_info
['i18n_type']
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(
308 host
= vars['hostname'],
309 source_lang
= source_lang
,
310 i18n_type
= i18n_type
)
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.")
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
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"\
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
350 _go_to_dir(path_to_tx
)
351 except UnInitializedError
, e
:
352 utils
.logger
.error(e
)
355 # instantiate the project.Project
356 prj
= project
.Project(path_to_tx
)
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
,
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." %
374 raise Exception("You haven't specified a source language.")
377 _go_to_dir(path_to_tx
)
378 except UnInitializedError
, e
:
379 utils
.logger
.error(e
)
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
))
386 # instantiate the project.Project
387 prj
= project
.Project(path_to_tx
)
388 root_dir
= os
.path
.abspath(path_to_tx
)
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.")
393 logger
.info("Setting source file for resource %s.%s ( %s -> %s )." %
(
394 proj
, res
, lang
, path_to_file
))
396 path_to_file
= relpath(path_to_file
, root_dir
)
398 prj
= project
.Project(path_to_tx
)
400 # FIXME: Check also if the path to source file already exists.
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
:
409 prj
.config
.set("%s.%s" %
(proj
, res
), "source_file",
411 prj
.config
.set("%s.%s" %
(proj
, res
), "source_lang",
417 def _set_translation(path_to_tx
, resource
, lang
, path_to_file
):
418 """Reusable method to set translation file."""
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." %
427 _go_to_dir(path_to_tx
)
428 except UnInitializedError
, e
:
429 utils
.logger
.error(e
)
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
)
436 # instantiate the project.Project
437 prj
= project
.Project(path_to_tx
)
438 root_dir
= os
.path
.abspath(path_to_tx
)
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.")
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!")
447 logger
.info("Updating translations for resource %s ( %s -> %s )." %
(resource
,
449 path_to_file
= relpath(path_to_file
, root_dir
)
450 prj
.config
.set("%s.%s" %
(proj
, res
), "trans.%s" % lang
,
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
)
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"),
485 def cmd_help(argv
, path_to_tx
):
486 """List all available commands"""
487 parser
= help_parser()
488 (options
, args
) = parser
.parse_args(argv
)
490 parser
.error("Multiple arguments received. Exiting...")
493 fns
= utils
.discover_commands()
495 # Print help for specific command
498 fns
[argv
[0]](['--help'], path_to_tx
)
500 utils
.logger
.error("Command %s not found" % argv
[0])
501 # or print summary of all commands
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
509 logger
.info("Transifex command line client.\n")
510 logger
.info("Available commands are:")
512 logger
.info(" %-15s\t%s" %
(key
, fns
[key
].func_doc
))
513 logger
.info("\nFor more information run %s command --help" % sys
.argv
[0])
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
)
529 def _go_to_dir(path
):
530 """Change the current working directory to the directory specified as
534 path: The path to chdor to.
536 UnInitializedError, in case the directory has not been initialized.
539 raise UnInitializedError(
540 "Directory has not been initialzied. "
541 "Did you forget to run 'tx init' first?"
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
)
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
)
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
)
564 def _set_project_option(resource
, name
, value
, path_to_tx
, func_name
):
565 """Save the option to the project config file."""
569 logger
.debug("Setting the %s for all resources." % name
)
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
)