Hello community, here is the log from the commit of package transifex-client for openSUSE:Factory checked in at 2017-05-06 18:30:00 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/transifex-client (Old) and /work/SRC/openSUSE:Factory/.transifex-client.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Package is "transifex-client" Sat May 6 18:30:00 2017 rev:14 rq:492639 version:0.12.4 Changes: -------- --- /work/SRC/openSUSE:Factory/transifex-client/transifex-client.changes 2016-09-27 13:43:58.000000000 +0200 +++ /work/SRC/openSUSE:Factory/.transifex-client.new/transifex-client.changes 2017-05-06 18:30:16.397962598 +0200 @@ -1,0 +2,7 @@ +Wed May 3 12:37:29 UTC 2017 - tchvatal@suse.com + +- Version update to 0.12.4: + * XLIFF and tokens support +- Switch to python3, no need to keep python2 around + +------------------------------------------------------------------- Old: ---- 0.12.2.tar.gz New: ---- 0.12.4.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ transifex-client.spec ++++++ --- /var/tmp/diff_new_pack.jj0nEV/_old 2017-05-06 18:30:17.497807404 +0200 +++ /var/tmp/diff_new_pack.jj0nEV/_new 2017-05-06 18:30:17.497807404 +0200 @@ -1,7 +1,7 @@ # # spec file for package transifex-client # -# Copyright (c) 2016 SUSE LINUX GmbH, Nuernberg, Germany. +# Copyright (c) 2017 SUSE LINUX GmbH, Nuernberg, Germany. # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -17,18 +17,18 @@ Name: transifex-client -Version: 0.12.2 +Version: 0.12.4 Release: 0 Summary: Transifex Command-line Client License: GPL-2.0 Group: Productivity/Text/Utilities Url: https://github.com/transifex/transifex-client Source: https://github.com/transifex/transifex-client/archive/%{version}.tar.gz -BuildRequires: python-mock -BuildRequires: python-setuptools -BuildRequires: python-urllib3 -Requires: python-setuptools -Requires: python-urllib3 +BuildRequires: python3-mock +BuildRequires: python3-setuptools +BuildRequires: python3-urllib3 +Requires: python3-setuptools +Requires: python3-urllib3 BuildArch: noarch %description @@ -46,20 +46,20 @@ %setup -q %build -python setup.py build +python3 setup.py build %install -python setup.py install --prefix=%{_prefix} --root=%{buildroot} +python3 setup.py install --prefix=%{_prefix} --root=%{buildroot} # remove pem file -rm %{buildroot}/%{python_sitelib}/txclib/cacert.pem +rm %{buildroot}/%{python3_sitelib}/txclib/cacert.pem %check -python setup.py test +python3 setup.py test %files %defattr(-,root,root) -%doc README.rst LICENSE +%doc README.md LICENSE %{_bindir}/tx -%{python_sitelib}/* +%{python3_sitelib}/* %changelog ++++++ 0.12.2.tar.gz -> 0.12.4.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/transifex-client-0.12.2/.gitignore new/transifex-client-0.12.4/.gitignore --- old/transifex-client-0.12.2/.gitignore 2016-08-09 16:39:55.000000000 +0200 +++ new/transifex-client-0.12.4/.gitignore 2017-02-06 17:05:52.000000000 +0100 @@ -5,3 +5,7 @@ *egg-info* /build /dist +*.swp +.tox/ +.coverage +.eggs diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/transifex-client-0.12.2/MANIFEST.in new/transifex-client-0.12.4/MANIFEST.in --- old/transifex-client-0.12.2/MANIFEST.in 2016-08-09 16:39:55.000000000 +0200 +++ new/transifex-client-0.12.4/MANIFEST.in 2017-02-06 17:05:52.000000000 +0100 @@ -3,7 +3,7 @@ include requirements.txt # Docs -include LICENSE README.rst +include LICENSE README.md recursive-include docs * # Tests diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/transifex-client-0.12.2/README.md new/transifex-client-0.12.4/README.md --- old/transifex-client-0.12.2/README.md 2016-08-09 16:39:55.000000000 +0200 +++ new/transifex-client-0.12.4/README.md 2017-02-06 17:05:52.000000000 +0100 @@ -3,6 +3,7 @@ [![image](https://circleci.com/gh/transifex/transifex-client/tree/master.svg?style=shield&circle-token=33aafd984726261eff1b73278a0cf761382c478a)](https://circleci.com/gh/transifex/transifex-client/tree/master) [![image](https://ci.appveyor.com/api/projects/status/github/transifex/transifex-client?branch=master&svg=true)](https://ci.appveyor.com/project/transifex/transifex-client/branch/master) [![codecov](https://codecov.io/gh/transifex/transifex-client/branch/master/graph/badge.svg)](https://codecov.io/gh/transifex/transifex-client) +[![PyPI version](https://badge.fury.io/py/transifex-client.svg)](https://badge.fury.io/py/transifex-client) Description --- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/transifex-client-0.12.2/README.rst new/transifex-client-0.12.4/README.rst --- old/transifex-client-0.12.2/README.rst 2016-08-09 16:39:55.000000000 +0200 +++ new/transifex-client-0.12.4/README.rst 1970-01-01 01:00:00.000000000 +0100 @@ -1,69 +0,0 @@ -.. image:: https://circleci.com/gh/transifex/transifex-client/tree/master.svg?style=shi... - :target: https://circleci.com/gh/transifex/transifex-client/tree/master -.. image:: https://ci.appveyor.com/api/projects/status/github/transifex/transifex-clien... - :target: https://ci.appveyor.com/project/transifex/transifex-client/branch/master -.. image:: https://codecov.io/gh/transifex/transifex-client/branch/master/graph/badge.s... - :target: https://codecov.io/gh/transifex/transifex-client - - - -============================= - Transifex Command-Line Tool -============================= - -The Transifex Command-line Tool enables you to manage your translations within a project without the need of an elaborate UI system. - -You can use the command line tool to create new resources, map locale files to translations, and synchronize your Transifex project with your local repository. Translators and localization managers can use it to handle large volumes of translation files. The Transifex Command-line Tool can help to enable continuous integration workflows and can be run from CI servers like Jenkins and Bamboo. - -Check the full documentation at http://docs.transifex.com/client/ - -Installing -========== - -You can install the latest version of transifex-client running ``pip -install transifex-client`` or ``easy_install transifex-client``. - - -Build transifex-client for Windows -================================== - -1. Download transifex-client sources via git or github archive. - - a. ``git clone https://github.com/transifex/transifex-client.git`` - b. Download and unpack https://github.com/transifex/transifex-client/archive/master.zip - -2. Download and install Python_. - - At this step choose right version of python: 2 or 3 and x86 or x86-64 instruction set. - - Make sure pip marked for installation(default for latest installers). - -3. Install PyInstaller_. - - Suppose that Python installed to ``C:\Program Files\Python35-32`` - - Make ``python.exe`` accessible via PATH environment variable or cd to directory containing python.exe. - - :: - - python -m pip install pyinstaller - - This command will install ``PyInstaller`` package and its dependencies. - -4. Build ``transifex-client`` distribution. - - Change directory to transifex-client folder and run command: - - :: - - python -m PyInstaller contrib/tx.spec - # or - pyinstaller contrib/tx.spec - -5. ``tx.exe`` - - ``dist/tx.exe`` will be created as the result of build process. - - -.. _Python: https://www.python.org/downloads/windows/ -.. _PyInstaller: http://www.pyinstaller.org diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/transifex-client-0.12.2/codecov.yml new/transifex-client-0.12.4/codecov.yml --- old/transifex-client-0.12.2/codecov.yml 2016-08-09 16:39:55.000000000 +0200 +++ new/transifex-client-0.12.4/codecov.yml 2017-02-06 17:05:52.000000000 +0100 @@ -14,7 +14,8 @@ notify: slack: default: - url: https://hooks.slack.com/services/T03KMKKP3/B1FDFATJ4/6VxCfmwcDxTuvcgMyV3Q5O1... + url: + secret:4FCetOj2urXyoT02ONiv62804o/mPp++K5/L3QlnSVdeovhfAQWVlROA+ZKPGdi0atAN+8lfoZIfk8qPY5JgGQOkcPMCoxVL+OaAgLWJlBdB7j5YfHERyf0LH6neP8E0qAqzk8xYFv3wqGSmcxwx1DRS5qx3LwZNuIoyF2Nhsg4= threshold: null branches: null attachments: "sunburst, diff" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/transifex-client-0.12.2/contrib/tx_commands.sh new/transifex-client-0.12.4/contrib/tx_commands.sh --- old/transifex-client-0.12.2/contrib/tx_commands.sh 2016-08-09 16:39:55.000000000 +0200 +++ new/transifex-client-0.12.4/contrib/tx_commands.sh 2017-02-06 17:05:52.000000000 +0100 @@ -6,12 +6,28 @@ # Exit on fail set -e +# Set up repo, tx config rm -rf txci git clone https://github.com/transifex/txci.git cd txci rm -rf .tx $TX init --host="https://www.transifex.com" --user=$TRANSIFEX_USER --pass=$TRANSIFEX_PASSWORD $TX set --auto-local -r txci.$BRANCH -s en 'locale/<lang>/LC_MESSAGES/django.po' -t PO --execute + +# push/pull without XLIFF $TX --traceback push -s $TX --traceback pull -l pt_BR -f + +# # Push dummy translation to pt_BR language +# cp locale/en/LC_MESSAGES/django.po locale/pt_BR/LC_MESSAGES/django.po +# yes | $TX --traceback push -t -l pt_BR -f +# +# # try to download translation xliff +# $TX --traceback pull -l pt_BR -f --xliff +# echo 'Checking if translation xlf file has been downloaded...' +# ls locale/pt_BR/LC_MESSAGES/django.po.xlf +# +# # upload xliff +# yes | $TX --traceback push -t -l pt_BR -f --xliff + $TX --traceback delete -f diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/transifex-client-0.12.2/setup.py new/transifex-client-0.12.4/setup.py --- old/transifex-client-0.12.2/setup.py 2016-08-09 16:39:55.000000000 +0200 +++ new/transifex-client-0.12.4/setup.py 2017-02-06 17:05:52.000000000 +0100 @@ -10,12 +10,13 @@ with open(filename, 'r', encoding='UTF-8') as f: return f.read() + setup( name="transifex-client", version=txclib.__version__, entry_points={'console_scripts': ['tx=txclib.cmdline:main']}, description="A command line interface for Transifex", - long_description=get_file_content('README.rst'), + long_description=get_file_content('README.md'), author="Transifex", author_email="admin@transifex.com", url="https://www.transifex.com", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/transifex-client-0.12.2/tests/test_project.py new/transifex-client-0.12.4/tests/test_project.py --- old/transifex-client-0.12.2/tests/test_project.py 2016-08-09 16:39:55.000000000 +0200 +++ new/transifex-client-0.12.4/tests/test_project.py 2017-02-06 17:05:52.000000000 +0100 @@ -8,12 +8,66 @@ import simplejson as json from mock import Mock, patch -from txclib.project import Project +from txclib.project import Project, ProjectNotInit from txclib.config import Flipdict class TestProject(unittest.TestCase): + @patch('txclib.utils.find_dot_tx') + def test_get_tx_dir_path(self, m_find_dot_tx): + """Test _get_tx_dir_path function""" + expected_path = '/tmp/' + m_find_dot_tx.return_value = expected_path + p = Project(init=False) + path = p._get_tx_dir_path(path_to_tx=None) + self.assertEqual(path, expected_path) + m_find_dot_tx.assert_called_once_with() + + expected_path = '/opt/' + path = p._get_tx_dir_path(path_to_tx=expected_path) + self.assertEqual(path, expected_path) + # make sure it has not been called twice + m_find_dot_tx.assert_called_once_with() + + @patch('os.path.exists') + def test_get_config_file_path(self, m_exists): + """Test _get_config_file_path function""" + p = Project(init=False) + m_exists.return_value = True + p._get_config_file_path('/tmp/') + m_exists.assert_called_once_with('/tmp/.tx/config') + + m_exists.return_value = False + with self.assertRaises(ProjectNotInit): + p._get_config_file_path('/tmp/') + + @patch('txclib.utils.confirm') + @patch('txclib.config.configparser') + def test_getset_host_credentials(self, m_parser, m_confirm): + p = Project(init=False) + # let suppose a token has been set at the config + dummy_token = 'salala' + p.txrc = m_parser + p.txrc.add_section = Mock() + p.txrc.set = Mock() + p.txrc.get = Mock() + p.txrc.get.side_effect = ['api', dummy_token, None, None] + p.txrc_file = '/tmp' + username, password = p.getset_host_credentials('test') + self.assertEqual(username, 'api') + self.assertEqual(password, dummy_token) + + # let's try to get credentials for someone without + # a token + p.txrc.get.side_effect = [ + 'username', + 'passw0rdz' + ] + username, password = p.getset_host_credentials('test') + self.assertEqual(username, 'username') + self.assertEqual(password, 'passw0rdz') + def test_extract_fields(self): """Test the functions that extract a field from a stats object.""" stats = { @@ -379,7 +433,6 @@ """Test finding new transaltions to add.""" with patch.object(self.p, 'do_url_request') as resource_mock: resource_mock.return_value = json.dumps(self.details), "utf-8" - files_keys = self.langs new_trans = self.p._new_translations_to_add for force in [True, False]: res = new_trans( @@ -510,8 +563,7 @@ res = self.p._should_download('en', self.stats, None, True) self.assertEqual(res, True) - with patch.object(self.p, '_remote_is_newer') as local_file_mock: - local_file_mock = False + with patch.object(self.p, '_remote_is_newer'): res = self.p._should_download('pt', self.stats, None, False) self.assertEqual(res, True) res = self.p._should_download('pt', self.stats, None, True) @@ -565,7 +617,6 @@ def test_i18n_type(self): p = Project(init=False) - type_string = 'type' i18n_type = 'PO' with patch.object(p, 'config', create=True) as config_mock: p.set_i18n_type([], i18n_type) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/transifex-client-0.12.2/txclib/__init__.py new/transifex-client-0.12.4/txclib/__init__.py --- old/transifex-client-0.12.2/txclib/__init__.py 2016-08-09 16:39:55.000000000 +0200 +++ new/transifex-client-0.12.4/txclib/__init__.py 2017-02-06 17:05:52.000000000 +0100 @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # https://www.python.org/dev/peps/pep-0440/#examples-of-compliant-version-sche... -__version__ = '0.12.2' +__version__ = '0.12.4' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/transifex-client-0.12.2/txclib/cmdline.py new/transifex-client-0.12.4/txclib/cmdline.py --- old/transifex-client-0.12.2/txclib/cmdline.py 2016-08-09 16:39:55.000000000 +0200 +++ new/transifex-client-0.12.4/txclib/cmdline.py 2017-02-06 17:05:52.000000000 +0100 @@ -104,6 +104,7 @@ try: utils.exec_command(cmd, args[1:], path_to_tx) except SSLError as e: + logger.error("SSl error %s" % e) sys.exit(1) except utils.UnknownCommandError: logger.error("tx: Command %s not found" % cmd) @@ -118,6 +119,7 @@ logger.error(formatted_lines[-1]) sys.exit(1) + # Run baby :) ... run if __name__ == "__main__": # sys.argv[0] is the name of the script that we’re running. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/transifex-client-0.12.2/txclib/commands.py new/transifex-client-0.12.4/txclib/commands.py --- old/transifex-client-0.12.2/txclib/commands.py 2016-08-09 16:39:55.000000000 +0200 +++ new/transifex-client-0.12.4/txclib/commands.py 2017-02-06 17:05:52.000000000 +0100 @@ -18,7 +18,6 @@ import re import shutil import sys -from optparse import OptionParser, OptionGroup try: import configparser @@ -28,7 +27,6 @@ from six.moves import input from txclib import utils, project -from txclib.utils import parse_json, compile_json, files_in_project from txclib.config import OrderedRawConfigParser from txclib.exceptions import UnInitializedError from txclib.parsers import delete_parser, help_parser, parse_csv_option, \ @@ -48,30 +46,26 @@ else: path_to_tx = os.getcwd() - if os.path.isdir(os.path.join(path_to_tx, ".tx")): + save = options.save + # if we already have a config file and we are not told to override it + # in the args we have to ask + if os.path.isdir(os.path.join(path_to_tx, ".tx")) and not save: logger.info("tx: There is already a tx folder!") - reinit = 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 = input("Do you want to delete it and " - "reinit the project? [y/N]: ") - if not reinit or reinit in ['N', 'n', 'NO', 'no', 'No']: + if not utils.confirm( + prompt='Do you want to delete it and reinit the project?', + default=False + ): return # Clean the old settings # FIXME: take a backup else: + save = True 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 input("Transifex instance [%s]: " % default_transifex) @@ -85,6 +79,7 @@ if not os.path.exists(config_file): # The path to the config file (.tx/config) logger.info("Creating skeleton...") + # Handle the credentials through transifexrc config = OrderedRawConfigParser() config.add_section('main') config.set('main', 'host', transifex_host) @@ -95,8 +90,9 @@ fh.close() prj = project.Project(path_to_tx) - prj.getset_host_credentials(transifex_host, user=options.user, - password=options.password) + prj.getset_host_credentials(transifex_host, username=options.user, + password=options.password, + token=options.token, save=save) prj.save() logger.info("Done.") @@ -213,7 +209,7 @@ # 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 f_path in files_in_project(curpath): + for f_path in utils.files_in_project(curpath): match = expr_rec.match(posix_path(f_path)) if match: lang = match.group(1) @@ -240,7 +236,6 @@ 'file': os.path.relpath(source_file, curpath)}) prj = project.Project(path_to_tx) - root_dir = os.path.abspath(path_to_tx) if execute: try: @@ -342,6 +337,7 @@ languages = parse_csv_option(options.languages) resources = parse_csv_option(options.resources) skip = options.skip_errors + xliff = options.xliff 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, " @@ -351,7 +347,8 @@ force=force_creation, resources=resources, languages=languages, skip=skip, source=options.push_source, translations=options.push_translations, - no_interactive=options.no_interactive + no_interactive=options.no_interactive, + xliff=xliff ) logger.info("Done.") @@ -366,6 +363,8 @@ languages = parse_csv_option(options.languages) resources = parse_csv_option(options.resources) pseudo = options.pseudo + # Should we download as xliff? + xliff = options.xliff skip = options.skip_errors minimum_perc = options.minimum_perc or None @@ -381,7 +380,7 @@ 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, pseudo=pseudo + mode=options.mode, pseudo=pseudo, xliff=xliff ) logger.info("Done.") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/transifex-client-0.12.2/txclib/parsers.py new/transifex-client-0.12.4/txclib/parsers.py --- old/transifex-client-0.12.2/txclib/parsers.py 2016-08-09 16:39:55.000000000 +0200 +++ new/transifex-client-0.12.4/txclib/parsers.py 2017-02-06 17:05:52.000000000 +0100 @@ -67,6 +67,17 @@ help="Specify username for Transifex server.") parser.add_option("--pass", action="store", dest="password", default=None, help="Specify password for Transifex server.") + parser.add_option( + "--force-save", + action="store_true", + dest="save", + default=False, + help="Override .transifexrc file with the given credentials." + ) + + parser.add_option("--token", action="store", dest="token", default=None, + help="Specify an api token.\nYou can get one from" + " user's settings") return parser @@ -118,6 +129,9 @@ "'reviewed'). See http://bit.ly/pullmode for available values." ) ) + parser.add_option("-x", "--xliff", action="store_true", dest="xliff", + default=False, help="Apply this option to download " + "file as xliff.") return parser @@ -156,6 +170,9 @@ parser.add_option("--no-interactive", action="store_true", dest="no_interactive", default=False, help="Don't require user input when forcing a push.") + parser.add_option("-x", "--xliff", action="store_true", dest="xliff", + default=False, help="Apply this option to upload " + "file as xliff.") return parser diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/transifex-client-0.12.2/txclib/project.py new/transifex-client-0.12.4/txclib/project.py --- old/transifex-client-0.12.2/txclib/project.py 2016-08-09 16:39:55.000000000 +0200 +++ new/transifex-client-0.12.4/txclib/project.py 2017-02-06 17:05:52.000000000 +0100 @@ -11,6 +11,12 @@ import six try: + import urlparse + from urllib import urlencode +except: # For Python 3 + import urllib.parse as urlparse + from urllib.parse import urlencode +try: import configparser except ImportError: import ConfigParser as configparser @@ -24,8 +30,7 @@ from txclib.log import logger from txclib.processors import visit_hostname from txclib.paths import posix_path, native_path, posix_sep - - +from txclib.utils import confirm class ProjectNotInit(Exception): @@ -147,36 +152,90 @@ mask = os.umask(0o077) open(txrc_file, 'w').close() os.umask(mask) + if os.path.exists(txrc_file): + logger.info('Created %s ' % txrc_file) + else: + logger.info('Could not create %s ' % txrc_file) return txrc_file def validate_config(self): """To ensure the json structure is correctly formed.""" pass - def getset_host_credentials(self, host, user=None, password=None): + def getset_host_credentials( + self, + host, + username=None, + password=None, + token=None, + save=False + ): """Read .transifexrc and report user, - pass for a specific host else ask the user for input. + pass or a token for a specific host else ask the user for input. """ - try: - username = self.txrc.get(host, 'username') - passwd = self.txrc.get(host, 'password') - except (configparser.NoOptionError, configparser.NoSectionError): - logger.info("No entry found for host %s. Creating..." % host) - username = user or input("Please enter your transifex username: ") - while (not username): - username = input("Please enter your transifex username: ") - passwd = password - while (not passwd): - passwd = getpass.getpass() + # from_config is a flag that tells us if we got the credentials + # from the config file + from_config = False + # first check if a token has been given it should override everything + if token: + password = token + username = 'api' + # if neither a token nor a username or a password were given + # try to get them from the rc file + elif not (username and password): + try: + username = self.txrc.get(host, 'username') + password = self.txrc.get(host, 'password') + except (configparser.NoOptionError, configparser.NoSectionError): + # if the rc has no credentials, we have to ask the user and + # update the rc file + save = True + # Ask the user if they have an api token + if confirm( + prompt="\nDid you know that you can create an api" + "token under your transifex user's settings?\n" + "(Read more at https://docs.transifex.com/api/" + "introduction#authentication)\n" + "So, do you have an api token?", + default=False + + ): + token_msg = "Please enter your api token: " + while not token: + token = input(token_msg) + # Since we got a token, we use api as the username + # and the token as the password + username = 'api' + password = token + else: + username_msg = "Please enter your transifex username: " + while not username: + username = input(username_msg) + while (not password): + password = getpass.getpass() + else: + from_config = True + + # lets see if there is a default username or a password + # unless we got the files from the config + if not from_config: + try: + username = self.txrc.get(host, 'username') + password = self.txrc.get(host, 'password') + except (configparser.NoOptionError, configparser.NoSectionError): + # if we do not have defaults, save the give credentials + save = True + if save: logger.info("Updating %s file..." % self.txrc_file) - self.txrc.add_section(host) + if not self.txrc.has_section(host): + logger.info("No entry found for host %s. Creating..." % host) + self.txrc.add_section(host) self.txrc.set(host, 'username', username) - self.txrc.set(host, 'password', passwd) - self.txrc.set(host, 'token', '') + self.txrc.set(host, 'password', password) self.txrc.set(host, 'hostname', host) - - return username, passwd + self.save() + return username, password def set_remote_resource(self, resource, source_lang, i18n_type, host, file_filter=None): @@ -261,7 +320,7 @@ else: return native_path(source_file) - def get_resource_files(self, resource): + def get_resource_files(self, resource, xliff=False): """Get a dict for all files assigned to a resource. First we calculate the files matching the file expression and then we apply all translation excpetions. @@ -279,6 +338,9 @@ file_filter = self.config.get(resource, "file_filter") except configparser.NoOptionError: file_filter = "$^" + if xliff: + # update the file-path in case of xliff option + file_filter += '.xlf' source_lang = self.config.get(resource, "source_lang") source_file = self.get_source_file(resource) expr_re = utils.regex_from_filefilter(file_filter, self.root) @@ -380,11 +442,12 @@ def pull(self, languages=[], resources=[], overwrite=True, fetchall=False, fetchsource=False, force=False, skip=False, minimum_perc=0, - mode=None, pseudo=False): + mode=None, pseudo=False, xliff=False): """Pull all translations file from transifex server.""" self.minimum_perc = minimum_perc resource_list = self.get_chosen_resources(resources) skip_decode = False + params = {} if mode == 'reviewed': url = 'pull_reviewed_file' @@ -472,8 +535,13 @@ if pull_languages: logger.debug("Pulling languages for: %s" % pull_languages) msg = "Pulling translations for resource %s (source: %s)" + if xliff: + msg += " [xliff format]" logger.info(msg % (resource, sfile)) + if xliff: + params.update({'file': 'xliff'}) + for lang in pull_languages: local_lang = lang if lang in list(lang_map.values()): @@ -496,7 +564,9 @@ 'force': force, 'mode': mode, } - if not self._should_update_translation(**kwargs): + + # xliff files should be always pulled + if not xliff and not self._should_update_translation(**kwargs): msg = "Skipping '%s' translation (file: %s)." logger.info( msg % (utils.color_text(remote_lang, "RED"), @@ -504,6 +574,9 @@ ) continue + if xliff: + local_file += '.xlf' + if not overwrite: local_file = ("%s.new" % local_file) logger.warning( @@ -512,7 +585,8 @@ ) try: r, charset = self.do_url_request( - url, language=remote_lang, skip_decode=skip_decode + url, language=remote_lang, skip_decode=skip_decode, + params=params ) except Exception as e: if isinstance(e, SSLError) or not skip: @@ -562,20 +636,30 @@ ) r, charset = self.do_url_request( - url, language=remote_lang, skip_decode=skip_decode + url, language=remote_lang, skip_decode=skip_decode, + params=params ) + if xliff: + local_file += '.xlf' + self._save_file(local_file, charset, r) def push(self, source=False, translations=False, force=False, - resources=[], languages=[], skip=False, no_interactive=False): + resources=[], languages=[], skip=False, no_interactive=False, + xliff=False): """Push all the resources""" resource_list = self.get_chosen_resources(resources) self.skip = skip self.force = force + + params = {} + if xliff: + params.update({'file_type': 'xliff'}) + for resource in resource_list: push_languages = [] project_slug, resource_slug = resource.split('.', 1) - files = self.get_resource_files(resource) + files = self.get_resource_files(resource, xliff=xliff) slang = self.get_resource_option(resource, 'source_lang') sfile = self.get_source_file(resource) lang_map = self.get_resource_lang_mapping(resource) @@ -624,6 +708,7 @@ files=[("%s;%s" % (resource_slug, slang), self.get_full_path(sfile) )], + params=params, ) except Exception as e: if isinstance(e, SSLError) or not skip: @@ -695,7 +780,8 @@ 'push_translation', multipart=True, method='PUT', files=[("%s;%s" % (resource_slug, remote_lang), self.get_full_path(local_file) - )], language=remote_lang + )], language=remote_lang, + params=params, ) logger.debug("Translation %s pushed." % remote_lang) except utils.HttpNotFound: @@ -818,17 +904,17 @@ raise def do_url_request(self, api_call, multipart=False, data=None, - files=[], method="GET", skip_decode=False, **kwargs): + files=[], method="GET", skip_decode=False, + params={}, **kwargs): """Issues a url request.""" # Read the credentials from the config file (.transifexrc) host = self.url_info['host'] + username, passwd = self.getset_host_credentials(host) try: - username = self.txrc.get(host, 'username') - passwd = self.txrc.get(host, 'password') hostname = self.txrc.get(host, 'hostname') except configparser.NoSectionError: raise Exception( - "No user credentials found for host %s. Edit" + "No entry found for host %s. Edit" " ~/.transifexrc and add the appropriate" " info in there." % host ) @@ -838,6 +924,18 @@ kwargs.update(self.url_info) url = API_URLS[api_call] % kwargs + # in case of GET we need to add xliff option as get parameter + if params and method == 'GET': + # update url params + # in case we need to add extra params on a url, we first get the + # already existing query, create a dict which will be merged with + # the extra params and finally put it back in the url + url_parts = list(urlparse.urlparse(url)) + query = dict(urlparse.parse_qsl(url_parts[4])) + query.update(params) + url_parts[4] = urlencode(query) + url = urlparse.urlunparse(url_parts) + if multipart: for info, filename in files: # FIXME: It works because we only pass to files argument @@ -848,6 +946,9 @@ "language": info.split(';')[1], "uploaded_file": (name, open(filename, 'rb').read()) } + # in case of PUT we add xliff option as form data + if method == 'PUT': + data.update(params) return utils.make_request( method, hostname, url, username, passwd, data, skip_decode=skip_decode diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/transifex-client-0.12.2/txclib/utils.py new/transifex-client-0.12.4/txclib/utils.py --- old/transifex-client-0.12.2/txclib/utils.py 2016-08-09 16:39:55.000000000 +0200 +++ new/transifex-client-0.12.4/txclib/utils.py 2017-02-06 17:05:52.000000000 +0100 @@ -1,4 +1,5 @@ from __future__ import unicode_literals +import functools import os import sys import re @@ -6,18 +7,19 @@ import urllib3 import collections import six -import ssl try: - from json import loads as parse_json, dumps as compile_json + from json import loads as parse_json except ImportError: - from simplejson import loads as parse_json, dumps as compile_json + from simplejson import loads as parse_json from email.parser import Parser from urllib3.exceptions import SSLError from six.moves import input from txclib.urls import API_URLS -from txclib.exceptions import UnknownCommandError, HttpNotFound, HttpNotAuthorized +from txclib.exceptions import ( + UnknownCommandError, HttpNotFound, HttpNotAuthorized +) from txclib.paths import posix_path, native_path, posix_sep from txclib.web import user_agent_identifier, certs_file from txclib.log import logger @@ -105,7 +107,7 @@ def make_request(method, host, url, username, password, fields=None, - skip_decode=False): + skip_decode=False, get_params={}): # Initialize http and https pool managers num_pools = 1 @@ -153,7 +155,9 @@ response = None try: manager = managers[scheme] - response = manager.request( + # All arguments must be bytes, not unicode + encoded_request = encode_args(manager.request) + response = encoded_request( method, host + url, headers=dict(headers), @@ -183,7 +187,6 @@ def get_details(api_call, username, password, *args, **kwargs): """ Get the tx project info through the API. - This function can also be used to check the existence of a project. """ url = API_URLS[api_call] % kwargs @@ -200,7 +203,6 @@ def valid_slug(slug): """ Check if a slug contains only valid characters. - Valid chars include [-_\w] """ try: @@ -258,7 +260,6 @@ def confirm(prompt='Continue?', default=True): """ Prompt the user for a Yes/No answer. - Args: prompt: The text displayed to the user ([Y/n] will be appended) default: If the default value will be yes or no @@ -266,10 +267,10 @@ valid_yes = ['Y', 'y', 'Yes', 'yes', ] valid_no = ['N', 'n', 'No', 'no', ] if default: - prompt = prompt + '[Y/n]' + prompt = prompt + ' [Y/n]: ' valid_yes.append('') else: - prompt = prompt + '[y/N]' + prompt = prompt + ' [y/N]: ' valid_no.append('') ans = input(prompt) @@ -294,7 +295,6 @@ This command can be used to colorify command line output. If the shell doesn't support this or the --disable-colors options has been set, it just returns the plain text. - Usage: print "%s" % color_text("This text is red", "RED") """ @@ -308,7 +308,6 @@ def files_in_project(curpath): """ Iterate over the files in the project. - Return each file under ``curpath`` with its absolute name. """ visited = set() @@ -333,3 +332,47 @@ ) for removal in removals: dirs.remove(removal) + + +def encode_args(func): + # we have to patch func in order to make tests work. + # sadly mock does not have the attributes needed for functools + # so we need to set the manually + if not hasattr(func, '__name__'): + functools.update_wrapper(func, str.split, ('__name__', )) + + @functools.wraps(func) + def decorated(*args, **kwargs): + new_args = _encode_anything(args) + new_kwargs = _encode_anything(kwargs, keys_as_unicode=True) + return func(*new_args, **new_kwargs) + return decorated + + +def _encode_anything(thing, encoding='utf-8', keys_as_unicode=False): + # Handle python versions + if sys.version_info.major == 3: + return thing + + if isinstance(thing, str): + return thing + elif isinstance(thing, unicode): + return thing.encode(encoding) + elif isinstance(thing, list): + return [_encode_anything(item) for item in thing] + elif isinstance(thing, tuple): + return tuple(_encode_anything(list(thing))) + elif isinstance(thing, dict): + # I know this is weird, but when using kwargs in python-3, the keys + # should be str, not bytes + if keys_as_unicode: + return {key: _encode_anything(value) + for key, value in thing.items()} + else: + return {_encode_anything(key): _encode_anything(value) + for key, value in thing.items()} + elif thing is None: + return thing + else: + raise TypeError("Could not encode, unknown type: {}". + format(type(thing)))