88bb46bf34a74a971b31ac9db56ba641958ec499
   1 # -*- coding: utf-8 -*- 
  12 from txclib
.web 
import * 
  13 from txclib
.utils 
import * 
  14 from txclib
.urls 
import API_URLS
 
  15 from txclib
.config 
import OrderedRawConfigParser
, Flipdict
 
  16 from txclib
.log 
import logger
 
  17 from txclib
.http_utils 
import http_response
 
  18 from txclib
.processors 
import visit_hostname
 
  21 class ProjectNotInit(Exception): 
  25 class Project(object): 
  27     Represents an association between the local and remote project instances. 
  30     def __init__(self
, path_to_tx
=None, init
=True): 
  32         Initialize the Project attributes. 
  35             self
._init(path_to_tx
) 
  37     def _init(self
, path_to_tx
=None): 
  38         instructions 
= "Run 'tx init' to initialize your project first!" 
  40             self
.root 
= self
._get_tx_dir_path(path_to_tx
) 
  41             self
.config_file 
= self
._get_config_file_path(self
.root
) 
  42             self
.config 
= self
._read_config_file(self
.config_file
) 
  43             self
.txrc_file 
= self
._get_transifex_file() 
  44             self
.txrc 
= self
._get_transifex_config(self
.txrc_file
) 
  45         except ProjectNotInit
, e
: 
  46             logger
.error('\n'.join([unicode(e
), instructions
])) 
  49     def _get_config_file_path(self
, root_path
): 
  50         """Check the .tx/config file exists.""" 
  51         config_file 
= os
.path
.join(root_path
, ".tx", "config") 
  52         logger
.debug("Config file is %s" % config_file
) 
  53         if not os
.path
.exists(config_file
): 
  54             msg 
= "Cannot find the config file (.tx/config)!" 
  55             raise ProjectNotInit(msg
) 
  58     def _get_tx_dir_path(self
, path_to_tx
): 
  59         """Check the .tx directory exists.""" 
  60         root_path 
= path_to_tx 
or find_dot_tx() 
  61         logger
.debug("Path to tx is %s." % root_path
) 
  63             msg 
= "Cannot find any .tx directory!" 
  64             raise ProjectNotInit(msg
) 
  67     def _read_config_file(self
, config_file
): 
  68         """Parse the config file and return its contents.""" 
  69         config 
= OrderedRawConfigParser() 
  71             config
.read(config_file
) 
  72         except Exception, err
: 
  73             msg 
= "Cannot open/parse .tx/config file: %s" % err
 
  74             raise ProjectNotInit(msg
) 
  77     def _get_transifex_config(self
, txrc_file
): 
  78         """Read the configuration from the .transifexrc file.""" 
  79         txrc 
= OrderedRawConfigParser() 
  83             msg 
= "Cannot read global configuration file: %s" % e
 
  84             raise ProjectNotInit(msg
) 
  85         self
._migrate_txrc_file(txrc
) 
  88     def _migrate_txrc_file(self
, txrc
): 
  89         """Migrate the txrc file, if needed.""" 
  90         for section 
in txrc
.sections(): 
  91             orig_hostname 
= txrc
.get(section
, 'hostname') 
  92             hostname 
= visit_hostname(orig_hostname
) 
  93             if hostname 
!= orig_hostname
: 
  94                 msg 
= "Hostname %s should be changed to %s." 
  95                 logger
.info(msg % 
(orig_hostname
, hostname
)) 
  96                 if (sys
.stdin
.isatty() and sys
.stdout
.isatty() and 
  97                     confirm('Change it now? ', default
=True)): 
  98                     txrc
.set(section
, 'hostname', hostname
) 
  99                     msg 
= 'Hostname changed' 
 102                     hostname 
= orig_hostname
 
 103             self
