Skip to content

Commit 5fca0c1

Browse files
committed
moved flagset classes to model, polish in input_validator and storage helper
1 parent 23380af commit 5fca0c1

File tree

17 files changed

+387
-352
lines changed

17 files changed

+387
-352
lines changed

splitio/client/client.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -404,9 +404,9 @@ 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 == []:
409-
_LOGGER.warning("No valid Flag set or no feature flags found for evaluating treatments")
409+
_LOGGER.warning("%s: No valid Flag set or no feature flags found for evaluating treatments" % (method))
410410
return {}
411411

412412
if 'config' in method.value:
@@ -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,10 +428,10 @@ 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)
431+
sanitized_flag_sets = input_validator.validate_flag_sets(flag_sets, method_name)
432432
feature_flags_by_set = self._split_storage.get_feature_flags_by_sets(sanitized_flag_sets)
433433
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))
434+
_LOGGER.warning("%s: Fetching feature flags for flag set %s encountered an error, skipping this flag set." % (method_name, flag_sets))
435435
return []
436436
return feature_flags_by_set
437437

splitio/client/config.py

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
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__)
1011
DEFAULT_DATA_SAMPLING = 1
11-
_FLAG_SETS_REGEX = '^[a-z0-9][_a-z0-9]{0,49}$'
1212

1313
DEFAULT_CONFIG = {
1414
'operationMode': 'standalone',
@@ -119,42 +119,6 @@ def _sanitize_impressions_mode(storage_type, mode, refresh_rate=None):
119119

120120
return mode, refresh_rate
121121

122-
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 sorted(list(sanitized_flag_sets))
157-
158122
def sanitize(sdk_key, config):
159123
"""
160124
Look for inconsistencies or ill-formed configs and tune it accordingly.
@@ -179,6 +143,6 @@ def sanitize(sdk_key, config):
179143
_LOGGER.warning('metricRefreshRate parameter minimum value is 60 seconds, defaulting to 3600 seconds.')
180144
processed['metricsRefreshRate'] = 3600
181145

182-
processed['FlagSetsFilter'] = sanitize_flag_sets(processed['FlagSetsFilter']) if processed['FlagSetsFilter'] is not None else None
146+
processed['FlagSetsFilter'] = validate_flag_sets(processed['FlagSetsFilter'], 'SDK Config') if processed['FlagSetsFilter'] is not None else None
183147

184148
return processed

splitio/client/input_validator.py

Lines changed: 46 additions & 14 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):
@@ -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(
@@ -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,34 @@ 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 re.search(_FLAG_SETS_REGEX, flag_set) is None or re.search(_FLAG_SETS_REGEX, flag_set).group() != flag_set:
595+
_LOGGER.warning("%s: 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)
596+
continue
597+
598+
sanitized_flag_sets.add(flag_set.strip())
599+
600+
return sorted(list(sanitized_flag_sets))

splitio/models/flag_sets.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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+
if self.flag_set_exist(flag_set):
77+
return self.sets_feature_flag_map[flag_set]
78+
79+
def add_flag_set(self, flag_set):
80+
"""
81+
Add new flag set to storage
82+
83+
:param flag_set: set name
84+
:type flag_set: str
85+
"""
86+
with self._lock:
87+
if not self.flag_set_exist(flag_set):
88+
self.sets_feature_flag_map[flag_set] = set()
89+
90+
def remove_flag_set(self, flag_set):
91+
"""
92+
Remove existing flag set from storage
93+
94+
:param flag_set: set name
95+
:type flag_set: str
96+
"""
97+
with self._lock:
98+
if self.flag_set_exist(flag_set):
99+
del self.sets_feature_flag_map[flag_set]
100+
101+
def add_feature_flag_to_flag_set(self, flag_set, feature_flag):
102+
"""
103+
Add a feature flag to existing flag set
104+
105+
:param flag_set: set name
106+
:type flag_set: str
107+
:param feature_flag: feature flag name
108+
:type feature_flag: str
109+
"""
110+
with self._lock:
111+
if self.flag_set_exist(flag_set):
112+
self.sets_feature_flag_map[flag_set].add(feature_flag)
113+
114+
def remove_feature_flag_to_flag_set(self, flag_set, feature_flag):
115+
"""
116+
Remove a feature flag from existing flag set
117+
118+
:param flag_set: set name
119+
:type flag_set: str
120+
:param feature_flag: feature flag name
121+
:type feature_flag: str
122+
"""
123+
with self._lock:
124+
if self.flag_set_exist(flag_set):
125+
self.sets_feature_flag_map[flag_set].remove(feature_flag)

0 commit comments

Comments
 (0)