splitio-client 9.5.1__tar.gz → 9.6.1__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 (144) hide show
  1. {splitio_client-9.5.1 → splitio_client-9.6.1}/PKG-INFO +3 -3
  2. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/api/commons.py +14 -1
  3. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/api/splits.py +2 -0
  4. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/api/telemetry.py +0 -1
  5. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/client/client.py +133 -5
  6. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/client/config.py +10 -4
  7. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/client/factory.py +21 -11
  8. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/client/input_validator.py +52 -20
  9. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/engine/telemetry.py +30 -12
  10. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/splits.py +18 -5
  11. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/telemetry.py +97 -48
  12. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/token.py +12 -21
  13. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/push/manager.py +2 -1
  14. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/push/splitworker.py +7 -12
  15. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/storage/__init__.py +48 -28
  16. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/storage/adapters/redis.py +8 -0
  17. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/storage/inmemmory.py +175 -20
  18. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/storage/pluggable.py +116 -50
  19. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/storage/redis.py +110 -84
  20. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/sync/split.py +31 -26
  21. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/sync/synchronizer.py +6 -2
  22. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/tasks/util/asynctask.py +2 -2
  23. splitio_client-9.6.1/splitio/util/storage_helper.py +71 -0
  24. splitio_client-9.6.1/splitio/version.py +1 -0
  25. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio_client.egg-info/PKG-INFO +3 -3
  26. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio_client.egg-info/SOURCES.txt +2 -30
  27. splitio_client-9.5.1/splitio/version.py +0 -1
  28. splitio_client-9.5.1/tests/__init__.py +0 -0
  29. splitio_client-9.5.1/tests/helpers/__init__.py +0 -9
  30. splitio_client-9.5.1/tests/helpers/mockserver.py +0 -249
  31. splitio_client-9.5.1/tests/integration/__init__.py +0 -42
  32. splitio_client-9.5.1/tests/integration/test_client_e2e.py +0 -1878
  33. splitio_client-9.5.1/tests/integration/test_pluggable_integration.py +0 -247
  34. splitio_client-9.5.1/tests/integration/test_redis_integration.py +0 -244
  35. splitio_client-9.5.1/tests/integration/test_streaming_e2e.py +0 -1478
  36. splitio_client-9.5.1/tests/push/__init__.py +0 -0
  37. splitio_client-9.5.1/tests/push/test_manager.py +0 -230
  38. splitio_client-9.5.1/tests/push/test_parser.py +0 -116
  39. splitio_client-9.5.1/tests/push/test_processor.py +0 -58
  40. splitio_client-9.5.1/tests/push/test_segment_worker.py +0 -60
  41. splitio_client-9.5.1/tests/push/test_split_worker.py +0 -206
  42. splitio_client-9.5.1/tests/push/test_splitsse.py +0 -126
  43. splitio_client-9.5.1/tests/push/test_sse.py +0 -125
  44. splitio_client-9.5.1/tests/push/test_status_tracker.py +0 -195
  45. splitio_client-9.5.1/tests/recorder/__init__.py +0 -0
  46. splitio_client-9.5.1/tests/recorder/test_recorder.py +0 -79
  47. splitio_client-9.5.1/tests/sync/__init__.py +0 -0
  48. splitio_client-9.5.1/tests/sync/test_events_synchronizer.py +0 -68
  49. splitio_client-9.5.1/tests/sync/test_impressions_count_synchronizer.py +0 -38
  50. splitio_client-9.5.1/tests/sync/test_impressions_synchronizer.py +0 -68
  51. splitio_client-9.5.1/tests/sync/test_manager.py +0 -123
  52. splitio_client-9.5.1/tests/sync/test_segments_synchronizer.py +0 -358
  53. splitio_client-9.5.1/tests/sync/test_splits_synchronizer.py +0 -524
  54. splitio_client-9.5.1/tests/sync/test_synchronizer.py +0 -400
  55. splitio_client-9.5.1/tests/sync/test_telemetry.py +0 -140
  56. splitio_client-9.5.1/tests/sync/test_unique_keys_sync.py +0 -54
  57. {splitio_client-9.5.1 → splitio_client-9.6.1}/README.md +0 -0
  58. {splitio_client-9.5.1 → splitio_client-9.6.1}/setup.cfg +0 -0
  59. {splitio_client-9.5.1 → splitio_client-9.6.1}/setup.py +0 -0
  60. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/__init__.py +0 -0
  61. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/api/__init__.py +0 -0
  62. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/api/auth.py +0 -0
  63. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/api/client.py +0 -0
  64. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/api/events.py +0 -0
  65. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/api/impressions.py +0 -0
  66. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/api/segments.py +0 -0
  67. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/client/__init__.py +0 -0
  68. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/client/key.py +0 -0
  69. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/client/listener.py +0 -0
  70. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/client/localhost.py +0 -0
  71. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/client/manager.py +0 -0
  72. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/client/util.py +0 -0
  73. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/engine/__init__.py +0 -0
  74. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/engine/cache/__init__.py +0 -0
  75. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/engine/cache/lru.py +0 -0
  76. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/engine/evaluator.py +0 -0
  77. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/engine/filters.py +0 -0
  78. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/engine/hashfns/__init__.py +0 -0
  79. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/engine/hashfns/legacy.py +0 -0
  80. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/engine/hashfns/murmur3py.py +0 -0
  81. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/engine/impressions/__init__.py +0 -0
  82. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/engine/impressions/adapters.py +0 -0
  83. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/engine/impressions/impressions.py +0 -0
  84. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/engine/impressions/manager.py +0 -0
  85. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/engine/impressions/strategies.py +0 -0
  86. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/engine/impressions/unique_keys_tracker.py +0 -0
  87. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/engine/splitters.py +0 -0
  88. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/exceptions.py +0 -0
  89. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/factories.py +0 -0
  90. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/impressions.py +0 -0
  91. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/key.py +0 -0
  92. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/__init__.py +0 -0
  93. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/datatypes.py +0 -0
  94. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/events.py +0 -0
  95. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/grammar/__init__.py +0 -0
  96. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/grammar/condition.py +0 -0
  97. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/grammar/matchers/__init__.py +0 -0
  98. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/grammar/matchers/base.py +0 -0
  99. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/grammar/matchers/keys.py +0 -0
  100. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/grammar/matchers/misc.py +0 -0
  101. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/grammar/matchers/numeric.py +0 -0
  102. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/grammar/matchers/sets.py +0 -0
  103. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/grammar/matchers/string.py +0 -0
  104. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/grammar/partitions.py +0 -0
  105. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/impressions.py +0 -0
  106. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/notification.py +0 -0
  107. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/models/segments.py +0 -0
  108. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/push/__init__.py +0 -0
  109. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/push/parser.py +0 -0
  110. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/push/processor.py +0 -0
  111. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/push/segmentworker.py +0 -0
  112. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/push/splitsse.py +0 -0
  113. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/push/sse.py +0 -0
  114. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/push/status_tracker.py +0 -0
  115. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/recorder/__init__.py +0 -0
  116. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/recorder/recorder.py +0 -0
  117. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/storage/adapters/__init__.py +0 -0
  118. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/storage/adapters/cache_trait.py +0 -0
  119. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/storage/adapters/util.py +0 -0
  120. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/sync/__init__.py +0 -0
  121. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/sync/event.py +0 -0
  122. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/sync/impression.py +0 -0
  123. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/sync/manager.py +0 -0
  124. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/sync/segment.py +0 -0
  125. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/sync/telemetry.py +0 -0
  126. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/sync/unique_keys.py +0 -0
  127. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/sync/util.py +0 -0
  128. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/tasks/__init__.py +0 -0
  129. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/tasks/events_sync.py +0 -0
  130. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/tasks/impressions_sync.py +0 -0
  131. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/tasks/segment_sync.py +0 -0
  132. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/tasks/split_sync.py +0 -0
  133. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/tasks/telemetry_sync.py +0 -0
  134. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/tasks/unique_keys_sync.py +0 -0
  135. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/tasks/util/__init__.py +0 -0
  136. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/tasks/util/workerpool.py +0 -0
  137. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/util/__init__.py +0 -0
  138. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/util/backoff.py +0 -0
  139. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/util/decorators.py +0 -0
  140. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/util/threadutil.py +0 -0
  141. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio/util/time.py +0 -0
  142. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio_client.egg-info/dependency_links.txt +0 -0
  143. {splitio_client-9.5.1 → splitio_client-9.6.1}/splitio_client.egg-info/requires.txt +0 -0
  144. {splitio_client-9.5.1 → splitio_client-9.6.1}/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: 9.5.1
