splitio-client 10.0.0__tar.gz → 10.1.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 (120) hide show
  1. {splitio_client-10.0.0 → splitio_client-10.1.0}/PKG-INFO +3 -2
  2. {splitio_client-10.0.0 → splitio_client-10.1.0}/setup.py +4 -2
  3. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/api/client.py +244 -42
  4. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/client/config.py +21 -1
  5. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/client/factory.py +23 -9
  6. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/optional/loaders.py +12 -3
  7. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/push/manager.py +6 -1
  8. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/push/splitsse.py +5 -1
  9. splitio_client-10.1.0/splitio/version.py +1 -0
  10. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio_client.egg-info/PKG-INFO +3 -2
  11. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio_client.egg-info/requires.txt +4 -0
  12. splitio_client-10.0.0/splitio/version.py +0 -1
  13. {splitio_client-10.0.0 → splitio_client-10.1.0}/README.md +0 -0
  14. {splitio_client-10.0.0 → splitio_client-10.1.0}/setup.cfg +0 -0
  15. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/__init__.py +0 -0
  16. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/api/__init__.py +0 -0
  17. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/api/auth.py +0 -0
  18. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/api/commons.py +0 -0
  19. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/api/events.py +0 -0
  20. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/api/impressions.py +0 -0
  21. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/api/segments.py +0 -0
  22. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/api/splits.py +0 -0
  23. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/api/telemetry.py +0 -0
  24. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/client/__init__.py +0 -0
  25. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/client/client.py +0 -0
  26. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/client/input_validator.py +0 -0
  27. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/client/key.py +0 -0
  28. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/client/listener.py +0 -0
  29. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/client/localhost.py +0 -0
  30. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/client/manager.py +0 -0
  31. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/client/util.py +0 -0
  32. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/engine/__init__.py +0 -0
  33. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/engine/cache/__init__.py +0 -0
  34. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/engine/cache/lru.py +0 -0
  35. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/engine/evaluator.py +0 -0
  36. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/engine/filters.py +0 -0
  37. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/engine/hashfns/__init__.py +0 -0
  38. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/engine/hashfns/legacy.py +0 -0
  39. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/engine/hashfns/murmur3py.py +0 -0
  40. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/engine/impressions/__init__.py +0 -0
  41. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/engine/impressions/adapters.py +0 -0
  42. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/engine/impressions/impressions.py +0 -0
  43. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/engine/impressions/manager.py +0 -0
  44. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/engine/impressions/strategies.py +0 -0
  45. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/engine/impressions/unique_keys_tracker.py +0 -0
  46. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/engine/splitters.py +0 -0
  47. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/engine/telemetry.py +0 -0
  48. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/exceptions.py +0 -0
  49. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/factories.py +0 -0
  50. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/impressions.py +0 -0
  51. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/key.py +0 -0
  52. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/__init__.py +0 -0
  53. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/datatypes.py +0 -0
  54. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/events.py +0 -0
  55. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/grammar/__init__.py +0 -0
  56. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/grammar/condition.py +0 -0
  57. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/grammar/matchers/__init__.py +0 -0
  58. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/grammar/matchers/base.py +0 -0
  59. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/grammar/matchers/keys.py +0 -0
  60. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/grammar/matchers/misc.py +0 -0
  61. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/grammar/matchers/numeric.py +0 -0
  62. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/grammar/matchers/semver.py +0 -0
  63. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/grammar/matchers/sets.py +0 -0
  64. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/grammar/matchers/string.py +0 -0
  65. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/grammar/matchers/utils/__init__.py +0 -0
  66. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/grammar/matchers/utils/utils.py +0 -0
  67. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/grammar/partitions.py +0 -0
  68. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/impressions.py +0 -0
  69. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/notification.py +0 -0
  70. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/segments.py +0 -0
  71. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/splits.py +0 -0
  72. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/telemetry.py +0 -0
  73. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/models/token.py +0 -0
  74. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/optional/__init__.py +0 -0
  75. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/push/__init__.py +0 -0
  76. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/push/parser.py +0 -0
  77. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/push/processor.py +0 -0
  78. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/push/sse.py +0 -0
  79. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/push/status_tracker.py +0 -0
  80. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/push/workers.py +0 -0
  81. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/recorder/__init__.py +0 -0
  82. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/recorder/recorder.py +0 -0
  83. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/spec.py +0 -0
  84. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/storage/__init__.py +0 -0
  85. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/storage/adapters/__init__.py +0 -0
  86. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/storage/adapters/cache_trait.py +0 -0
  87. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/storage/adapters/redis.py +0 -0
  88. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/storage/adapters/util.py +0 -0
  89. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/storage/inmemmory.py +0 -0
  90. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/storage/pluggable.py +0 -0
  91. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/storage/redis.py +0 -0
  92. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/sync/__init__.py +0 -0
  93. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/sync/event.py +0 -0
  94. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/sync/impression.py +0 -0
  95. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/sync/manager.py +0 -0
  96. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/sync/segment.py +0 -0
  97. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/sync/split.py +0 -0
  98. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/sync/synchronizer.py +0 -0
  99. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/sync/telemetry.py +0 -0
  100. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/sync/unique_keys.py +0 -0
  101. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/sync/util.py +0 -0
  102. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/tasks/__init__.py +0 -0
  103. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/tasks/events_sync.py +0 -0
  104. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/tasks/impressions_sync.py +0 -0
  105. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/tasks/segment_sync.py +0 -0
  106. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/tasks/split_sync.py +0 -0
  107. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/tasks/telemetry_sync.py +0 -0
  108. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/tasks/unique_keys_sync.py +0 -0
  109. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/tasks/util/__init__.py +0 -0
  110. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/tasks/util/asynctask.py +0 -0
  111. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/tasks/util/workerpool.py +0 -0
  112. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/util/__init__.py +0 -0
  113. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/util/backoff.py +0 -0
  114. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/util/decorators.py +0 -0
  115. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/util/storage_helper.py +0 -0
  116. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/util/threadutil.py +0 -0
  117. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio/util/time.py +0 -0
  118. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio_client.egg-info/SOURCES.txt +0 -0
  119. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio_client.egg-info/dependency_links.txt +0 -0
  120. {splitio_client-10.0.0 → splitio_client-10.1.0}/splitio_client.egg-info/top_level.txt +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: splitio_client
