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
)