transifex-python 3.7.0__tar.gz → 3.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. {transifex_python-3.7.0/transifex_python.egg-info → transifex_python-3.8.0}/PKG-INFO +48 -1
  2. transifex_python-3.8.0/README.md +95 -0
  3. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/core/test_cds.py +302 -3
  4. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/core/test_init.py +11 -0
  5. transifex_python-3.8.0/tests/native/django/test_apps.py +20 -0
  6. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/_version.py +3 -3
  7. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/__init__.py +8 -0
  8. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/cds.py +134 -24
  9. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/core.py +10 -1
  10. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/apps.py +2 -0
  11. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/settings.py +6 -0
  12. {transifex_python-3.7.0 → transifex_python-3.8.0/transifex_python.egg-info}/PKG-INFO +48 -1
  13. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex_python.egg-info/SOURCES.txt +1 -0
  14. transifex_python-3.7.0/README.md +0 -48
  15. {transifex_python-3.7.0 → transifex_python-3.8.0}/LICENSE +0 -0
  16. {transifex_python-3.7.0 → transifex_python-3.8.0}/MANIFEST.in +0 -0
  17. {transifex_python-3.7.0 → transifex_python-3.8.0}/setup.cfg +0 -0
  18. {transifex_python-3.7.0 → transifex_python-3.8.0}/setup.py +0 -0
  19. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/api/__init__.py +0 -0
  20. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/api/constants.py +0 -0
  21. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/api/payloads.py +0 -0
  22. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/api/test_apis.py +0 -0
  23. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/api/test_bulk.py +0 -0
  24. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/api/test_exceptions.py +0 -0
  25. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/api/test_plural_lists.py +0 -0
  26. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/api/test_queryset.py +0 -0
  27. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/api/test_relationships.py +0 -0
  28. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/api/test_single_resource.py +0 -0
  29. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/common/__init__.py +0 -0
  30. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/common/test_is_plural.py +0 -0
  31. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/common/test_strings.py +0 -0
  32. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/__init__.py +0 -0
  33. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/conftest.py +0 -0
  34. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/core/__init__.py +0 -0
  35. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/core/test_cache.py +0 -0
  36. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/core/test_core.py +0 -0
  37. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/core/test_daemon.py +0 -0
  38. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/core/test_parsing.py +0 -0
  39. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/core/test_rendering.py +0 -0
  40. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/core/test_tools/__init__.py +0 -0
  41. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/core/test_tools/test_migrations/__init__.py +0 -0
  42. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/core/test_tools/test_migrations/test_mark.py +0 -0
  43. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/core/test_tools/test_migrations/test_models.py +0 -0
  44. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/core/test_tools/test_migrations/test_review.py +0 -0
  45. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/core/test_tools/test_migrations/test_save.py +0 -0
  46. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/django/__init__.py +0 -0
  47. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/django/settings.py +0 -0
  48. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/django/test_commands/__init__.py +0 -0
  49. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/django/test_commands/test_invalidatetransifex.py +0 -0
  50. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/django/test_commands/test_migratetransifex.py +0 -0
  51. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/django/test_commands/test_pushtransifex.py +0 -0
  52. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/django/test_templatetag.py +0 -0
  53. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/django/test_tools/__init__.py +0 -0
  54. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/django/test_tools/test_migrations/__init__.py +0 -0
  55. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/django/test_tools/test_migrations/test_templatetags.py +0 -0
  56. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/django/test_utils/__init__.py +0 -0
  57. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/django/test_utils/test_init.py +0 -0
  58. {transifex_python-3.7.0 → transifex_python-3.8.0}/tests/native/django/test_utils/test_templates.py +0 -0
  59. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/__init__.py +0 -0
  60. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/api/__init__.py +0 -0
  61. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/api/exceptions.py +0 -0
  62. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/api/jsonapi/__init__.py +0 -0
  63. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/api/jsonapi/apis.py +0 -0
  64. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/api/jsonapi/auth.py +0 -0
  65. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/api/jsonapi/collections.py +0 -0
  66. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/api/jsonapi/compat.py +0 -0
  67. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/api/jsonapi/exceptions.py +0 -0
  68. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/api/jsonapi/resources.py +0 -0
  69. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/api/jsonapi/utils.py +0 -0
  70. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/common/__init__.py +0 -0
  71. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/common/_compat.py +0 -0
  72. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/common/console.py +0 -0
  73. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/common/strings.py +0 -0
  74. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/common/utils.py +0 -0
  75. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/cache.py +0 -0
  76. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/consts.py +0 -0
  77. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/daemon.py +0 -0
  78. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/__init__.py +0 -0
  79. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/compat.py +0 -0
  80. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/management/__init__.py +0 -0
  81. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/management/commands/__init__.py +0 -0
  82. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/management/commands/transifex.py +0 -0
  83. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/management/common.py +0 -0
  84. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/management/utils/__init__.py +0 -0
  85. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/management/utils/base.py +0 -0
  86. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/management/utils/invalidate.py +0 -0
  87. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/management/utils/migrate.py +0 -0
  88. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/management/utils/push.py +0 -0
  89. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/management/utils/try_templatetag.py +0 -0
  90. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/templatetags/__init__.py +0 -0
  91. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/templatetags/transifex.py +0 -0
  92. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/templatetags/utils.py +0 -0
  93. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/tools/__init__.py +0 -0
  94. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/tools/migrations/__init__.py +0 -0
  95. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/tools/migrations/templatetags.py +0 -0
  96. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/utils/__init__.py +0 -0
  97. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/django/utils/templates.py +0 -0
  98. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/parsing.py +0 -0
  99. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/rendering.py +0 -0
  100. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/settings.py +0 -0
  101. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/tools/__init__.py +0 -0
  102. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/tools/migrations/__init__.py +0 -0
  103. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/tools/migrations/execution.py +0 -0
  104. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/tools/migrations/gettext.py +0 -0
  105. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/tools/migrations/mark.py +0 -0
  106. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/tools/migrations/models.py +0 -0
  107. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/tools/migrations/review.py +0 -0
  108. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex/native/tools/migrations/save.py +0 -0
  109. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex_python.egg-info/dependency_links.txt +0 -0
  110. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex_python.egg-info/requires.txt +0 -0
  111. {transifex_python-3.7.0 → transifex_python-3.8.0}/transifex_python.egg-info/top_level.txt +0 -0
  112. {transifex_python-3.7.0 → transifex_python-3.8.0}/versioneer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: transifex-python
