def test_retry_fetch_translations(self): cds_host = 'https://some.host' cds_handler = CDSHandler( ['el', 'en'], 'some_token', host=cds_host, ) responses.add(responses.GET, cds_host + '/content/el', status=500) responses.add(responses.GET, cds_host + '/content/el', status=202) responses.add(responses.GET, cds_host + '/content/el', json={'data': { 'source': { 'string': "translation" } }}, status=200) translations = cds_handler.fetch_translations('el') assert (translations == { 'el': (True, { 'source': { 'string': "translation" } }) })
def test_invalidate_no_secret(self): cds_handler = CDSHandler( ['el', 'en'], 'some_token', ) with pytest.raises(Exception): cds_handler.invalidate_cache(False)
def test_get_headers(self): cds_host = 'https://some.host' cds_handler = CDSHandler(['el', 'en'], 'some_token', secret='some_secret', host=cds_host) assert cds_handler._get_headers() == { 'Authorization': 'Bearer some_token', 'Accept-Encoding': 'gzip', 'Accept-Version': 'v2', 'X-NATIVE-SDK': 'python', } assert cds_handler._get_headers(use_secret=True) == { 'Authorization': 'Bearer some_token:some_secret', 'Accept-Encoding': 'gzip', 'Accept-Version': 'v2', 'X-NATIVE-SDK': 'python', } headers = cds_handler._get_headers(use_secret=True, etag='something') assert headers == { 'Authorization': 'Bearer some_token:some_secret', 'Accept-Encoding': 'gzip', 'Accept-Version': 'v2', 'X-NATIVE-SDK': 'python', 'If-None-Match': 'something', }
def test_retry_fetch_languages(self): cds_host = 'https://some.host' cds_handler = CDSHandler( ['el', 'en'], 'some_token', host=cds_host, ) responses.add(responses.GET, cds_host + '/languages', status=500) responses.add(responses.GET, cds_host + '/languages', status=202) responses.add(responses.GET, cds_host + '/languages', json={ 'data': [{ 'code': "el" }, { 'code': "en" }], 'meta': { 'some_key': "some_value" } }, status=200) languages_response = cds_handler.fetch_languages() assert self._lang_lists_equal(languages_response, [{ 'code': 'el' }, { 'code': 'en' }])
def test_push_source_strings_no_secret(self): cds_handler = CDSHandler( ['el', 'en'], 'some_token', ) with pytest.raises(Exception): cds_handler.push_source_strings([], False)
def init( self, languages, token, secret=None, cds_host=None, missing_policy=None, error_policy=None ): """Create an instance of the core framework class. Also warms up the cache by fetching the translations from the CDS. :param list languages: a list of language codes for the languages configured in the application :param str token: the API token to use for connecting to the CDS :param str secret: the additional secret to use for pushing source content :param str cds_host: an optional host for the Content Delivery Service, defaults to the host provided by Transifex :param AbstractRenderingPolicy missing_policy: an optional policy to use for returning strings when a translation is missing :param AbstractErrorPolicy error_policy: an optional policy to determine how to handle rendering errors """ self._languages = languages self._cache = MemoryCache() self._missing_policy = missing_policy or SourceStringPolicy() self._error_policy = error_policy or SourceStringErrorPolicy() self._cds_handler = CDSHandler( self._languages, token, secret=secret, host=cds_host ) self.initialized = True
def test_push_source_strings(self, patched_logger): cds_host = 'https://some.host' cds_handler = CDSHandler(['el', 'en'], 'some_token', secret='some_secret', host=cds_host) # test push no content responses.add(responses.POST, cds_host + '/content/', status=200, json={'data': []}) cds_handler.push_source_strings([], False) assert patched_logger.error.call_count == 0 # test push with content responses.add(responses.POST, cds_host + '/content/', status=200, json={'data': []}) source_string = SourceString('some_string') cds_handler.push_source_strings([source_string], False) assert patched_logger.error.call_count == 0 responses.reset() # test wrong data format responses.add(responses.POST, cds_host + '/content/', status=422, json={ "status": 422, "message": "Invalid Payload", "details": [{ "message": "\"string\" is required", "path": ["some_key1", "string"], "type": "any.required", "context": { "key": "string", "label": "string" } }] }) # we don't care about the payload this time, just want to # see how the service handles the errors cds_handler.push_source_strings([], False) # The actual error message differs between Python 2 and Python 3 messages = [ 'Error pushing source strings to CDS: UnknownError ' '(`422 Client Error: {err} for url: ' 'https://some.host/content/`)'.format(err=x) for x in ('Unprocessable Entity', 'None') ] assert patched_logger.error.call_args[0][0] in messages
def test_invalidate(self, patched_logger): cds_host = 'https://some.host' cds_handler = CDSHandler(['el', 'en'], 'some_token', secret='some_secret', host=cds_host) # test invalidate responses.add(responses.POST, cds_host + '/invalidate', status=200, json={'data': { 'count': 5 }}) cds_handler.invalidate_cache(False) assert patched_logger.error.call_count == 0 # test purge responses.add(responses.POST, cds_host + '/purge', status=200, json={'data': { 'count': 5 }}) cds_handler.invalidate_cache(True) assert patched_logger.error.call_count == 0 responses.reset() # test response error responses.add(responses.POST, cds_host + '/invalidate', status=422, json={ "status": 422, }) # we don't care about the payload this time, just want to # see how the service handles the errors cds_handler.invalidate_cache(False) # The actual error message differs between Python 2 and Python 3 messages = [ 'Error invalidating CDS: UnknownError ' '(`422 Client Error: {err} for url: ' 'https://some.host/invalidate`)'.format(err=x) for x in ('Unprocessable Entity', 'None') ] assert patched_logger.error.call_args[0][0] in messages
def test_fetch_translations_etags_management(self, patched_logger): cds_host = 'https://some.host' cds_handler = CDSHandler(['el', 'en'], 'some_token', host=cds_host) # add response for languages responses.add(responses.GET, cds_host + '/languages', json={ "data": [ { "code": "el", }, { "code": "en", }, ], "meta": { "some_key": "some_value" } }, status=200) # add response for translations responses.add(responses.GET, cds_host + '/content/el', json={ 'data': { 'key1': { 'string': 'key1_el' }, 'key2': { 'string': 'key2_el' }, }, 'meta': { "some_key": "some_value" } }, status=200, headers={'ETag': 'some_unique_tag_is_here'}) responses.add( responses.GET, cds_host + '/content/en', # whatever, we don't care about the content of json repsone atm. json={}, status=304) resp = cds_handler.fetch_translations() assert resp == { 'el': (True, { 'key1': { 'string': 'key1_el' }, 'key2': { 'string': 'key2_el' }, }), 'en': (False, {}) } assert cds_handler.etags.get('el') == 'some_unique_tag_is_here'
def test_fetch_languages(self, patched_logger): cds_host = 'https://some.host' cds_handler = CDSHandler(['el', 'en'], 'some_token', host=cds_host) # correct response responses.add(responses.GET, cds_host + '/languages', json={ "data": [ { "code": "el", }, { "code": "en", }, ], "meta": { "some_key": "some_value" } }, status=200) languages_response = cds_handler.fetch_languages() assert self._lang_lists_equal(languages_response, [{ 'code': 'el' }, { 'code': 'en' }]) assert patched_logger.error.call_count == 0 responses.reset() # wrong payload structure responses.add(responses.GET, cds_host + '/languages', json={ "wrong_key": [ { "code": "el", }, { "code": "en", }, ], "meta": { "some_key": "some_value" } }, status=200) assert cds_handler.fetch_languages() == [] patched_logger.error.assert_called_with( 'Error retrieving languages from CDS: Malformed response') responses.reset() # bad request responses.add(responses.GET, cds_host + '/languages', json={ "data": [ { "code": "el", }, { "code": "en", }, ], "meta": { "some_key": "some_value" } }, status=400) assert cds_handler.fetch_languages() == [] patched_logger.error.assert_called_with( 'Error retrieving languages from CDS: UnknownError (`400 Client ' 'Error: Bad Request for url: https://some.host/languages`)') responses.reset() # unauthorized responses.add(responses.GET, cds_host + '/languages', json={ "data": [ { "code": "el", }, { "code": "en", }, ], "meta": { "some_key": "some_value" } }, status=403) assert cds_handler.fetch_languages() == [] patched_logger.error.assert_called_with( 'Error retrieving languages from CDS: UnknownError (`403 Client ' 'Error: Forbidden for url: https://some.host/languages`)') responses.reset() # connection error assert cds_handler.fetch_languages() == [] patched_logger.error.assert_called_with( 'Error retrieving languages from CDS: ConnectionError') responses.reset()
def test_fetch_translations(self, patched_logger): cds_host = 'https://some.host' cds_handler = CDSHandler(['el', 'en', 'fr'], 'some_token', host=cds_host) # add response for languages responses.add(responses.GET, cds_host + '/languages', json={ "data": [ { "code": "el", }, { "code": "en", }, { "code": "fr", }, ], "meta": { "some_key": "some_value" } }, status=200) # add response for translations responses.add(responses.GET, cds_host + '/content/el', json={ 'data': { 'key1': { 'string': 'key1_el' }, 'key2': { 'string': 'key2_el' }, }, 'meta': { "some_key": "some_value" } }, status=200) responses.add(responses.GET, cds_host + '/content/en', json={ 'data': { 'key1': { 'string': 'key1_en' }, 'key2': { 'string': 'key2_en' }, }, 'meta': {} }, status=200) # add response bad status response for a language here responses.add(responses.GET, cds_host + '/content/fr', status=404) resp = cds_handler.fetch_translations() assert resp == { 'el': (True, { 'key1': { 'string': 'key1_el' }, 'key2': { 'string': 'key2_el' }, }), 'en': (True, { 'key1': { 'string': 'key1_en' }, 'key2': { 'string': 'key2_en' }, }), 'fr': (False, {}) # that is due to the error status in response } responses.reset() # test fetch_languages fails with connection error responses.add(responses.GET, cds_host + '/languages', status=500) resp = cds_handler.fetch_translations() assert resp == {} patched_logger.error.assert_called_with( 'Error retrieving languages from CDS: UnknownError ' '(`500 Server Error: Internal Server Error for url: ' 'https://some.host/languages`)') responses.reset() patched_logger.reset_mock() # test language code responses.add(responses.GET, cds_host + '/content/el', json={ 'data': { 'key1': { 'string': 'key1_el' }, 'key2': { 'string': 'key2_el' }, }, 'meta': { "some_key": "some_value" } }, status=200) resp = cds_handler.fetch_translations(language_code='el') assert resp == { 'el': (True, { 'key1': { 'string': 'key1_el' }, 'key2': { 'string': 'key2_el' }, }) } responses.reset() assert patched_logger.error.call_count == 0 # test connection_error resp = cds_handler.fetch_translations(language_code='el') patched_logger.error.assert_called_with( 'Error retrieving translations from CDS: ConnectionError') assert resp == {'el': (False, {})}
class TxNative(object): """The main class of the framework, responsible for orchestrating all behavior.""" def __init__(self): # The class uses an untypical initialization scheme, defining # an init() method, instead of initializing inside the constructor # This is necessary for allowing it to be initialized by its clients # with proper arguments, while at the same time being very easy # to import and use a single "global" instance self._cache = None self._languages = [] self._missing_policy = None self._cds_handler = None self.initialized = False def init( self, languages, token, secret=None, cds_host=None, missing_policy=None, error_policy=None ): """Create an instance of the core framework class. Also warms up the cache by fetching the translations from the CDS. :param list languages: a list of language codes for the languages configured in the application :param str token: the API token to use for connecting to the CDS :param str secret: the additional secret to use for pushing source content :param str cds_host: an optional host for the Content Delivery Service, defaults to the host provided by Transifex :param AbstractRenderingPolicy missing_policy: an optional policy to use for returning strings when a translation is missing :param AbstractErrorPolicy error_policy: an optional policy to determine how to handle rendering errors """ self._languages = languages self._cache = MemoryCache() self._missing_policy = missing_policy or SourceStringPolicy() self._error_policy = error_policy or SourceStringErrorPolicy() self._cds_handler = CDSHandler( self._languages, token, secret=secret, host=cds_host ) self.initialized = True def translate( self, source_string, language_code, is_source=False, _context=None, escape=True, params=None ): """Translate the given string to the provided language. :param unicode source_string: the source string to get the translation for e.g. 'Order: {num, plural, one {A table} other {{num} tables}}' :param str language_code: the language to translate to :param bool is_source: a boolean indicating whether `translate` is being used for the source language :param unicode _context: an optional context that accompanies the string :param bool escape: if True, the returned string will be HTML-escaped, otherwise it won't :param dict params: optional parameters to replace any placeholders found in the translation string :return: the rendered string :rtype: unicode """ if params is None: params = {} self._check_initialization() translation_template = self.get_translation(source_string, language_code, _context, is_source) return self.render_translation(translation_template, params, source_string, language_code, escape) def get_translation(self, source_string, language_code, _context, is_source=False): """ Try to retrieve the translation. A translation is a serialized source_string with ICU format support, e.g. '{num, plural, one {Ένα τραπέζι} other {{num} τραπέζια}}' """ if is_source: translation_template = source_string else: pluralized, plurals = parse_plurals(source_string) key = generate_key(string=source_string, context=_context) translation_template = self._cache.get(key, language_code) if (translation_template is not None and pluralized and translation_template.startswith('{???')): variable_name = source_string[1:source_string.index(',')].\ strip() translation_template = ('{' + variable_name + translation_template[4:]) return translation_template def render_translation(self, translation_template, params, source_string, language_code, escape=False): """ Replace the variables in the ICU translation """ try: return StringRenderer.render( source_string=source_string, string_to_render=translation_template, language_code=language_code, escape=escape, missing_policy=self._missing_policy, params=params, ) except Exception: return self._error_policy.get( source_string=source_string, translation=translation_template, language_code=language_code, escape=escape, params=params, ) def fetch_translations(self): """Fetch fresh content from the CDS.""" self._check_initialization() self._cache.update(self._cds_handler.fetch_translations()) def push_source_strings(self, strings, purge=False): """Push the given source strings to the CDS. :param list strings: a list of SourceString objects :param bool purge: True deletes destination source content not included in pushed content. False appends the pushed content to destination source content. :return: a tuple containing the status code and the content of the response :rtype: tuple """ self._check_initialization() response = self._cds_handler.push_source_strings(strings, purge) return response.status_code, json.loads(response.content) def _check_initialization(self): """Raise an exception if the class has not been initialized. :raise NotInitializedError: if the class hasn't been initialized """ if not self.initialized: raise NotInitializedError( 'TxNative is not initialized, make sure you call init() first.' )