+++ /dev/null
-.tx
-*pyc
-*pyo
-*~
-*egg-info*
+++ /dev/null
-Releasing
-=========
-
-To create a new release:
-
-1. Update local rep and update the version in ``setup.py``::
-
- $ hg pull -u
- $ vim setup.py
-
-2. Test::
-
- $ python setup.py clean sdist
- $ cd dist
- $ tar zxf ...
- $ cd transifex-client
- ...test
-
-3. Package and upload on PyPI::
-
- $ python setup.py clean sdist bdist_egg upload
+++ /dev/null
- GNU GENERAL PUBLIC LICENSE
- Version 2, June 1991
-
- Copyright (C) 1989, 1991 Free Software Foundation, Inc.
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The licenses for most software are designed to take away your
-freedom to share and change it. By contrast, the GNU General Public
-License is intended to guarantee your freedom to share and change free
-software--to make sure the software is free for all its users. This
-General Public License applies to most of the Free Software
-Foundation's software and to any other program whose authors commit to
-using it. (Some other Free Software Foundation software is covered by
-the GNU Library General Public License instead.) You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-this service if you wish), that you receive source code or can get it
-if you want it, that you can change the software or use pieces of it
-in new free programs; and that you know you can do these things.
-
- To protect your rights, we need to make restrictions that forbid
-anyone to deny you these rights or to ask you to surrender the rights.
-These restrictions translate to certain responsibilities for you if you
-distribute copies of the software, or if you modify it.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must give the recipients all the rights that
-you have. You must make sure that they, too, receive or can get the
-source code. And you must show them these terms so they know their
-rights.
-
- We protect your rights with two steps: (1) copyright the software, and
-(2) offer you this license which gives you legal permission to copy,
-distribute and/or modify the software.
-
- Also, for each author's protection and ours, we want to make certain
-that everyone understands that there is no warranty for this free
-software. If the software is modified by someone else and passed on, we
-want its recipients to know that what they have is not the original, so
-that any problems introduced by others will not reflect on the original
-authors' reputations.
-
- Finally, any free program is threatened constantly by software
-patents. We wish to avoid the danger that redistributors of a free
-program will individually obtain patent licenses, in effect making the
-program proprietary. To prevent this, we have made it clear that any
-patent must be licensed for everyone's free use or not licensed at all.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- GNU GENERAL PUBLIC LICENSE
- TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
- 0. This License applies to any program or other work which contains
-a notice placed by the copyright holder saying it may be distributed
-under the terms of this General Public License. The "Program", below,
-refers to any such program or work, and a "work based on the Program"
-means either the Program or any derivative work under copyright law:
-that is to say, a work containing the Program or a portion of it,
-either verbatim or with modifications and/or translated into another
-language. (Hereinafter, translation is included without limitation in
-the term "modification".) Each licensee is addressed as "you".
-
-Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope. The act of
-running the Program is not restricted, and the output from the Program
-is covered only if its contents constitute a work based on the
-Program (independent of having been made by running the Program).
-Whether that is true depends on what the Program does.
-
- 1. You may copy and distribute verbatim copies of the Program's
-source code as you receive it, in any medium, provided that you
-conspicuously and appropriately publish on each copy an appropriate
-copyright notice and disclaimer of warranty; keep intact all the
-notices that refer to this License and to the absence of any warranty;
-and give any other recipients of the Program a copy of this License
-along with the Program.
-
-You may charge a fee for the physical act of transferring a copy, and
-you may at your option offer warranty protection in exchange for a fee.
-
- 2. You may modify your copy or copies of the Program or any portion
-of it, thus forming a work based on the Program, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
-
- a) You must cause the modified files to carry prominent notices
- stating that you changed the files and the date of any change.
-
- b) You must cause any work that you distribute or publish, that in
- whole or in part contains or is derived from the Program or any
- part thereof, to be licensed as a whole at no charge to all third
- parties under the terms of this License.
-
- c) If the modified program normally reads commands interactively
- when run, you must cause it, when started running for such
- interactive use in the most ordinary way, to print or display an
- announcement including an appropriate copyright notice and a
- notice that there is no warranty (or else, saying that you provide
- a warranty) and that users may redistribute the program under
- these conditions, and telling the user how to view a copy of this
- License. (Exception: if the Program itself is interactive but
- does not normally print such an announcement, your work based on
- the Program is not required to print an announcement.)
-
-These requirements apply to the modified work as a whole. If
-identifiable sections of that work are not derived from the Program,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works. But when you
-distribute the same sections as part of a whole which is a work based
-on the Program, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Program.
-
-In addition, mere aggregation of another work not based on the Program
-with the Program (or with a work based on the Program) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
-
- 3. You may copy and distribute the Program (or a work based on it,
-under Section 2) in object code or executable form under the terms of
-Sections 1 and 2 above provided that you also do one of the following:
-
- a) Accompany it with the complete corresponding machine-readable
- source code, which must be distributed under the terms of Sections
- 1 and 2 above on a medium customarily used for software
- interchange; or,
-
- b) Accompany it with a written offer, valid for at least three
- years, to give any third party, for a charge no more than your
- cost of physically performing source distribution, a complete
- machine-readable copy of the corresponding source code, to be
- distributed under the terms of Sections 1 and 2 above on a medium
- customarily used for software interchange; or,
-
- c) Accompany it with the information you received as to the offer
- to distribute corresponding source code. (This alternative is
- allowed only for noncommercial distribution and only if you
- received the program in object code or executable form with such
- an offer, in accord with Subsection b above.)
-
-The source code for a work means the preferred form of the work for
-making modifications to it. For an executable work, complete source
-code means all the source code for all modules it contains, plus any
-associated interface definition files, plus the scripts used to
-control compilation and installation of the executable. However, as a
-special exception, the source code distributed need not include
-anything that is normally distributed (in either source or binary
-form) with the major components (compiler, kernel, and so on) of the
-operating system on which the executable runs, unless that component
-itself accompanies the executable.
-
-If distribution of executable or object code is made by offering
-access to copy from a designated place, then offering equivalent
-access to copy the source code from the same place counts as
-distribution of the source code, even though third parties are not
-compelled to copy the source along with the object code.
-
- 4. You may not copy, modify, sublicense, or distribute the Program
-except as expressly provided under this License. Any attempt
-otherwise to copy, modify, sublicense or distribute the Program is
-void, and will automatically terminate your rights under this License.
-However, parties who have received copies, or rights, from you under
-this License will not have their licenses terminated so long as such
-parties remain in full compliance.
-
- 5. You are not required to accept this License, since you have not
-signed it. However, nothing else grants you permission to modify or
-distribute the Program or its derivative works. These actions are
-prohibited by law if you do not accept this License. Therefore, by
-modifying or distributing the Program (or any work based on the
-Program), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Program or works based on it.
-
- 6. Each time you redistribute the Program (or any work based on the
-Program), the recipient automatically receives a license from the
-original licensor to copy, distribute or modify the Program subject to
-these terms and conditions. You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties to
-this License.
-
- 7. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Program at all. For example, if a patent
-license would not permit royalty-free redistribution of the Program by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Program.
-
-If any portion of this section is held invalid or unenforceable under
-any particular circumstance, the balance of the section is intended to
-apply and the section as a whole is intended to apply in other
-circumstances.
-
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system, which is
-implemented by public license practices. Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
-
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
-
- 8. If the distribution and/or use of the Program is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Program under this License
-may add an explicit geographical distribution limitation excluding
-those countries, so that distribution is permitted only in or among
-countries not thus excluded. In such case, this License incorporates
-the limitation as if written in the body of this License.
-
- 9. The Free Software Foundation may publish revised and/or new versions
-of the General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-Each version is given a distinguishing version number. If the Program
-specifies a version number of this License which applies to it and
-"any later version", you have the option of following the terms and
-conditions either of that version or of any later version published by
-the Free Software Foundation. If the Program does not specify a
-version number of this License, you may choose any version ever
-published by the Free Software Foundation.
-
- 10. If you wish to incorporate parts of the Program into other free
-programs whose distribution conditions are different, write to the author
-to ask for permission. For software which is copyrighted by the Free
-Software Foundation, write to the Free Software Foundation; we sometimes
-make exceptions for this. Our decision will be guided by the two goals
-of preserving the free status of all derivatives of our free software and
-of promoting the sharing and reuse of software generally.
-
- NO WARRANTY
-
- 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO
-WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
-EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
-OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY
-KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
-PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME
-THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
-WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
-AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU
-FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
-CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
-PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
-RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
-FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF
-SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
-DAMAGES.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these
-terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-convey the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
- <one line to give the program's name and a brief idea of what it does.>
- Copyright (C) <year> <name of author>
-
- This program is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program; if not, write to the Free Software
- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-
-
-Also add information on how to contact you by electronic and paper mail.
-
-If the program is interactive, make it output a short notice like this
-when it starts in an interactive mode:
-
- Gnomovision version 69, Copyright (C) year name of author
- Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, the commands you use may
-be called something other than `show w' and `show c'; they could even be
-mouse-clicks or menu items--whatever suits your program.
-
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the program, if
-necessary. Here is a sample; alter the names:
-
- Yoyodyne, Inc., hereby disclaims all copyright interest in the program
- `Gnomovision' (which makes passes at compilers) written by James Hacker.
-
- <signature of Ty Coon>, 1 April 1989
- Ty Coon, President of Vice
-
-This General Public License does not permit incorporating your program into
-proprietary programs. If your program is a subroutine library, you may
-consider it more useful to permit linking proprietary applications with the
-library. If this is what you want to do, use the GNU Library General
-Public License instead of this License.
-
+++ /dev/null
-include tx
-
-# Docs
-include LICENSE README.rst
-recursive-include docs *
-
+++ /dev/null
-
-=============================
- Transifex Command-Line Tool
-=============================
-
-The Transifex Command-line Client is a command line tool that enables
-you to easily manage your translations within a project without the need
-of an elaborate UI system.
-
-You can use the command line client to easily create new resources, map
-locale files to translations and synchronize your Transifex project with
-your local repository and vice verca. Translators and localization
-managers can also use it to handle large volumes of translation files
-easily and without much hassle.
-
-Check the full documentation at
-http://help.transifex.com/user-guide/client/
-
-
-Installing
-==========
-
-You can install the latest version of transifex-client running ``pip
-install transifex-client`` or ``easy_install transifex-client``
-You can also install the `in-development version`_ of transifex-client
-with ``pip install transifex-client==dev`` or ``easy_install
-transifex-client==dev``.
-
-.. _in-development version: http://code.transifex.com/transifex-client/
-
+++ /dev/null
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-import os
-import glob
-from codecs import BOM
-
-from setuptools import setup, find_packages
-from setuptools.command.build_py import build_py as _build_py
-
-from txclib import get_version
-
-readme_file = open(u'README.rst')
-long_description = readme_file.read()
-readme_file.close()
-if long_description.startswith(BOM):
- long_description = long_description.lstrip(BOM)
-long_description = long_description.decode('utf-8')
-
-package_data = {
- '': ['LICENSE', 'README.rst'],
-}
-
-scripts = ['tx']
-
-install_requires = []
-try:
- import json
-except ImportError:
- install_requires.append('simplejson')
-
-setup(
- name="transifex-client",
- version=get_version(),
- scripts=scripts,
- description="A command line interface for Transifex",
- long_description=long_description,
- author="Transifex",
- author_email="info@transifex.com",
- url="https://www.transifex.com",
- license="GPLv2",
- dependency_links = [
- ],
- setup_requires = [
- ],
- install_requires = install_requires,
- tests_require = ["mock", ],
- data_files=[
- ],
- test_suite="tests",
- zip_safe=False,
- packages=['txclib', ],
- include_package_data=True,
- package_data = package_data,
- keywords = ('translation', 'localization', 'internationalization',),
-)
+++ /dev/null
-# -*- coding: utf-8 -*-
-
-"""
-Unit tests for processor functions.
-"""
-
-import unittest
-from urlparse import urlparse
-from txclib.processors import hostname_tld_migration, hostname_ssl_migration
-
-
-class TestHostname(unittest.TestCase):
- """Test for hostname processors."""
-
- def test_tld_migration_needed(self):
- """
- Test the tld migration of Transifex, when needed.
- """
- hostnames = [
- 'http://transifex.net', 'http://www.transifex.net',
- 'https://fedora.transifex.net',
- ]
- for h in hostnames:
- hostname = hostname_tld_migration(h)
- self.assertTrue(hostname.endswith('com'))
- orig_hostname = 'http://www.transifex.net/path/'
- hostname = hostname_tld_migration(orig_hostname)
- self.assertEqual(hostname, orig_hostname.replace('net', 'com', 1))
-
- def test_tld_migration_needed(self):
- """
- Test that unneeded tld migrations are detected correctly.
- """
- hostnames = [
- 'https://www.transifex.com', 'http://fedora.transifex.com',
- 'http://www.example.net/path/'
- ]
- for h in hostnames:
- hostname = hostname_tld_migration(h)
- self.assertEqual(hostname, h)
-
- def test_no_scheme_specified(self):
- """
- Test that, if no scheme has been specified, the https one will be used.
- """
- hostname = '//transifex.net'
- hostname = hostname_ssl_migration(hostname)
- self.assertTrue(hostname.startswith('https://'))
-
- def test_http_replacement(self):
- """Test the replacement of http with https."""
- hostnames = [
- 'http://transifex.com', 'http://transifex.net/http/',
- 'http://www.transifex.com/path/'
- ]
- for h in hostnames:
- hostname = hostname_ssl_migration(h)
- self.assertEqual(hostname[:8], 'https://')
- self.assertEqual(hostname[7:], h[6:])
-
- def test_no_http_replacement_needed(self):
- """Test that http will not be replaces with https, when not needed."""
- for h in ['http://example.com', 'http://example.com/http/']:
- hostname = hostname_ssl_migration(h)
- self.assertEqual(hostname, hostname)
+++ /dev/null
-# -*- coding: utf-8 -*-
-
-from __future__ import with_statement
-import unittest
-import contextlib
-import itertools
-try:
- import json
-except ImportError:
- import simplejson as json
-from mock import Mock, patch
-
-from txclib.project import Project
-from txclib.config import Flipdict
-
-
-class TestProject(unittest.TestCase):
-
- def test_extract_fields(self):
- """Test the functions that extract a field from a stats object."""
- stats = {
- 'completed': '80%',
- 'last_update': '00:00',
- 'foo': 'bar',
- }
- self.assertEqual(
- stats['completed'], '%s%%' % Project._extract_completed(stats)
- )
- self.assertEqual(stats['last_update'], Project._extract_updated(stats))
-
- def test_specifying_resources(self):
- """Test the various ways to specify resources in a project."""
- p = Project(init=False)
- resources = [
- 'proj1.res1',
- 'proj2.res2',
- 'transifex.txn',
- 'transifex.txo',
- ]
- with patch.object(p, 'get_resource_list') as mock:
- mock.return_value = resources
- cmd_args = [
- 'proj1.res1', '*1*', 'transifex*', '*r*',
- '*o', 'transifex.tx?', 'transifex.txn',
- ]
- results = [
- ['proj1.res1', ],
- ['proj1.res1', ],
- ['transifex.txn', 'transifex.txo', ],
- ['proj1.res1', 'proj2.res2', 'transifex.txn', 'transifex.txo', ],
- ['transifex.txo', ],
- ['transifex.txn', 'transifex.txo', ],
- ['transifex.txn', ],
- [],
- ]
-
- for i, arg in enumerate(cmd_args):
- resources = [arg]
- self.assertEqual(p.get_chosen_resources(resources), results[i])
-
- # wrong argument
- resources = ['*trasnifex*', ]
- self.assertRaises(Exception, p.get_chosen_resources, resources)
-
-
-class TestProjectMinimumPercent(unittest.TestCase):
- """Test the minimum-perc option."""
-
- def setUp(self):
- super(TestProjectMinimumPercent, self).setUp()
- self.p = Project(init=False)
- self.p.minimum_perc = None
- self.p.resource = "resource"
-
- def test_cmd_option(self):
- """Test command-line option."""
- self.p.minimum_perc = 20
- results = itertools.cycle([80, 90 ])
- def side_effect(*args):
- return results.next()
-
- with patch.object(self.p, "get_resource_option") as mock:
- mock.side_effect = side_effect
- self.assertFalse(self.p._satisfies_min_translated({'completed': '12%'}))
- self.assertTrue(self.p._satisfies_min_translated({'completed': '20%'}))
- self.assertTrue(self.p._satisfies_min_translated({'completed': '30%'}))
-
- def test_global_only(self):
- """Test only global option."""
- results = itertools.cycle([80, None ])
- def side_effect(*args):
- return results.next()
-
- with patch.object(self.p, "get_resource_option") as mock:
- mock.side_effect = side_effect
- self.assertFalse(self.p._satisfies_min_translated({'completed': '70%'}))
- self.assertTrue(self.p._satisfies_min_translated({'completed': '80%'}))
- self.assertTrue(self.p._satisfies_min_translated({'completed': '90%'}))
-
- def test_local_lower_than_global(self):
- """Test the case where the local option is lower than the global."""
- results = itertools.cycle([80, 70 ])
- def side_effect(*args):
- return results.next()
-
- with patch.object(self.p, "get_resource_option") as mock:
- mock.side_effect = side_effect
- self.assertFalse(self.p._satisfies_min_translated({'completed': '60%'}))
- self.assertTrue(self.p._satisfies_min_translated({'completed': '70%'}))
- self.assertTrue(self.p._satisfies_min_translated({'completed': '80%'}))
- self.assertTrue(self.p._satisfies_min_translated({'completed': '90%'}))
-
- def test_local_higher_than_global(self):
- """Test the case where the local option is lower than the global."""
- results = itertools.cycle([60, 70 ])
- def side_effect(*args):
- return results.next()
-
- with patch.object(self.p, "get_resource_option") as mock:
- mock.side_effect = side_effect
- self.assertFalse(self.p._satisfies_min_translated({'completed': '60%'}))
- self.assertTrue(self.p._satisfies_min_translated({'completed': '70%'}))
- self.assertTrue(self.p._satisfies_min_translated({'completed': '80%'}))
- self.assertTrue(self.p._satisfies_min_translated({'completed': '90%'}))
-
- def test_local_only(self):
- """Test the case where the local option is lower than the global."""
- results = itertools.cycle([None, 70 ])
- def side_effect(*args):
- return results.next()
-
- with patch.object(self.p, "get_resource_option") as mock:
- mock.side_effect = side_effect
- self.assertFalse(self.p._satisfies_min_translated({'completed': '60%'}))
- self.assertTrue(self.p._satisfies_min_translated({'completed': '70%'}))
- self.assertTrue(self.p._satisfies_min_translated({'completed': '80%'}))
- self.assertTrue(self.p._satisfies_min_translated({'completed': '90%'}))
-
- def test_no_option(self):
- """"Test the case there is nothing defined."""
- results = itertools.cycle([None, None ])
- def side_effect(*args):
- return results.next()
-
- with patch.object(self.p, "get_resource_option") as mock:
- mock.side_effect = side_effect
- self.assertTrue(self.p._satisfies_min_translated({'completed': '0%'}))
- self.assertTrue(self.p._satisfies_min_translated({'completed': '10%'}))
- self.assertTrue(self.p._satisfies_min_translated({'completed': '90%'}))
-
-
-class TestProjectFilters(unittest.TestCase):
- """Test filters used to decide whether to push/pull a translation or not."""
-
- def setUp(self):
- super(TestProjectFilters, self).setUp()
- self.p = Project(init=False)
- self.p.minimum_perc = None
- self.p.resource = "resource"
- self.stats = {
- 'en': {
- 'completed': '100%', 'last_update': '2011-11-01 15:00:00',
- }, 'el': {
- 'completed': '60%', 'last_update': '2011-11-01 15:00:00',
- }, 'pt': {
- 'completed': '70%', 'last_update': '2011-11-01 15:00:00',
- },
- }
- self.langs = self.stats.keys()
-
- def test_add_translation(self):
- """Test filters for adding translations.
-
- We do not test here for minimum percentages.
- """
- with patch.object(self.p, "get_resource_option") as mock:
- mock.return_value = None
- should_add = self.p._should_add_translation
- for force in [True, False]:
- for lang in self.langs:
- self.assertTrue(should_add(lang, self.stats, force))
-
- # unknown language
- self.assertFalse(should_add('es', self.stats))
-
- def test_update_translation(self):
- """Test filters for updating a translation.
-
- We do not test here for minimum percentages.
- """
- with patch.object(self.p, "get_resource_option") as mock:
- mock.return_value = None
-
- should_update = self.p._should_update_translation
- force = True
- for lang in self.langs:
- self.assertTrue(should_update(lang, self.stats, 'foo', force))
-
- force = False # reminder
- local_file = 'foo'
-
- # unknown language
- self.assertFalse(should_update('es', self.stats, local_file))
-
- # no local file
- with patch.object(self.p, "_get_time_of_local_file") as time_mock:
- time_mock.return_value = None
- with patch.object(self.p, "get_full_path") as path_mock:
- path_mock.return_value = "foo"
- for lang in self.langs:
- self.assertTrue(
- should_update(lang, self.stats, local_file)
- )
-
- # older local files
- local_times = [self.p._generate_timestamp('2011-11-01 14:00:59')]
- results = itertools.cycle(local_times)
- def side_effect(*args):
- return results.next()
-
- with patch.object(self.p, "_get_time_of_local_file") as time_mock:
- time_mock.side_effect = side_effect
- with patch.object(self.p, "get_full_path") as path_mock:
- path_mock.return_value = "foo"
- for lang in self.langs:
- self.assertTrue(
- should_update(lang, self.stats, local_file)
- )
-
- # newer local files
- local_times = [self.p._generate_timestamp('2011-11-01 15:01:59')]
- results = itertools.cycle(local_times)
- def side_effect(*args):
- return results.next()
-
- with patch.object(self.p, "_get_time_of_local_file") as time_mock:
- time_mock.side_effect = side_effect
- with patch.object(self.p, "get_full_path") as path_mock:
- path_mock.return_value = "foo"
- for lang in self.langs:
- self.assertFalse(
- should_update(lang, self.stats, local_file)
- )
-
- def test_push_translation(self):
- """Test filters for pushing a translation file."""
- with patch.object(self.p, "get_resource_option") as mock:
- mock.return_value = None
-
- local_file = 'foo'
- should_push = self.p._should_push_translation
- force = True
- for lang in self.langs:
- self.assertTrue(should_push(lang, self.stats, local_file, force))
-
- force = False # reminder
-
- # unknown language
- self.assertTrue(should_push('es', self.stats, local_file))
-
- # older local files
- local_times = [self.p._generate_timestamp('2011-11-01 14:00:59')]
- results = itertools.cycle(local_times)
- def side_effect(*args):
- return results.next()
-
- with patch.object(self.p, "_get_time_of_local_file") as time_mock:
- time_mock.side_effect = side_effect
- with patch.object(self.p, "get_full_path") as path_mock:
- path_mock.return_value = "foo"
- for lang in self.langs:
- self.assertFalse(
- should_push(lang, self.stats, local_file)
- )
-
- # newer local files
- local_times = [self.p._generate_timestamp('2011-11-01 15:01:59')]
- results = itertools.cycle(local_times)
- def side_effect(*args):
- return results.next()
-
- with patch.object(self.p, "_get_time_of_local_file") as time_mock:
- time_mock.side_effect = side_effect
- with patch.object(self.p, "get_full_path") as path_mock:
- path_mock.return_value = "foo"
- for lang in self.langs:
- self.assertTrue(
- should_push(lang, self.stats, local_file)
- )
-
-
-class TestProjectPull(unittest.TestCase):
- """Test bits & pieces of the pull method."""
-
- def setUp(self):
- super(TestProjectPull, self).setUp()
- self.p = Project(init=False)
- self.p.minimum_perc = None
- self.p.resource = "resource"
- self.p.host = 'foo'
- self.p.project_slug = 'foo'
- self.p.resource_slug = 'foo'
- self.stats = {
- 'en': {
- 'completed': '100%', 'last_update': '2011-11-01 15:00:00',
- }, 'el': {
- 'completed': '60%', 'last_update': '2011-11-01 15:00:00',
- }, 'pt': {
- 'completed': '70%', 'last_update': '2011-11-01 15:00:00',
- },
- }
- self.langs = self.stats.keys()
- self.files = dict(zip(self.langs, itertools.repeat(None)))
- self.details = {'available_languages': []}
- for lang in self.langs:
- self.details['available_languages'].append({'code': lang})
- self.slang = 'en'
- self.lang_map = Flipdict()
-
- def test_new_translations(self):
- """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)
- files_keys = self.langs
- new_trans = self.p._new_translations_to_add
- for force in [True, False]:
- res = new_trans(
- self.files, self.slang, self.lang_map, self.stats, force
- )
- self.assertEquals(res, set([]))
-
- with patch.object(self.p, '_should_add_translation') as filter_mock:
- filter_mock.return_value = True
- for force in [True, False]:
- res = new_trans(
- {'el': None}, self.slang, self.lang_map, self.stats, force
- )
- self.assertEquals(res, set(['pt']))
- for force in [True, False]:
- res = new_trans(
- {}, self.slang, self.lang_map, self.stats, force
- )
- self.assertEquals(res, set(['el', 'pt']))
-
- files = {}
- files['pt_PT'] = None
- lang_map = {'pt': 'pt_PT'}
- for force in [True, False]:
- res = new_trans(
- files, self.slang, lang_map, self.stats, force
- )
- self.assertEquals(res, set(['el']))
-
- def test_languages_to_pull_empty_initial_list(self):
- """Test determining the languages to pull, when the initial
- list is empty.
- """
- languages = []
- force = False
-
- res = self.p._languages_to_pull(
- languages, self.files, self.lang_map, self.stats, force
- )
- existing = res[0]
- new = res[1]
- self.assertEquals(existing, set(['el', 'en', 'pt']))
- self.assertFalse(new)
-
- del self.files['el']
- self.files['el-gr'] = None
- self.lang_map['el'] = 'el-gr'
- res = self.p._languages_to_pull(
- languages, self.files, self.lang_map, self.stats, force
- )
- existing = res[0]
- new = res[1]
- self.assertEquals(existing, set(['el', 'en', 'pt']))
- self.assertFalse(new)
-
- def test_languages_to_pull_with_initial_list(self):
- """Test determining the languages to pull, then there is a
- language selection from the user.
- """
- languages = ['el', 'en']
- self.lang_map['el'] = 'el-gr'
- del self.files['el']
- self.files['el-gr'] = None
- force = False
-
- with patch.object(self.p, '_should_add_translation') as mock:
- mock.return_value = True
- res = self.p._languages_to_pull(
- languages, self.files, self.lang_map, self.stats, force
- )
- existing = res[0]
- new = res[1]
- self.assertEquals(existing, set(['en', 'el-gr', ]))
- self.assertFalse(new)
-
- mock.return_value = False
- res = self.p._languages_to_pull(
- languages, self.files, self.lang_map, self.stats, force
- )
- existing = res[0]
- new = res[1]
- self.assertEquals(existing, set(['en', 'el-gr', ]))
- self.assertFalse(new)
-
- del self.files['el-gr']
- mock.return_value = True
- res = self.p._languages_to_pull(
- languages, self.files, self.lang_map, self.stats, force
- )
- existing = res[0]
- new = res[1]
- self.assertEquals(existing, set(['en', ]))
- self.assertEquals(new, set(['el', ]))
-
- mock.return_value = False
- res = self.p._languages_to_pull(
- languages, self.files, self.lang_map, self.stats, force
- )
- existing = res[0]
- new = res[1]
- self.assertEquals(existing, set(['en', ]))
- self.assertEquals(new, set([]))
-
- def test_in_combination_with_force_option(self):
- """Test the minumum-perc option along with -f."""
- with patch.object(self.p, 'get_resource_option') as mock:
- mock.return_value = 70
-
- res = self.p._should_download('de', self.stats, None, False)
- self.assertEquals(res, False)
- res = self.p._should_download('el', self.stats, None, False)
- self.assertEquals(res, False)
- res = self.p._should_download('el', self.stats, None, True)
- self.assertEquals(res, False)
- res = self.p._should_download('en', self.stats, None, False)
- self.assertEquals(res, True)
- res = self.p._should_download('en', self.stats, None, True)
- self.assertEquals(res, True)
-
- with patch.object(self.p, '_remote_is_newer') as local_file_mock:
- local_file_mock = False
- res = self.p._should_download('pt', self.stats, None, False)
- self.assertEquals(res, True)
- res = self.p._should_download('pt', self.stats, None, True)
- self.assertEquals(res, True)
-
-
-class TestFormats(unittest.TestCase):
- """Tests for the supported formats."""
-
- def setUp(self):
- self.p = Project(init=False)
-
- def test_extensions(self):
- """Test returning the correct extension for a format."""
- sample_formats = {
- 'PO': {'file-extensions': '.po, .pot'},
- 'QT': {'file-extensions': '.ts'},
- }
- extensions = ['.po', '.ts', '', ]
- with patch.object(self.p, "do_url_request") as mock:
- mock.return_value = json.dumps(sample_formats)
- for (type_, ext) in zip(['PO', 'QT', 'NONE', ], extensions):
- extension = self.p._extension_for(type_)
- self.assertEquals(extension, ext)
-
-
-class TestOptions(unittest.TestCase):
- """Test the methods related to parsing the configuration file."""
-
- def setUp(self):
- self.p = Project(init=False)
-
- def test_get_option(self):
- """Test _get_option method."""
- with contextlib.nested(
- patch.object(self.p, 'get_resource_option'),
- patch.object(self.p, 'config', create=True)
- ) as (rmock, cmock):
- rmock.return_value = 'resource'
- cmock.has_option.return_value = 'main'
- cmock.get.return_value = 'main'
- self.assertEqual(self.p._get_option(None, None), 'resource')
- rmock.return_value = None
- cmock.has_option.return_value = 'main'
- cmock.get.return_value = 'main'
- self.assertEqual(self.p._get_option(None, None), 'main')
- cmock.has_option.return_value = None
- self.assertIs(self.p._get_option(None, None), None)
-
-
-class TestConfigurationOptions(unittest.TestCase):
- """Test the various configuration options."""
-
- 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)
- calls = config_mock.method_calls
- self.assertEquals('set', calls[0][0])
- self.assertEquals('main', calls[0][1][0])
- p.set_i18n_type(['transifex.txo'], 'PO')
- calls = config_mock.method_calls
- self.assertEquals('set', calls[0][0])
- p.set_i18n_type(['transifex.txo', 'transifex.txn'], 'PO')
- calls = config_mock.method_calls
- self.assertEquals('set', calls[0][0])
- self.assertEquals('set', calls[1][0])
-
-
-class TestStats(unittest.TestCase):
- """Test the access to the stats objects."""
-
- def setUp(self):
- self.stats = Mock()
- self.stats.__getitem__ = Mock()
- self.stats.__getitem__.return_value = '12%'
-
- def test_field_used_per_mode(self):
- """Test the fields used for each mode."""
- Project._extract_completed(self.stats, 'translate')
- self.stats.__getitem__.assert_called_with('completed')
- Project._extract_completed(self.stats, 'reviewed')
- self.stats.__getitem__.assert_called_with('reviewed_percentage')
-
+++ /dev/null
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-from optparse import OptionParser, OptionValueError
-import os
-import sys
-
-from txclib import utils
-from txclib import get_version
-from txclib.log import set_log_level, logger
-
-reload(sys) # WTF? Otherwise setdefaultencoding doesn't work
-
-# This block ensures that ^C interrupts are handled quietly.
-try:
- import signal
-
- def exithandler(signum,frame):
- signal.signal(signal.SIGINT, signal.SIG_IGN)
- signal.signal(signal.SIGTERM, signal.SIG_IGN)
- sys.exit(1)
-
- signal.signal(signal.SIGINT, exithandler)
- signal.signal(signal.SIGTERM, exithandler)
- if hasattr(signal, 'SIGPIPE'):
- signal.signal(signal.SIGPIPE, signal.SIG_DFL)
-
-except KeyboardInterrupt:
- sys.exit(1)
-
-# When we open file with f = codecs.open we specifi FROM what encoding to read
-# This sets the encoding for the strings which are created with f.read()
-sys.setdefaultencoding('utf-8')
-
-
-def main(argv):
- """
- Here we parse the flags (short, long) and we instantiate the classes.
- """
- usage = "usage: %prog [options] command [cmd_options]"
- description = "This is the Transifex command line client which"\
- " allows you to manage your translations locally and sync"\
- " them with the master Transifex server.\nIf you'd like to"\
- " check the available commands issue `%prog help` or if you"\
- " just want help with a specific command issue `%prog help"\
- " command`"
-
- parser = OptionParser(
- usage=usage, version=get_version(), description=description
- )
- parser.disable_interspersed_args()
- parser.add_option(
- "-d", "--debug", action="store_true", dest="debug",
- default=False, help=("enable debug messages")
- )
- parser.add_option(
- "-q", "--quiet", action="store_true", dest="quiet",
- default=False, help="don't print status messages to stdout"
- )
- parser.add_option(
- "-r", "--root", action="store", dest="root_dir", type="string",
- default=None, help="change root directory (default is cwd)"
- )
- parser.add_option(
- "--traceback", action="store_true", dest="trace", default=False,
- help="print full traceback on exceptions"
- )
- parser.add_option(
- "--disable-colors", action="store_true", dest="color_disable",
- default=(os.name == 'nt' or not sys.stdout.isatty()),
- help="disable colors in the output of commands"
- )
- (options, args) = parser.parse_args()
-
- if len(args) < 1:
- parser.error("No command was given")
-
- utils.DISABLE_COLORS = options.color_disable
-
- # set log level
- if options.quiet:
- set_log_level('WARNING')
- elif options.debug:
- set_log_level('DEBUG')
-
- # find .tx
- path_to_tx = options.root_dir or utils.find_dot_tx()
-
-
- cmd = args[0]
- try:
- utils.exec_command(cmd, args[1:], path_to_tx)
- except utils.UnknownCommandError:
- logger.error("tx: Command %s not found" % cmd)
- except SystemExit:
- sys.exit()
- except:
- import traceback
- if options.trace:
- traceback.print_exc()
- else:
- formatted_lines = traceback.format_exc().splitlines()
- 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.
- main(sys.argv[1:])
+++ /dev/null
-# -*- coding: utf-8 -*-
-
-"""
-Copyright (C) 2010 by Indifex (www.indifex.com), see AUTHORS.
-License: BSD, see LICENSE for details.
-
-For further information visit http://code.indifex.com/transifex-client
-"""
-
-
-VERSION = (0, 9, 0, 'devel')
-
-def get_version():
- version = '%s.%s' % (VERSION[0], VERSION[1])
- if VERSION[2]:
- version = '%s.%s' % (version, VERSION[2])
- if VERSION[3] != 'final':
- version = '%s %s' % (version, VERSION[3])
- return version
+++ /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()
+++ /dev/null
-import ConfigParser
-
-
-class OrderedRawConfigParser( ConfigParser.RawConfigParser ):
- """
- Overload standard Class ConfigParser.RawConfigParser
- """
- def write(self, fp):
- """Write an .ini-format representation of the configuration state."""
- if self._defaults:
- fp.write("[%s]\n" % DEFAULTSECT)
- for key in sorted( self._defaults ):
- fp.write( "%s = %s\n" % (key, str( self._defaults[ key ]
- ).replace('\n', '\n\t')) )
- fp.write("\n")
- for section in self._sections:
- fp.write("[%s]\n" % section)
- for key in sorted( self._sections[section] ):
- if key != "__name__":
- fp.write("%s = %s\n" %
- (key, str( self._sections[section][ key ]
- ).replace('\n', '\n\t')))
- fp.write("\n")
-
- optionxform = str
-
-
-_NOTFOUND = object()
-
-
-class Flipdict(dict):
- """An injective (one-to-one) python dict. Ensures that each key maps
- to a unique value, and each value maps back to that same key.
-
- Code mostly taken from here:
- http://code.activestate.com/recipes/576968-flipdict-python-dict-that-also-maintains-a-one-to-/
- """
-
- def __init__(self, *args, **kw):
- self._flip = dict.__new__(self.__class__)
- setattr(self._flip, "_flip", self)
- for key, val in dict(*args, **kw).iteritems():
- self[key] = val
-
- @property
- def flip(self):
- """The inverse mapping."""
- return self._flip
-
- def __repr__(self):
- return "%s(%r)" % (self.__class__.__name__, dict(self))
-
- __str__ = __repr__
-
- def copy(self):
- return self.__class__(self)
-
- @classmethod
- def fromkeys(cls, keys, value=None):
- return cls(dict.fromkeys(keys, value))
-
- def __setitem__(self, key, val):
- k = self._flip.get(val, _NOTFOUND)
- if not (k is _NOTFOUND or k==key):
- raise KeyError('(key,val) would erase mapping for value %r' % val)
-
- v = self.get(key, _NOTFOUND)
- if v is not _NOTFOUND:
- dict.__delitem__(self._flip, v)
-
- dict.__setitem__(self, key, val)
- dict.__setitem__(self._flip, val, key)
-
- def setdefault(self, key, default = None):
- # Copied from python's UserDict.DictMixin code.
- try:
- return self[key]
- except KeyError:
- self[key] = default
- return default
-
- def update(self, other = None, **kwargs):
- # Copied from python's UserDict.DictMixin code.
- # Make progressively weaker assumptions about "other"
- if other is None:
- pass
- elif hasattr(other, 'iteritems'): # iteritems saves memory and lookups
- for k, v in other.iteritems():
- self[k] = v
- elif hasattr(other, 'keys'):
- for k in other.keys():
- self[k] = other[k]
- else:
- for k, v in other:
- self[k] = v
- if kwargs:
- self.update(kwargs)
-
- def __delitem__(self, key):
- val = dict.pop(self, key)
- dict.__delitem__(self._flip, val)
-
- def pop(self, key, *args):
- val = dict.pop(self, key, *args)
- dict.__delitem__(self._flip, val)
- return val
-
- def popitem(self):
- key, val = dict.popitem(self)
- dict.__delitem__(self._flip, val)
- return key, val
-
- def clear(self):
- dict.clear(self)
- dict.clear(self._flip)
+++ /dev/null
-# -*- coding: utf-8 -*-
-
-"""
-Exception classes for the tx client.
-"""
-
-
-class UnInitializedError(Exception):
- """The project directory has not been initialized."""
-
-
-class UnknownCommandError(Exception):
- """The provided command is not supported."""
+++ /dev/null
-# -*- coding: utf-8 -*-
-
-"""
-HTTP-related utility functions.
-"""
-
-from __future__ import with_statement
-import gzip
-try:
- import cStringIO as StringIO
-except ImportError:
- import StringIO
-
-
-def _gzip_decode(gzip_data):
- """
- Unzip gzipped data and return them.
-
- :param gzip_data: Gzipped data.
- :returns: The actual data.
- """
- try:
- gzip_data = StringIO.StringIO(gzip_data)
- gzip_file = gzip.GzipFile(fileobj=gzip_data)
- data = gzip_file.read()
- return data
- finally:
- gzip_data.close()
-
-
-def http_response(response):
- """
- Return the response of a HTTP request.
-
- If the response has been gzipped, gunzip it first.
-
- :param response: The raw response of a HTTP request.
- :returns: A response suitable to be used by clients.
- """
- metadata = response.info()
- data = response.read()
- response.close()
- if metadata.get('content-encoding') == 'gzip':
- return _gzip_decode(data)
- else:
- return data
+++ /dev/null
-# -*- coding: utf-8 -*-
-
-"""
-Add logging capabilities to tx-client.
-"""
-
-import sys
-import logging
-
-_logger = logging.getLogger('txclib')
-_logger.setLevel(logging.INFO)
-
-_formatter = logging.Formatter('%(message)s')
-
-_error_handler = logging.StreamHandler(sys.stderr)
-_error_handler.setLevel(logging.ERROR)
-_error_handler.setFormatter(_formatter)
-_logger.addHandler(_error_handler)
-
-_msg_handler = logging.StreamHandler(sys.stdout)
-_msg_handler.setLevel(logging.DEBUG)
-_msg_handler.setFormatter(_formatter)
-_msg_filter = logging.Filter()
-_msg_filter.filter = lambda r: r.levelno < logging.ERROR
-_msg_handler.addFilter(_msg_filter)
-_logger.addHandler(_msg_handler)
-
-logger = _logger
-
-
-def set_log_level(level):
- """Set the level for the logger.
-
- Args:
- level: A string among DEBUG, INFO, WARNING, ERROR, CRITICAL.
- """
- logger.setLevel(getattr(logging, level))
+++ /dev/null
-# -*- coding: utf-8 -*-
-
-from optparse import OptionParser, OptionGroup
-
-
-class EpilogParser(OptionParser):
- def format_epilog(self, formatter):
- return self.epilog
-
-
-def delete_parser():
- """Return the command-line parser for the delete command."""
- usage = "usage: %prog [tx_options] delete OPTION [OPTIONS]"
- description = (
- "This command deletes translations for a resource in the remote server."
- )
- epilog = (
- "\nExamples:\n"
- " To delete a translation:\n "
- "$ tx delete -r project.resource -l <lang_code>\n\n"
- " To delete a resource:\n $ tx delete -r project.resource\n"
- )
- parser = EpilogParser(usage=usage, description=description, epilog=epilog)
- parser.add_option(
- "-r", "--resource", action="store", dest="resources", default=None,
- help="Specify the resource you want to delete (defaults to all)"
- )
- parser.add_option(
- "-l","--language", action="store", dest="languages",
- default=None, help="Specify the translation you want to delete"
- )
- parser.add_option(
- "--skip", action="store_true", dest="skip_errors", default=False,
- help="Don't stop on errors."
- )
- parser.add_option(
- "-f","--force", action="store_true", dest="force_delete",
- default=False, help="Delete an entity forcefully."
- )
- return parser
-
-
-def help_parser():
- """Return the command-line parser for the help command."""
- usage="usage: %prog help command"
- description="Lists all available commands in the transifex command"\
- " client. If a command is specified, the help page of the specific"\
- " command is displayed instead."
-
- parser = OptionParser(usage=usage, description=description)
- return parser
-
-
-def init_parser():
- """Return the command-line parser for the init command."""
- usage="usage: %prog [tx_options] init <path>"
- description="This command initializes a new project for use with"\
- " transifex. It is recommended to execute this command in the"\
- " top level directory of your project so that you can include"\
- " all files under it in transifex. If no path is provided, the"\
- " current working dir will be used."
- parser = OptionParser(usage=usage, description=description)
- parser.add_option("--host", action="store", dest="host",
- default=None, help="Specify a default Transifex host.")
- parser.add_option("--user", action="store", dest="user",
- default=None, help="Specify username for Transifex server.")
- parser.add_option("--pass", action="store", dest="password",
- default=None, help="Specify password for Transifex server.")
- return parser
-
-
-def pull_parser():
- """Return the command-line parser for the pull command."""
- usage="usage: %prog [tx_options] pull [options]"
- description="This command pulls all outstanding changes from the remote"\
- " Transifex server to the local repository. By default, only the"\
- " files that are watched by Transifex will be updated but if you"\
- " want to fetch the translations for new languages as well, use the"\
- " -a|--all option. (Note: new translations are saved in the .tx folder"\
- " and require the user to manually rename them and add then in "\
- " transifex using the set_translation command)."
- parser = OptionParser(usage=usage,description=description)
- parser.add_option("-l","--language", action="store", dest="languages",
- default=[], help="Specify which translations you want to pull"
- " (defaults to all)")
- parser.add_option("-r","--resource", action="store", dest="resources",
- default=[], help="Specify the resource for which you want to pull"
- " the translations (defaults to all)")
- parser.add_option("-a","--all", action="store_true", dest="fetchall",
- default=False, help="Fetch all translation files from server (even new"
- " ones)")
- parser.add_option("-s","--source", action="store_true", dest="fetchsource",
- default=False, help="Force the fetching of the source file (default:"
- " False)")
- parser.add_option("-f","--force", action="store_true", dest="force",
- default=False, help="Force download of translations files.")
- parser.add_option("--skip", action="store_true", dest="skip_errors",
- default=False, help="Don't stop on errors. Useful when pushing many"
- " files concurrently.")
- parser.add_option("--disable-overwrite", action="store_false",
- dest="overwrite", default=True,
- help="By default transifex will fetch new translations files and"\
- " replace existing ones. Use this flag if you want to disable"\
- " this feature")
- parser.add_option("--minimum-perc", action="store", type="int",
- dest="minimum_perc", default=0,
- help="Specify the minimum acceptable percentage of a translation "
- "in order to download it.")
- parser.add_option(
- "--mode", action="store", dest="mode", help=(
- "Specify the mode of the translation file to pull (e.g. "
- "'reviewed'). See http://bit.ly/txcmod1 for available values."
- )
- )
- return parser
-
-
-def push_parser():
- """Return the command-line parser for the push command."""
- usage="usage: %prog [tx_options] push [options]"
- description="This command pushes all local files that have been added to"\
- " Transifex to the remote server. All new translations are merged"\
- " with existing ones and if a language doesn't exists then it gets"\
- " created. If you want to push the source file as well (either"\
- " because this is your first time running the client or because"\
- " you just have updated with new entries), use the -f|--force option."\
- " By default, this command will push all files which are watched by"\
- " Transifex but you can filter this per resource or/and language."
- parser = OptionParser(usage=usage, description=description)
- parser.add_option("-l","--language", action="store", dest="languages",
- default=None, help="Specify which translations you want to push"
- " (defaults to all)")
- parser.add_option("-r","--resource", action="store", dest="resources",
- default=None, help="Specify the resource for which you want to push"
- " the translations (defaults to all)")
- parser.add_option("-f","--force", action="store_true", dest="force_creation",
- default=False, help="Push source files without checking modification"
- " times.")
- parser.add_option("--skip", action="store_true", dest="skip_errors",
- default=False, help="Don't stop on errors. Useful when pushing many"
- " files concurrently.")
- parser.add_option("-s", "--source", action="store_true", dest="push_source",
- default=False, help="Push the source file to the server.")
-
- parser.add_option("-t", "--translations", action="store_true", dest="push_translations",
- default=False, help="Push the translation files to the server")
- parser.add_option("--no-interactive", action="store_true", dest="no_interactive",
- default=False, help="Don't require user input when forcing a push.")
- return parser
-
-
-def set_parser():
- """Return the command-line parser for the set command."""
- usage="usage: %prog [tx_options] set [options] [args]"
- description="This command can be used to create a mapping between files"\
- " and projects either using local files or using files from a remote"\
- " Transifex server."
- epilog="\nExamples:\n"\
- " To set the source file:\n $ tx set -r project.resource --source -l en <file>\n\n"\
- " To set a single translation file:\n $ tx set -r project.resource -l de <file>\n\n"\
- " To automatically detect and assign the source files and translations:\n"\
- " $ tx set --auto-local -r project.resource 'expr' --source-lang en\n\n"\
- " To set a specific file as a source and auto detect translations:\n"\
- " $ tx set --auto-local -r project.resource 'expr' --source-lang en"\
- " --source-file <file>\n\n"\
- " To set a remote release/resource/project:\n"\
- " $ tx set --auto-remote <transifex-url>\n"
- parser = EpilogParser(usage=usage, description=description, epilog=epilog)
- parser.add_option("--auto-local", action="store_true", dest="local",
- default=False, help="Used when auto configuring local project.")
- parser.add_option("--auto-remote", action="store_true", dest="remote",
- default=False, help="Used when adding remote files from Transifex"
- " server.")
- parser.add_option("-r","--resource", action="store", dest="resource",
- default=None, help="Specify the slug of the resource that you're"
- " setting up (This must be in the following format:"
- " `project_slug.resource_slug`).")
- parser.add_option(
- "--source", action="store_true", dest="is_source", default=False,
- help=(
- "Specify that the given file is a source file "
- "[doesn't work with the --auto-* commands]."
- )
- )
- parser.add_option("-l","--language", action="store", dest="language",
- default=None, help="Specify which translations you want to pull"
- " [doesn't work with the --auto-* commands].")
- parser.add_option("-t", "--type", action="store", dest="i18n_type",
- help=(
- "Specify the i18n type of the resource(s). This is only needed, if "
- "the resource(s) does not exist yet in Transifex. For a list of "
- "available i18n types, see "
- "http://help.transifex.com/features/formats.html"
- )
- )
- parser.add_option("--minimum-perc", action="store", dest="minimum_perc",
- help=(
- "Specify the minimum acceptable percentage of a translation "
- "in order to download it."
- )
- )
- parser.add_option(
- "--mode", action="store", dest="mode", help=(
- "Specify the mode of the translation file to pull (e.g. "
- "'reviewed'). See http://help.transifex.com/features/client/"
- "index.html#defining-the-mode-of-the-translated-file for the"
- "available values."
- )
- )
- group = OptionGroup(parser, "Extended options", "These options can only be"
- " used with the --auto-local command.")
- group.add_option("-s","--source-language", action="store",
- dest="source_language",
- default=None, help="Specify the source language of a resource"
- " [requires --auto-local].")
- group.add_option("-f","--source-file", action="store", dest="source_file",
- default=None, help="Specify the source file of a resource [requires"
- " --auto-local].")
- group.add_option("--execute", action="store_true", dest="execute",
- default=False, help="Execute commands [requires --auto-local].")
- parser.add_option_group(group)
- return parser
-
-
-def status_parser():
- """Return the command-line parser for the status command."""
- usage="usage: %prog [tx_options] status [options]"
- description="Prints the status of the current project by reading the"\
- " data in the configuration file."
- parser = OptionParser(usage=usage,description=description)
- parser.add_option("-r","--resource", action="store", dest="resources",
- default=[], help="Specify resources")
- return parser
-
-
-def parse_csv_option(option):
- """Return a list out of the comma-separated option or an empty list."""
- if option:
- return option.split(',')
- else:
- return []
+++ /dev/null
-# -*- coding: utf-8 -*-
-
-"""
-Module for API-related calls.
-"""
-
-import urlparse
-
-
-def hostname_tld_migration(hostname):
- """
- Migrate transifex.net to transifex.com.
-
- :param hostname: The hostname to migrate (if needed).
- :returns: A hostname with the transifex.com domain (if needed).
- """
- parts = urlparse.urlparse(hostname)
- if parts.hostname.endswith('transifex.net'):
- hostname = hostname.replace('transifex.net', 'transifex.com', 1)
- return hostname
-
-
-def hostname_ssl_migration(hostname):
- """
- Migrate Transifex hostnames to use HTTPS.
-
- :param hostname: The hostname to migrate (if needed).
- :returns: A https hostname (if needed).
- """
- parts = urlparse.urlparse(hostname)
- is_transifex = (
- parts.hostname[-14:-3] == '.transifex.' or
- parts.hostname == 'transifex.net' or
- parts.hostname == 'transifex.com'
- )
- is_https = parts.scheme == 'https'
- if is_transifex and not is_https:
- if not parts.scheme:
- hostname = 'https:' + hostname
- else:
- hostname = hostname.replace(parts.scheme, 'https', 1)
- return hostname
-
-
-def visit_hostname(hostname):
- """
- Have a chance to visit a hostname before actually using it.
-
- :param hostname: The original hostname.
- :returns: The hostname with the necessary changes.
- """
- for processor in [hostname_ssl_migration, hostname_tld_migration, ]:
- hostname = processor(hostname)
- return hostname
+++ /dev/null
-# -*- coding: utf-8 -*-
-import base64
-import copy
-import getpass
-import os
-import re
-import fnmatch
-import urllib2
-import datetime, time
-import ConfigParser
-
-from txclib.web import *
-from txclib.utils import *
-from txclib.urls import API_URLS
-from txclib.config import OrderedRawConfigParser, Flipdict
-from txclib.log import logger
-from txclib.http_utils import http_response
-from txclib.processors import visit_hostname
-
-
-class ProjectNotInit(Exception):
- pass
-
-
-class Project(object):
- """
- Represents an association between the local and remote project instances.
- """
-
- def __init__(self, path_to_tx=None, init=True):
- """
- Initialize the Project attributes.
- """
- if init:
- self._init(path_to_tx)
-
- def _init(self, path_to_tx=None):
- instructions = "Run 'tx init' to initialize your project first!"
- try:
- self.root = self._get_tx_dir_path(path_to_tx)
- self.config_file = self._get_config_file_path(self.root)
- self.config = self._read_config_file(self.config_file)
- self.txrc_file = self._get_transifex_file()
- self.txrc = self._get_transifex_config(self.txrc_file)
- except ProjectNotInit, e:
- logger.error('\n'.join([unicode(e), instructions]))
- raise
-
- def _get_config_file_path(self, root_path):
- """Check the .tx/config file exists."""
- config_file = os.path.join(root_path, ".tx", "config")
- logger.debug("Config file is %s" % config_file)
- if not os.path.exists(config_file):
- msg = "Cannot find the config file (.tx/config)!"
- raise ProjectNotInit(msg)
- return config_file
-
- def _get_tx_dir_path(self, path_to_tx):
- """Check the .tx directory exists."""
- root_path = path_to_tx or find_dot_tx()
- logger.debug("Path to tx is %s." % root_path)
- if not root_path:
- msg = "Cannot find any .tx directory!"
- raise ProjectNotInit(msg)
- return root_path
-
- def _read_config_file(self, config_file):
- """Parse the config file and return its contents."""
- config = OrderedRawConfigParser()
- try:
- config.read(config_file)
- except Exception, err:
- msg = "Cannot open/parse .tx/config file: %s" % err
- raise ProjectNotInit(msg)
- return config
-
- def _get_transifex_config(self, txrc_file):
- """Read the configuration from the .transifexrc file."""
- txrc = OrderedRawConfigParser()
- try:
- txrc.read(txrc_file)
- except Exception, e:
- msg = "Cannot read global configuration file: %s" % e
- raise ProjectNotInit(msg)
- self._migrate_txrc_file(txrc)
- return txrc
-
- def _migrate_txrc_file(self, txrc):
- """Migrate the txrc file, if needed."""
- for section in txrc.sections():
- orig_hostname = txrc.get(section, 'hostname')
- hostname = visit_hostname(orig_hostname)
- if hostname != orig_hostname:
- msg = "Hostname %s should be changed to %s."
- logger.info(msg % (orig_hostname, hostname))
- if (sys.stdin.isatty() and sys.stdout.isatty() and
- confirm('Change it now? ', default=True)):
- txrc.set(section, 'hostname', hostname)
- msg = 'Hostname changed'
- logger.info(msg)
- else:
- hostname = orig_hostname
- self._save_txrc_file(txrc)
- return txrc
-
- def _get_transifex_file(self, directory=None):
- """Fetch the path of the .transifexrc file.
-
- It is in the home directory ofthe user by default.
- """
- if directory is None:
- directory = os.path.expanduser('~')
- txrc_file = os.path.join(directory, ".transifexrc")
- logger.debug(".transifexrc file is at %s" % directory)
- if not os.path.exists(txrc_file):
- msg = "No authentication data found."
- logger.info(msg)
- mask = os.umask(077)
- open(txrc_file, 'w').close()
- os.umask(mask)
- 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):
- """
- Read .transifexrc and report user,pass 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 raw_input("Please enter your transifex username: ")
- while (not username):
- username = raw_input("Please enter your transifex username: ")
- passwd = password
- while (not passwd):
- passwd = getpass.getpass()
-
- logger.info("Updating %s file..." % self.txrc_file)
- 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, 'hostname', host)
-
- return username, passwd
-
- def set_remote_resource(self, resource, source_lang, i18n_type, host,
- file_filter="translations<sep>%(proj)s.%(res)s<sep><lang>.%(extension)s"):
- """
- Method to handle the add/conf of a remote resource.
- """
- if not self.config.has_section(resource):
- self.config.add_section(resource)
-
- p_slug, r_slug = resource.split('.')
- file_filter = file_filter.replace("<sep>", r"%s" % os.path.sep)
- self.url_info = {
- 'host': host,
- 'project': p_slug,
- 'resource': r_slug
- }
- extension = self._extension_for(i18n_type)[1:]
-
- self.config.set(resource, 'source_lang', source_lang)
- self.config.set(
- resource, 'file_filter',
- file_filter % {'proj': p_slug, 'res': r_slug, 'extension': extension}
- )
- if host != self.config.get('main', 'host'):
- self.config.set(resource, 'host', host)
-
- def get_resource_host(self, resource):
- """
- Returns the host that the resource is configured to use. If there is no
- such option we return the default one
- """
- if self.config.has_option(resource, 'host'):
- return self.config.get(resource, 'host')
- return self.config.get('main', 'host')
-
- def get_resource_lang_mapping(self, resource):
- """
- Get language mappings for a specific resource.
- """
- lang_map = Flipdict()
- try:
- args = self.config.get("main", "lang_map")
- for arg in args.replace(' ', '').split(','):
- k,v = arg.split(":")
- lang_map.update({k:v})
- except ConfigParser.NoOptionError:
- pass
- except (ValueError, KeyError):
- raise Exception("Your lang map configuration is not correct.")
-
- if self.config.has_section(resource):
- res_lang_map = Flipdict()
- try:
- args = self.config.get(resource, "lang_map")
- for arg in args.replace(' ', '').split(','):
- k,v = arg.split(":")
- res_lang_map.update({k:v})
- except ConfigParser.NoOptionError:
- pass
- except (ValueError, KeyError):
- raise Exception("Your lang map configuration is not correct.")
-
- # merge the lang maps and return result
- lang_map.update(res_lang_map)
-
- return lang_map
-
-
- def get_resource_files(self, resource):
- """
- 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. The resulting dict will be in this format:
-
- { 'en': 'path/foo/en/bar.po', 'de': 'path/foo/de/bar.po', 'es': 'path/exceptions/es.po'}
-
- NOTE: All paths are relative to the root of the project
- """
- tr_files = {}
- if self.config.has_section(resource):
- try:
- file_filter = self.config.get(resource, "file_filter")
- except ConfigParser.NoOptionError:
- file_filter = "$^"
- source_lang = self.config.get(resource, "source_lang")
- source_file = self.get_resource_option(resource, 'source_file') or None
- expr_re = regex_from_filefilter(file_filter, self.root)
- expr_rec = re.compile(expr_re)
- for root, dirs, files in os.walk(self.root):
- 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)
- if lang != source_lang:
- f_path = relpath(f_path, self.root)
- if f_path != source_file:
- tr_files.update({lang: f_path})
-
- for (name, value) in self.config.items(resource):
- if name.startswith("trans."):
- lang = name.split('.')[1]
- # delete language which has same file
- if value in tr_files.values():
- keys = []
- for k, v in tr_files.iteritems():
- if v == value:
- keys.append(k)
- if len(keys) == 1:
- del tr_files[keys[0]]
- else:
- raise Exception("Your configuration seems wrong."\
- " You have multiple languages pointing to"\
- " the same file.")
- # Add language with correct file
- tr_files.update({lang:value})
-
- return tr_files
-
- return None
-
- def get_resource_option(self, resource, option):
- """
- Return the requested option for a specific resource
-
- If there is no such option, we return None
- """
-
- if self.config.has_section(resource):
- if self.config.has_option(resource, option):
- return self.config.get(resource, option)
- return None
-
- def get_resource_list(self, project=None):
- """
- Parse config file and return tuples with the following format
-
- [ (project_slug, resource_slug), (..., ...)]
- """
-
- resource_list= []
- for r in self.config.sections():
- if r == 'main':
- continue
- p_slug, r_slug = r.split('.', 1)
- if project and p_slug != project:
- continue
- resource_list.append(r)
-
- return resource_list
-
- def save(self):
- """
- Store the config dictionary in the .tx/config file of the project.
- """
- self._save_tx_config()
- self._save_txrc_file()
-
- def _save_tx_config(self, config=None):
- """Save the local config file."""
- if config is None:
- config = self.config
- fh = open(self.config_file,"w")
- config.write(fh)
- fh.close()
-
- def _save_txrc_file(self, txrc=None):
- """Save the .transifexrc file."""
- if txrc is None:
- txrc = self.txrc
- mask = os.umask(077)
- fh = open(self.txrc_file, 'w')
- txrc.write(fh)
- fh.close()
- os.umask(mask)
-
- def get_full_path(self, relpath):
- if relpath[0] == "/":
- return relpath
- else:
- return os.path.join(self.root, relpath)
-
- def pull(self, languages=[], resources=[], overwrite=True, fetchall=False,
- fetchsource=False, force=False, skip=False, minimum_perc=0, mode=None):
- """Pull all translations file from transifex server."""
- self.minimum_perc = minimum_perc
- resource_list = self.get_chosen_resources(resources)
-
- if mode == 'reviewed':
- url = 'pull_reviewed_file'
- elif mode == 'translator':
- url = 'pull_translator_file'
- elif mode == 'developer':
- url = 'pull_developer_file'
- else:
- url = 'pull_file'
-
- for resource in resource_list:
- logger.debug("Handling resource %s" % resource)
- self.resource = resource
- project_slug, resource_slug = resource.split('.')
- files = self.get_resource_files(resource)
- slang = self.get_resource_option(resource, 'source_lang')
- sfile = self.get_resource_option(resource, 'source_file')
- lang_map = self.get_resource_lang_mapping(resource)
- host = self.get_resource_host(resource)
- logger.debug("Language mapping is: %s" % lang_map)
- if mode is None:
- mode = self._get_option(resource, 'mode')
- self.url_info = {
- 'host': host,
- 'project': project_slug,
- 'resource': resource_slug
- }
- logger.debug("URL data are: %s" % self.url_info)
-
- stats = self._get_stats_for_resource()
-
-
- try:
- file_filter = self.config.get(resource, 'file_filter')
- except ConfigParser.NoOptionError:
- file_filter = None
-
- # Pull source file
- pull_languages = set([])
- new_translations = set([])
-
- if fetchall:
- new_translations = self._new_translations_to_add(
- files, slang, lang_map, stats, force
- )
- if new_translations:
- msg = "New translations found for the following languages: %s"
- logger.info(msg % ', '.join(new_translations))
-
- existing, new = self._languages_to_pull(
- languages, files, lang_map, stats, force
- )
- pull_languages |= existing
- new_translations |= new
- logger.debug("Adding to new translations: %s" % new)
-
- if fetchsource:
- if sfile and slang not in pull_languages:
- pull_languages.add(slang)
- elif slang not in new_translations:
- new_translations.add(slang)
-
- if pull_languages:
- logger.debug("Pulling languages for: %s" % pull_languages)
- msg = "Pulling translations for resource %s (source: %s)"
- logger.info(msg % (resource, sfile))
-
- for lang in pull_languages:
- local_lang = lang
- if lang in lang_map.values():
- remote_lang = lang_map.flip[lang]
- else:
- remote_lang = lang
- if languages and lang not in pull_languages:
- logger.debug("Skipping language %s" % lang)
- continue
- if lang != slang:
- local_file = files.get(lang, None) or files[lang_map[lang]]
- else:
- local_file = sfile
- logger.debug("Using file %s" % local_file)
-
- kwargs = {
- 'lang': remote_lang,
- 'stats': stats,
- 'local_file': local_file,
- 'force': force,
- 'mode': mode,
- }
- if not self._should_update_translation(**kwargs):
- msg = "Skipping '%s' translation (file: %s)."
- logger.info(
- msg % (color_text(remote_lang, "RED"), local_file)
- )
- continue
-
- if not overwrite:
- local_file = ("%s.new" % local_file)
- logger.warning(
- " -> %s: %s" % (color_text(remote_lang, "RED"), local_file)
- )
- try:
- r = self.do_url_request(url, language=remote_lang)
- except Exception,e:
- if not skip:
- raise e
- else:
- logger.error(e)
- continue
- base_dir = os.path.split(local_file)[0]
- mkdir_p(base_dir)
- fd = open(local_file, 'wb')
- fd.write(r)
- fd.close()
-
- if new_translations:
- msg = "Pulling new translations for resource %s (source: %s)"
- logger.info(msg % (resource, sfile))
- for lang in new_translations:
- if lang in lang_map.keys():
- local_lang = lang_map[lang]
- else:
- local_lang = lang
- remote_lang = lang
- if file_filter:
- local_file = relpath(os.path.join(self.root,
- file_filter.replace('<lang>', local_lang)), os.curdir)
- else:
- trans_dir = os.path.join(self.root, ".tx", resource)
- if not os.path.exists(trans_dir):
- os.mkdir(trans_dir)
- local_file = relpath(os.path.join(trans_dir, '%s_translation' %
- local_lang, os.curdir))
-
- if lang != slang:
- satisfies_min = self._satisfies_min_translated(
- stats[remote_lang], mode
- )
- if not satisfies_min:
- msg = "Skipping language %s due to used options."
- logger.info(msg % lang)
- continue
- logger.warning(
- " -> %s: %s" % (color_text(remote_lang, "RED"), local_file)
- )
- r = self.do_url_request(url, language=remote_lang)
-
- base_dir = os.path.split(local_file)[0]
- mkdir_p(base_dir)
- fd = open(local_file, 'wb')
- fd.write(r)
- fd.close()
-
- def push(self, source=False, translations=False, force=False, resources=[], languages=[],
- skip=False, no_interactive=False):
- """
- Push all the resources
- """
- resource_list = self.get_chosen_resources(resources)
- self.skip = skip
- self.force = force
- for resource in resource_list:
- push_languages = []
- project_slug, resource_slug = resource.split('.')
- files = self.get_resource_files(resource)
- slang = self.get_resource_option(resource, 'source_lang')
- sfile = self.get_resource_option(resource, 'source_file')
- lang_map = self.get_resource_lang_mapping(resource)
- host = self.get_resource_host(resource)
- logger.debug("Language mapping is: %s" % lang_map)
- logger.debug("Using host %s" % host)
- self.url_info = {
- 'host': host,
- 'project': project_slug,
- 'resource': resource_slug
- }
-
- logger.info("Pushing translations for resource %s:" % resource)
-
- stats = self._get_stats_for_resource()
-
- if force and not no_interactive:
- answer = raw_input("Warning: By using --force, the uploaded"
- " files will overwrite remote translations, even if they"
- " are newer than your uploaded files.\nAre you sure you"
- " want to continue? [y/N] ")
-
- if not answer in ["", 'Y', 'y', "yes", 'YES']:
- return
-
- if source:
- if sfile == None:
- logger.error("You don't seem to have a proper source file"
- " mapping for resource %s. Try without the --source"
- " option or set a source file first and then try again." %
- resource)
- continue
- # Push source file
- try:
- logger.warning("Pushing source file (%s)" % sfile)
- if not self._resource_exists(stats):
- logger.info("Resource does not exist. Creating...")
- fileinfo = "%s;%s" % (resource_slug, slang)
- filename = self.get_full_path(sfile)
- self._create_resource(resource, project_slug, fileinfo, filename)
- self.do_url_request(
- 'push_source', multipart=True, method="PUT",
- files=[(
- "%s;%s" % (resource_slug, slang)
- , self.get_full_path(sfile)
- )],
- )
- except Exception, e:
- if not skip:
- raise
- else:
- logger.error(e)
- else:
- try:
- self.do_url_request('resource_details')
- except Exception, e:
- code = getattr(e, 'code', None)
- if code == 404:
- msg = "Resource %s doesn't exist on the server."
- logger.error(msg % resource)
- continue
-
- if translations:
- # Check if given language codes exist
- if not languages:
- push_languages = files.keys()
- else:
- push_languages = []
- f_langs = files.keys()
- for l in languages:
- if l in lang_map.keys():
- l = lang_map[l]
- push_languages.append(l)
- if l not in f_langs:
- msg = "Warning: No mapping found for language code '%s'."
- logger.error(msg % color_text(l,"RED"))
- logger.debug("Languages to push are %s" % push_languages)
-
- # Push translation files one by one
- for lang in push_languages:
- local_lang = lang
- if lang in lang_map.values():
- remote_lang = lang_map.flip[lang]
- else:
- remote_lang = lang
-
- local_file = files[local_lang]
-
- kwargs = {
- 'lang': remote_lang,
- 'stats': stats,
- 'local_file': local_file,
- 'force': force,
- }
- if not self._should_push_translation(**kwargs):
- msg = "Skipping '%s' translation (file: %s)."
- logger.info(msg % (color_text(lang, "RED"), local_file))
- continue
-
- msg = "Pushing '%s' translations (file: %s)"
- logger.warning(
- msg % (color_text(remote_lang, "RED"), local_file)
- )
- try:
- self.do_url_request(
- 'push_translation', multipart=True, method='PUT',
- files=[(
- "%s;%s" % (resource_slug, remote_lang),
- self.get_full_path(local_file)
- )], language=remote_lang
- )
- logger.debug("Translation %s pushed." % remote_lang)
- except Exception, e:
- if not skip:
- raise e
- else:
- logger.error(e)
-
- def delete(self, resources=[], languages=[], skip=False, force=False):
- """Delete translations."""
- resource_list = self.get_chosen_resources(resources)
- self.skip = skip
- self.force = force
-
- if not languages:
- delete_func = self._delete_resource
- else:
- delete_func = self._delete_translations
-
- for resource in resource_list:
- project_slug, resource_slug = resource.split('.')
- host = self.get_resource_host(resource)
- self.url_info = {
- 'host': host,
- 'project': project_slug,
- 'resource': resource_slug
- }
- logger.debug("URL data are: %s" % self.url_info)
- project_details = parse_json(
- self.do_url_request('project_details', project=self)
- )
- teams = project_details['teams']
- stats = self._get_stats_for_resource()
- delete_func(project_details, resource, stats, languages)
-
- def _delete_resource(self, project_details, resource, stats, *args):
- """Delete a resource from Transifex."""
- project_slug, resource_slug = resource.split('.')
- project_resource_slugs = [
- r['slug'] for r in project_details['resources']
- ]
- logger.info("Deleting resource %s:" % resource)
- if resource_slug not in project_resource_slugs:
- if not self.skip:
- msg = "Skipping: %s : Resource does not exist."
- logger.info(msg % resource)
- return
- if not self.force:
- slang = self.get_resource_option(resource, 'source_lang')
- for language in stats:
- if language == slang:
- continue
- if int(stats[language]['translated_entities']) > 0:
- msg = (
- "Skipping: %s : Unable to delete resource because it "
- "has a not empty %s translation.\nPlease use -f or "
- "--force option to delete this resource."
- )
- logger.info(msg % (resource, language))
- return
- try:
- self.do_url_request('delete_resource', method="DELETE")
- self.config.remove_section(resource)
- self.save()
- msg = "Deleted resource %s of project %s."
- logger.info(msg % (resource_slug, project_slug))
- except Exception, e:
- msg = "Unable to delete resource %s of project %s."
- logger.error(msg % (resource_slug, project_slug))
- if not self.skip:
- raise
-
- def _delete_translations(self, project_details, resource, stats, languages):
- """Delete the specified translations for the specified resource."""
- logger.info("Deleting translations from resource %s:" % resource)
- for language in languages:
- self._delete_translation(project_details, resource, stats, language)
-
- def _delete_translation(self, project_details, resource, stats, language):
- """Delete a specific translation from the specified resource."""
- project_slug, resource_slug = resource.split('.')
- if language not in stats:
- if not self.skip:
- msg = "Skipping %s: Translation does not exist."
- logger.warning(msg % (language))
- return
- if not self.force:
- teams = project_details['teams']
- if language in teams:
- msg = (
- "Skipping %s: Unable to delete translation because it is "
- "associated with a team.\nPlease use -f or --force option "
- "to delete this translation."
- )
- logger.warning(msg % language)
- return
- if int(stats[language]['translated_entities']) > 0:
- msg = (
- "Skipping %s: Unable to delete translation because it "
- "is not empty.\nPlease use -f or --force option to delete "
- "this translation."
- )
- logger.warning(msg % language)
- return
- try:
- self.do_url_request(
- 'delete_translation', language=language, method="DELETE"
- )
- msg = "Deleted language %s from resource %s of project %s."
- logger.info(msg % (language, resource_slug, project_slug))
- except Exception, e:
- msg = "Unable to delete translation %s"
- logger.error(msg % language)
- if not self.skip:
- raise
-
- def do_url_request(self, api_call, multipart=False, data=None,
- files=[], encoding=None, method="GET", **kwargs):
- """
- Issues a url request.
- """
- # Read the credentials from the config file (.transifexrc)
- host = self.url_info['host']
- try:
- username = self.txrc.get(host, 'username')
- passwd = self.txrc.get(host, 'password')
- token = self.txrc.get(host, 'token')
- hostname = self.txrc.get(host, 'hostname')
- except ConfigParser.NoSectionError:
- raise Exception("No user credentials found for host %s. Edit"
- " ~/.transifexrc and add the appropriate info in there." %
- host)
-
- # Create the Url
- kwargs['hostname'] = hostname
- kwargs.update(self.url_info)
- url = (API_URLS[api_call] % kwargs).encode('UTF-8')
- logger.debug(url)
-
- opener = None
- headers = None
- req = None
-
- if multipart:
- opener = urllib2.build_opener(MultipartPostHandler)
- for info,filename in files:
- data = { "resource" : info.split(';')[0],
- "language" : info.split(';')[1],
- "uploaded_file" : open(filename,'rb') }
-
- urllib2.install_opener(opener)
- req = RequestWithMethod(url=url, data=data, method=method)
- else:
- req = RequestWithMethod(url=url, data=data, method=method)
- if encoding:
- req.add_header("Content-Type",encoding)
-
- base64string = base64.encodestring('%s:%s' % (username, passwd))[:-1]
- authheader = "Basic %s" % base64string
- req.add_header("Authorization", authheader)
- req.add_header("Accept-Encoding", "gzip,deflate")
- req.add_header("User-Agent", user_agent_identifier())
-
- try:
- response = urllib2.urlopen(req, timeout=300)
- return http_response(response)
- except urllib2.HTTPError, e:
- if e.code in [401, 403, 404]:
- raise e
- elif 200 <= e.code < 300:
- return None
- else:
- # For other requests, we should print the message as well
- raise Exception("Remote server replied: %s" % e.read())
- except urllib2.URLError, e:
- error = e.args[0]
- raise Exception("Remote server replied: %s" % error[1])
-
-
- def _should_update_translation(self, lang, stats, local_file, force=False,
- mode=None):
- """Whether a translation should be udpated from Transifex.
-
- We use the following criteria for that:
- - If user requested to force the download.
- - If language exists in Transifex.
- - If the local file is older than the Transifex's file.
- - If the user requested a x% completion.
-
- Args:
- lang: The language code to check.
- stats: The (global) statistics object.
- local_file: The local translation file.
- force: A boolean flag.
- mode: The mode for the translation.
- Returns:
- True or False.
- """
- return self._should_download(lang, stats, local_file, force)
-
- def _should_add_translation(self, lang, stats, force=False, mode=None):
- """Whether a translation should be added from Transifex.
-
- We use the following criteria for that:
- - If user requested to force the download.
- - If language exists in Transifex.
- - If the user requested a x% completion.
-
- Args:
- lang: The language code to check.
- stats: The (global) statistics object.
- force: A boolean flag.
- mode: The mode for the translation.
- Returns:
- True or False.
- """
- return self._should_download(lang, stats, None, force)
-
- def _should_download(self, lang, stats, local_file=None, force=False,
- mode=None):
- """Return whether a translation should be downloaded.
-
- If local_file is None, skip the timestamps check (the file does
- not exist locally).
- """
- try:
- lang_stats = stats[lang]
- except KeyError, e:
- logger.debug("No lang %s in statistics" % lang)
- return False
-
- satisfies_min = self._satisfies_min_translated(lang_stats, mode)
- if not satisfies_min:
- return False
-
- if force:
- logger.debug("Downloading translation due to -f")
- return True
-
- if local_file is not None:
- remote_update = self._extract_updated(lang_stats)
- if not self._remote_is_newer(remote_update, local_file):
- logger.debug("Local is newer than remote for lang %s" % lang)
- return False
- return True
-
- def _should_push_translation(self, lang, stats, local_file, force=False):
- """Return whether a local translation file should be
- pushed to Trasnifex.
-
- We use the following criteria for that:
- - If user requested to force the upload.
- - If language exists in Transifex.
- - If local file is younger than the remote file.
-
- Args:
- lang: The language code to check.
- stats: The (global) statistics object.
- local_file: The local translation file.
- force: A boolean flag.
- Returns:
- True or False.
- """
- if force:
- logger.debug("Push translation due to -f.")
- return True
- try:
- lang_stats = stats[lang]
- except KeyError, e:
- logger.debug("Language %s does not exist in Transifex." % lang)
- return True
- if local_file is not None:
- remote_update = self._extract_updated(lang_stats)
- if self._remote_is_newer(remote_update, local_file):
- msg = "Remote translation is newer than local file for lang %s"
- logger.debug(msg % lang)
- return False
- return True
-
- def _generate_timestamp(self, update_datetime):
- """Generate a UNIX timestamp from the argument.
-
- Args:
- update_datetime: The datetime in the format used by Transifex.
- Returns:
- A float, representing the timestamp that corresponds to the
- argument.
- """
- time_format = "%Y-%m-%d %H:%M:%S"
- return time.mktime(
- datetime.datetime(
- *time.strptime(update_datetime, time_format)[0:5]
- ).utctimetuple()
- )
-
- def _get_time_of_local_file(self, path):
- """Get the modified time of the path_.
-
- Args:
- path: The path we want the mtime for.
- Returns:
- The time as a timestamp or None, if the file does not exist
- """
- if not os.path.exists(path):
- return None
- return time.mktime(time.gmtime(os.path.getmtime(path)))
-
- def _satisfies_min_translated(self, stats, mode=None):
- """Check whether a translation fulfills the filter used for
- minimum translated percentage.
-
- Args:
- perc: The current translation percentage.
- Returns:
- True or False
- """
- cur = self._extract_completed(stats, mode)
- option_name = 'minimum_perc'
- if self.minimum_perc is not None:
- minimum_percent = self.minimum_perc
- else:
- global_minimum = int(
- self.get_resource_option('main', option_name) or 0
- )
- resource_minimum = int(
- self.get_resource_option(
- self.resource, option_name
- ) or global_minimum
- )
- minimum_percent = resource_minimum
- return cur >= minimum_percent
-
- def _remote_is_newer(self, remote_updated, local_file):
- """Check whether the remote translation is newer that the local file.
-
- Args:
- remote_updated: The date and time the translation was last
- updated remotely.
- local_file: The local file.
- Returns:
- True or False.
- """
- if remote_updated is None:
- logger.debug("No remote time")
- return False
- remote_time = self._generate_timestamp(remote_updated)
- local_time = self._get_time_of_local_file(
- self.get_full_path(local_file)
- )
- logger.debug(
- "Remote time is %s and local %s" % (remote_time, local_time)
- )
- if local_time is not None and remote_time < local_time:
- return False
- return True
-
- @classmethod
- def _extract_completed(cls, stats, mode=None):
- """Extract the information for the translated percentage from the stats.
-
- Args:
- stats: The stats object for a language as returned by Transifex.
- mode: The mode of translations requested.
- Returns:
- The percentage of translation as integer.
- """
- if mode == 'reviewed':
- key = 'reviewed_percentage'
- else:
- key = 'completed'
- try:
- return int(stats[key][:-1])
- except KeyError, e:
- return 0
-
- @classmethod
- def _extract_updated(cls, stats):
- """Extract the information for the last update of a translation.
-
- Args:
- stats: The stats object for a language as returned by Transifex.
- Returns:
- The last update field.
- """
- try:
- return stats['last_update']
- except KeyError, e:
- return None
-
- def _new_translations_to_add(self, files, slang, lang_map,
- stats, force=False):
- """Return a list of translations which are new to the
- local installation.
- """
- new_translations = []
- timestamp = time.time()
- langs = stats.keys()
- logger.debug("Available languages are: %s" % langs)
-
- for lang in langs:
- lang_exists = lang in files.keys()
- lang_is_source = lang == slang
- mapped_lang_exists = (
- lang in lang_map and lang_map[lang] in files.keys()
- )
- if lang_exists or lang_is_source or mapped_lang_exists:
- continue
- if self._should_add_translation(lang, stats, force):
- new_translations.append(lang)
- return set(new_translations)
-
- def _get_stats_for_resource(self):
- """Get the statistics information for a resource."""
- try:
- r = self.do_url_request('resource_stats')
- logger.debug("Statistics response is %s" % r)
- stats = parse_json(r)
- except urllib2.HTTPError, e:
- logger.debug("Resource not found: %s" % e)
- stats = {}
- except Exception,e:
- logger.debug("Network error: %s" % e)
- raise
- return stats
-
- def get_chosen_resources(self, resources):
- """Get the resources the user selected.
-
- Support wildcards in the resources specified by the user.
-
- Args:
- resources: A list of resources as specified in command-line or
- an empty list.
- Returns:
- A list of resources.
- """
- configured_resources = self.get_resource_list()
- if not resources:
- return configured_resources
-
- selected_resources = []
- for resource in resources:
- found = False
- for full_name in configured_resources:
- if fnmatch.fnmatch(full_name, resource):
- selected_resources.append(full_name)
- found = True
- if not found:
- msg = "Specified resource '%s' does not exist."
- raise Exception(msg % resource)
- logger.debug("Operating on resources: %s" % selected_resources)
- return selected_resources
-
- def _languages_to_pull(self, languages, files, lang_map, stats, force):
- """Get a set of langauges to pull.
-
- Args:
- languages: A list of languages the user selected in cmd.
- files: A dictionary of current local translation files.
- Returns:
- A tuple of a set of existing languages and new translations.
- """
- if not languages:
- pull_languages = set([])
- pull_languages |= set(files.keys())
- mapped_files = []
- for lang in pull_languages:
- if lang in lang_map.flip:
- mapped_files.append(lang_map.flip[lang])
- pull_languages -= set(lang_map.flip.keys())
- pull_languages |= set(mapped_files)
- return (pull_languages, set([]))
- else:
- pull_languages = []
- new_translations = []
- f_langs = files.keys()
- for l in languages:
- if l not in f_langs and not (l in lang_map and lang_map[l] in f_langs):
- if self._should_add_translation(l, stats, force):
- new_translations.append(l)
- else:
- if l in lang_map.keys():
- l = lang_map[l]
- pull_languages.append(l)
- return (set(pull_languages), set(new_translations))
-
- def _extension_for(self, i18n_type):
- """Return the extension used for the specified type."""
- try:
- res = parse_json(self.do_url_request('formats'))
- return res[i18n_type]['file-extensions'].split(',')[0]
- except Exception,e:
- logger.error(e)
- return ''
-
- def _resource_exists(self, stats):
- """Check if resource exists.
-
- Args:
- stats: The statistics dict as returned by Tx.
- Returns:
- True, if the resource exists in the server.
- """
- return bool(stats)
-
- def _create_resource(self, resource, pslug, fileinfo, filename, **kwargs):
- """Create a resource.
-
- Args:
- resource: The full resource name.
- pslug: The slug of the project.
- fileinfo: The information of the resource.
- filename: The name of the file.
- Raises:
- URLError, in case of a problem.
- """
- multipart = True
- method = "POST"
- api_call = 'create_resource'
-
- host = self.url_info['host']
- try:
- username = self.txrc.get(host, 'username')
- passwd = self.txrc.get(host, 'password')
- token = self.txrc.get(host, 'token')
- hostname = self.txrc.get(host, 'hostname')
- except ConfigParser.NoSectionError:
- raise Exception("No user credentials found for host %s. Edit"
- " ~/.transifexrc and add the appropriate info in there." %
- host)
-
- # Create the Url
- kwargs['hostname'] = hostname
- kwargs.update(self.url_info)
- kwargs['project'] = pslug
- url = (API_URLS[api_call] % kwargs).encode('UTF-8')
-
- opener = None
- headers = None
- req = None
-
- i18n_type = self._get_option(resource, 'type')
- if i18n_type is None:
- logger.error(
- "Please define the resource type in .tx/config (eg. type = PO)."
- " More info: http://bit.ly/txcl-rt"
- )
-
- opener = urllib2.build_opener(MultipartPostHandler)
- data = {
- "slug": fileinfo.split(';')[0],
- "name": fileinfo.split(';')[0],
- "uploaded_file": open(filename,'rb'),
- "i18n_type": i18n_type
- }
- urllib2.install_opener(opener)
- req = RequestWithMethod(url=url, data=data, method=method)
-
- base64string = base64.encodestring('%s:%s' % (username, passwd))[:-1]
- authheader = "Basic %s" % base64string
- req.add_header("Authorization", authheader)
-
- try:
- fh = urllib2.urlopen(req)
- except urllib2.HTTPError, e:
- if e.code in [401, 403, 404]:
- raise e
- else:
- # For other requests, we should print the message as well
- raise Exception("Remote server replied: %s" % e.read())
- except urllib2.URLError, e:
- error = e.args[0]
- raise Exception("Remote server replied: %s" % error[1])
-
- raw = fh.read()
- fh.close()
- return raw
-
- def _get_option(self, resource, option):
- """Get the value for the option in the config file.
-
- If the option is not in the resource section, look for it in
- the project.
-
- Args:
- resource: The resource name.
- option: The option the value of which we are interested in.
- Returns:
- The option value or None, if it does not exist.
- """
- value = self.get_resource_option(resource, option)
- if value is None:
- if self.config.has_option('main', option):
- return self.config.get('main', option)
- return value
-
- def set_i18n_type(self, resources, i18n_type):
- """Set the type for the specified resources."""
- self._set_resource_option(resources, key='type', value=i18n_type)
-
- def set_min_perc(self, resources, perc):
- """Set the minimum percentage for the resources."""
- self._set_resource_option(resources, key='minimum_perc', value=perc)
-
- def set_default_mode(self, resources, mode):
- """Set the default mode for the specified resources."""
- self._set_resource_option(resources, key='mode', value=mode)
-
- def _set_resource_option(self, resources, key, value):
- """Set options in the config file.
-
- If resources is empty. set the option globally.
- """
- if not resources:
- self.config.set('main', key, value)
- return
- for r in resources:
- self.config.set(r, key, value)
+++ /dev/null
-# These are the Transifex API urls
-
-API_URLS = {
- 'get_resources': '%(hostname)s/api/2/project/%(project)s/resources/',
- 'project_details': '%(hostname)s/api/2/project/%(project)s/?details',
- 'resource_details': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/',
- 'release_details': '%(hostname)s/api/2/project/%(project)s/release/%(release)s/',
- 'pull_file': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/translation/%(language)s/?file',
- 'pull_reviewed_file': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/translation/%(language)s/?file&mode=reviewed',
- 'pull_translator_file': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/translation/%(language)s/?file&mode=translated',
- 'pull_developer_file': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/translation/%(language)s/?file&mode=default',
- 'resource_stats': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/stats/',
- 'create_resource': '%(hostname)s/api/2/project/%(project)s/resources/',
- 'push_source': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/content/',
- 'push_translation': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/translation/%(language)s/',
- 'delete_translation': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/translation/%(language)s/',
- 'formats': '%(hostname)s/api/2/formats/',
- 'delete_resource': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/',
-}
-
-
+++ /dev/null
-import os, sys, re, errno
-try:
- from json import loads as parse_json, dumps as compile_json
-except ImportError:
- from simplejson import loads as parse_json, dumps as compile_json
-import urllib2 # This should go and instead use do_url_request everywhere
-
-from urls import API_URLS
-from txclib.log import logger
-from txclib.exceptions import UnknownCommandError
-
-
-def find_dot_tx(path = os.path.curdir, previous = None):
- """
- Return the path where .tx folder is found.
-
- The 'path' should be a DIRECTORY.
- This process is functioning recursively from the current directory to each
- one of the ancestors dirs.
- """
- path = os.path.abspath(path)
- if path == previous:
- return None
- joined = os.path.join(path, ".tx")
- if os.path.isdir(joined):
- return path
- else:
- return find_dot_tx(os.path.dirname(path), path)
-
-
-#################################################
-# Parse file filter expressions and create regex
-
-def regex_from_filefilter(file_filter, root_path = os.path.curdir):
- """
- Create proper regex from <lang> expression
- """
- # Force expr to be a valid regex expr (escaped) but keep <lang> intact
- expr_re = re.escape(os.path.join(root_path, file_filter))
- expr_re = expr_re.replace("\\<lang\\>", '<lang>').replace(
- '<lang>', '([^%(sep)s]+)' % { 'sep': re.escape(os.path.sep)})
-
- return "^%s$" % expr_re
-
-
-TX_URLS = {
- 'resource': '(?P<hostname>https?://(\w|\.|:|-)+)/projects/p/(?P<project>(\w|-)+)/resource/(?P<resource>(\w|-)+)/?$',
- 'release': '(?P<hostname>https?://(\w|\.|:|-)+)/projects/p/(?P<project>(\w|-)+)/r/(?P<release>(\w|-)+)/?$',
- 'project': '(?P<hostname>https?://(\w|\.|:|-)+)/projects/p/(?P<project>(\w|-)+)/?$',
-}
-
-
-def parse_tx_url(url):
- """
- Try to match given url to any of the valid url patterns specified in
- TX_URLS. If not match is found, we raise exception
- """
- for type in TX_URLS.keys():
- pattern = TX_URLS[type]
- m = re.match(pattern, url)
- if m:
- return type, m.groupdict()
-
- raise Exception("tx: Malformed url given. Please refer to our docs: http://bit.ly/txautor")
-
-
-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.
- """
- import base64
- url = (API_URLS[api_call] % (kwargs)).encode('UTF-8')
-
- req = urllib2.Request(url=url)
- base64string = base64.encodestring('%s:%s' % (username, password))[:-1]
- authheader = "Basic %s" % base64string
- req.add_header("Authorization", authheader)
-
- try:
- fh = urllib2.urlopen(req)
- raw = fh.read()
- fh.close()
- remote_project = parse_json(raw)
- except urllib2.HTTPError, e:
- if e.code in [401, 403, 404]:
- raise e
- else:
- # For other requests, we should print the message as well
- raise Exception("Remote server replied: %s" % e.read())
- except urllib2.URLError, e:
- error = e.args[0]
- raise Exception("Remote server replied: %s" % error[1])
-
- return remote_project
-
-
-def valid_slug(slug):
- """
- Check if a slug contains only valid characters.
-
- Valid chars include [-_\w]
- """
- try:
- a, b = slug.split('.')
- except ValueError:
- return False
- else:
- if re.match("^[A-Za-z0-9_-]*$", a) and re.match("^[A-Za-z0-9_-]*$", b):
- return True
- return False
-
-
-def discover_commands():
- """
- Inspect commands.py and find all available commands
- """
- import inspect
- from txclib import commands
-
- command_table = {}
- fns = inspect.getmembers(commands, inspect.isfunction)
-
- for name, fn in fns:
- if name.startswith("cmd_"):
- command_table.update({
- name.split("cmd_")[1]:fn
- })
-
- return command_table
-
-
-def exec_command(command, *args, **kwargs):
- """
- Execute given command
- """
- commands = discover_commands()
- try:
- cmd_fn = commands[command]
- except KeyError:
- raise UnknownCommandError
- cmd_fn(*args,**kwargs)
-
-
-def mkdir_p(path):
- try:
- if path:
- os.makedirs(path)
- except OSError, exc: # Python >2.5
- if exc.errno == errno.EEXIST:
- pass
- else:
- raise
-
-
-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
- """
- valid_yes = ['Y', 'y', 'Yes', 'yes', ]
- valid_no = ['N', 'n', 'No', 'no', ]
- if default:
- prompt = prompt + '[Y/n]'
- valid_yes.append('')
- else:
- prompt = prompt + '[y/N]'
- valid_no.append('')
-
- ans = raw_input(prompt)
- while (ans not in valid_yes and ans not in valid_no):
- ans = raw_input(prompt)
-
- return ans in valid_yes
-
-
-# Stuff for command line colored output
-
-COLORS = [
- 'BLACK', 'RED', 'GREEN', 'YELLOW',
- 'BLUE', 'MAGENTA', 'CYAN', 'WHITE'
-]
-
-DISABLE_COLORS = False
-
-
-def color_text(text, color_name, bold=False):
- """
- 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")
- """
- if color_name in COLORS and not DISABLE_COLORS:
- return '\033[%s;%sm%s\033[0m' % (
- int(bold), COLORS.index(color_name) + 30, text)
- else:
- return text
-
-
-##############################################
-# relpath implementation taken from Python 2.7
-
-if not hasattr(os.path, 'relpath'):
- if os.path is sys.modules.get('ntpath'):
- def relpath(path, start=os.path.curdir):
- """Return a relative version of a path"""
-
- if not path:
- raise ValueError("no path specified")
- start_list = os.path.abspath(start).split(os.path.sep)
- path_list = os.path.abspath(path).split(os.path.sep)
- if start_list[0].lower() != path_list[0].lower():
- unc_path, rest = os.path.splitunc(path)
- unc_start, rest = os.path.splitunc(start)
- if bool(unc_path) ^ bool(unc_start):
- raise ValueError("Cannot mix UNC and non-UNC paths (%s and %s)"
- % (path, start))
- else:
- raise ValueError("path is on drive %s, start on drive %s"
- % (path_list[0], start_list[0]))
- # Work out how much of the filepath is shared by start and path.
- for i in range(min(len(start_list), len(path_list))):
- if start_list[i].lower() != path_list[i].lower():
- break
- else:
- i += 1
-
- rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:]
- if not rel_list:
- return os.path.curdir
- return os.path.join(*rel_list)
-
- else:
- # default to posixpath definition
- def relpath(path, start=os.path.curdir):
- """Return a relative version of a path"""
-
- if not path:
- raise ValueError("no path specified")
-
- start_list = os.path.abspath(start).split(os.path.sep)
- path_list = os.path.abspath(path).split(os.path.sep)
-
- # Work out how much of the filepath is shared by start and path.
- i = len(os.path.commonprefix([start_list, path_list]))
-
- rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:]
- if not rel_list:
- return os.path.curdir
- return os.path.join(*rel_list)
-else:
- from os.path import relpath
+++ /dev/null
-# -*- coding: utf-8 -*-
-import urllib2
-import itertools, mimetools, mimetypes
-import platform
-from txclib import get_version
-
-# Helper class to enable urllib2 to handle PUT/DELETE requests as well
-class RequestWithMethod(urllib2.Request):
- """Workaround for using DELETE with urllib2"""
- def __init__(self, url, method, data=None, headers={},
- origin_req_host=None, unverifiable=False):
- self._method = method
- urllib2.Request.__init__(self, url, data=data, headers=headers,
- origin_req_host=None, unverifiable=False)
-
- def get_method(self):
- return self._method
-
-import urllib
-import os, stat
-from cStringIO import StringIO
-
-class Callable:
- def __init__(self, anycallable):
- self.__call__ = anycallable
-
-# Controls how sequences are uncoded. If true, elements may be given multiple
-# values by assigning a sequence.
-doseq = 1
-
-class MultipartPostHandler(urllib2.BaseHandler):
- handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first
-
- def http_request(self, request):
- data = request.get_data()
- if data is not None and type(data) != str:
- v_files = []
- v_vars = []
- try:
- for(key, value) in data.items():
- if type(value) == file:
- v_files.append((key, value))
- else:
- v_vars.append((key, value))
- except TypeError:
- systype, value, traceback = sys.exc_info()
- raise TypeError, "not a valid non-string sequence or mapping object", traceback
-
- if len(v_files) == 0:
- data = urllib.urlencode(v_vars, doseq)
- else:
- boundary, data = self.multipart_encode(v_vars, v_files)
-
- contenttype = 'multipart/form-data; boundary=%s' % boundary
- if(request.has_header('Content-Type')
- and request.get_header('Content-Type').find('multipart/form-data') != 0):
- print "Replacing %s with %s" % (request.get_header('content-type'), 'multipart/form-data')
- request.add_unredirected_header('Content-Type', contenttype)
-
- request.add_data(data)
-
- return request
-
- def multipart_encode(vars, files, boundary = None, buf = None):
- if boundary is None:
- boundary = mimetools.choose_boundary()
- if buf is None:
- buf = StringIO()
- for(key, value) in vars:
- buf.write('--%s\r\n' % boundary)
- buf.write('Content-Disposition: form-data; name="%s"' % key)
- buf.write('\r\n\r\n' + value + '\r\n')
- for(key, fd) in files:
- file_size = os.fstat(fd.fileno())[stat.ST_SIZE]
- filename = fd.name.split('/')[-1]
- contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
- buf.write('--%s\r\n' % boundary)
- buf.write('Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename))
- buf.write('Content-Type: %s\r\n' % contenttype)
- # buffer += 'Content-Length: %s\r\n' % file_size
- fd.seek(0)
- buf.write('\r\n' + fd.read() + '\r\n')
- buf.write('--' + boundary + '--\r\n\r\n')
- buf = buf.getvalue()
- return boundary, buf
- multipart_encode = Callable(multipart_encode)
-
- https_request = http_request
-
-
-def user_agent_identifier():
- """Return the user agent for the client."""
- client_info = (get_version(), platform.system(), platform.machine())
- return "txclient/%s (%s %s)" % client_info