3
+ Version: 9.6.1
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/9.5.1
9
+ Download-URL: https://github.com/splitio/python-client/tarball/9.6.1
10
10
  Description: UNKNOWN
11
11
  Platform: UNKNOWN
12
12
  Classifier: Environment :: Console
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python
15
15
  Classifier: Programming Language :: Python :: 2
16
16
  Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Topic :: Software Development :: Libraries
18
+ Provides-Extra: test
18
19
  Provides-Extra: redis
19
20
  Provides-Extra: uwsgi
20
- Provides-Extra: test
21
21
  Provides-Extra: cpphash
@@ -57,7 +57,7 @@ def record_telemetry(status_code, elapsed, metric_name, telemetry_runtime_produc
57
57
  class FetchOptions(object):
58
58
  """Fetch Options object."""
59
59
 
60
- def __init__(self, cache_control_headers=False, change_number=None):
60
+ def __init__(self, cache_control_headers=False, change_number=None, sets=None):
61
61
  """
62
62
  Class constructor.
63
63
 
@@ -66,9 +66,13 @@ class FetchOptions(object):
66
66
 
67
67
  :param change_number: ChangeNumber to use for bypassing CDN in request.
68
68
  :type change_number: int
69
+
70
+ :param sets: list of flag sets
71
+ :type sets: list
69
72
  """
70
73
  self._cache_control_headers = cache_control_headers
71
74
  self._change_number = change_number
75
+ self._sets = sets
72
76
 
73
77
  @property
74
78
  def cache_control_headers(self):
@@ -80,12 +84,19 @@ class FetchOptions(object):
80
84
  """Return change number."""
81
85
  return self._change_number
82
86
 
87
+ @property
88
+ def sets(self):
89
+ """Return sets."""
90
+ return self._sets
91
+
83
92
  def __eq__(self, other):
84
93
  """Match between other options."""
85
94
  if self._cache_control_headers != other._cache_control_headers:
86
95
  return False
87
96
  if self._change_number != other._change_number:
88
97
  return False
98
+ if self._sets != other._sets:
99
+ return False
89
100
  return True
90
101
 
91
102
 
@@ -113,4 +124,6 @@ def build_fetch(change_number, fetch_options, metadata):
113
124
  extra_headers[_CACHE_CONTROL] = _CACHE_CONTROL_NO_CACHE
114
125
  if fetch_options.change_number is not None:
115
126
  query['till'] = fetch_options.change_number
127
+ if fetch_options.sets is not None:
128
+ query['sets'] = fetch_options.sets
116
129
  return query, extra_headers
@@ -59,6 +59,8 @@ class SplitsAPI(object): # pylint: disable=too-few-public-methods
59
59
  if 200 <= response.status_code < 300:
60
60
  return json.loads(response.body)
61
61
  else:
62
+ if response.status_code == 414:
63
+ _LOGGER.error('Error fetching feature flags; the amount of flag sets provided are too big, causing uri length error.')
62
64
  raise APIException(response.body, response.status_code)
63
65
  except HttpClientException as exc:
64
66
  _LOGGER.error('Error fetching feature flags because an exception was raised by the HTTPClient')
@@ -76,7 +76,6 @@ class TelemetryAPI(object): # pylint: disable=too-few-public-methods
76
76
  'Error posting init config because an exception was raised by the HTTPClient'
77
77
  )