._save_txrc_file(txrc
) 
 106     def _get_transifex_file(self
, directory
=None): 
 107         """Fetch the path of the .transifexrc file. 
 109         It is in the home directory ofthe user by default. 
 111         if directory 
is None: 
 112             directory 
= os
.path
.expanduser('~') 
 113         txrc_file 
= os
.path
.join(directory
, ".transifexrc") 
 114         logger
.debug(".transifexrc file is at %s" % directory
) 
 115         if not os
.path
.exists(txrc_file
): 
 116             msg 
= "No authentication data found." 
 119             open(txrc_file
, 'w').close() 
 123     def validate_config(self
): 
 125         To ensure the json structure is correctly formed. 
 129     def getset_host_credentials(self
, host
, user
=None, password
=None): 
 131         Read .transifexrc and report user,pass for a specific host else ask the 
 135             username 
= self
.txrc
.get(host
, 'username') 
 136             passwd 
= self
.txrc
.get(host
, 'password') 
 137         except (ConfigParser
.NoOptionError
, ConfigParser
.NoSectionError
): 
 138             logger
.info("No entry found for host %s. Creating..." % host
) 
 139             username 
= user 
or raw_input("Please enter your transifex username: ") 
 140             while (not username
): 
 141                 username 
= raw_input("Please enter your transifex username: ") 
 144                 passwd 
= getpass
.getpass() 
 146             logger
.info("Updating %s file..." % self
.txrc_file
) 
 147             self
.txrc
.add_section(host
) 
 148             self
.txrc
.set(host
, 'username', username
) 
 149             self
.txrc
.set(host
, 'password', passwd
) 
 150             self
.txrc
.set(host
, 'token', '') 
 151             self
.txrc
.set(host
, 'hostname', host
) 
 153         return username
, passwd
 
 155     def set_remote_resource(self
, resource
, source_lang
, i18n_type
, host
, 
 156             file_filter
="translations<sep>%(proj)s.%(res)s<sep><lang>.%(extension)s"): 
 158         Method to handle the add/conf of a remote resource. 
 160         if not self
.config
.has_section(resource
): 
 161             self
.config
.add_section(resource
) 
 163         p_slug
, r_slug 
= resource
.split('.') 
 164         file_filter 
= file_filter
.replace("<sep>", r
"%s" % os
.path
.sep
) 
 170         extension 
= self
._extension_for(i18n_type
)[1:] 
 172         self
.config
.set(resource
, 'source_lang', source_lang
) 
 174             resource
, 'file_filter', 
 175             file_filter % 
{'proj': p_slug
, 'res': r_slug
, 'extension': extension
} 
 177         if host 
!= self
.config
.get('main', 'host'): 
 178             self
.config
.set(resource
, 'host', host
) 
 180     def get_resource_host(self
, resource
): 
 182         Returns the host that the resource is configured to use. If there is no 
 183         such option we return the default one 
 185         if self
.config
.has_option(resource
, 'host'): 
 186             return self
.config
.get(resource
, 'host') 
 187         return self
.config
.get('main', 'host') 
 189     def get_resource_lang_mapping(self
, resource
): 
 191         Get language mappings for a specific resource. 
 193         lang_map 
= Flipdict() 
 195             args 
= self
.config
.get("main", "lang_map") 
 196             for arg 
in args
.replace(' ', '').split(','): 
 198                 lang_map
.update({k
:v
}) 
 199         except ConfigParser
.NoOptionError
: 
 201         except (ValueError, KeyError): 
 202             raise Exception("Your lang map configuration is not correct.") 
 204         if self
.config
.has_section(resource
): 
 205             res_lang_map 
= Flipdict() 
 207                 args 
= self
.config
.get(resource
, "lang_map") 
 208                 for arg 
in args
.replace(' ', '').split(','): 
 210                     res_lang_map
.update({k
:v
}) 
 211             except ConfigParser
.NoOptionError
: 
 213             except (ValueError, KeyError): 
 214                 raise Exception("Your lang map configuration is not correct.") 
 216         # merge the lang maps and return result 
 217         lang_map
.update(res_lang_map
) 
 222     def get_resource_files(self
, resource
): 
 224         Get a dict for all files assigned to a resource. First we calculate the 
 225         files matching the file expression and then we apply all translation 
 226         excpetions. The resulting dict will be in this format: 
 228         { 'en': 'path/foo/en/bar.po', 'de': 'path/foo/de/bar.po', 'es': 'path/exceptions/es.po'} 
 230         NOTE: All paths are relative to the root of the project 
 233         if self
.config
.has_section(resource
): 
 235                 file_filter 
= self
.config
.get(resource
, "file_filter") 
 236             except ConfigParser
.NoOptionError
: 
 238             source_lang 
= self
.config
.get(resource
, "source_lang") 
 239             source_file 
= self
.get_resource_option(resource
, 'source_file') or None 
 240             expr_re 
= regex_from_filefilter(file_filter
, self
.root
) 
 241             expr_rec 
= re
.compile(expr_re
) 
 242             for root
, dirs
, files 
in os
.walk(self
.root
): 
 244                     f_path 
= os
.path
.abspath(os
.path
.join(root
, f
)) 
 245                     match 
= expr_rec
.match(f_path
) 
 247                         lang 
= match
.group(1) 
 248                         if lang 
!= source_lang
: 
 249                             f_path 
= relpath(f_path
, self
.root
) 
 250                             if f_path 
!= source_file
: 
 251                                 tr_files
.update({lang
: f_path
}) 
 253             for (name
, value
) in self
.config
.items(resource
): 
 254                 if name
.startswith("trans."): 
 255                     lang 
= name
.split('.')[1] 
 256                     # delete language which has same file 
 257                     if value 
in tr_files
.values(): 
 259                         for k
, v 
in tr_files
.iteritems(): 
 263                             del tr_files
[keys
[0]] 
 265                             raise Exception("Your configuration seems wrong."\
 
 266                                 " You have multiple languages pointing to"\
 
 268                     # Add language with correct file 
 269                     tr_files
.update({lang
:value
}) 
 275     def get_resource_option(self
, resource
, option
): 
 277         Return the requested option for a specific resource 
 279         If there is no such option, we return None 
 282         if self
.config
.has_section(resource
): 
 283             if self
.config
.has_option(resource
, option
): 
 284                 return self
.config
.get(resource
, option
) 
 287     def get_resource_list(self
, project
=None): 
 289         Parse config file and return tuples with the following format 
 291         [ (project_slug, resource_slug), (..., ...)] 
 295         for r 
in self
.config
.sections(): 
 298             p_slug
, r_slug 
= r
.split('.', 1) 
 299             if project 
and p_slug 
!= project
: 
 301             resource_list
.append(r
) 
 307         Store the config dictionary in the .tx/config file of the project. 
 309         self
._save_tx_config() 
 310         self
._save_txrc_file() 
 312     def _save_tx_config(self
, config
=None): 
 313         """Save the local config file.""" 
 316         fh 
= open(self
.config_file
,"w") 
 320     def _save_txrc_file(self
, txrc
=None): 
 321         """Save the .transifexrc file.""" 
 325         fh 
= open(self
.txrc_file
, 'w') 
 330     def get_full_path(self
, relpath
): 
 331         if relpath
[0] == "/": 
 334             return os
.path
.join(self
.root
, relpath
) 
 336     def pull(self
, languages
=[], resources
=[], overwrite
=True, fetchall
=False, 
 337         fetchsource
=False, force
=False, skip
=False, minimum_perc
=0, mode
=None): 
 338         """Pull all translations file from transifex server.""" 
 339         self
.minimum_perc 
= minimum_perc
 
 340         resource_list 
= self
.get_chosen_resources(resources
) 
 342         if mode 
== 'reviewed': 
 343             url 
= 'pull_reviewed_file' 
 344         elif mode 
== 'translator': 
 345             url 
= 'pull_translator_file' 
 346         elif mode 
== 'developer': 
 347             url 
= 'pull_developer_file' 
 351         for resource 
in resource_list
: 
 352             logger
.debug("Handling resource %s" % resource
) 
 353             self
.resource 
= resource
 
 354             project_slug
, resource_slug 
= resource
.split('.') 
 355             files 
= self
.get_resource_files(resource
) 
 356             slang 
= self
.get_resource_option(resource
, 'source_lang') 
 357             sfile 
= self
.get_resource_option(resource
, 'source_file') 
 358             lang_map 
= self
.get_resource_lang_mapping(resource
) 
 359             host 
= self
.get_resource_host(resource
) 
 360             logger
.debug("Language mapping is: %s" % lang_map
) 
 362                 mode 
= self
._get_option(resource
, 'mode') 
 365                 'project': project_slug
, 
 366                 'resource': resource_slug
 
 368             logger
.debug("URL data are: %s" % self
.url_info
) 
 370             stats 
= self
._get_stats_for_resource() 
 374                 file_filter 
= self
.config
.get(resource
, 'file_filter') 
 375             except ConfigParser
.NoOptionError
: 
 379             pull_languages 
= set([]) 
 380             new_translations 
= set([]) 
 383                 new_translations 
= self
._new_translations_to_add( 
 384                     files
, slang
, lang_map
, stats
, force
 
 387                     msg 
= "New translations found for the following languages: %s" 
 388                     logger
.info(msg % 
', '.join(new_translations
)) 
 390             existing
, new 
= self
._languages_to_pull( 
 391                 languages
, files
, lang_map
, stats
, force
 
 393             pull_languages |
= existing
 
 394             new_translations |
= new
 
 395             logger
.debug("Adding to new translations: %s" % new
) 
 398                 if sfile 
and slang 
not in pull_languages
: 
 399                     pull_languages
.add(slang
) 
 400                 elif slang 
not in new_translations
: 
 401                     new_translations
.add(slang
) 
 404                 logger
.debug("Pulling languages for: %s" % pull_languages
) 
 405                 msg 
= "Pulling translations for resource %s (source: %s)" 
 406                 logger
.info(msg % 
(resource
, sfile
)) 
 408             for lang 
in pull_languages
: 
 410                 if lang 
in lang_map
.values(): 
 411                     remote_lang 
= lang_map
.flip
[lang
] 
 414                 if languages 
and lang 
not in pull_languages
: 
 415                     logger
.debug("Skipping language %s" % lang
) 
 418                     local_file 
= files
.get(lang
, None) or files
[lang_map
[lang
]] 
 421                 logger
.debug("Using file %s" % local_file
) 
 426                     'local_file': local_file
, 
 430                 if not self
._should_update_translation(**kwargs
): 
 431                     msg 
= "Skipping '%s' translation (file: %s)." 
 433                         msg % 
(color_text(remote_lang
, "RED"), local_file
) 
 438                     local_file 
= ("%s.new" % local_file
) 
 440                     " -> %s: %s" % 
(color_text(remote_lang
, "RED"), local_file
) 
 443                     r 
= self
.do_url_request(url
, language
=remote_lang
) 
 450                 base_dir 
= os
.path
.split(local_file
)[0] 
 452                 fd 
= open(local_file
, 'wb') 
 457                 msg 
= "Pulling new translations for resource %s (source: %s)" 
 458                 logger
.info(msg % 
(resource
, sfile
)) 
 459                 for lang 
in new_translations
: 
 460                     if lang 
in lang_map
.keys(): 
 461                         local_lang 
= lang_map
[lang
] 
 466                         local_file 
= relpath(os
.path
.join(self
.root
, 
 467                             file_filter
.replace('<lang>', local_lang
)), os
.curdir
) 
 469                         trans_dir 
= os
.path
.join(self
.root
, ".tx", resource
) 
 470                         if not os
.path
.exists(trans_dir
): 
 472                         local_file 
= relpath(os
.path
.join(trans_dir
, '%s_translation' % 
 473                             local_lang
, os
.curdir
)) 
 476                         satisfies_min 
= self
._satisfies_min_translated( 
 477                             stats
[remote_lang
], mode
 
 479                         if not satisfies_min
: 
 480                             msg 
= "Skipping language %s due to used options." 
 481                             logger
.info(msg % lang
) 
 484                         " -> %s: %s" % 
(color_text(remote_lang
, "RED"), local_file
) 
 486                     r 
= self
.do_url_request(url
, language
=remote_lang
) 
 488                     base_dir 
= os
.path
.split(local_file
)[0] 
 490                     fd 
= open(local_file
, 'wb') 
 494     def push(self
, source
=False, translations
=False, force
=False, resources
=[], languages
=[], 
 495         skip
=False, no_interactive
=False): 
 497         Push all the resources 
 499         resource_list 
= self
.get_chosen_resources(resources
) 
 502         for resource 
in resource_list
: 
 504             project_slug
, resource_slug 
= resource
.split('.') 
 505             files 
= self
.get_resource_files(resource
) 
 506             slang 
= self
.get_resource_option(resource
, 'source_lang') 
 507             sfile 
= self
.get_resource_option(resource
, 'source_file') 
 508             lang_map 
= self
.get_resource_lang_mapping(resource
) 
 509             host 
= self
.get_resource_host(resource
) 
 510             logger
.debug("Language mapping is: %s" % lang_map
) 
 511             logger
.debug("Using host %s" % host
) 
 514                 'project': project_slug
, 
 515                 'resource': resource_slug
 
 518             logger
.info("Pushing translations for resource %s:" % resource
) 
 520             stats 
= self
._get_stats_for_resource() 
 522             if force 
and not no_interactive
: 
 523                 answer 
= raw_input("Warning: By using --force, the uploaded" 
 524                     " files will overwrite remote translations, even if they" 
 525                     " are newer than your uploaded files.\nAre you sure you" 
 526                     " want to continue? [y/N] ") 
 528                 if not answer 
in ["", 'Y', 'y', "yes", 'YES']: 
 533                     logger
.error("You don't seem to have a proper source file" 
 534                         " mapping for resource %s. Try without the --source" 
 535                         " option or set a source file first and then try again." % 
 540                     logger
.warning("Pushing source file (%s)" % sfile
) 
 541                     if not self
._resource_exists(stats
): 
 542                         logger
.info("Resource does not exist.  Creating...") 
 543                         fileinfo 
= "%s;%s" % 
(resource_slug
, slang
) 
 544                         filename 
= self
.get_full_path(sfile
) 
 545                         self
._create_resource(resource
, project_slug
, fileinfo
, filename
) 
 547                         'push_source', multipart
=True, method
="PUT", 
 549                                 "%s;%s" % 
(resource_slug
, slang
) 
 550                                 , self
.get_full_path(sfile
) 
 560                     self
.do_url_request('resource_details') 
 562                     code 
= getattr(e
, 'code', None) 
 564                         msg 
= "Resource %s doesn't exist on the server." 
 565                         logger
.error(msg % resource
) 
 569                 # Check if given language codes exist 
 571                     push_languages 
= files
.keys() 
 574                     f_langs 
= files
.keys() 
 576                         if l 
in lang_map
.keys(): 
 578                         push_languages
.append(l
) 
 580                             msg 
= "Warning: No mapping found for language code '%s'." 
 581                             logger
.error(msg % color_text
(l
,"RED")) 
 582                 logger
.debug("Languages to push are %s" % push_languages
) 
 584                 # Push translation files one by one 
 585                 for lang 
in push_languages
: 
 587                     if lang 
in lang_map
.values(): 
 588                         remote_lang 
= lang_map
.flip
[lang
] 
 592                     local_file 
= files
[local_lang
] 
 597                         'local_file': local_file
, 
 600                     if not self
._should_push_translation(**kwargs
): 
 601                         msg 
= "Skipping '%s' translation (file: %s)." 
 602                         logger
.info(msg % 
(color_text(lang
, "RED"), local_file
)) 
 605                     msg 
= "Pushing '%s' translations (file: %s)" 
 607                          msg % 
(color_text(remote_lang
, "RED"), local_file
) 
 611                             'push_translation', multipart
=True, method
='PUT', 
 613                                     "%s;%s" % 
(resource_slug
, remote_lang
), 
 614                                     self
.get_full_path(local_file
) 
 615                             )], language
=remote_lang
 
 617                         logger
.debug("Translation %s pushed." % remote_lang
) 
 624     def delete(self
, resources
=[], languages
=[], skip
=False, force
=False): 
 625         """Delete translations.""" 
 626         resource_list 
= self
.get_chosen_resources(resources
) 
 631             delete_func 
= self
._delete_resource
 
 633             delete_func 
= self
._delete_translations
 
 635         for resource 
in resource_list
: 
 636             project_slug
, resource_slug 
= resource
.split('.') 
 637             host 
= self
.get_resource_host(resource
) 
 640                 'project': project_slug
, 
 641                 'resource': resource_slug
 
 643             logger
.debug("URL data are: %s" % self
.url_info
) 
 644             project_details 
= parse_json( 
 645                 self
.do_url_request('project_details', project
=self
) 
 647             teams 
= project_details
['teams'] 
 648             stats 
= self
._get_stats_for_resource() 
 649             delete_func(project_details
, resource
, stats
, languages
) 
 651     def _delete_resource(self
, project_details
, resource
, stats
, *args
): 
 652         """Delete a resource from Transifex.""" 
 653         project_slug
, resource_slug 
= resource
.split('.') 
 654         project_resource_slugs 
= [ 
 655             r
['slug'] for r 
in project_details
['resources'] 
 657         logger
.info("Deleting resource %s:" % resource
) 
 658         if resource_slug 
not in project_resource_slugs
: 
 660                 msg 
= "Skipping: %s : Resource does not exist." 
 661                 logger
.info(msg % resource
) 
 664             slang 
= self
.get_resource_option(resource
, 'source_lang') 
 665             for language 
in stats
: 
 666                 if language 
== slang
: 
 668                 if int(stats
[language
]['translated_entities']) > 0: 
 670                         "Skipping: %s : Unable to delete resource because it " 
 671                         "has a not empty %s translation.\nPlease use -f or " 
 672                         "--force option to delete this resource." 
 674                     logger
.info(msg % 
(resource
, language
)) 
 677             self
.do_url_request('delete_resource', method
="DELETE") 
 678             self
.config
.remove_section(resource
) 
 680             msg 
= "Deleted resource %s of project %s." 
 681             logger
.info(msg % 
(resource_slug
, project_slug
)) 
 683             msg 
= "Unable to delete resource %s of project %s." 
 684             logger
.error(msg % 
(resource_slug
, project_slug
)) 
 688     def _delete_translations(self
, project_details
, resource
, stats
, languages
): 
 689         """Delete the specified translations for the specified resource.""" 
 690         logger
.info("Deleting translations from resource %s:" % resource
) 
 691         for language 
in languages
: 
 692             self
._delete_translation(project_details
, resource
, stats
, language
) 
 694     def _delete_translation(self
, project_details
, resource
, stats
, language
): 
 695         """Delete a specific translation from the specified resource.""" 
 696         project_slug
, resource_slug 
= resource
.split('.') 
 697         if language 
not in stats
: 
 699                 msg 
= "Skipping %s: Translation does not exist." 
 700                 logger
.warning(msg % 
(language
)) 
 703             teams 
= project_details
['teams'] 
 704             if language 
in teams
: 
 706                     "Skipping %s: Unable to delete translation because it is " 
 707                     "associated with a team.\nPlease use -f or --force option " 
 708                     "to delete this translation." 
 710                 logger
.warning(msg % language
) 
 712             if int(stats
[language
]['translated_entities']) > 0: 
 714                     "Skipping %s: Unable to delete translation because it " 
 715                     "is not empty.\nPlease use -f or --force option to delete " 
 718                 logger
.warning(msg % language
) 
 722                 'delete_translation', language
=language
, method
="DELETE" 
 724             msg 
= "Deleted language %s from resource %s of project %s." 
 725             logger
.info(msg % 
(language
, resource_slug
, project_slug
)) 
 727             msg 
= "Unable to delete translation %s" 
 728             logger
.error(msg % language
) 
 732     def do_url_request(self
, api_call
, multipart
=False, data
=None, 
 733                        files
=[], encoding
=None, method
="GET", **kwargs
): 
 735         Issues a url request. 
 737         # Read the credentials from the config file (.transifexrc) 
 738         host 
= self
.url_info
['host'] 
 740             username 
= self
.txrc
.get(host
, 'username') 
 741             passwd 
= self
.txrc
.get(host
, 'password') 
 742             token 
= self
.txrc
.get(host
, 'token') 
 743             hostname 
= self
.txrc
.get(host
, 'hostname') 
 744         except ConfigParser
.NoSectionError
: 
 745             raise Exception("No user credentials found for host %s. Edit" 
 746                 " ~/.transifexrc and add the appropriate info in there." % 
 750         kwargs
['hostname'] = hostname
 
 751         kwargs
.update(self
.url_info
) 
 752         url 
= (API_URLS
[api_call
] % kwargs
).encode('UTF-8') 
 760             opener 
= urllib2
.build_opener(MultipartPostHandler
) 
 761             for info
,filename 
in files
: 
 762                 data 
= { "resource" : info
.split(';')[0], 
 763                          "language" : info
.split(';')[1], 
 764                          "uploaded_file" :  open(filename
,'rb') } 
 766             urllib2
.install_opener(opener
) 
 767             req 
= RequestWithMethod(url
=url
, data
=data
, method
=method
) 
 769             req 
= RequestWithMethod(url
=url
, data
=data
, method
=method
) 
 771                 req
.add_header("Content-Type",encoding
) 
 773         base64string 
= base64
.encodestring('%s:%s' % 
(username
, passwd
))[:-1] 
 774         authheader 
= "Basic %s" % base64string
 
 775         req
.add_header("Authorization", authheader
) 
 776         req
.add_header("Accept-Encoding", "gzip,deflate") 
 777         req
.add_header("User-Agent", user_agent_identifier()) 
 780             response 
= urllib2
.urlopen(req
, timeout
=300) 
 781             return http_response(response
) 
 782         except urllib2
.HTTPError
, e
: 
 783             if e
.code 
in [401, 403, 404]: 
 785             elif 200 <= e
.code 
< 300: 
 788                 # For other requests, we should print the message as well 
 789                 raise Exception("Remote server replied: %s" % e
.read()) 
 790         except urllib2
.URLError
, e
: 
 792             raise Exception("Remote server replied: %s" % error
[1]) 
 795     def _should_update_translation(self
, lang
, stats
, local_file
, force
=False, 
 797         """Whether a translation should be udpated from Transifex. 
 799         We use the following criteria for that: 
 800         - If user requested to force the download. 
 801         - If language exists in Transifex. 
 802         - If the local file is older than the Transifex's file. 
 803         - If the user requested a x% completion. 
 806             lang: The language code to check. 
 807             stats: The (global) statistics object. 
 808             local_file: The local translation file. 
 809             force: A boolean flag. 
 810             mode: The mode for the translation. 
 814         return self