3
- Version: 3.7.0
3
+ Version: 3.8.0
4
4
  Summary: Transifex Python Toolkit
5
5
  Home-page: https://github.com/transifex/transifex-python
6
6
  Author: Transifex
@@ -69,6 +69,53 @@ To learn more about using Transifex Python toolkit check:
69
69
  * For a general overview visit [Transifex Native overview](https://developers.transifex.com/docs/native?utm_campaign=tx-native&utm_source=github&utm_medium=link)
70
70
  * For some common questions & answers check our [Transifex Native community](https://community.transifex.com/c/transifex-native/17)
71
71
 
72
+ # Django settings reference
73
+
74
+ The Transifex Native Django sdk is controlled via a set of configuration options defined in Django settings:
75
+
76
+ - TRANSIFEX_TOKEN: API token that connects your application to a Transifex project.
77
+ Must be set for both pushing source strings and fetching translations.
78
+
79
+ - TRANSIFEX_SECRET: Secret used together with the token for authenticated operations against CDS (e.g. pushing source content, invalidating cache).
80
+
81
+ - TRANSIFEX_CDS_HOST: Override the default CDS host (https://cds.svc.transifex.net).
82
+
83
+ - TRANSIFEX_FILTER_STATUS: Optional CDS filter[status] parameter used when fetching translations (e.g. "reviewed", "proofread"). If not set, CDS returns all available statuses.
84
+
85
+ - TRANSIFEX_FILTER_TAGS: Optional CDS filter[tags] parameter used when fetching translations. Use this to limit fetched content to specific tags.
86
+
87
+ - TRANSIFEX_MISSING_POLICY: Custom “missing translation” policy class.
88
+ Defaults to the built-in SourceStringPolicy when not provided.
89
+
90
+ - TRANSIFEX_ERROR_POLICY: Custom error handling policy class.
91
+ Defaults to SourceErrorPolicy.
92
+
93
+ - TRANSIFEX_CACHE
94
+ 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).
95
+
96
+ - SKIP_TRANSLATIONS_SYNC: If True, disables automatic translation sync (OTA) for this environment.
97
+
98
+ - 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.
99
+
100
+ - 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.
101
+
102
+ - TRANSIFEX_FETCH_TIMEOUT:
103
+ Approximate maximum time in seconds for one fetch of languages and
104
+ translations from CDS, including retries. It can be exceeded, e.g. by DNS
105
+ resolution, a host with several IP addresses, or a response that keeps
106
+ sending data slowly (the read timeout applies between received bytes, not to
107
+ the whole response). Languages are fetched in LANGUAGES order; those not
108
+ reached in time are skipped until the next sync, or for the life of the
109
+ process if TRANSIFEX_SYNC_INTERVAL is 0.
110
+ 0 (default) = no global timeout: if CDS keeps answering 202, the fetch, and
111
+ so startup, waits until it stops. Set it when translations are fetched at
112
+ startup, e.g. under gunicorn.
113
+
114
+ - TRANSIFEX_REQUEST_TIMEOUT:
115
+ Timeout in seconds for each request made when fetching from CDS, either a
116
+ number greater than 0 or a `(connect, read)` tuple as accepted by `requests`.
117
+ Invalid values are logged and the default is used. Defaults to `(10, 60)`.
118
+
72
119
  # License
73
120
 
74
121
  Licensed under Apache License 2.0, see `LICENSE` file.
@@ -0,0 +1,95 @@
1
+ ![Test suite](https://github.com/transifex/transifex-python/workflows/Test%20suite/badge.svg?branch=master)
2
+ [![codecov](https://codecov.io/gh/transifex/transifex-python/branch/master/graph/badge.svg)](https://codecov.io/gh/transifex/transifex-python)
3
+
4
+ # Transifex Python Toolkit
5
+
6
+ Transifex Python Toolkit is a collection of tools that allow you to easily localize your Django and Python applications using Transifex. The toolkit features fetching translations over the air (OTA) to your apps.
7
+
8
+ This project adheres to the Contributor Covenant [code of conduct](/CODE_OF_CONDUCT.md). To contribute to Transifex Python Toolkit, please check out the [contribution guidelines](/CONTRIBUTING.md).
9
+
10
+ # Upgrade to v2
11
+
12
+ If you are upgrading from the `1.x.x` version, please read this [migration guide](https://github.com/transifex/transifex-python/blob/HEAD/UPGRADE_TO_V2.md), as there are breaking changes in place.
13
+
14
+ # Quick starting guide
15
+
16
+ 1. Install toolkit in your code `$ pip install transifex-python`
17
+ 2. Add a provided TOKEN and SECRET in your config, connecting your application with a Transifex project
18
+ 3. Add internationalization commands in your code
19
+ ```HTML+Django
20
+ <!-- Django app template example -->
21
+
22
+ {% load transifex %}
23
+ <p>{% t "Hello!" %}</p>
24
+ <p>{% t "I want to be translated." %}</p>
25
+ ```
26
+
27
+ ```python
28
+ # Django view sample
29
+ from transifex.native.django import t
30
+
31
+ output = {
32
+ "msg1": t("Welcome aboard!"),
33
+ "msg2": t("It's great to have you here!"),
34
+ }
35
+ return JsonResponse(output)
36
+ ```
37
+ 4. Push strings to your connected Transifex project `./manage.py transifex push`
38
+ 5. When translations are added in your Transifex project are automatically made available
39
+
40
+ To learn more about using Transifex Python toolkit check:
41
+ * [Quickstart guide for Django web application](https://developers.transifex.com/docs/django-sdk?utm_campaign=tx-native&utm_source=github&utm_medium=link)
42
+ * [Quickstart guide for Python application](https://developers.transifex.com/docs/python-sdk?utm_campaign=tx-native&utm_source=github&utm_medium=link)
43
+ * For a general overview visit [Transifex Native overview](https://developers.transifex.com/docs/native?utm_campaign=tx-native&utm_source=github&utm_medium=link)
44
+ * For some common questions & answers check our [Transifex Native community](https://community.transifex.com/c/transifex-native/17)
45
+
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_TIMEOUT:
77
+ Approximate maximum time in seconds for one fetch of languages and
78
+ translations from CDS, including retries. It can be exceeded, e.g. by DNS
79
+ resolution, a host with several IP addresses, or a response that keeps
80
+ sending data slowly (the read timeout applies between received bytes, not to
81
+ the whole response). Languages are fetched in LANGUAGES order; those not
82
+ reached in time are skipped until the next sync, or for the life of the
83
+ process if TRANSIFEX_SYNC_INTERVAL is 0.
84
+ 0 (default) = no global timeout: if CDS keeps answering 202, the fetch, and
85
+ so startup, waits until it stops. Set it when translations are fetched at
86
+ startup, e.g. under gunicorn.
87
+
88
+ - TRANSIFEX_REQUEST_TIMEOUT:
89
+ Timeout in seconds for each request made when fetching from CDS, either a
90
+ number greater than 0 or a `(connect, read)` tuple as accepted by `requests`.
91
+ Invalid values are logged and the default is used. Defaults to `(10, 60)`.
92
+
93
+ # License
94
+
95
+ Licensed under Apache License 2.0, see `LICENSE` file.
@@ -1,12 +1,13 @@
1
1
  from operator import itemgetter
2
2
 
3
3
  import pytest
4
+ import requests
4
5
  import responses
5
- from mock import patch
6
- from transifex.native.cds import CDSHandler
6
+ from urllib3.exceptions import ReadTimeoutError
7
+ from mock import patch, MagicMock
8
+ from transifex.native.cds import DEFAULT_REQUEST_TIMEOUT, CDSHandler
7
9
  from transifex.native.parsing import SourceString
8
10
 
9
-
10
11
  class TestCDSHandler(object):
11
12
 
12
13
  def _lang_lists_equal(self, list_1, list_2):
@@ -635,3 +636,301 @@ class TestCDSHandler(object):
635
636
  for x in ('Unprocessable Entity', 'None')
636
637
  ]
637
638
  assert patched_logger.error.call_args[0][0] in messages
639
+
640
+ @staticmethod
641
+ def _json_response(status_code, payload=None):
642
+ response = MagicMock()
643
+ response.ok = status_code < 400
644
+ response.status_code = status_code
645
+ response.headers = {}
646
+ response.json.return_value = payload
647
+ return response
648
+
649
+ @patch('transifex.native.cds.time.sleep')
650
+ @patch('transifex.native.cds.time.monotonic')
651
+ @patch('transifex.native.cds.requests.get')
652
+ def test_retry_get_request_raises_timeout_on_202_at_deadline(
653
+ self, mock_get, mock_time, mock_sleep
654
+ ):
655
+ cds_handler = CDSHandler(['el'], 'some_token', fetch_timeout=5)
656
+ mock_get.return_value = self._json_response(202)
657
+ # deadline 0 -> 5; requests at 0.1 and 2.2; after the second 202
658
+ # the 2s retry delay would pass the deadline
659
+ mock_time.side_effect = [0, 0.1, 0.1, 2.2, 3.5]
660
+
661
+ with pytest.raises(requests.Timeout):
662
+ cds_handler.retry_get_request('https://some.host/languages')
663
+
664
+ assert mock_get.call_count == 2
665
+ assert mock_sleep.call_count == 1
666
+
667
+ @patch('transifex.native.cds.time.sleep')
668
+ @patch('transifex.native.cds.requests.get')
669
+ def test_retry_get_request_returns_unexpected_status(
670
+ self, mock_get, mock_sleep
671
+ ):
672
+ cds_handler = CDSHandler(['el'], 'some_token')
673
+ response = self._json_response(600)
674
+ mock_get.return_value = response
675
+
676
+ assert cds_handler.retry_get_request('https://some.host/x') is response
677
+ assert mock_get.call_count == 1
678
+ mock_sleep.assert_not_called()
679
+
680
+ @patch('transifex.native.cds.logger')
681
+ @patch('transifex.native.cds.time.sleep')
682
+ @patch('transifex.native.cds.time.monotonic')
683
+ @patch('transifex.native.cds.requests.get')
684
+ def test_fetch_translations_202_at_deadline_is_a_timeout(
685
+ self, mock_get, mock_time, mock_sleep, patched_logger
686
+ ):
687
+ cds_handler = CDSHandler(['el'], 'some_token', fetch_timeout=1)
688
+ cds_handler.etags.set('el', 'old-etag')
689
+ mock_get.return_value = self._json_response(202, {'data': {}})
690
+ # deadline 0 -> 1; checked at 0.1, request at 0.2, 202 at 0.3 leaves
691
+ # less than the 2s retry delay
692
+ mock_time.side_effect = [0, 0.1, 0.2, 0.3]
693
+
694
+ translations = cds_handler.fetch_translations(language_code='el')
695
+
696
+ assert translations == {'el': (False, {})}
697
+ assert cds_handler.etags.get('el') == 'old-etag'
698
+ patched_logger.error.assert_called_once_with(
699
+ 'Error retrieving translations from CDS: Timeout')
700
+
701
+ @patch('transifex.native.cds.time.monotonic')
702
+ @patch('transifex.native.cds.requests.get')
703
+ def test_request_timeout_is_capped_by_deadline(self, mock_get, mock_time):
704
+ mock_get.return_value = self._json_response(200, {'data': []})
705
+ mock_time.side_effect = [0, 3]
706
+
707
+ CDSHandler(['el'], 'some_token', fetch_timeout=5).fetch_languages()
708
+
709
+ assert mock_get.call_args[1]['timeout'] == (2, 2)
710
+
711
+ @patch('transifex.native.cds.time.monotonic')
712
+ @patch('transifex.native.cds.requests.get')
713
+ def test_no_request_once_deadline_has_passed(self, mock_get, mock_time):
714
+ mock_time.side_effect = [0, 6]
715
+ cds_handler = CDSHandler(['el'], 'some_token', fetch_timeout=5)
716
+
717
+ with pytest.raises(requests.Timeout):
718
+ cds_handler.retry_get_request('https://some.host/languages')
719
+ mock_get.assert_not_called()
720
+
721
+ @patch('transifex.native.cds.logger')
722
+ def test_invalid_fetch_timeout_disables_it(self, patched_logger):
723
+ for value, expected in ((None, 0), ('30', 30), (2, 2), ('abc', 0)):
724
+ cds_handler = CDSHandler(['el'], 'some_token', fetch_timeout=value)
725
+ assert cds_handler.fetch_timeout == expected
726
+ patched_logger.error.assert_called_once_with(
727
+ 'Invalid fetch_timeout `abc`, fetch timeout disabled')
728
+
729
+ @patch('transifex.native.cds.requests.get')
730
+ def test_fetch_requests_use_request_timeout(self, mock_get):
731
+ mock_get.return_value = self._json_response(200, {'data': []})
732
+
733
+ for request_timeout, expected in (
734
+ (None, (10, 60)),
735
+ ((3, 7), (3, 7)),
736
+ (5, 5),
737
+ ):
738
+ mock_get.reset_mock()
739
+ kwargs = {} if request_timeout is None else {
740
+ 'request_timeout': request_timeout}
741
+ cds_handler = CDSHandler(['el'], 'some_token', **kwargs)
742
+
743
+ cds_handler.fetch_languages()
744
+ cds_handler.fetch_translations(language_code='el')
745
+
746
+ timeouts = [c[1]['timeout'] for c in mock_get.call_args_list]
747
+ assert timeouts == [expected, expected]
748
+
749
+ @patch('transifex.native.cds.logger')
750
+ @patch('transifex.native.cds.requests.get')
751
+ def test_fetch_logs_request_timeouts(self, mock_get, patched_logger):
752
+ cds_handler = CDSHandler(['el'], 'some_token')
753
+
754
+ for exception in (requests.ReadTimeout(), requests.ConnectTimeout()):
755
+ mock_get.side_effect = exception
756
+ patched_logger.reset_mock()
757
+
758
+ assert cds_handler.fetch_languages() == []
759
+ assert cds_handler.fetch_translations(language_code='el') == {
760
+ 'el': (False, {})
761
+ }
762
+ assert [c[0][0] for c in patched_logger.error.call_args_list] == [
763
+ 'Error retrieving languages from CDS: Timeout',
764
+ 'Error retrieving translations from CDS: Timeout',
765
+ ]
766
+
767
+ @patch('transifex.native.cds.logger')
768
+ @patch('transifex.native.cds.time.monotonic')
769
+ @patch('transifex.native.cds.requests.get')
770
+ def test_fetch_timeout_bounds_whole_fetch(
771
+ self, mock_get, mock_time, patched_logger
772
+ ):
773
+ cds_handler = CDSHandler(['el', 'en'], 'some_token', fetch_timeout=5)
774
+ mock_get.side_effect = [
775
+ self._json_response(200, {'data': [{'code': 'el'},
776
+ {'code': 'en'}]}),
777
+ self._json_response(200, {'data': {'key': {'string': 'x'}}}),
778
+ ]
779
+ # deadline 0 -> 5; languages request at 0.5, first language checked
780
+ # and requested at 1, second language checked at 10
781
+ mock_time.side_effect = [0, 0.5, 1, 1, 10]
782
+
783
+ translations = cds_handler.fetch_translations()
784
+
785
+ assert mock_get.call_count == 2
786
+ assert sorted(translations.values(), key=lambda v: v[0]) == [
787
+ (False, {}),
788
+ (True, {'key': {'string': 'x'}}),
789
+ ]
790
+ skipped = [lang for lang, (refresh, _) in translations.items()
791
+ if not refresh]
792
+ patched_logger.error.assert_called_once_with(
793
+ 'Error retrieving translations from CDS: fetch timeout reached, '
794
+ 'skipped `{}`'.format(skipped[0])
795
+ )
796
+
797
+ @patch('transifex.native.cds.time.monotonic')
798
+ @patch('transifex.native.cds.requests.get')
799
+ def test_number_request_timeout_is_capped_by_deadline(
800
+ self, mock_get, mock_time
801
+ ):
802
+ mock_get.return_value = self._json_response(200, {'data': []})
803
+ mock_time.side_effect = [0, 3]
804
+ cds_handler = CDSHandler(['el'], 'some_token', fetch_timeout=5,
805
+ request_timeout=30)
806
+
807
+ cds_handler.fetch_languages()
808
+
809
+ assert mock_get.call_args[1]['timeout'] == 2
810
+
811
+ @patch('transifex.native.cds.time.sleep')
812
+ @patch('transifex.native.cds.time.monotonic')
813
+ @patch('transifex.native.cds.requests.get')
814
+ def test_retry_get_request_returns_5xx_at_deadline(
815
+ self, mock_get, mock_time, mock_sleep
816
+ ):
817
+ response = self._json_response(503)
818
+ mock_get.return_value = response
819
+ # deadline 0 -> 1; request at 0.1, 503 at 0.2 leaves less than 2s
820
+ mock_time.side_effect = [0, 0.1, 0.2]
821
+ cds_handler = CDSHandler(['el'], 'some_token', fetch_timeout=1)
822
+
823
+ assert cds_handler.retry_get_request('https://some.host/x') is response
824
+ assert mock_get.call_count == 1
825
+ mock_sleep.assert_not_called()
826
+
827
+ @patch('transifex.native.cds.time.monotonic')
828
+ @patch('transifex.native.cds.requests.get')
829
+ def test_none_request_timeout_parts_are_capped_by_deadline(
830
+ self, mock_get, mock_time
831
+ ):
832
+ mock_get.return_value = self._json_response(200, {'data': []})
833
+
834
+ for request_timeout, expected in (((2, None), (2, 3)), (None, 3)):
835
+ mock_time.side_effect = [0, 2]
836
+ cds_handler = CDSHandler(['el'], 'some_token', fetch_timeout=5,
837
+ request_timeout=request_timeout)
838
+
839
+ assert cds_handler.fetch_languages() == []
840
+ assert mock_get.call_args[1]['timeout'] == expected
841
+
842
+ @patch('transifex.native.cds.time.sleep')
843
+ @patch('transifex.native.cds.requests.get')
844
+ def test_retry_get_request_5xx_retries_without_deadline(
845
+ self, mock_get, mock_sleep
846
+ ):
847
+ response = self._json_response(503)
848
+ mock_get.return_value = response
849
+
850
+ cds_handler = CDSHandler(['el'], 'some_token')
851
+ assert cds_handler.retry_get_request('https://some.host/x') is response
852
+
853
+ assert mock_get.call_count == 4
854
+ assert [c[0][0] for c in mock_sleep.call_args_list] == [2, 4, 6]
855
+
856
+ @patch('transifex.native.cds.time.sleep')
857
+ @patch('transifex.native.cds.time.monotonic')
858
+ @patch('transifex.native.cds.requests.get')
859
+ def test_retry_get_request_202_then_200_within_deadline(
860
+ self, mock_get, mock_time, mock_sleep
861
+ ):
862
+ ok = self._json_response(200, {'data': []})
863
+ mock_get.side_effect = [self._json_response(202), ok]
864
+ # deadline 0 -> 10; requests at 0.1 and 2.2, 202 checked at 0.2
865
+ mock_time.side_effect = [0, 0.1, 0.2, 2.2]
866
+ cds_handler = CDSHandler(['el'], 'some_token', fetch_timeout=10)
867
+
868
+ assert cds_handler.retry_get_request('https://some.host/x') is ok
869
+ mock_sleep.assert_called_once_with(2)
870
+
871
+ @patch('transifex.native.cds.requests.get')
872
+ def test_fetch_translations_follows_configured_language_order(
873
+ self, mock_get
874
+ ):
875
+ languages = self._json_response(200, {'data': [
876
+ {'code': 'el'}, {'code': 'en'}, {'code': 'fr'}, {'code': 'de'}]})
877
+ content = self._json_response(200, {'data': {}})
878
+
879
+ for fetch_all_langs, expected in (
880
+ (False, ['fr', 'el']),
881
+ (True, ['fr', 'el', 'en', 'de']),
882
+ ):
883
+ mock_get.reset_mock()
884
+ mock_get.side_effect = [languages] + [content] * 4
885
+ cds_handler = CDSHandler(['fr', 'it', 'el', 'fr'], 'some_token',
886
+ host='https://some.host',
887
+ fetch_all_langs=fetch_all_langs)
888
+
889
+ cds_handler.fetch_translations()
890
+
891
+ requested = [c[0][0].rsplit('/', 1)[1]
892
+ for c in mock_get.call_args_list[1:]]
893
+ assert requested == expected
894
+
895
+ @patch('transifex.native.cds.requests.get')
896
+ def test_fetch_all_langs_without_configured_languages(self, mock_get):
897
+ mock_get.side_effect = [
898
+ self._json_response(200, {'data': [{'code': 'el'}]}),
899
+ self._json_response(200, {'data': {}}),
900
+ ]
901
+ cds_handler = CDSHandler(None, 'some_token',
902
+ host='https://some.host',
903
+ fetch_all_langs=True)
904
+
905
+ assert cds_handler.fetch_translations() == {'el': (True, {})}
906
+
907
+ @patch('transifex.native.cds.logger')
908
+ def test_invalid_request_timeout_uses_default(self, patched_logger):
909
+ for value, expected in (
910
+ (None, None),
911
+ (5, 5),
912
+ ((3, None), (3, None)),
913
+ ([3, 30], (3, 30)),
914
+ (0, DEFAULT_REQUEST_TIMEOUT),
915
+ ('30', DEFAULT_REQUEST_TIMEOUT),
916
+ (True, DEFAULT_REQUEST_TIMEOUT),
917
+ ((1, 2, 3), DEFAULT_REQUEST_TIMEOUT),
918
+ ):
919
+ cds_handler = CDSHandler(['el'], 'some_token',
920
+ request_timeout=value)
921
+ assert cds_handler.request_timeout == expected
922
+ assert patched_logger.error.call_count == 4
923
+
924
+ @patch('transifex.native.cds.logger')
925
+ @patch('transifex.native.cds.requests.get')
926
+ def test_read_timeout_in_body_is_logged_as_timeout(
927
+ self, mock_get, patched_logger
928
+ ):
929
+ mock_get.side_effect = requests.ConnectionError(
930
+ ReadTimeoutError(None, None, 'Read timed out.'))
931
+ cds_handler = CDSHandler(['el'], 'some_token',
932
+ host='https://some.host')
933
+
934
+ assert cds_handler.fetch_translations('el') == {'el': (False, {})}
935
+ patched_logger.error.assert_called_once_with(
936
+ 'Error retrieving translations from CDS: Timeout')
@@ -52,3 +52,14 @@ class TestModuleInit(object):
52
52
  assert isinstance(_tx._missing_policy, SourceStringPolicy)
53
53
  assert isinstance(_tx._error_policy, SourceStringErrorPolicy)
54
54
  assert isinstance(_tx._cache, MemoryCache)
55
+
56
+ def test_init_passes_timeouts_to_cds_handler(self, reset_tx):
57
+ native.init('mytoken', ['lang1'], fetch_timeout=7,
58
+ request_timeout=(1, 2))
59
+ assert _tx._cds_handler.fetch_timeout == 7
60
+ assert _tx._cds_handler.request_timeout == (1, 2)
61
+
62
+ def test_init_default_request_timeout(self, reset_tx):
63
+ native.init('mytoken', ['lang1'])
64
+ assert _tx._cds_handler.fetch_timeout == 0
65
+ assert _tx._cds_handler.request_timeout == (10, 60)
@@ -0,0 +1,20 @@
1
+ from django.apps import apps
2
+ from mock import patch
3
+ from transifex.native.django import settings as native_settings
4
+ from transifex.native.django.apps import NativeConfig
5
+
6
+
7
+ def _native_config():
8
+ return next(config for config in apps.get_app_configs()
9
+ if isinstance(config, NativeConfig))
10
+
11
+
12
+ @patch.object(native_settings, 'TRANSIFEX_REQUEST_TIMEOUT', (1, 2))
13
+ @patch.object(native_settings, 'TRANSIFEX_FETCH_TIMEOUT', 7)
14
+ @patch('transifex.native.django.apps.init')
15
+ def test_ready_passes_timeout_settings(mock_init):
16
+ _native_config().ready()
17
+
18
+ kwargs = mock_init.call_args[1]
19
+ assert kwargs['fetch_timeout'] == 7
20
+ assert kwargs['request_timeout'] == (1, 2)
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2025-11-14T10:11:09+0200",
11
+ "date": "2026-11-13T15:12:29+0200",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "dbd2ca382211f853affe61e70a2ff4dcfd33fac2",
15
- "version": "3.7.0"
14
+ "full-revisionid": "55901f750a65c35413540bf26f11f31562a75e4f",
15
+ "version": "3.8.0"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -8,6 +8,8 @@ def init(
8
8
  fetch_all_langs=False,
9
9
  filter_tags=None,
10
10
  filter_status=None,
11
+ fetch_timeout=0,
12
+ request_timeout=None,
11
13
  ):
12
14
  """Initialize the framework.
13
15
 
@@ -25,6 +27,10 @@ def init(
25
27
  :param bool fetch_all_langs: force pull all remote languages
26
28
  :param str filter_tags: fetch only content with tags
27
29
  :param str filter_status: fetch only content with specific translation status
30
+ :param int fetch_timeout: maximum time in seconds to wait when fetching
31
+ translations or locales from CDS
32
+ :param request_timeout: seconds for each CDS fetch request, a number or
33
+ a (connect, read) tuple; defaults to (10, 60)
28
34
  """
29
35
  if not tx.initialized:
30
36
  tx.init(
@@ -38,6 +44,8 @@ def init(
38
44
  fetch_all_langs=fetch_all_langs,
39
45
  filter_tags=filter_tags,
40
46
  filter_status=filter_status,
47
+ fetch_timeout=fetch_timeout,
48
+ request_timeout=request_timeout,
41
49
  )
42
50
 
43
51