78
78
  _LOGGER.debug('Error: ', exc_info=True)
79
- raise APIException('Init config data not flushed properly.') from exc
80
79
 
81
80
  def record_stats(self, stats):
82
81
  """
@@ -6,7 +6,7 @@ from splitio.engine.splitters import Splitter
6
6
  from splitio.models.impressions import Impression, Label
7
7
  from splitio.models.events import Event, EventWrapper
8
8
  from splitio.models.telemetry import get_latency_bucket_index, MethodExceptionsAndLatencies
9
- from splitio.client import input_validator
9
+ from splitio.client import input_validator, config
10
10
  from splitio.util.time import get_current_epoch_time_ms, utctime_ms
11
11
 
12
12
  _LOGGER = logging.getLogger(__name__)
@@ -59,8 +59,9 @@ class Client(object): # pylint: disable=too-many-instance-attributes
59
59
  """Return whether the factory holding this client has been destroyed."""
60
60
  return self._factory.destroyed
61
61
 
62
- def _evaluate_if_ready(self, matching_key, bucketing_key, feature, attributes=None):
62
+ def _evaluate_if_ready(self, matching_key, bucketing_key, feature, method, attributes=None):
63
63
  if not self.ready:
64
+ _LOGGER.warning("%s: The SDK is not ready, results may be incorrect for feature flag %s. Make sure to wait for SDK readiness before using this method", method, feature)
64
65
  self._telemetry_init_producer.record_not_ready_usage()
65
66
  return {
66
67
  'treatment': CONTROL,
@@ -102,7 +103,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
102
103
  or not input_validator.validate_attributes(attributes, method_name):
103
104
  return CONTROL, None
104
105
 
105
- result = self._evaluate_if_ready(matching_key, bucketing_key, feature_flag, attributes)
106
+ result = self._evaluate_if_ready(matching_key, bucketing_key, feature_flag, method_name, attributes)
106
107
 
107
108
  impression = self._build_impression(
108
109
  matching_key,
@@ -167,7 +168,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
167
168
 
168
169
  try:
169
170
  evaluations = self._evaluate_features_if_ready(matching_key, bucketing_key,
170
- list(feature_flags), attributes)
171
+ list(feature_flags), method_name, attributes)
171
172
 
172
173
  for feature_flag in feature_flags:
173
174
  try:
@@ -212,8 +213,9 @@ class Client(object): # pylint: disable=too-many-instance-attributes
212
213
  _LOGGER.debug('Error: ', exc_info=True)
213
214
  return input_validator.generate_control_treatments(list(feature_flags), method_name)
214
215
 
215
- def _evaluate_features_if_ready(self, matching_key, bucketing_key, feature_flags, attributes=None):
216
+ def _evaluate_features_if_ready(self, matching_key, bucketing_key, feature_flags, method, attributes=None):
216
217
  if not self.ready:
218
+ _LOGGER.warning("%s: The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", method, ', '.join([feature for feature in feature_flags]))
217
219
  self._telemetry_init_producer.record_not_ready_usage()
218
220
  return {
219
221
  feature_flag: {
@@ -309,6 +311,132 @@ class Client(object): # pylint: disable=too-many-instance-attributes
309
311
  MethodExceptionsAndLatencies.TREATMENTS)
310
312
  return {feature_flag: result[0] for (feature_flag, result) in with_config.items()}
311
313
 
314
+ def get_treatments_by_flag_set(self, key, flag_set, attributes=None):
315
+ """
316
+ Get treatments for feature flags that contain given flag set.
317
+
318
+ This method never raises an exception. If there's a problem, the appropriate log message
319
+ will be generated and the method will return the CONTROL treatment.
320
+
321
+ :param key: The key for which to get the treatment
322
+ :type key: str
323
+ :param flag_set: flag set
324
+ :type flag_sets: str
325
+ :param attributes: An optional dictionary of attributes
326
+ :type attributes: dict
327
+
328
+ :return: Dictionary with the result of all the feature flags provided
329
+ :rtype: dict
330
+ """
331
+ return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, attributes)
332
+
333
+ def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None):
334
+ """
335
+ Get treatments for feature flags that contain given flag sets.
336
+
337
+ This method never raises an exception. If there's a problem, the appropriate log message
338
+ will be generated and the method will return the CONTROL treatment.
339
+
340
+ :param key: The key for which to get the treatment
341
+ :type key: str
342
+ :param flag_sets: list of flag sets
343
+ :type flag_sets: list
344
+ :param attributes: An optional dictionary of attributes
345
+ :type attributes: dict
346
+
347
+ :return: Dictionary with the result of all the feature flags provided
348
+ :rtype: dict
349
+ """
350
+ return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, attributes)
351
+
352
+ def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None):
353
+ """
354
+ Get treatments for feature flags that contain given flag set.
355
+
356
+ This method never raises an exception. If there's a problem, the appropriate log message
357
+ will be generated and the method will return the CONTROL treatment.
358
+
359
+ :param key: The key for which to get the treatment
360
+ :type key: str
361
+ :param flag_set: flag set
362
+ :type flag_sets: str
363
+ :param attributes: An optional dictionary of attributes
364
+ :type attributes: dict
365
+
366
+ :return: Dictionary with the result of all the feature flags provided
367
+ :rtype: dict
368
+ """
369
+ return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, attributes)
370
+
371
+ def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=None):
372
+ """
373
+ Get treatments for feature flags that contain given flag set.
374
+
375
+ This method never raises an exception. If there's a problem, the appropriate log message
376
+ will be generated and the method will return the CONTROL treatment.
377
+
378
+ :param key: The key for which to get the treatment
379
+ :type key: str
380
+ :param flag_set: flag set
381
+ :type flag_sets: str
382
+ :param attributes: An optional dictionary of attributes
383
+ :type attributes: dict
384
+
385
+ :return: Dictionary with the result of all the feature flags provided
386
+ :rtype: dict
387
+ """
388
+ return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, attributes)
389
+
390
+ def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None):
391
+ """
392
+ Get treatments for feature flags that contain given flag sets.
393
+
394
+ This method never raises an exception. If there's a problem, the appropriate log message
395
+ will be generated and the method will return the CONTROL treatment.
396
+
397
+ :param key: The key for which to get the treatment
398
+ :type key: str
399
+ :param flag_sets: list of flag sets
400
+ :type flag_sets: list
401
+ :param method: Treatment by flag set method flavor
402
+ :type method: splitio.models.telemetry.MethodExceptionsAndLatencies
403
+ :param attributes: An optional dictionary of attributes
404
+ :type attributes: dict
405
+
406
+ :return: Dictionary with the result of all the feature flags provided
407
+ :rtype: dict
408
+ """
409
+ feature_flags_names = self._get_feature_flag_names_by_flag_sets(flag_sets, method.value)
410
+ if feature_flags_names == []:
411
+ _LOGGER.warning("%s: No valid Flag set or no feature flags found for evaluating treatments" % (method.value))
412
+ return {}
413
+
414
+ if 'config' in method.value:
415
+ return self._make_evaluations(key, feature_flags_names, attributes, method.value,
416
+ method)
417
+
418
+ with_config = self._make_evaluations(key, feature_flags_names, attributes, method.value,
419
+ method)
420
+ return {feature_flag: result[0] for (feature_flag, result) in with_config.items()}
421
+
422
+
423
+ def _get_feature_flag_names_by_flag_sets(self, flag_sets, method_name):
424
+ """
425
+ Sanitize given flag sets and return list of feature flag names associated with them
426
+
427
+ :param flag_sets: list of flag sets
428
+ :type flag_sets: list
429
+
430
+ :return: list of feature flag names
431
+ :rtype: list
432
+ """
433
+ sanitized_flag_sets = input_validator.validate_flag_sets(flag_sets, method_name)
434
+ feature_flags_by_set = self._split_storage.get_feature_flags_by_sets(sanitized_flag_sets)
435
+ if feature_flags_by_set is None:
436
+ _LOGGER.warning("Fetching feature flags for flag set %s encountered an error, skipping this flag set." % (flag_sets))
437
+ return []
438
+ return feature_flags_by_set
439
+
312
440
  def _build_impression( # pylint: disable=too-many-arguments
313
441
  self,
314
442
  matching_key,
@@ -3,12 +3,12 @@ import os.path
3
3
  import logging
4
4
 
5
5
  from splitio.engine.impressions import ImpressionsMode
6
+ from splitio.client.input_validator import validate_flag_sets
6
7
 
7
8
 
8
9
  _LOGGER = logging.getLogger(__name__)
9
10
  DEFAULT_DATA_SAMPLING = 1
10
11
 
11
-
12
12
  DEFAULT_CONFIG = {
13
13
  'operationMode': 'standalone',
14
14
  'connectionTimeout': 1500,
@@ -31,6 +31,7 @@ DEFAULT_CONFIG = {
31
31
  'redisHost': 'localhost',
32
32
  'redisPort': 6379,
33
33
  'redisDb': 0,
34
+ 'redisUsername': None,
34
35
  'redisPassword': None,
35
36
  'redisSocketTimeout': None,
36
37
  'redisSocketConnectTimeout': None,
@@ -58,10 +59,10 @@ DEFAULT_CONFIG = {
58
59
  'dataSampling': DEFAULT_DATA_SAMPLING,
59
60
  'storageWrapper': None,
60
61
  'storagePrefix': None,
61
- 'storageType': None
62
+ 'storageType': None,
63
+ 'flagSetsFilter': None
62
64
  }
63
65
 
64
-
65
66
  def _parse_operation_mode(sdk_key, config):
66
67
  """