._should_download(lang
, stats
, local_file
, force
) 
 816     def _should_add_translation(self
, lang
, stats
, force
=False, mode
=None): 
 817         """Whether a translation should be added from Transifex. 
 819         We use the following criteria for that: 
 820         - If user requested to force the download. 
 821         - If language exists in Transifex. 
 822         - If the user requested a x% completion. 
 825             lang: The language code to check. 
 826             stats: The (global) statistics object. 
 827             force: A boolean flag. 
 828             mode: The mode for the translation. 
 832         return self
._should_download(lang
, stats
, None, force
) 
 834     def _should_download(self
, lang
, stats
, local_file
=None, force
=False, 
 836         """Return whether a translation should be downloaded. 
 838         If local_file is None, skip the timestamps check (the file does 
 842             lang_stats 
= stats
[lang
] 
 844             logger
.debug("No lang %s in statistics" % lang
) 
 847         satisfies_min 
= self
._satisfies_min_translated(lang_stats
, mode
) 
 848         if not satisfies_min
: 
 852             logger
.debug("Downloading translation due to -f") 
 855         if local_file 
is not None: 
 856             remote_update 
= self
._extract_updated(lang_stats
) 
 857             if not self
._remote_is_newer(remote_update
, local_file
): 
 858                 logger
.debug("Local is newer than remote for lang %s" % lang
) 
 862     def _should_push_translation(self
, lang
, stats
, local_file
, force
=False): 
 863         """Return whether a local translation file should be 
 866         We use the following criteria for that: 
 867         - If user requested to force the upload. 
 868         - If language exists in Transifex. 
 869         - If local file is younger than the remote file. 
 872             lang: The language code to check. 
 873             stats: The (global) statistics object. 
 874             local_file: The local translation file. 
 875             force: A boolean flag. 
 880             logger
.debug("Push translation due to -f.") 
 883             lang_stats 
= stats
[lang
] 
 885             logger
.debug("Language %s does not exist in Transifex." % lang
) 
 887         if local_file 
is not None: 
 888             remote_update 
= self
._extract_updated(lang_stats
) 
 889             if self
._remote_is_newer(remote_update
, local_file
): 
 890                 msg  
= "Remote translation is newer than local file for lang %s" 
 891                 logger
.debug(msg % lang
) 
 895     def _generate_timestamp(self
, update_datetime
): 
 896         """Generate a UNIX timestamp from the argument. 
 899             update_datetime: The datetime in the format used by Transifex. 
 901             A float, representing the timestamp that corresponds to the 
 904         time_format 
= "%Y-%m-%d %H:%M:%S" 
 907                 *time
.strptime(update_datetime
, time_format
)[0:5] 
 911     def _get_time_of_local_file(self
, path
): 
 912         """Get the modified time of the path_. 
 915             path: The path we want the mtime for. 
 917             The time as a timestamp or None, if the file does not exist 
 919         if not os
.path
.exists(path
): 
 921         return time
.mktime(time
.gmtime(os
.path
.getmtime(path
))) 
 923     def _satisfies_min_translated(self
, stats
, mode
=None): 
 924         """Check whether a translation fulfills the filter used for 
 925         minimum translated percentage. 
 928             perc: The current translation percentage. 
 932         cur 
= self
._extract_completed(stats
, mode
) 
 933         option_name 
= 'minimum_perc' 
 934         if self
.minimum_perc 
is not None: 
 935             minimum_percent 
= self
.minimum_perc
 
 937             global_minimum 
= int( 
 938                 self
.get_resource_option('main', option_name
) or 0 
 940             resource_minimum 
= int( 
 941                 self
.get_resource_option( 
 942                     self
.resource
, option_name
 
 945             minimum_percent 
= resource_minimum
 
 946         return cur 
>= minimum_percent
 
 948     def _remote_is_newer(self
, remote_updated
, local_file
): 
 949         """Check whether the remote translation is newer that the local file. 
 952             remote_updated: The date and time the translation was last 
 954             local_file: The local file. 
 958         if remote_updated 
is None: 
 959             logger
.debug("No remote time") 
 961         remote_time 
= self
._generate_timestamp(remote_updated
) 
 962         local_time 
= self
._get_time_of_local_file( 
 963             self
.get_full_path(local_file
) 
 966             "Remote time is %s and local %s" % 
(remote_time
, local_time
) 
 968         if local_time 
is not None and remote_time 
< local_time
: 
 973     def _extract_completed(cls
, stats
, mode
=None): 
 974         """Extract the information for the translated percentage from the stats. 
 977             stats: The stats object for a language as returned by Transifex. 
 978             mode: The mode of translations requested. 
 980             The percentage of translation as integer. 
 982         if mode 
== 'reviewed': 
 983             key 
= 'reviewed_percentage' 
 987             return int(stats
[key
][:-1]) 
 992     def _extract_updated(cls
, stats
): 
 993         """Extract the  information for the last update of a translation. 
 996             stats: The stats object for a language as returned by Transifex. 
 998             The last update field. 
1001             return stats
['last_update'] 
1005     def _new_translations_to_add(self
, files
, slang
, lang_map
, 
1006                                  stats
, force
=False): 
1007         """Return a list of translations which are new to the 
1010         new_translations 
= [] 
1011         timestamp 
= time
.time() 
1012         langs 
= stats
.keys() 
1013         logger
.debug("Available languages are: %s" % langs
) 
1016             lang_exists 
= lang 
in files
.keys() 
1017             lang_is_source 
= lang 
== slang
 
1018             mapped_lang_exists 
= ( 
1019                 lang 
in lang_map 
and lang_map
[lang
] in files
.keys() 
1021             if lang_exists 
or lang_is_source 
or mapped_lang_exists
: 
1023             if self
._should_add_translation(lang
, stats
, force
): 
1024                 new_translations
.append(lang
) 
1025         return set(new_translations
) 
1027     def _get_stats_for_resource(self
): 
1028         """Get the statistics information for a resource.""" 
1030             r 
= self
.do_url_request('resource_stats') 
1031             logger
.debug("Statistics response is %s" % r
) 
1032             stats 
= parse_json(r
) 
1033         except urllib2
.HTTPError
, e
: 
1034             logger
.debug("Resource not found: %s" % e
) 
1037             logger
.debug("Network error: %s" % e
) 
1041     def get_chosen_resources(self
, resources
): 
1042         """Get the resources the user selected. 
1044         Support wildcards in the resources specified by the user. 
1047             resources: A list of resources as specified in command-line or 
1050             A list of resources. 
1052         configured_resources 
= self
.get_resource_list() 
1054             return configured_resources
 
1056         selected_resources 
= [] 
1057         for resource 
in resources
: 
1059             for full_name 
in configured_resources
: 
1060                 if fnmatch
.fnmatch(full_name
, resource
): 
1061                     selected_resources
.append(full_name
) 
1064                 msg 
= "Specified resource '%s' does not exist." 
1065                 raise Exception(msg % resource
) 
1066         logger
.debug("Operating on resources: %s" % selected_resources
) 
1067         return selected_resources
 
1069     def _languages_to_pull(self
, languages
, files
, lang_map
, stats
, force
): 
1070         """Get a set of langauges to pull. 
1073             languages: A list of languages the user selected in cmd. 
1074             files: A dictionary of current local translation files. 
1076             A tuple of a set of existing languages and new translations. 
1079             pull_languages 
= set([]) 
1080             pull_languages |
= set(files
.keys()) 
1082             for lang 
in pull_languages
: 
1083                 if lang 
in lang_map
.flip
: 
1084                     mapped_files
.append(lang_map
.flip
[lang
]) 
1085             pull_languages 
-= set(lang_map
.flip
.keys()) 
1086             pull_languages |
= set(mapped_files
) 
1087             return (pull_languages
, set([])) 
1090             new_translations 
= [] 
1091             f_langs 
= files
.keys() 
1093                 if l 
not in f_langs 
and not (l 
in lang_map 
and lang_map
[l
] in f_langs
): 
1094                     if self
._should_add_translation(l
, stats
, force
): 
1095                         new_translations
.append(l
) 
1097                     if l 
in lang_map
.keys(): 
1099                     pull_languages
.append(l
) 
1100             return (set(pull_languages
), set(new_translations
)) 
1102     def _extension_for(self
, i18n_type
): 
1103         """Return the extension used for the specified type.""" 
1105             res 
= parse_json(self
.do_url_request('formats')) 
1106             return res
[i18n_type
]['file-extensions'].split(',')[0] 
1111     def _resource_exists(self
, stats
): 
1112         """Check if resource exists. 
1115             stats: The statistics dict as returned by Tx. 
1117             True, if the resource exists in the server. 
1121     def _create_resource(self
, resource
, pslug
, fileinfo
, filename
, **kwargs
): 
1122         """Create a resource. 
1125             resource: The full resource name. 
1126             pslug: The slug of the project. 
1127             fileinfo: The information of the resource. 
1128             filename: The name of the file. 
1130             URLError, in case of a problem. 
1134         api_call 
= 'create_resource' 
1136         host 
= self
.url_info
['host'] 
1138             username 
= self
.txrc
.get(host
, 'username') 
1139             passwd 
= self
.txrc
.get(host
, 'password') 
1140             token 
= self
.txrc
.get(host
, 'token') 
1141             hostname 
= self
.txrc
.get(host
, 'hostname') 
1142         except ConfigParser
.NoSectionError
: 
1143             raise Exception("No user credentials found for host %s. Edit" 
1144                 " ~/.transifexrc and add the appropriate info in there." % 
1148         kwargs
['hostname'] = hostname
 
1149         kwargs
.update(self
.url_info
) 
1150         kwargs
['project'] = pslug
 
1151         url 
= (API_URLS
[api_call
] % kwargs
).encode('UTF-8') 
1157         i18n_type 
= self
._get_option(resource
, 'type') 
1158         if i18n_type 
is None: 
1160                 "Please define the resource type in .tx/config (eg. type = PO)." 
1161                 " More info: http://bit.ly/txcl-rt" 
1164         opener 
= urllib2
.build_opener(MultipartPostHandler
) 
1166             "slug": fileinfo
.split(';')[0], 
1167             "name": fileinfo
.split(';')[0], 
1168             "uploaded_file":  open(filename
,'rb'), 
1169             "i18n_type": i18n_type
 
1171         urllib2
.install_opener(opener
) 
1172         req 
= RequestWithMethod(url
=url
, data
=data
, method
=method
) 
1174         base64string 
= base64
.encodestring('%s:%s' % 
(username
, passwd
))[:-1] 
1175         authheader 
= "Basic %s" % base64string
 
1176         req
.add_header("Authorization", authheader
) 
1179             fh 
= urllib2
.urlopen(req
) 
1180         except urllib2
.HTTPError
, e
: 
1181             if e
.code 
in [401, 403, 404]: 
1184                 # For other requests, we should print the message as well 
1185                 raise Exception("Remote server replied: %s" % e
.read()) 
1186         except urllib2
.URLError
, e
: 
1188             raise Exception("Remote server replied: %s" % error
[1]) 
1194     def _get_option(self
, resource
, option
): 
1195         """Get the value for the option in the config file. 
1197         If the option is not in the resource section, look for it in 
1201             resource: The resource name. 
1202             option: The option the value of which we are interested in. 
1204             The option value or None, if it does not exist. 
1206         value 
= self
.get_resource_option(resource
, option
) 
1208             if self
.config
.has_option('main', option
): 
1209                 return self
.config
.get('main', option
) 
1212     def set_i18n_type(self
, resources
, i18n_type
): 
1213         """Set the type for the specified resources.""" 
1214         self
._set_resource_option(resources
, key
='type', value
=i18n_type
) 
1216     def set_min_perc(self
, resources
, perc
): 
1217         """Set the minimum percentage for the resources.""" 
1218         self
._set_resource_option(resources
, key
='minimum_perc', value
=perc
) 
1220     def set_default_mode(self
, resources
, mode
): 
1221         """Set the default mode for the specified resources.""" 
1222         self
._set_resource_option(resources
, key
='mode', value
=mode
) 
1224     def _set_resource_option(self
, resources
, key
, value
): 
1225         """Set options in the config file. 
1227         If resources is empty. set the option globally. 
1230             self
.config
.set('main', key
, value
) 
1233             self
.config
.set(r
, key
, value
)