3
- Version: 10.0.0
3
+ Version: 10.1.0
4
4
  Summary: Split.io Python Client
5
5
  Home-page: https://github.com/splitio/python-client
6
6
  Author: Patricio Echague, Sebastian Arrubia
7
7
  Author-email: pato@split.io, sebastian@split.io
8
8
  License: Apache License 2.0
9
- Download-URL: https://github.com/splitio/python-client/tarball/10.0.0
9
+ Download-URL: https://github.com/splitio/python-client/tarball/10.1.0
10
10
  Description: # Split Python SDK
11
11
  ![Build Status](https://github.com/splitio/python-client/actions/workflows/ci.yml/badge.svg?branch=master)
12
12
 
@@ -98,3 +98,4 @@ Provides-Extra: redis
98
98
  Provides-Extra: uwsgi
99
99
  Provides-Extra: cpphash
100
100
  Provides-Extra: asyncio
101
+ Provides-Extra: kerberos
@@ -16,7 +16,8 @@ TESTS_REQUIRES = [
16
16
  'attrs==22.1.0',
17
17
  'pytest-asyncio==0.21.0',
18
18
  'aiohttp>=3.8.4',
19
- 'aiofiles>=23.1.0'
19
+ 'aiofiles>=23.1.0',
20
+ 'requests-kerberos>=0.15.0'
20
21
  ]
21
22
 
22
23
  INSTALL_REQUIRES = [
@@ -46,7 +47,8 @@ setup(
46
47
  'redis': ['redis>=2.10.5'],
47
48
  'uwsgi': ['uwsgi>=2.0.0'],
48
49
  'cpphash': ['mmh3cffi==0.2.1'],
49
- 'asyncio': ['aiohttp>=3.8.4', 'aiofiles>=23.1.0']
50
+ 'asyncio': ['aiohttp>=3.8.4', 'aiofiles>=23.1.0'],
51
+ 'kerberos': ['requests-kerberos>=0.15.0']
50
52
  },
51
53
  setup_requires=['pytest-runner', 'pluggy==1.0.0;python_version<"3.8"'],
52
54
  classifiers=[
@@ -5,7 +5,11 @@ import urllib
5
5
  import abc
6
6
  import logging
7
7
  import json
8
+ import threading
9
+ from urllib3.util import parse_url
8
10
 
11
+ from splitio.optional.loaders import HTTPKerberosAuth, OPTIONAL
12
+ from splitio.client.config import AuthenticateScheme
9
13
  from splitio.optional.loaders import aiohttp
10
14
  from splitio.util.time import get_current_epoch_time_ms
11
15
 
@@ -15,7 +19,7 @@ AUTH_URL = 'https://auth.split.io/api'
15
19
  TELEMETRY_URL = 'https://telemetry.split.io/api'
16
20
 
17
21
  _LOGGER = logging.getLogger(__name__)
18
-
22
+ _EXC_MSG = '{source} library is throwing exceptions'
19
23
 
20
24
  HttpResponse = namedtuple('HttpResponse', ['status_code', 'body', 'headers'])
21
25
 
@@ -67,6 +71,24 @@ class HttpClientException(Exception):
67
71
  """
68
72
  Exception.__init__(self, message)
69
73
 
74
+ class HTTPAdapterWithProxyKerberosAuth(requests.adapters.HTTPAdapter):
75
+ """HTTPAdapter override for Kerberos Proxy auth"""
76
+
77
+ def __init__(self, principal=None, password=None):
78
+ requests.adapters.HTTPAdapter.__init__(self)
79
+ self._principal = principal
80
+ self._password = password
81
+
82
+ def proxy_headers(self, proxy):
83
+ headers = {}
84
+ if self._principal is not None:
85
+ auth = HTTPKerberosAuth(principal=self._principal, password=self._password)
86
+ else:
87
+ auth = HTTPKerberosAuth()
88
+ negotiate_details = auth.generate_request_header(None, parse_url(proxy).host, is_preemptive=True)
89
+ headers['Proxy-Authorization'] = negotiate_details
90
+ return headers
91
+
70
92
  class HttpClientBase(object, metaclass=abc.ABCMeta):
71
93
  """HttpClient wrapper template."""
72
94
 
@@ -91,6 +113,28 @@ class HttpClientBase(object, metaclass=abc.ABCMeta):
91
113
  self._telemetry_runtime_producer = telemetry_runtime_producer
92
114
  self._metric_name = metric_name
93
115
 
116
+ def _get_headers(self, extra_headers, sdk_key):
117
+ headers = _build_basic_headers(sdk_key)
118
+ if extra_headers is not None:
119
+ headers.update(extra_headers)
120
+ return headers
121
+
122
+ def _record_telemetry(self, status_code, elapsed):
123
+ """
124
+ Record Telemetry info
125
+
126
+ :param status_code: http request status code
127
+ :type status_code: int
128
+
129
+ :param elapsed: response time elapsed.
130
+ :type status_code: int
131
+ """
132
+ self._telemetry_runtime_producer.record_sync_latency(self._metric_name, elapsed)
133
+ if 200 <= status_code < 300:
134
+ self._telemetry_runtime_producer.record_successful_sync(self._metric_name, get_current_epoch_time_ms())
135
+ return
136
+
137
+ self._telemetry_runtime_producer.record_sync_error(self._metric_name, status_code)
94
138
 
95
139
  class HttpClient(HttpClientBase):
96
140
  """HttpClient wrapper."""
@@ -110,6 +154,7 @@ class HttpClient(HttpClientBase):
110
154
  :param telemetry_url: Optional alternative telemetry URL.
111
155
  :type telemetry_url: str
112
156
  """
157
+ _LOGGER.debug("Initializing httpclient")
113
158
  self._timeout = timeout/1000 if timeout else None # Convert ms to seconds.
114
159
  self._urls = _construct_urls(sdk_url, events_url, auth_url, telemetry_url)
115
160
 
@@ -131,23 +176,19 @@ class HttpClient(HttpClientBase):
131
176
  :return: Tuple of status_code & response text
132
177
  :rtype: HttpResponse
133
178
  """
134
- headers = _build_basic_headers(sdk_key)
135
- if extra_headers is not None:
136
- headers.update(extra_headers)
137
-
138
179
  start = get_current_epoch_time_ms()
139
180
  try:
140
181
  response = requests.get(
141
182
  _build_url(server, path, self._urls),
142
183
  params=query,
143
- headers=headers,
184
+ headers=self._get_headers(extra_headers, sdk_key),
144
185
  timeout=self._timeout
145
186
  )
146
187
  self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start)
147
188
  return HttpResponse(response.status_code, response.text, response.headers)
148
189
 
149
190
  except Exception as exc: # pylint: disable=broad-except
150
- raise HttpClientException('requests library is throwing exceptions') from exc
191
+ raise HttpClientException(_EXC_MSG.format(source='request')) from exc
151
192
 
152
193
  def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # pylint: disable=too-many-arguments
153
194
  """
@@ -169,42 +210,19 @@ class HttpClient(HttpClientBase):
169
210
  :return: Tuple of status_code & response text
170
211
  :rtype: HttpResponse
171
212
  """
172
- headers = _build_basic_headers(sdk_key)
173
-
174
- if extra_headers is not None:
175
- headers.update(extra_headers)
176
-
177
213
  start = get_current_epoch_time_ms()
178
214
  try:
179
215
  response = requests.post(
180
216
  _build_url(server, path, self._urls),
181
217
  json=body,
182
218
  params=query,
183
- headers=headers,
184
- timeout=self._timeout
219
+ headers=self._get_headers(extra_headers, sdk_key),
220
+ timeout=self._timeout,
185
221
  )
186
222
  self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start)
187
223
  return HttpResponse(response.status_code, response.text, response.headers)
188
-
189
224
  except Exception as exc: # pylint: disable=broad-except
190
- raise HttpClientException('requests library is throwing exceptions') from exc
191
-
192
- def _record_telemetry(self, status_code, elapsed):
193
- """
194
- Record Telemetry info
195
-
196
- :param status_code: http request status code
197
- :type status_code: int
198
-
199
- :param elapsed: response time elapsed.
200
- :type status_code: int
201
- """
202
- self._telemetry_runtime_producer.record_sync_latency(self._metric_name, elapsed)
203
- if 200 <= status_code < 300:
204
- self._telemetry_runtime_producer.record_successful_sync(self._metric_name, get_current_epoch_time_ms())
205
- return
206
- self._telemetry_runtime_producer.record_sync_error(self._metric_name, status_code)
207
-
225
+ raise HttpClientException(_EXC_MSG.format(source='request')) from exc
208
226
 
209
227
  class HttpClientAsync(HttpClientBase):
210
228
  """HttpClientAsync wrapper."""
@@ -243,10 +261,8 @@ class HttpClientAsync(HttpClientBase):
243
261
  :return: Tuple of status_code & response text
244
262
  :rtype: HttpResponse
245
263
  """
246
- headers = _build_basic_headers(apikey)
247
- if extra_headers is not None:
248
- headers.update(extra_headers)
249
264
  start = get_current_epoch_time_ms()
265
+ headers = self._get_headers(extra_headers, apikey)
250
266
  try:
251
267
  url = _build_url(server, path, self._urls)
252
268
  _LOGGER.debug("GET request: %s", url)
@@ -266,7 +282,7 @@ class HttpClientAsync(HttpClientBase):
266
282
  return HttpResponse(response.status, body, response.headers)
267
283
 
268
284
  except aiohttp.ClientError as exc: # pylint: disable=broad-except
269
- raise HttpClientException('aiohttp library is throwing exceptions') from exc
285
+ raise HttpClientException(_EXC_MSG.format(source='aiohttp')) from exc
270
286
 
271
287
  async def post(self, server, path, apikey, body, query=None, extra_headers=None): # pylint: disable=too-many-arguments
272
288
  """
@@ -286,9 +302,7 @@ class HttpClientAsync(HttpClientBase):
286
302
  :return: Tuple of status_code & response text
287
303
  :rtype: HttpResponse
288
304
  """
289
- headers = _build_basic_headers(apikey)
290
- if extra_headers is not None:
291
- headers.update(extra_headers)
305
+ headers = self._get_headers(extra_headers, apikey)
292
306
  start = get_current_epoch_time_ms()
293
307
  try:
294
308
  headers['Accept-Encoding'] = 'gzip'
@@ -312,7 +326,7 @@ class HttpClientAsync(HttpClientBase):
312
326
  return HttpResponse(response.status, body, response.headers)
313
327
 
314
328
  except aiohttp.ClientError as exc: # pylint: disable=broad-except
315
- raise HttpClientException('aiohttp library is throwing exceptions') from exc
329
+ raise HttpClientException(_EXC_MSG.format(source='aiohttp')) from exc
316
330
 
317
331
  async def _record_telemetry(self, status_code, elapsed):
318
332
  """
@@ -333,4 +347,192 @@ class HttpClientAsync(HttpClientBase):
333
347
 
334
348
  async def close_session(self):
335
349
  if not self._session.closed:
336
- await self._session.close()
350
+ await self._session.close()
351
+
352
+ class HttpClientKerberos(HttpClientBase):
353
+ """HttpClient wrapper."""
354
+
355
+ def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, telemetry_url=None, authentication_scheme=None, authentication_params=None):
356
+ """
357
+ Class constructor.
358
+
359
+ :param timeout: How many milliseconds to wait until the server responds.
360
+ :type timeout: int
361
+ :param sdk_url: Optional alternative sdk URL.
362
+ :type sdk_url: str
363
+ :param events_url: Optional alternative events URL.
364
+ :type events_url: str
365
+ :param auth_url: Optional alternative auth URL.
366
+ :type auth_url: str
367
+ :param telemetry_url: Optional alternative telemetry URL.
368
+ :type telemetry_url: str
369
+ :param authentication_scheme: Optional authentication scheme to use.
370
+ :type authentication_scheme: splitio.client.config.AuthenticateScheme
371
+ :param authentication_params: Optional authentication username and password to use.
372
+ :type authentication_params: [str, str]
373
+ """
374
+ _LOGGER.debug("Initializing httpclient for Kerberos auth")
375
+ self._timeout = timeout/1000 if timeout else None # Convert ms to seconds.
376
+ self._urls = _construct_urls(sdk_url, events_url, auth_url, telemetry_url)
377
+ self._authentication_scheme = authentication_scheme
378
+ self._authentication_params = authentication_params
379
+ self._lock = threading.RLock()
380
+ self._sessions = {'sdk': requests.Session(),
381
+ 'events': requests.Session(),
382
+ 'auth': requests.Session(),
383
+ 'telemetry': requests.Session()}
384
+ self._set_authentication()
385
+
386
+ def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: disable=too-many-arguments
387
+ """
388
+ Issue a get request.
389
+ :param server: Whether the request is for SDK server, Events server or Auth server.
390
+ :typee server: str
391
+ :param path: path to append to the host url.
392
+ :type path: str
393
+ :param sdk_key: sdk key.
394
+ :type sdk_key: str
395
+ :param query: Query string passed as dictionary.
396
+ :type query: dict
397
+ :param extra_headers: key/value pairs of possible extra headers.
398
+ :type extra_headers: dict
399
+
400
+ :return: Tuple of status_code & response text
401
+ :rtype: HttpResponse
402
+ """
403
+ with self._lock:
404
+ start = get_current_epoch_time_ms()
405
+ try:
406
+ return self._do_get(server, path, sdk_key, query, extra_headers, start)
407
+
408
+ except requests.exceptions.ProxyError as exc:
409
+ _LOGGER.debug("Proxy Exception caught, resetting the http session")
410
+ self._sessions[server].close()
411
+ self._sessions[server] = requests.Session()
412
+ self._set_authentication(server_name=server)
413
+ try:
414
+ return self._do_get(server, path, sdk_key, query, extra_headers, start)
415
+
416
+ except Exception as exc:
417
+ raise HttpClientException(_EXC_MSG.format(source='request')) from exc
418
+
419
+ except Exception as exc: # pylint: disable=broad-except
420
+ raise HttpClientException(_EXC_MSG.format(source='request')) from exc
421
+
422
+ def _do_get(self, server, path, sdk_key, query, extra_headers, start):
423
+ """
424
+ Issue a get request.
425
+ :param server: Whether the request is for SDK server, Events server or Auth server.
426
+ :typee server: str
427
+ :param path: path to append to the host url.
428
+ :type path: str
429
+ :param sdk_key: sdk key.
430
+ :type sdk_key: str
431
+ :param query: Query string passed as dictionary.
432
+ :type query: dict
433
+ :param extra_headers: key/value pairs of possible extra headers.
434
+ :type extra_headers: dict
435
+
436
+ :return: Tuple of status_code & response text
437
+ :rtype: HttpResponse
438
+ """
439
+ with self._sessions[server].get(
440
+ _build_url(server, path, self._urls),
441
+ headers=self._get_headers(extra_headers, sdk_key),
442
+ params=query,
443
+ timeout=self._timeout
444
+ ) as response:
445
+ self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start)
446
+ return HttpResponse(response.status_code, response.text, response.headers)
447
+
448
+ def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # pylint: disable=too-many-arguments
449
+ """
450
+ Issue a POST request.
451
+
452
+ :param server: Whether the request is for SDK server or Events server.
453
+ :typee server: str
454
+ :param path: path to append to the host url.
455
+ :type path: str
456
+ :param sdk_key: sdk key.
457
+ :type sdk_key: str
458
+ :param body: body sent in the request.
459
+ :type body: str
460
+ :param query: Query string passed as dictionary.
461
+ :type query: dict
462
+ :param extra_headers: key/value pairs of possible extra headers.
463
+ :type extra_headers: dict
464
+
465
+ :return: Tuple of status_code & response text
466
+ :rtype: HttpResponse
467
+ """
468
+ with self._lock:
469
+ start = get_current_epoch_time_ms()
470
+ try:
471
+ return self._do_post(server, path, sdk_key, query, extra_headers, body, start)
472
+
473
+ except requests.exceptions.ProxyError as exc:
474
+ _LOGGER.debug("Proxy Exception caught, resetting the http session")
475
+ self._sessions[server].close()
476
+ self._sessions[server] = requests.Session()
477
+ self._set_authentication(server_name=server)
478
+ try:
479
+ return self._do_post(server, path, sdk_key, query, extra_headers, body, start)
480
+
481
+ except Exception as exc:
482
+ raise HttpClientException(_EXC_MSG.format(source='request')) from exc
483
+
484
+ except Exception as exc: # pylint: disable=broad-except
485
+ raise HttpClientException(_EXC_MSG.format(source='request')) from exc
486
+
487
+ def _do_post(self, server, path, sdk_key, query, extra_headers, body, start):
488
+ """
489
+ Issue a POST request.
490
+
491
+ :param server: Whether the request is for SDK server or Events server.
492
+ :typee server: str
493
+ :param path: path to append to the host url.
494
+ :type path: str
495
+ :param sdk_key: sdk key.
496
+ :type sdk_key: str
497
+ :param body: body sent in the request.
498
+ :type body: str
499
+ :param query: Query string passed as dictionary.
500
+ :type query: dict
501
+ :param extra_headers: key/value pairs of possible extra headers.
502
+ :type extra_headers: dict
503
+
504
+ :return: Tuple of status_code & response text
505
+ :rtype: HttpResponse
506
+ """
507
+ with self._sessions[server].post(
508
+ _build_url(server, path, self._urls),
509
+ params=query,
510
+ headers=self._get_headers(extra_headers, sdk_key),
511
+ json=body,
512
+ timeout=self._timeout,
513
+ ) as response:
514
+ self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start)
515
+ return HttpResponse(response.status_code, response.text, response.headers)
516
+
517
+ def _set_authentication(self, server_name=None):
518
+ """
519
+ Set the authentication for all self._sessions variables based on authentication scheme.
520
+
521
+ :param server: If set, will only add the auth for its session variable, otherwise will set all sessions.
522
+ :typee server: str
523
+ """
524
+ for server in ['sdk', 'events', 'auth', 'telemetry']:
525
+ if server_name is not None and server_name != server:
526
+ continue
527
+ if self._authentication_scheme == AuthenticateScheme.KERBEROS_SPNEGO:
528
+ _LOGGER.debug("Using Kerberos Spnego Authentication")
529
+ if self._authentication_params != [None, None]:
530
+ self._sessions[server].auth = HTTPKerberosAuth(principal=self._authentication_params[0], password=self._authentication_params[1], mutual_authentication=OPTIONAL)
531
+ else:
532
+ self._sessions[server].auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL)
533
+ elif self._authentication_scheme == AuthenticateScheme.KERBEROS_PROXY:
534
+ _LOGGER.debug("Using Kerberos Proxy Authentication")
535
+ if self._authentication_params != [None, None]:
536
+ self._sessions[server].mount('https://', HTTPAdapterWithProxyKerberosAuth(principal=self._authentication_params[0], password=self._authentication_params[1]))
537
+ else:
538
+ self._sessions[server].mount('https://', HTTPAdapterWithProxyKerberosAuth())
@@ -1,6 +1,7 @@
1
1
  """Default settings for the Split.IO SDK Python client."""
2
2
  import os.path
3
3
  import logging
4
+ from enum import Enum
4
5
 
5
6
  from splitio.engine.impressions import ImpressionsMode
6
7
  from splitio.client.input_validator import validate_flag_sets
@@ -8,6 +9,12 @@ from splitio.client.input_validator import validate_flag_sets
8
9
  _LOGGER = logging.getLogger(__name__)
9
10
  DEFAULT_DATA_SAMPLING = 1
10
11
 
12
+ class AuthenticateScheme(Enum):
13
+ """Authentication Scheme."""
14
+ NONE = 'NONE'
15
+ KERBEROS_SPNEGO = 'KERBEROS_SPNEGO'
16
+ KERBEROS_PROXY = 'KERBEROS_PROXY'
17
+
11
18
  DEFAULT_CONFIG = {
12
19
  'operationMode': 'standalone',
13
20
  'connectionTimeout': 1500,
@@ -59,7 +66,10 @@ DEFAULT_CONFIG = {
59
66
  'storageWrapper': None,
60
67
  'storagePrefix': None,
61
68
  'storageType': None,
62
- 'flagSetsFilter': None
69
+ 'flagSetsFilter': None,
70
+ 'httpAuthenticateScheme': AuthenticateScheme.NONE,
71
+ 'kerberosPrincipalUser': None,
72
+ 'kerberosPrincipalPassword': None
63
73
  }
64
74
 
65
75
  def _parse_operation_mode(sdk_key, config):
@@ -148,4 +158,14 @@ def sanitize(sdk_key, config):
148
158
  else:
149
159
  processed['flagSetsFilter'] = sorted(validate_flag_sets(processed['flagSetsFilter'], 'SDK Config')) if processed['flagSetsFilter'] is not None else None
150
160
 
161
+ if config.get('httpAuthenticateScheme') is not None:
162
+ try:
163
+ authenticate_scheme = AuthenticateScheme(config['httpAuthenticateScheme'].upper())
164
+ except (ValueError, AttributeError):
165
+ authenticate_scheme = AuthenticateScheme.NONE
166
+ _LOGGER.warning('You passed an invalid HttpAuthenticationScheme, HttpAuthenticationScheme should be ' \
167
+ 'one of the following values: `none`, `kerberos_proxy` or `kerberos_spnego`. '
168
+ ' Defaulting to `none` mode.')
169
+ processed["httpAuthenticateScheme"] = authenticate_scheme
170
+
151
171
  return processed
@@ -7,8 +7,8 @@ from enum import Enum
7
7
  from splitio.optional.loaders import asyncio
8
8
  from splitio.client.client import Client, ClientAsync
9
9
  from splitio.client import input_validator
10
+ from splitio.client.config import sanitize as sanitize_config, DEFAULT_DATA_SAMPLING, AuthenticateScheme
10
11
  from splitio.client.manager import SplitManager, SplitManagerAsync
11
- from splitio.client.config import sanitize as sanitize_config, DEFAULT_DATA_SAMPLING
12
12
  from splitio.client import util
13
13
  from splitio.client.listener import ImpressionListenerWrapper, ImpressionListenerWrapperAsync
14
14
  from splitio.engine.impressions.impressions import Manager as ImpressionsManager
@@ -33,7 +33,7 @@ from splitio.storage.pluggable import PluggableEventsStorage, PluggableImpressio
33
33
  PluggableImpressionsStorageAsync, PluggableSegmentStorageAsync, PluggableSplitStorageAsync
34
34
 
35
35
  # APIs
36
- from splitio.api.client import HttpClient, HttpClientAsync
36
+ from splitio.api.client import HttpClient, HttpClientAsync, HttpClientKerberos
37
37
  from splitio.api.splits import SplitsAPI, SplitsAPIAsync
38
38
  from splitio.api.segments import SegmentsAPI, SegmentsAPIAsync
39
39
  from splitio.api.impressions import ImpressionsAPI, ImpressionsAPIAsync
@@ -508,13 +508,27 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl
508
508
  telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer()
509
509
  telemetry_init_producer = telemetry_producer.get_telemetry_init_producer()
510
510
 
511
- http_client = HttpClient(
512
- sdk_url=sdk_url,
513
- events_url=events_url,
514
- auth_url=auth_api_base_url,
515
- telemetry_url=telemetry_api_base_url,
516
- timeout=cfg.get('connectionTimeout')
517
- )
511
+ authentication_params = None
512
+ if cfg.get("httpAuthenticateScheme") in [AuthenticateScheme.KERBEROS_SPNEGO, AuthenticateScheme.KERBEROS_PROXY]:
513
+ authentication_params = [cfg.get("kerberosPrincipalUser"),
514
+ cfg.get("kerberosPrincipalPassword")]
515
+ http_client = HttpClientKerberos(
516
+ sdk_url=sdk_url,
517
+ events_url=events_url,
518
+ auth_url=auth_api_base_url,
519
+ telemetry_url=telemetry_api_base_url,
520
+ timeout=cfg.get('connectionTimeout'),
521
+ authentication_scheme = cfg.get("httpAuthenticateScheme"),
522
+ authentication_params = authentication_params
523
+ )
524
+ else:
525
+ http_client = HttpClient(
526
+ sdk_url=sdk_url,
527
+ events_url=events_url,
528
+ auth_url=auth_api_base_url,
529
+ telemetry_url=telemetry_api_base_url,
530
+ timeout=cfg.get('connectionTimeout'),
531
+ )
518
532
 
519
533
  sdk_metadata = util.get_metadata(cfg)
520
534
  apis = {
@@ -14,8 +14,17 @@ except ImportError:
14
14
  asyncio = missing_asyncio_dependencies
15
15
  aiofiles = missing_asyncio_dependencies
16
16
 
17
+ try:
18
+ from requests_kerberos import HTTPKerberosAuth, OPTIONAL
19
+ except ImportError:
20
+ def missing_auth_dependencies(*_, **__):
21
+ """Fail if missing dependencies are used."""
22
+ raise NotImplementedError(
23
+ 'Missing kerberos auth dependency. '
24
+ 'Please use `pip install splitio_client[kerberos]` to install the sdk with kerberos auth support'
25
+ )
26
+ HTTPKerberosAuth = missing_auth_dependencies
27
+ OPTIONAL = missing_auth_dependencies
28
+
17
29
  async def _anext(it):
18
30
  return await it.__anext__()
19
-
20
- if sys.version_info.major < 3 or sys.version_info.minor < 10:
21
- anext = _anext
@@ -2,7 +2,9 @@
2
2
  import logging
3
3
  from threading import Timer
4
4
  import abc
5
- from splitio.optional.loaders import asyncio, anext
5
+ import sys
6
+
7
+ from splitio.optional.loaders import asyncio
6
8
  from splitio.api import APIException
7
9
  from splitio.util.time import get_current_epoch_time_ms
8
10
  from splitio.push import AuthException
@@ -14,6 +16,9 @@ from splitio.push.processor import MessageProcessor, MessageProcessorAsync
14
16
  from splitio.push.status_tracker import PushStatusTracker, Status, PushStatusTrackerAsync
15
17
  from splitio.models.telemetry import StreamingEventTypes
16
18
 
19
+ if sys.version_info.major == 3 and sys.version_info.minor < 10:
20
+ from splitio.optional.loaders import _anext as anext
21
+
17
22
  _TOKEN_REFRESH_GRACE_PERIOD = 10 * 60 # 10 minutes
18
23
 
19
24
  _LOGGER = logging.getLogger(__name__)
@@ -3,11 +3,15 @@ import logging
3
3
  import threading
4
4
  from enum import Enum
5
5
  import abc
6
+ import sys
6
7
 
7
8
  from splitio.push.sse import SSEClient, SSEClientAsync, SSE_EVENT_ERROR
8
9
  from splitio.util.threadutil import EventGroup
9
10
  from splitio.api import headers_from_metadata
10
- from splitio.optional.loaders import anext, asyncio
11
+ from splitio.optional.loaders import asyncio
12
+
13
+ if sys.version_info.major == 3 and sys.version_info.minor < 10:
14
+ from splitio.optional.loaders import _anext as anext
11
15
 
12
16
  _LOGGER = logging.getLogger(__name__)
13
17
 
@@ -0,0 +1 @@
1
+ __version__ = '10.1.0'
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: splitio-client
3
- Version: 10.0.0
3
+ Version: 10.1.0
4
4
  Summary: Split.io Python Client
5
5
  Home-page: https://github.com/splitio/python-client
6
6
  Author: Patricio Echague, Sebastian Arrubia
7
7
  Author-email: pato@split.io, sebastian@split.io
8
8
  License: Apache License 2.0
9
- Download-URL: https://github.com/splitio/python-client/tarball/10.0.0
9
+ Download-URL: https://github.com/splitio/python-client/tarball/10.1.0
10
10
  Description: # Split Python SDK
11
11
  ![Build Status](https://github.com/splitio/python-client/actions/workflows/ci.yml/badge.svg?branch=master)
12
12
 
@@ -98,3 +98,4 @@ Provides-Extra: redis
98
98
  Provides-Extra: uwsgi
99
99
  Provides-Extra: cpphash
100
100
  Provides-Extra: asyncio
101
+ Provides-Extra: kerberos
@@ -13,6 +13,9 @@ aiofiles>=23.1.0
13
13
  [cpphash]
14
14
  mmh3cffi==0.2.1
15
15
 
16
+ [kerberos]
17
+ requests-kerberos>=0.15.0
18
+
16
19
  [redis]
17
20
  redis>=2.10.5
18
21
 
@@ -29,6 +32,7 @@ attrs==22.1.0
29
32
  pytest-asyncio==0.21.0
30
33
  aiohttp>=3.8.4
31
34
  aiofiles>=23.1.0
35
+ requests-kerberos>=0.15.0
32
36
 
33
37
  [uwsgi]
34
38
  uwsgi>=2.0.0
@@ -1 +0,0 @@
1
- __version__ = '10.0.0'