67
68
  Process incoming config to determine operation mode and storage type
@@ -118,7 +119,6 @@ def _sanitize_impressions_mode(storage_type, mode, refresh_rate=None):
118
119
 
119
120
  return mode, refresh_rate
120
121
 
121
-
122
122
  def sanitize(sdk_key, config):
123
123
  """
124
124
  Look for inconsistencies or ill-formed configs and tune it accordingly.
@@ -143,4 +143,10 @@ def sanitize(sdk_key, config):
143
143
  _LOGGER.warning('metricRefreshRate parameter minimum value is 60 seconds, defaulting to 3600 seconds.')
144
144
  processed['metricsRefreshRate'] = 3600
145
145
 
146
+ if config['operationMode'] == 'consumer' and config.get('flagSetsFilter') is not None:
147
+ processed['flagSetsFilter'] = None
148
+ _LOGGER.warning('config: FlagSets filter is not applicable for Consumer modes where the SDK does keep rollout data in sync. FlagSet filter was discarded.')
149
+ else:
150
+ processed['flagSetsFilter'] = sorted(validate_flag_sets(processed['flagSetsFilter'], 'SDK Config')) if processed['flagSetsFilter'] is not None else None
151
+
146
152
  return processed
@@ -312,7 +312,8 @@ def _wrap_impression_listener(listener, metadata):
312
312
 
313
313
 
314
314
  def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pylint:disable=too-many-arguments,too-many-locals
315
- auth_api_base_url=None, streaming_api_base_url=None, telemetry_api_base_url=None):
315
+ auth_api_base_url=None, streaming_api_base_url=None, telemetry_api_base_url=None,
316
+ total_flag_sets=0, invalid_flag_sets=0):
316
317
  """Build and return a split factory tailored to the supplied config."""
317
318
  if not input_validator.validate_factory_instantiation(api_key):
318
319
  return None
@@ -350,7 +351,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl
350
351
  }
351
352
 
352
353
  storages = {
353
- 'splits': InMemorySplitStorage(),
354
+ 'splits': InMemorySplitStorage(cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []),
354
355
  'segments': InMemorySegmentStorage(),
355
356
  'impressions': InMemoryImpressionStorage(cfg['impressionsQueueSize'], telemetry_runtime_producer),
356
357
  'events': InMemoryEventStorage(cfg['eventsQueueSize'], telemetry_runtime_producer),
@@ -416,7 +417,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl
416
417
  telemetry_evaluation_producer
417
418
  )
418
419
 
419
- telemetry_init_producer.record_config(cfg, extra_cfg)
420
+ telemetry_init_producer.record_config(cfg, extra_cfg, total_flag_sets, invalid_flag_sets)
420
421
 
421
422
  if preforked_initialization:
422
423
  synchronizer.sync_all(max_retry_attempts=_MAX_RETRY_SYNC_ALL)
@@ -440,7 +441,7 @@ def _build_redis_factory(api_key, cfg):
440
441
  cache_enabled = cfg.get('redisLocalCacheEnabled', False)
441
442
  cache_ttl = cfg.get('redisLocalCacheTTL', 5)
442
443
  storages = {
443
- 'splits': RedisSplitStorage(redis_adapter, cache_enabled, cache_ttl),
444
+ 'splits': RedisSplitStorage(redis_adapter, cache_enabled, cache_ttl, []),
444
445
  'segments': RedisSegmentStorage(redis_adapter),
445
446
  'impressions': RedisImpressionsStorage(redis_adapter, sdk_metadata),
446
447
  'events': RedisEventsStorage(redis_adapter, sdk_metadata),
@@ -495,7 +496,7 @@ def _build_redis_factory(api_key, cfg):
495
496
  initialization_thread = threading.Thread(target=manager.start, name="SDKInitializer", daemon=True)
496
497
  initialization_thread.start()
497
498
 
498
- telemetry_init_producer.record_config(cfg, {})
499
+ telemetry_init_producer.record_config(cfg, {}, 0, 0)
499
500
 
500
501
  split_factory = SplitFactory(
501
502
  api_key,
@@ -523,7 +524,7 @@ def _build_pluggable_factory(api_key, cfg):
523
524
  pluggable_adapter = cfg.get('storageWrapper')
524
525
  storage_prefix = cfg.get('storagePrefix')
525
526
  storages = {
526
- 'splits': PluggableSplitStorage(pluggable_adapter, storage_prefix),
527
+ 'splits': PluggableSplitStorage(pluggable_adapter, storage_prefix, []),
527
528
  'segments': PluggableSegmentStorage(pluggable_adapter, storage_prefix),
528
529
  'impressions': PluggableImpressionsStorage(pluggable_adapter, sdk_metadata, storage_prefix),
529
530
  'events': PluggableEventsStorage(pluggable_adapter, sdk_metadata, storage_prefix),
@@ -573,7 +574,7 @@ def _build_pluggable_factory(api_key, cfg):
573
574
  initialization_thread = threading.Thread(target=manager.start, name="SDKInitializer", daemon=True)
574
575
  initialization_thread.start()
575
576
 
576
- telemetry_init_producer.record_config(cfg, {})
577
+ telemetry_init_producer.record_config(cfg, {}, 0, 0)
577
578
 
578
579
  split_factory = SplitFactory(
579
580
  api_key,
@@ -600,7 +601,7 @@ def _build_localhost_factory(cfg):
600
601
  telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer()
601
602
 
602
603
  storages = {
603
- 'splits': InMemorySplitStorage(),
604
+ 'splits': InMemorySplitStorage(cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []),
604
605
  'segments': InMemorySegmentStorage(), # not used, just to avoid possible future errors.
605
606
  'impressions': LocalhostImpressionsStorage(),
606
607
  'events': LocalhostEventsStorage(),
@@ -684,7 +685,14 @@ def get_factory(api_key, **kwargs):
684
685
  _INSTANTIATED_FACTORIES.update([api_key])
685
686
  _INSTANTIATED_FACTORIES_LOCK.release()
686
687
 
687
- config = sanitize_config(api_key, kwargs.get('config', {}))
688
+ config_raw = kwargs.get('config', {})
689
+ total_flag_sets = 0
690
+ invalid_flag_sets = 0
691
+ if config_raw.get('flagSetsFilter') is not None and isinstance(config_raw.get('flagSetsFilter'), list):
692
+ total_flag_sets = len(config_raw.get('flagSetsFilter'))
693
+ invalid_flag_sets = total_flag_sets - len(input_validator.validate_flag_sets(config_raw.get('flagSetsFilter'), 'Telemetry Init'))
694
+
695
+ config = sanitize_config(api_key, config_raw)
688
696
 
689
697
  if config['operationMode'] == 'localhost':
690
698
  split_factory = _build_localhost_factory(config)
@@ -700,7 +708,9 @@ def get_factory(api_key, **kwargs):
700
708
  kwargs.get('events_api_base_url'),
701
709
  kwargs.get('auth_api_base_url'),
702
710
  kwargs.get('streaming_api_base_url'),
703
- kwargs.get('telemetry_api_base_url'))
711
+ kwargs.get('telemetry_api_base_url'),
712
+ total_flag_sets,
713
+ invalid_flag_sets)
704
714
 
705
715
  return split_factory
706
716
 
@@ -712,4 +722,4 @@ def _get_active_and_redundant_count():
712
722
  redundant_factory_count += _INSTANTIATED_FACTORIES[item] - 1
713
723
  active_factory_count += _INSTANTIATED_FACTORIES[item]
714
724
  _INSTANTIATED_FACTORIES_LOCK.release()
715
- return redundant_factory_count, active_factory_count
725
+ return redundant_factory_count, active_factory_count
@@ -15,6 +15,7 @@ _LOGGER = logging.getLogger(__name__)
15
15
  MAX_LENGTH = 250
16
16
  EVENT_TYPE_PATTERN = r'^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$'
17
17
  MAX_PROPERTIES_LENGTH_BYTES = 32768
18
+ _FLAG_SETS_REGEX = '^[a-z0-9][_a-z0-9]{0,49}$'
18
19
 
19
20
 
20
21
  def _check_not_null(value, name, operation):
@@ -79,7 +80,8 @@ def _check_string_not_empty(value, name, operation):
79
80
  return True
80
81
 
81
82
 
82
- def _check_string_matches(value, operation, pattern):
83
+
84
+ def _check_string_matches(value, operation, pattern, name, length):
83
85
  """
