--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+In this file we have all the top level commands for the transifex client.
+Since we're using a way to automatically list them and execute them, when
+adding code to this file you must take care of the following:
+ * Added functions must begin with 'cmd_' followed by the actual name of the
+ command being used in the command line (eg cmd_init)
+ * The description for each function that we display to the user is read from
+ the func_doc attribute which reads the doc string. So, when adding
+ docstring to a new function make sure you add an oneliner which is
+ descriptive and is meant to be seen by the user.
+ * When including libraries, it's best if you include modules instead of
+ functions because that way our function resolution will work faster and the
+ chances of overlapping are minimal
+ * All functions should use the OptionParser and should have a usage and
+ descripition field.
+"""
+import os
+import re, shutil
+import sys
+from optparse import OptionParser, OptionGroup
+import ConfigParser
+
+
+from txclib import utils, project
+from txclib.utils import parse_json, compile_json, relpath
+from txclib.config import OrderedRawConfigParser
+from txclib.exceptions import UnInitializedError
+from txclib.parsers import delete_parser, help_parser, parse_csv_option, \
+ status_parser, pull_parser, set_parser, push_parser, init_parser
+from txclib.log import logger
+
+
+def cmd_init(argv, path_to_tx):
+ "Initialize a new transifex project."
+ parser = init_parser()
+ (options, args) = parser.parse_args(argv)
+ if len(args) > 1:
+ parser.error("Too many arguments were provided. Aborting...")
+ if args:
+ path_to_tx = args[0]
+ else:
+ path_to_tx = os.getcwd()
+
+ if os.path.isdir(os.path.join(path_to_tx,".tx")):
+ logger.info("tx: There is already a tx folder!")
+ reinit = raw_input("Do you want to delete it and reinit the project? [y/N]: ")
+ while (reinit != 'y' and reinit != 'Y' and reinit != 'N' and reinit != 'n' and reinit != ''):
+ reinit = raw_input("Do you want to delete it and reinit the project? [y/N]: ")
+ if not reinit or reinit in ['N', 'n', 'NO', 'no', 'No']:
+ return
+ # Clean the old settings
+ # FIXME: take a backup
+ else:
+ rm_dir = os.path.join(path_to_tx, ".tx")
+ shutil.rmtree(rm_dir)
+
+ logger.info("Creating .tx folder...")
+ os.mkdir(os.path.join(path_to_tx,".tx"))
+
+ # Handle the credentials through transifexrc
+ home = os.path.expanduser("~")
+ txrc = os.path.join(home, ".transifexrc")
+ config = OrderedRawConfigParser()
+
+ default_transifex = "https://www.transifex.com"
+ transifex_host = options.host or raw_input("Transifex instance [%s]: " % default_transifex)
+
+ if not transifex_host:
+ transifex_host = default_transifex
+ if not transifex_host.startswith(('http://', 'https://')):
+ transifex_host = 'https://' + transifex_host
+
+ config_file = os.path.join(path_to_tx, ".tx", "config")
+ if not os.path.exists(config_file):
+ # The path to the config file (.tx/config)
+ logger.info("Creating skeleton...")
+ config = OrderedRawConfigParser()
+ config.add_section('main')
+ config.set('main', 'host', transifex_host)
+ # Touch the file if it doesn't exist
+ logger.info("Creating config file...")
+ fh = open(config_file, 'w')
+ config.write(fh)
+ fh.close()
+
+ prj = project.Project(path_to_tx)
+ prj.getset_host_credentials(transifex_host, user=options.user,
+ password=options.password)
+ prj.save()
+ logger.info("Done.")
+
+
+def cmd_set(argv, path_to_tx):
+ "Add local or remote files under transifex"
+ parser = set_parser()
+ (options, args) = parser.parse_args(argv)
+
+ # Implement options/args checks
+ # TODO !!!!!!!
+ if options.local:
+ try:
+ expression = args[0]
+ except IndexError:
+ parser.error("Please specify an expression.")
+ if not options.resource:
+ parser.error("Please specify a resource")
+ if not options.source_language:
+ parser.error("Please specify a source language.")
+ if not '<lang>' in expression:
+ parser.error("The expression you have provided is not valid.")
+ if not utils.valid_slug(options.resource):
+ parser.error("Invalid resource slug. The format is <project_slug>"\
+ ".<resource_slug> and the valid characters include [_-\w].")
+ _auto_local(path_to_tx, options.resource,
+ source_language=options.source_language,
+ expression = expression, source_file=options.source_file,
+ execute=options.execute, regex=False)
+ if options.execute:
+ _set_minimum_perc(options.resource, options.minimum_perc, path_to_tx)
+ _set_mode(options.resource, options.mode, path_to_tx)
+ _set_type(options.resource, options.i18n_type, path_to_tx)
+ return
+
+ if options.remote:
+ try:
+ url = args[0]
+ except IndexError:
+ parser.error("Please specify an remote url")
+ _auto_remote(path_to_tx, url)
+ _set_minimum_perc(options.resource, options.minimum_perc, path_to_tx)
+ _set_mode(options.resource, options.mode, path_to_tx)
+ return
+
+ if options.is_source:
+ resource = options.resource
+ if not resource:
+ parser.error("You must specify a resource name with the"
+ " -r|--resource flag.")
+
+ lang = options.language
+ if not lang:
+ parser.error("Please specify a source language.")
+
+ if len(args) != 1:
+ parser.error("Please specify a file.")
+
+ if not utils.valid_slug(resource):
+ parser.error("Invalid resource slug. The format is <project_slug>"\
+ ".<resource_slug> and the valid characters include [_-\w].")
+
+ file = args[0]
+ # Calculate relative path
+ path_to_file = relpath(file, path_to_tx)
+ _set_source_file(path_to_tx, resource, options.language, path_to_file)
+ elif options.resource or options.language:
+ resource = options.resource
+ lang = options.language
+
+ if len(args) != 1:
+ parser.error("Please specify a file")
+
+ # Calculate relative path
+ path_to_file = relpath(args[0], path_to_tx)
+
+ try:
+ _go_to_dir(path_to_tx)
+ except UnInitializedError, e:
+ utils.logger.error(e)
+ return
+
+ if not utils.valid_slug(resource):
+ parser.error("Invalid resource slug. The format is <project_slug>"\
+ ".<resource_slug> and the valid characters include [_-\w].")
+ _set_translation(path_to_tx, resource, lang, path_to_file)
+
+ _set_mode(options.resource, options.mode, path_to_tx)
+ _set_type(options.resource, options.i18n_type, path_to_tx)
+ _set_minimum_perc(options.resource, options.minimum_perc, path_to_tx)
+
+ logger.info("Done.")
+ return
+
+
+def _auto_local(path_to_tx, resource, source_language, expression, execute=False,
+ source_file=None, regex=False):
+ """Auto configure local project."""
+ # The path everything will be relative to
+ curpath = os.path.abspath(os.curdir)
+
+ # Force expr to be a valid regex expr (escaped) but keep <lang> intact
+ expr_re = utils.regex_from_filefilter(expression, curpath)
+ expr_rec = re.compile(expr_re)
+
+ if not execute:
+ logger.info("Only printing the commands which will be run if the "
+ "--execute switch is specified.")
+
+ # First, let's construct a dictionary of all matching files.
+ # Note: Only the last matching file of a language will be stored.
+ translation_files = {}
+ for root, dirs, files in os.walk(curpath):
+ for f in files:
+ f_path = os.path.abspath(os.path.join(root, f))
+ match = expr_rec.match(f_path)
+ if match:
+ lang = match.group(1)
+ f_path = os.path.abspath(f_path)
+ if lang == source_language and not source_file:
+ source_file = f_path
+ else:
+ translation_files[lang] = f_path
+
+ if not source_file:
+ raise Exception("Could not find a source language file. Please run"
+ " set --source manually and then re-run this command or provide"
+ " the source file with the -s flag.")
+ if execute:
+ logger.info("Updating source for resource %s ( %s -> %s )." % (resource,
+ source_language, relpath(source_file, path_to_tx)))
+ _set_source_file(path_to_tx, resource, source_language,
+ relpath(source_file, path_to_tx))
+ else:
+ logger.info('\ntx set --source -r %(res)s -l %(lang)s %(file)s\n' % {
+ 'res': resource,
+ 'lang': source_language,
+ 'file': relpath(source_file, curpath)})
+
+ prj = project.Project(path_to_tx)
+ root_dir = os.path.abspath(path_to_tx)
+
+ if execute:
+ try:
+ prj.config.get("%s" % resource, "source_file")
+ except ConfigParser.NoSectionError:
+ raise Exception("No resource with slug \"%s\" was found.\nRun 'tx set --auto"
+ "-local -r %s \"expression\"' to do the initial configuration." % resource)
+
+ # Now let's handle the translation files.
+ if execute:
+ logger.info("Updating file expression for resource %s ( %s )." % (resource,
+ expression))
+ # Eval file_filter relative to root dir
+ file_filter = relpath(os.path.join(curpath, expression),
+ path_to_tx)
+ prj.config.set("%s" % resource, "file_filter", file_filter)
+ else:
+ for (lang, f_path) in sorted(translation_files.items()):
+ logger.info('tx set -r %(res)s -l %(lang)s %(file)s' % {
+ 'res': resource,
+ 'lang': lang,
+ 'file': relpath(f_path, curpath)})
+
+ if execute:
+ prj.save()
+
+
+def _auto_remote(path_to_tx, url):
+ """
+ Initialize a remote release/project/resource to the current directory.
+ """
+ logger.info("Auto configuring local project from remote URL...")
+
+ type, vars = utils.parse_tx_url(url)
+ prj = project.Project(path_to_tx)
+ username, password = prj.getset_host_credentials(vars['hostname'])
+
+ if type == 'project':
+ logger.info("Getting details for project %s" % vars['project'])
+ proj_info = utils.get_details('project_details',
+ username, password,
+ hostname = vars['hostname'], project = vars['project'])
+ resources = [ '.'.join([vars['project'], r['slug']]) for r in proj_info['resources'] ]
+ logger.info("%s resources found. Configuring..." % len(resources))
+ elif type == 'release':
+ logger.info("Getting details for release %s" % vars['release'])
+ rel_info = utils.get_details('release_details',
+ username, password, hostname = vars['hostname'],
+ project = vars['project'], release = vars['release'])
+ resources = []
+ for r in rel_info['resources']:
+ if r.has_key('project'):
+ resources.append('.'.join([r['project']['slug'], r['slug']]))
+ else:
+ resources.append('.'.join([vars['project'], r['slug']]))
+ logger.info("%s resources found. Configuring..." % len(resources))
+ elif type == 'resource':
+ logger.info("Getting details for resource %s" % vars['resource'])
+ resources = [ '.'.join([vars['project'], vars['resource']]) ]
+ else:
+ raise("Url '%s' is not recognized." % url)
+
+ for resource in resources:
+ logger.info("Configuring resource %s." % resource)
+ proj, res = resource.split('.')
+ res_info = utils.get_details('resource_details',
+ username, password, hostname = vars['hostname'],
+ project = proj, resource=res)
+ try:
+ source_lang = res_info['source_language_code']
+ i18n_type = res_info['i18n_type']
+ except KeyError:
+ raise Exception("Remote server seems to be running an unsupported version"
+ " of Transifex. Either update your server software of fallback"
+ " to a previous version of transifex-client.")
+ prj.set_remote_resource(
+ resource=resource,
+ host = vars['hostname'],
+ source_lang = source_lang,
+ i18n_type = i18n_type)
+
+ prj.save()
+
+
+def cmd_push(argv, path_to_tx):
+ "Push local files to remote server"
+ parser = push_parser()
+ (options, args) = parser.parse_args(argv)
+ force_creation = options.force_creation
+ languages = parse_csv_option(options.languages)
+ resources = parse_csv_option(options.resources)
+ skip = options.skip_errors
+ prj = project.Project(path_to_tx)
+ if not (options.push_source or options.push_translations):
+ parser.error("You need to specify at least one of the -s|--source,"
+ " -t|--translations flags with the push command.")
+
+ prj.push(
+ force=force_creation, resources=resources, languages=languages,
+ skip=skip, source=options.push_source,
+ translations=options.push_translations,
+ no_interactive=options.no_interactive
+ )
+ logger.info("Done.")
+
+
+def cmd_pull(argv, path_to_tx):
+ "Pull files from remote server to local repository"
+ parser = pull_parser()
+ (options, args) = parser.parse_args(argv)
+ if options.fetchall and options.languages:
+ parser.error("You can't user a language filter along with the"\
+ " -a|--all option")
+ languages = parse_csv_option(options.languages)
+ resources = parse_csv_option(options.resources)
+ skip = options.skip_errors
+ minimum_perc = options.minimum_perc or None
+
+ try:
+ _go_to_dir(path_to_tx)
+ except UnInitializedError, e:
+ utils.logger.error(e)
+ return
+
+ # instantiate the project.Project
+ prj = project.Project(path_to_tx)
+ prj.pull(
+ languages=languages, resources=resources, overwrite=options.overwrite,
+ fetchall=options.fetchall, fetchsource=options.fetchsource,
+ force=options.force, skip=skip, minimum_perc=minimum_perc,
+ mode=options.mode
+ )
+ logger.info("Done.")
+
+
+def _set_source_file(path_to_tx, resource, lang, path_to_file):
+ """Reusable method to set source file."""
+ proj, res = resource.split('.')
+ if not proj or not res:
+ raise Exception("\"%s.%s\" is not a valid resource identifier. It should"
+ " be in the following format project_slug.resource_slug." %
+ (proj, res))
+ if not lang:
+ raise Exception("You haven't specified a source language.")
+
+ try:
+ _go_to_dir(path_to_tx)
+ except UnInitializedError, e:
+ utils.logger.error(e)
+ return
+
+ if not os.path.exists(path_to_file):
+ raise Exception("tx: File ( %s ) does not exist." %
+ os.path.join(path_to_tx, path_to_file))
+
+ # instantiate the project.Project
+ prj = project.Project(path_to_tx)
+ root_dir = os.path.abspath(path_to_tx)
+
+ if root_dir not in os.path.normpath(os.path.abspath(path_to_file)):
+ raise Exception("File must be under the project root directory.")
+
+ logger.info("Setting source file for resource %s.%s ( %s -> %s )." % (
+ proj, res, lang, path_to_file))
+
+ path_to_file = relpath(path_to_file, root_dir)
+
+ prj = project.Project(path_to_tx)
+
+ # FIXME: Check also if the path to source file already exists.
+ try:
+ try:
+ prj.config.get("%s.%s" % (proj, res), "source_file")
+ except ConfigParser.NoSectionError:
+ prj.config.add_section("%s.%s" % (proj, res))
+ except ConfigParser.NoOptionError:
+ pass
+ finally:
+ prj.config.set("%s.%s" % (proj, res), "source_file",
+ path_to_file)
+ prj.config.set("%s.%s" % (proj, res), "source_lang",
+ lang)
+
+ prj.save()
+
+
+def _set_translation(path_to_tx, resource, lang, path_to_file):
+ """Reusable method to set translation file."""
+
+ proj, res = resource.split('.')
+ if not project or not resource:
+ raise Exception("\"%s\" is not a valid resource identifier. It should"
+ " be in the following format project_slug.resource_slug." %
+ resource)
+
+ try:
+ _go_to_dir(path_to_tx)
+ except UnInitializedError, e:
+ utils.logger.error(e)
+ return
+
+ # Warn the user if the file doesn't exist
+ if not os.path.exists(path_to_file):
+ logger.info("Warning: File '%s' doesn't exist." % path_to_file)
+
+ # instantiate the project.Project
+ prj = project.Project(path_to_tx)
+ root_dir = os.path.abspath(path_to_tx)
+
+ if root_dir not in os.path.normpath(os.path.abspath(path_to_file)):
+ raise Exception("File must be under the project root directory.")
+
+ if lang == prj.config.get("%s.%s" % (proj, res), "source_lang"):
+ raise Exception("tx: You cannot set translation file for the source language."
+ " Source languages contain the strings which will be translated!")
+
+ logger.info("Updating translations for resource %s ( %s -> %s )." % (resource,
+ lang, path_to_file))
+ path_to_file = relpath(path_to_file, root_dir)
+ prj.config.set("%s.%s" % (proj, res), "trans.%s" % lang,
+ path_to_file)
+
+ prj.save()
+
+
+def cmd_status(argv, path_to_tx):
+ "Print status of current project"
+ parser = status_parser()
+ (options, args) = parser.parse_args(argv)
+ resources = parse_csv_option(options.resources)
+ prj = project.Project(path_to_tx)
+ resources = prj.get_chosen_resources(resources)
+ resources_num = len(resources)
+ for idx, res in enumerate(resources):
+ p, r = res.split('.')
+ logger.info("%s -> %s (%s of %s)" % (p, r, idx + 1, resources_num))
+ logger.info("Translation Files:")
+ slang = prj.get_resource_option(res, 'source_lang')
+ sfile = prj.get_resource_option(res, 'source_file') or "N/A"
+ lang_map = prj.get_resource_lang_mapping(res)
+ logger.info(" - %s: %s (%s)" % (utils.color_text(slang, "RED"),
+ sfile, utils.color_text("source", "YELLOW")))
+ files = prj.get_resource_files(res)
+ fkeys = files.keys()
+ fkeys.sort()
+ for lang in fkeys:
+ local_lang = lang
+ if lang in lang_map.values():
+ local_lang = lang_map.flip[lang]
+ logger.info(" - %s: %s" % (utils.color_text(local_lang, "RED"),
+ files[lang]))
+ logger.info("")
+
+
+def cmd_help(argv, path_to_tx):
+ """List all available commands"""
+ parser = help_parser()
+ (options, args) = parser.parse_args(argv)
+ if len(args) > 1:
+ parser.error("Multiple arguments received. Exiting...")
+
+ # Get all commands
+ fns = utils.discover_commands()
+
+ # Print help for specific command
+ if len(args) == 1:
+ try:
+ fns[argv[0]](['--help'], path_to_tx)
+ except KeyError:
+ utils.logger.error("Command %s not found" % argv[0])
+ # or print summary of all commands
+
+ # the code below will only be executed if the KeyError exception is thrown
+ # becuase in all other cases the function called with --help will exit
+ # instead of return here
+ keys = fns.keys()
+ keys.sort()
+
+ logger.info("Transifex command line client.\n")
+ logger.info("Available commands are:")
+ for key in keys:
+ logger.info(" %-15s\t%s" % (key, fns[key].func_doc))
+ logger.info("\nFor more information run %s command --help" % sys.argv[0])
+
+
+def cmd_delete(argv, path_to_tx):
+ "Delete an accessible resource or translation in a remote server."
+ parser = delete_parser()
+ (options, args) = parser.parse_args(argv)
+ languages = parse_csv_option(options.languages)
+ resources = parse_csv_option(options.resources)
+ skip = options.skip_errors
+ force = options.force_delete
+ prj = project.Project(path_to_tx)
+ prj.delete(resources, languages, skip, force)
+ logger.info("Done.")
+
+
+def _go_to_dir(path):
+ """Change the current working directory to the directory specified as
+ argument.
+
+ Args:
+ path: The path to chdor to.
+ Raises:
+ UnInitializedError, in case the directory has not been initialized.
+ """
+ if path is None:
+ raise UnInitializedError(
+ "Directory has not been initialzied. "
+ "Did you forget to run 'tx init' first?"
+ )
+ os.chdir(path)
+
+
+def _set_minimum_perc(resource, value, path_to_tx):
+ """Set the minimum percentage in the .tx/config file."""
+ args = (resource, 'minimum_perc', value, path_to_tx, 'set_min_perc')
+ _set_project_option(*args)
+
+
+def _set_mode(resource, value, path_to_tx):
+ """Set the mode in the .tx/config file."""
+ args = (resource, 'mode', value, path_to_tx, 'set_default_mode')
+ _set_project_option(*args)
+
+
+def _set_type(resource, value, path_to_tx):
+ """Set the i18n type in the .tx/config file."""
+ args = (resource, 'type', value, path_to_tx, 'set_i18n_type')
+ _set_project_option(*args)
+
+
+def _set_project_option(resource, name, value, path_to_tx, func_name):
+ """Save the option to the project config file."""
+ if value is None:
+ return
+ if not resource:
+ logger.debug("Setting the %s for all resources." % name)
+ resources = []
+ else:
+ logger.debug("Setting the %s for resource %s." % (name, resource))
+ resources = [resource, ]
+ prj = project.Project(path_to_tx)
+ getattr(prj, func_name)(resources, value)
+ prj.save()