Skip to content

Commit d16c9ff

Browse files
committed
updated tests
1 parent 6b9b8ac commit d16c9ff

19 files changed

Lines changed: 564 additions & 288 deletions

splitio/client/client.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None):
404404
:return: Dictionary with the result of all the feature flags provided
405405
:rtype: dict
406406
"""
407-
feature_flags_names = self._get_feature_flag_names_by_flag_sets(flag_sets)
407+
feature_flags_names = self._get_feature_flag_names_by_flag_sets(flag_sets, method)
408408
if feature_flags_names == []:
409409
_LOGGER.warning("No valid Flag set or no feature flags found for evaluating treatments")
410410
return {}
@@ -418,7 +418,7 @@ def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None):
418418
return {feature_flag: result[0] for (feature_flag, result) in with_config.items()}
419419

420420

421-
def _get_feature_flag_names_by_flag_sets(self, flag_sets):
421+
def _get_feature_flag_names_by_flag_sets(self, flag_sets, method_name):
422422
"""
423423
Sanitize given flag sets and return list of feature flag names associated with them
424424
@@ -428,17 +428,12 @@ def _get_feature_flag_names_by_flag_sets(self, flag_sets):
428428
:return: list of feature flag names
429429
:rtype: list
430430
"""
431-
sanitized_flag_sets = config.sanitize_flag_sets(flag_sets)
432-
feature_flags = []
433-
for flag_set in sanitized_flag_sets:
434-
feature_flags_by_set = self._split_storage.get_feature_flags_by_sets(flag_set)
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_set))
437-
continue
438-
feature_flags.extend(feature_flags_by_set)
439-
feature_flags_names = []
440-
[feature_flags_names.append(feature_flag) for feature_flag in feature_flags]
441-
return feature_flags_names
431+
sanitized_flag_sets = input_validator.validate_flag_sets(flag_sets, method_name)
432+
feature_flags_by_set = self._split_storage.get_feature_flags_by_sets(sanitized_flag_sets)
433+
if feature_flags_by_set is None:
434+
_LOGGER.warning("Fetching feature flags for flag set %s encountered an error, skipping this flag set." % (flag_sets))
435+
return []
436+
return feature_flags_by_set
442437

443438
def _build_impression( # pylint: disable=too-many-arguments
444439
self,

splitio/client/config.py

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import re
55

66
from splitio.engine.impressions import ImpressionsMode
7+
from splitio.client.input_validator import validate_flag_sets
78

89

910
_LOGGER = logging.getLogger(__name__)
@@ -120,41 +121,6 @@ def _sanitize_impressions_mode(storage_type, mode, refresh_rate=None):
120121
return mode, refresh_rate
121122

122123

123-
def sanitize_flag_sets(flag_sets):
124-
"""
125-
Check supplied flag sets list
126-
127-
:param flag_set: list of flag sets
128-
:type flag_set: list[str]
129-
130-
:returns: Sanitized and sorted flag sets
131-
:rtype: list[str]
132-
"""
133-
if not isinstance(flag_sets, list):
134-
_LOGGER.warning("SDK config: FlagSets config parameters type should be list object, parameter is discarded")
135-
return []
136-
137-
sanitized_flag_sets = set()
138-
for flag_set in flag_sets:
139-
if not isinstance(flag_set, str):
140-
_LOGGER.warning("SDK config: Flag Set name %s should be str object, this flag set is discarded" % (flag_set))
141-
continue
142-
if flag_set != flag_set.strip():
143-
_LOGGER.warning("SDK config: Flag Set name %s has extra whitespace, trimming" % (flag_set))
144-
flag_set = flag_set.strip()
145-
146-
if flag_set != flag_set.lower():
147-
_LOGGER.warning("SDK config: Flag Set name %s should be all lowercase - converting string to lowercase" % (flag_set))
148-
flag_set = flag_set.lower()
149-
150-
if re.search(_FLAG_SETS_REGEX, flag_set) is None or re.search(_FLAG_SETS_REGEX, flag_set).group() != flag_set:
151-
_LOGGER.warning("SDK config: you passed %s, Flag Set must adhere to the regular expressions %s. This means a Flag Set must start with a letter, be in lowercase, alphanumeric and have a max length of 50 characteres. %s was discarded.", flag_set, _FLAG_SETS_REGEX, flag_set)
152-
continue
153-
154-
sanitized_flag_sets.add(flag_set.strip())
155-
156-
return list(sanitized_flag_sets)
157-
158124
def sanitize(sdk_key, config):
159125
"""
160126
Look for inconsistencies or ill-formed configs and tune it accordingly.
@@ -179,6 +145,6 @@ def sanitize(sdk_key, config):
179145
_LOGGER.warning('metricRefreshRate parameter minimum value is 60 seconds, defaulting to 3600 seconds.')
180146
processed['metricsRefreshRate'] = 3600
181147