84
86
  Check if value is adhere to a regular expression passed.
85
87
 
@@ -92,14 +94,14 @@ def _check_string_matches(value, operation, pattern):
92
94
  :return: The result of validation
93
95
  :rtype: True|False
94
96
  """
95
- if not re.match(pattern, value):
97
+ if re.search(pattern, value) is None or re.search(pattern, value).group() != value:
96
98
  _LOGGER.error(
97
99
  '%s: you passed %s, event_type must ' +
98
100
  'adhere to the regular expression %s. ' +
99
- 'This means an event name must be alphanumeric, cannot be more ' +
100
- 'than 80 characters long, and can only include a dash, underscore, ' +
101
+ 'This means %s must be alphanumeric, cannot be more ' +
102
+ 'than %s characters long, and can only include a dash, underscore, ' +
101
103
  'period, or colon as separators of alphanumeric characters.',
102
- operation, value, pattern
104
+ operation, value, pattern, name, length
103
105
  )
104
106
  return False
105
107
  return True
@@ -165,10 +167,7 @@ def _check_valid_object_key(key, name, operation):
165
167
  :return: The result of validation
166
168
  :rtype: str|None
167
169
  """
168
- if key is None:
169
- _LOGGER.error(
170
- '%s: you passed a null %s, %s must be a non-empty string.',
171
- operation, name, name)
170
+ if not _check_not_null(key, 'key', operation):
172
171
  return None
