Skip to content

Commit 0f0b0f9

Browse files
committed
Introduce fetch translations timeout setting
1 parent dbd2ca3 commit 0f0b0f9

7 files changed

Lines changed: 112 additions & 12 deletions

File tree

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,40 @@ To learn more about using Transifex Python toolkit check:
4343
* For a general overview visit [Transifex Native overview](https://developers.transifex.com/docs/native?utm_campaign=tx-native&utm_source=github&utm_medium=link)
4444
* For some common questions & answers check our [Transifex Native community](https://community.transifex.com/c/transifex-native/17)
4545

46+
# Django settings reference
47+
48+
The Transifex Native Django sdk is controlled via a set of configuration options defined in Django settings:
49+
50+
- TRANSIFEX_TOKEN: API token that connects your application to a Transifex project.
51+
Must be set for both pushing source strings and fetching translations.
52+
53+
- TRANSIFEX_SECRET: Secret used together with the token for authenticated operations against CDS (e.g. pushing source content, invalidating cache).
54+
55+
- TRANSIFEX_CDS_HOST: Override the default CDS host (https://cds.svc.transifex.net).
56+
57+
- TRANSIFEX_FILTER_STATUS: Optional CDS filter[status] parameter used when fetching translations (e.g. "reviewed", "proofread"). If not set, CDS returns all available statuses.
58+
59+
- TRANSIFEX_FILTER_TAGS: Optional CDS filter[tags] parameter used when fetching translations. Use this to limit fetched content to specific tags.
60+
61+
- TRANSIFEX_MISSING_POLICY: Custom “missing translation” policy class.
62+
Defaults to the built-in SourceStringPolicy when not provided.
63+
64+
- TRANSIFEX_ERROR_POLICY: Custom error handling policy class.
65+
Defaults to SourceErrorPolicy.
66+
67+
- TRANSIFEX_CACHE
68+
Custom cache implementation. Defaults to an in-memory cache (MemoryCache) if not provided. Can be used to integrate with a shared cache (e.g. Redis, memcached).
69+
70+
- SKIP_TRANSLATIONS_SYNC: If True, disables automatic translation sync (OTA) for this environment.
71+
72+
- TRANSIFEX_SYNC_INTERVAL: Interval in seconds for the background sync daemon that fetches translations. Default: 30 * 60 (30 minutes). Set to 0 to disable periodic sync and only fetch on startup.
73+
74+
- TRANSIFEX_FETCH_ALL_LANGUAGES: When True, fetch translations for all languages configured in CDS. When False (default), only fetch translations for languages listed in Django’s LANGUAGES setting.
75+
76+
- TRANSIFEX_FETCH_TEMEOUT:
77+
Maximum time in seconds to wait when fetching translations or locales from CDS.
78+
0 (default) = no global timeout.
79+
4680
# License
4781

4882
Licensed under Apache License 2.0, see `LICENSE` file.

tests/native/core/test_cds.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22

33
import pytest
44
import responses
5-
from mock import patch
5+
from mock import patch, MagicMock
66
from transifex.native.cds import CDSHandler
77
from transifex.native.parsing import SourceString
88

9-
109
class TestCDSHandler(object):
1110

1211
def _lang_lists_equal(self, list_1, list_2):
@@ -635,3 +634,39 @@ def test_invalidate(self, patched_logger):
635634
for x in ('Unprocessable Entity', 'None')
636635
]
637636
assert patched_logger.error.call_args[0][0] in messages
637+
638+
639+
@patch('transifex.native.cds.requests.get')
640+
@patch('transifex.native.cds.time.time')
641+
@patch('transifex.native.cds.time.sleep')
642+
def test_retry_get_request_times_out_on_202(self, mock_sleep, mock_time, mock_get):
643+
"""
644+
If CDS keeps returning 202 and fetch_timeout is set, retry_get_request
645+
should stop retrying after the timeout and return the last response.
646+
"""
647+
cds_handler = CDSHandler(
648+
['el', 'en'],
649+
'some_token',
650+
fetch_timeout=1, # 1 second total timeout
651+
)
652+
653+
# Always return a 202 response
654+
response_202 = MagicMock()
655+
response_202.status_code = 202
656+
mock_get.return_value = response_202
657+
658+
# Simulate time:
659+
# - first call to time.time() -> start_ts = 0
660+
# - after first loop iteration -> 0.5 (still under timeout)
661+
# - after second loop iteration -> 1.1 (exceeds timeout, should exit)
662+
mock_time.side_effect = [0, 0.5, 1.1]
663+
mock_sleep.return_value = None
664+
665+
result = cds_handler.retry_get_request('https://some.host/languages')
666+
667+
# We should have called GET twice (two 202s) and then exited due to timeout
668+
assert mock_get.call_count == 2
669+
assert result is response_202
670+
671+
# We should have slept twice for the two 202 responses
672+
assert mock_sleep.call_count == 2

transifex/native/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ def init(
88
fetch_all_langs=False,
99
filter_tags=None,
1010
filter_status=None,
11+
fetch_timeout=0,
1112
):
1213
"""Initialize the framework.
1314
@@ -25,6 +26,8 @@ def init(
2526
:param bool fetch_all_langs: force pull all remote languages
2627
:param str filter_tags: fetch only content with tags
2728
:param str filter_status: fetch only content with specific translation status
29+
:param int fetch_timeout: maximum time in seconds to wait when fetching
30+
translations or locales from CDS
2831
"""
2932
if not tx.initialized:
3033
tx.init(
@@ -38,6 +41,7 @@ def init(
3841
fetch_all_langs=fetch_all_langs,
3942
filter_tags=filter_tags,
4043
filter_status=filter_status,
44+
fetch_timeout=fetch_timeout,
4145
)
4246

4347

transifex/native/cds.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class CDSHandler(object):
5757

5858
def __init__(self, configured_languages, token, secret=None,
5959
host=TRANSIFEX_CDS_HOST, fetch_all_langs=False,
60-
filter_tags=None, filter_status=None):
60+
filter_tags=None, filter_status=None, fetch_timeout=0):
6161
"""Constructor.
6262
6363
:param list configured_languages: a list of language codes for the
@@ -73,6 +73,7 @@ def __init__(self, configured_languages, token, secret=None,
7373
self.secret = secret
7474
self.host = host or TRANSIFEX_CDS_HOST
7575
self.etags = EtagStore()
76+
self.fetch_timeout = fetch_timeout
7677

7778
def fetch_languages(self):
7879
"""Fetch the languages defined in the CDS for the specific project.
@@ -364,16 +365,33 @@ def _get_headers(self, use_secret=False, etag=None):
364365

365366
def retry_get_request(self, *args, **kwargs):
366367
""" Resilient function for GET requests """
367-
retries, last_response_status = 0, 202
368-
while (last_response_status == 202 or
369-
500 <= last_response_status < 600 and
370-
retries < MAX_RETRIES):
371-
372-
if 500 <= last_response_status < 600:
373-
retries += 1
374-
time.sleep(retries * RETRY_DELAY_SEC)
368+
retries_5xx = 0
369+
last_response_status = 202
370+
start_ts = time.time()
371+
max_total_seconds = self.fetch_timeout
375372

373+
while True:
376374
response = requests.get(*args, **kwargs)
377375
last_response_status = response.status_code
378376

379-
return response
377+
# Success or non-retryable status -> return immediately
378+
if last_response_status < 500 and last_response_status != 202:
379+
return response
380+
381+
# 202 handling
382+
if last_response_status == 202:
383+
time.sleep(RETRY_DELAY_SEC)
384+
385+
# 5xx handling
386+
elif 500 <= last_response_status < 600:
387+
retries_5xx += 1
388+
if retries_5xx > MAX_RETRIES:
389+
return response
390+
time.sleep(retries_5xx * RETRY_DELAY_SEC)
391+
392+
# Timeout handling
393+
if (
394+
max_total_seconds > 0 and
395+
(time.time() - start_ts) >= max_total_seconds
396+
):
397+
return response

transifex/native/core.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def init(
4242
missing_policy=None, error_policy=None, cache=None,
4343
fetch_all_langs=False, filter_tags=None,
4444
filter_status=None,
45+
fetch_timeout=0,
4546
):
4647
"""Create an instance of the core framework class.
4748
@@ -62,6 +63,8 @@ def init(
6263
:param bool fetch_all_langs: force pull all remote languages
6364
:param str filter_tags: fetch only content with tags
6465
:param str filter_status: fetch only content with specific translation status
66+
:param int fetch_timeout: maximum time in seconds to wait when fetching
67+
translations or locales from CDS
6568
"""
6669
self._languages = languages
6770
self._cache = cache or MemoryCache()
@@ -72,6 +75,7 @@ def init(
7275
fetch_all_langs=fetch_all_langs,
7376
filter_tags=filter_tags,
7477
filter_status=filter_status,
78+
fetch_timeout=fetch_timeout,
7579
)
7680
self.initialized = True
7781

transifex/native/django/apps.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def ready(self):
9494
fetch_all_langs=native_settings.TRANSIFEX_FETCH_ALL_LANGUAGES,
9595
filter_tags=native_settings.TRANSIFEX_FILTER_TAGS,
9696
filter_status=native_settings.TRANSIFEX_FILTER_STATUS,
97+
fetch_timeout=native_settings.TRANSIFEX_FETCH_TIMEOUT,
9798
)
9899

99100
if fetch_translations:

transifex/native/django/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@
1616
TRANSIFEX_FETCH_ALL_LANGUAGES = getattr(settings,
1717
'TRANSIFEX_FETCH_ALL_LANGUAGES',
1818
False)
19+
TRANSIFEX_FETCH_TIMEOUT = getattr(settings,
20+
'TRANSIFEX_FETCH_TIMEOUT',
21+
0)
22+

0 commit comments

Comments
 (0)