182-
processed['flagSetsFilter'] = sorted(sanitize_flag_sets(processed['flagSetsFilter'])) if processed['flagSetsFilter'] is not None else None
148+
processed['flagSetsFilter'] = sorted(validate_flag_sets(processed['flagSetsFilter'])) if processed['flagSetsFilter'] is not None else None
183149

184150
return processed

splitio/client/factory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ def _build_redis_factory(api_key, cfg):
440440
cache_enabled = cfg.get('redisLocalCacheEnabled', False)
441441
cache_ttl = cfg.get('redisLocalCacheTTL', 5)
442442
storages = {
443-
'splits': RedisSplitStorage(redis_adapter, cache_enabled, cache_ttl),
443+
'splits': RedisSplitStorage(redis_adapter, cache_enabled, cache_ttl, cfg['flagSetsFilter'] if cfg['flagSetsFilter'] is not None else []),
444444
'segments': RedisSegmentStorage(redis_adapter),
445445
'impressions': RedisImpressionsStorage(redis_adapter, sdk_metadata),
446446
'events': RedisEventsStorage(redis_adapter, sdk_metadata),

splitio/client/input_validator.py

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
MAX_LENGTH = 250
1616
EVENT_TYPE_PATTERN = r'^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$'
1717
MAX_PROPERTIES_LENGTH_BYTES = 32768
18+
_FLAG_SETS_REGEX = '^[a-z0-9][_a-z0-9]{0,49}$'
1819

1920

2021
def _check_not_null(value, name, operation):
@@ -79,7 +80,7 @@ def _check_string_not_empty(value, name, operation):
7980
return True
8081

8182

82-
def _check_string_matches(value, operation, pattern):
83+
def _check_string_matches(value, operation, pattern, name):
8384
"""
8485
Check if value is adhere to a regular expression passed.
8586
@@ -92,14 +93,14 @@ def _check_string_matches(value, operation, pattern):
9293
:return: The result of validation
9394
:rtype: True|False
9495
"""
95-
if not re.match(pattern, value):
96+
if re.search(pattern, value) is None or re.search(pattern, value).group() != value:
9697
_LOGGER.error(
9798
'%s: you passed %s, event_type must ' +
9899
'adhere to the regular expression %s. ' +
99-
'This means an event name must be alphanumeric, cannot be more ' +
100+
'This means %s must be alphanumeric, cannot be more ' +
100101
'than 80 characters long, and can only include a dash, underscore, ' +
101102
'period, or colon as separators of alphanumeric characters.',
102-
operation, value, pattern
103+
operation, value, pattern, name
103104
)
104105
return False
105106
return True
@@ -165,10 +166,7 @@ def _check_valid_object_key(key, name, operation):
165166
:return: The result of validation
166167
:rtype: str|None
167168
"""
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)
169+
if not _check_not_null(key, 'key', operation):
172170
return None
173171
if isinstance(key, str):
174172
if not _check_string_not_empty(key, name, operation):
@@ -179,7 +177,7 @@ def _check_valid_object_key(key, name, operation):
179177
return key_str
180178

181179

182-
def _remove_empty_spaces(value, operation):
180+
def _remove_empty_spaces(value, name, operation):
183181
"""
184182
Check if an string has whitespaces.
185183
@@ -192,10 +190,17 @@ def _remove_empty_spaces(value, operation):
192190
"""
193191
strip_value = value.strip()
194192
if value != strip_value:
195-
_LOGGER.warning("%s: feature flag name '%s' has extra whitespace, trimming.", operation, value)
193+
_LOGGER.warning("%s: %s '%s' has extra whitespace, trimming.", operation, name, value)
196194
return strip_value
197195

198196

197+
def _convert_str_to_lower(value, name, operation):
198+
lower_value = value.lower()
199+
if value != lower_value:
200+
_LOGGER.warning("%s: %s '%s' should be all lowercase - converting string to lowercase" % (operation, name, value))
201+
return lower_value
202+
203+
199204
def validate_key(key, method_name):
200205
"""
201206
Validate Key parameter for get_treatment/s.
@@ -211,8 +216,7 @@ def validate_key(key, method_name):
211216
"""
212217
matching_key_result = None
213218
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)
219+
if not _check_not_null(key, 'key', method_name):
216220
return None, None
217221

218222
if isinstance(key, Key):
@@ -255,7 +259,7 @@ def validate_feature_flag_name(feature_flag_name, should_validate_existance, fea
255259
)
256260
return None
257261

258-
return _remove_empty_spaces(feature_flag_name, method_name)
262+
return _remove_empty_spaces(feature_flag_name, 'feature flag name', method_name)
259263

260264

261265
def validate_track_key(key):
@@ -294,10 +298,7 @@ def validate_traffic_type(traffic_type, should_validate_existance, feature_flag_
294298
(not _check_is_string(traffic_type, 'traffic_type', 'track')) or \
295299
(not _check_string_not_empty(traffic_type, 'traffic_type', 'track')):
296300
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()
301+
traffic_type = _convert_str_to_lower(traffic_type, 'traffic type', 'track')
301302

302303
if should_validate_existance and not feature_flag_storage.is_valid_traffic_type(traffic_type):
303304
_LOGGER.warning(
@@ -322,7 +323,7 @@ def validate_event_type(event_type):
322323
if (not _check_not_null(event_type, 'event_type', 'track')) or \
323324
(not _check_is_string(event_type, 'event_type', 'track')) or \
324325
(not _check_string_not_empty(event_type, 'event_type', 'track')) or \
325-
(not _check_string_matches(event_type, 'track', EVENT_TYPE_PATTERN)):
326+
(not _check_string_matches(event_type, 'track', EVENT_TYPE_PATTERN, 'an event name')):
326327
return None
327328
return event_type
328329

@@ -390,7 +391,7 @@ def validate_feature_flags_get_treatments( # pylint: disable=invalid-name
390391
_LOGGER.error("%s: feature flag names must be a non-empty array.", method_name)
391392
return None, None
392393
filtered_feature_flags = set(
393-
_remove_empty_spaces(feature_flag, method_name) for feature_flag in feature_flags
394+
_remove_empty_spaces(feature_flag, 'feature flag name', method_name) for feature_flag in feature_flags
394395
if feature_flag is not None and
395396
_check_is_string(feature_flag, 'feature flag name', method_name) and
396397
_check_string_not_empty(feature_flag, 'feature flag name', method_name)
@@ -566,3 +567,33 @@ def validate_pluggable_adapter(config):
566567
_LOGGER.error("Pluggable adapter method %s has less than required arguments count: %s : " % (exp_method, len(get_method_args)))
567568
return False
568569
return True
570+
571+
def validate_flag_sets(flag_sets, method_name):
572+
"""
573+
Validate flag sets list
574+
575+
:param flag_set: list of flag sets
576+
:type flag_set: list[str]
577+
578+
:returns: Sanitized and sorted flag sets
579+
:rtype: list[str]
580+
"""
581+
if not isinstance(flag_sets, list):
582+
_LOGGER.warning("%s: flag sets parameter type should be list object, parameter is discarded" % (method_name))
583+
return []
584+
585+
sanitized_flag_sets = set()
586+
for flag_set in flag_sets:
587+
if not _check_not_null(flag_set, 'flag set', method_name):
588+
continue
589+
if not _check_is_string(flag_set, 'flag set', method_name):
590+
continue
591+
flag_set = _remove_empty_spaces(flag_set, 'flag set', method_name)
592+
flag_set = _convert_str_to_lower(flag_set, 'flag set', method_name)
593+
594+
if not _check_string_matches(flag_set, method_name, _FLAG_SETS_REGEX, 'a flag set'):
595+
continue
596+
597+
sanitized_flag_sets.add(flag_set)
598+
599+
return sorted(list(sanitized_flag_sets))

splitio/models/flag_sets.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""Flagsets classes."""
2+
import threading
3+
4+
class FlagSetsFilter(object):
5+
"""Config Flagsets Filter storage."""
6+
7+
def __init__(self, flag_sets=[]):
8+
"""Constructor."""
9+
self.flag_sets = set(flag_sets)
10+
self.should_filter = any(flag_sets)
11+
12+
def set_exist(self, flag_set):
13+
"""
14+
Check if a flagset exist in flagset filter
15+
16+
:param flag_set: set name
17+
:type flag_set: str
18+
19+
:rtype: bool
20+
"""
21+
if not self.should_filter:
22+
return True
23+
if not isinstance(flag_set, str) or flag_set == '':
24+
return False
25+
26+
return any(self.flag_sets.intersection(set([flag_set])))
27+
28+
def intersect(self, flag_sets):
29+
"""
30+
Check if a set exist in config flagset filter
31+
32+
:param flag_set: set of flagsets
33+
:type flag_set: set
34+
35+
:rtype: bool
36+
"""
37+
if not self.should_filter:
38+
return True
39+
if not isinstance(flag_sets, set) or len(flag_sets) == 0:
40+
return False
41+
return any(self.flag_sets.intersection(flag_sets))
42+
43+
44+
class FlagSets(object):
45+
"""InMemory Flagsets storage."""
46+
47+
def __init__(self, flag_sets=[]):
48+
"""Constructor."""
49+
self._lock = threading.RLock()
50+
self.sets_feature_flag_map = {}
51+
for flag_set in flag_sets:
52+
self.sets_feature_flag_map[flag_set] = set()
53+
54+
def flag_set_exist(self, flag_set):
55+
"""
56+
Check if a flagset exist in stored flagset
57+
58+
:param flag_set: set name
59+
:type flag_set: str
60+
61+
:rtype: bool
62+
"""
63+
with self._lock:
64+
return flag_set in self.sets_feature_flag_map.keys()
65+
66+
def get_flag_set(self, flag_set):
67+
"""
68+
fetch feature flags stored in a flag set
69+
70+
:param flag_set: set name
71+
:type flag_set: str
72+
73+
:rtype: list(str)
74+
"""
75+
with self._lock:
76+
return self.sets_feature_flag_map.get(flag_set)
77+
78+
def add_flag_set(self, flag_set):
79+
"""
80+
Add new flag set to storage
81+
82+
:param flag_set: set name
83+
:type flag_set: str
84+
"""
85+
with self._lock:
86+
if not self.flag_set_exist(flag_set):
87+
self.sets_feature_flag_map[flag_set] = set()
88+
89+
def remove_flag_set(self, flag_set):
90+
"""
91+
Remove existing flag set from storage
92+
93+
:param flag_set: set name
94+
:type flag_set: str
95+
"""
96+
with self._lock:
97+
if self.flag_set_exist(flag_set):
98+
del self.sets_feature_flag_map[flag_set]
99+
100+
def add_feature_flag_to_flag_set(self, flag_set, feature_flag):
101+
"""
102+
Add a feature flag to existing flag set
103+
104+
:param flag_set: set name
105+
:type flag_set: str
106+
:param feature_flag: feature flag name
107+
:type feature_flag: str
108+
"""
109+
with self._lock:
110+
if self.flag_set_exist(flag_set):
111+
self.sets_feature_flag_map[flag_set].add(feature_flag)
112+
113+
def remove_feature_flag_to_flag_set(self, flag_set, feature_flag):
114+
"""
115+
Remove a feature flag from existing flag set
116+
117+
:param flag_set: set name
118+
:type flag_set: str
119+
:param feature_flag: feature flag name
120+
:type feature_flag: str
121+
"""
122+
with self._lock:
123+
if self.flag_set_exist(flag_set):
124+
self.sets_feature_flag_map[flag_set].remove(feature_flag)

0 commit comments

Comments
 (0)