173
172
  if isinstance(key, str):
174
173
  if not _check_string_not_empty(key, name, operation):
@@ -179,7 +178,7 @@ def _check_valid_object_key(key, name, operation):
179
178
  return key_str
180
179
 
181
180
 
182
- def _remove_empty_spaces(value, operation):
181
+ def _remove_empty_spaces(value, name, operation):
183
182
  """
184
183
  Check if an string has whitespaces.
185
184
 
@@ -192,10 +191,17 @@ def _remove_empty_spaces(value, operation):
192
191
  """
193
192
  strip_value = value.strip()
194
193
  if value != strip_value:
195
- _LOGGER.warning("%s: feature flag name '%s' has extra whitespace, trimming.", operation, value)
194
+ _LOGGER.warning("%s: %s '%s' has extra whitespace, trimming.", operation, name, value)
196
195
  return strip_value
197
196
 
198
197
 
198
+ def _convert_str_to_lower(value, name, operation):
199
+ lower_value = value.lower()
200
+ if value != lower_value:
201
+ _LOGGER.warning("%s: %s '%s' should be all lowercase - converting string to lowercase" % (operation, name, value))
202
+ return lower_value
203
+
204
+
199
205
  def validate_key(key, method_name):
200
206
  """
201
207
  Validate Key parameter for get_treatment/s.
@@ -211,8 +217,7 @@ def validate_key(key, method_name):
211
217
  """
212
218
  matching_key_result = None
213
219
  bucketing_key_result = None
214
- if key is None:
215
- _LOGGER.error('%s: you passed a null key, key must be a non-empty string.', method_name)
220
+ if not _check_not_null(key, 'key', method_name):
216
221
  return None, None
217
222
 
218
223
  if isinstance(key, Key):
@@ -255,7 +260,7 @@ def validate_feature_flag_name(feature_flag_name, should_validate_existance, fea
255
260
  )
256
261
  return None
257
262
 
258
- return _remove_empty_spaces(feature_flag_name, method_name)
263
+ return _remove_empty_spaces(feature_flag_name, 'feature flag name', method_name)
259
264
 
260
265
 
261
266
  def validate_track_key(key):
@@ -294,10 +299,7 @@ def validate_traffic_type(traffic_type, should_validate_existance, feature_flag_
294
299
  (not _check_is_string(traffic_type, 'traffic_type', 'track')) or \
295
300
  (not _check_string_not_empty(traffic_type, 'traffic_type', 'track')):
296
301
  return None
297
- if not traffic_type.islower():
298
- _LOGGER.warning('track: %s should be all lowercase - converting string to lowercase.',
299
- traffic_type)
300
- traffic_type = traffic_type.lower()
302
+ traffic_type = _convert_str_to_lower(traffic_type, 'traffic type', 'track')
301
303
 
302
304
  if should_validate_existance and not feature_flag_storage.is_valid_traffic_type(traffic_type):
303
305
  _LOGGER.warning(
@@ -322,7 +324,7 @@ def validate_event_type(event_type):
322
324
  if (not _check_not_null(event_type, 'event_type', 'track')) or \
323
325
  (not _check_is_string(event_type, 'event_type', 'track')) or \
324
326
  (not _check_string_not_empty(event_type, 'event_type', 'track')) or \
325
- (not _check_string_matches(event_type, 'track', EVENT_TYPE_PATTERN)):
327
+ (not _check_string_matches(event_type, 'track', EVENT_TYPE_PATTERN, 'an event name', 80)):
326
328
  return None
327
329
  return event_type
328
330
 
@@ -390,7 +392,7 @@ def validate_feature_flags_get_treatments( # pylint: disable=invalid-name
390
392
  _LOGGER.error("%s: feature flag names must be a non-empty array.", method_name)
391
393
  return None, None
392
394
  filtered_feature_flags = set(
393
- _remove_empty_spaces(feature_flag, method_name) for feature_flag in feature_flags
395
+ _remove_empty_spaces(feature_flag, 'feature flag name', method_name) for feature_flag in feature_flags
394
396
  if feature_flag is not None and
395
397
  _check_is_string(feature_flag, 'feature flag name', method_name) and
396
398
  _check_string_not_empty(feature_flag, 'feature flag name', method_name)
@@ -566,3 +568,33 @@ def validate_pluggable_adapter(config):
566
568
  _LOGGER.error("Pluggable adapter method %s has less than required arguments count: %s : " % (exp_method, len(get_method_args)))
567
569
  return False
568
570
  return True
571
+
572
+ def validate_flag_sets(flag_sets, method_name):
573
+ """
574
+ Validate flag sets list
575
+
576
+ :param flag_set: list of flag sets
577
+ :type flag_set: list[str]
578
+
579
+ :returns: Sanitized and sorted flag sets
580
+ :rtype: list[str]
581
+ """
582
+ if not isinstance(flag_sets, list):
583
+ _LOGGER.warning("%s: flag sets parameter type should be list object, parameter is discarded" % (method_name))
584
+ return []
585
+
586
+ sanitized_flag_sets = set()
587
+ for flag_set in flag_sets:
588
+ if not _check_not_null(flag_set, 'flag set', method_name):
589
+ continue
590
+ if not _check_is_string(flag_set, 'flag set', method_name):
591
+ continue
592
+ flag_set = _remove_empty_spaces(flag_set, 'flag set', method_name)
593
+ flag_set = _convert_str_to_lower(flag_set, 'flag set', method_name)
594
+
595
+ if not _check_string_matches(flag_set, method_name, _FLAG_SETS_REGEX, 'a flag set', 50):
596
+ continue
597
+
598
+ sanitized_flag_sets.add(flag_set)
599
+
600
+ return list(sanitized_flag_sets)
@@ -36,9 +36,9 @@ class TelemetryInitProducer(object):
36
36
  """Constructor."""
37
37
  self._telemetry_storage = telemetry_storage
38
38
 
39
- def record_config(self, config, extra_config):
39
+ def record_config(self, config, extra_config, total_flag_sets=0, invalid_flag_sets=0):
40
40
  """Record configurations."""
41
- self._telemetry_storage.record_config(config, extra_config)
41
+ self._telemetry_storage.record_config(config, extra_config, total_flag_sets, invalid_flag_sets)
42
42
  current_app, app_worker_id = self._get_app_worker_id()
43
43
  if current_app is not None:
44
44
  self.add_config_tag("initilization:" + current_app)
@@ -48,6 +48,14 @@ class TelemetryInitProducer(object):
48
48
  """Record ready time."""
49
49
  self._telemetry_storage.record_ready_time(ready_time)
50
50
 
51
+ def record_flag_sets(self, flag_sets):
52
+ """Record flag sets."""
53
+ self._telemetry_storage.record_flag_sets(flag_sets)
54
+
55
+ def record_invalid_flag_sets(self, flag_sets):
56
+ """Record invalid flag sets."""
57
+ self._telemetry_storage.record_invalid_flag_sets(flag_sets)
58
+
51
59
  def record_bur_time_out(self):
52
60
  """Record block until ready timeout."""
53
61
  self._telemetry_storage.record_bur_time_out()
@@ -218,17 +226,27 @@ class TelemetryEvaluationConsumer(object):
218
226
  exceptions = self.pop_exceptions()['methodExceptions']
219
227
  latencies = self.pop_latencies()['methodLatencies']
220
228
  return {
221
- 'mE': {'t': exceptions['treatment'],
222
- 'ts': exceptions['treatments'],
223
- 'tc': exceptions['treatment_with_config'],
224
- 'tcs': exceptions['treatments_with_config'],
225
- 'tr': exceptions['track']
229
+ 'mE': {
230
+ 't': exceptions['treatment'],
231
+ 'ts': exceptions['treatments'],
232
+ 'tc': exceptions['treatment_with_config'],
233
+ 'tcs': exceptions['treatments_with_config'],
234
+ 'tf': exceptions['treatments_by_flag_set'],
235
+ 'tfs': exceptions['treatments_by_flag_sets'],
236
+ 'tcf': exceptions['treatments_with_config_by_flag_set'],
237
+ 'tcfs': exceptions['treatments_with_config_by_flag_sets'],
238
+ 'tr': exceptions['track']
226
239
  },
227
- 'mL': {'t': latencies['treatment'],
228
- 'ts': latencies['treatments'],
229
- 'tc': latencies['treatment_with_config'],
230
- 'tcs': latencies['treatments_with_config'],
231
- 'tr': latencies['track']
240
+ 'mL': {
241
+ 't': latencies['treatment'],
242
+ 'ts': latencies['treatments'],
243
+ 'tc': latencies['treatment_with_config'],
244
+ 'tcs': latencies['treatments_with_config'],
245
+ 'tf': latencies['treatments_by_flag_set'],
246
+ 'tfs': latencies['treatments_by_flag_sets'],
247
+ 'tcf': latencies['treatments_with_config_by_flag_set'],
248
+ 'tcfs': latencies['treatments_with_config_by_flag_sets'],
249
+ 'tr': latencies['track']
232
250
  },
233
251
  }
234
252