From 43a95e54e5c017c75ce2814b5c056fb94353ca43 Mon Sep 17 00:00:00 2001 From: Martin Redolatti Date: Wed, 30 Oct 2019 11:21:16 -0300 Subject: [PATCH 1/6] remove stack inspection code, forward names manually --- splitio/client/client.py | 181 ++++++++++++++++-------------- splitio/client/input_validator.py | 78 +++++-------- splitio/client/util.py | 21 ---- tests/client/test_client.py | 2 +- 4 files changed, 128 insertions(+), 154 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index f5299eec..1aa2db08 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -19,6 +19,8 @@ class Client(object): # pylint: disable=too-many-instance-attributes _METRIC_GET_TREATMENT = 'sdk.getTreatment' _METRIC_GET_TREATMENTS = 'sdk.getTreatments' + _METRIC_GET_TREATMENT_WITH_CONFIG = 'sdk.getTreatmentWithConfig' + _METRIC_GET_TREATMENTS_WITH_CONFIG = 'sdk.getTreatmentsWithConfig' def __init__(self, factory, labels_enabled=True, impression_listener=None): """ @@ -103,22 +105,7 @@ def _evaluate_if_ready(self, matching_key, bucketing_key, feature, attributes=No attributes ) - def get_treatment_with_config(self, key, feature, attributes=None): - """ - Get the treatment and config for a feature and key, with optional dictionary of attributes. - - This method never raises an exception. If there's a problem, the appropriate log message - will be generated and the method will return the CONTROL treatment. - - :param key: The key for which to get the treatment - :type key: str - :param feature: The name of the feature for which to get the treatment - :type feature: str - :param attributes: An optional dictionary of attributes - :type attributes: dict - :return: The treatment for the key and feature - :rtype: tuple(str, str) - """ + def _make_evaluation(self, key, feature, attributes, method_name, metric_name): try: if self.destroyed: self._logger.error("Client has already been destroyed - no calls possible") @@ -126,16 +113,17 @@ def get_treatment_with_config(self, key, feature, attributes=None): start = int(round(time.time() * 1000)) - matching_key, bucketing_key = input_validator.validate_key(key) + matching_key, bucketing_key = input_validator.validate_key(key, method_name) feature = input_validator.validate_feature_name( feature, self.ready, - self._factory._get_storage('splits') # pylint: disable=protected-access + self._factory._get_storage('splits'), # pylint: disable=protected-access + method_name ) if (matching_key is None and bucketing_key is None) \ or feature is None \ - or not input_validator.validate_attributes(attributes): + or not input_validator.validate_attributes(attributes, method_name): return CONTROL, None result = self._evaluate_if_ready(matching_key, bucketing_key, feature, attributes) @@ -150,7 +138,7 @@ def get_treatment_with_config(self, key, feature, attributes=None): start ) - self._record_stats(impression, start, self._METRIC_GET_TREATMENT) + self._record_stats([impression], start, metric_name) self._send_impression_to_listener(impression, attributes) return result['treatment'], result['configurations'] except Exception: # pylint: disable=broad-except @@ -166,80 +154,29 @@ def get_treatment_with_config(self, key, feature, attributes=None): bucketing_key, start ) - self._record_stats(impression, start, self._METRIC_GET_TREATMENT) + self._record_stats([impression], start, metric_name) self._send_impression_to_listener(impression, attributes) except Exception: # pylint: disable=broad-except self._logger.error('Error reporting impression into get_treatment exception block') self._logger.debug('Error: ', exc_info=True) return CONTROL, None - def get_treatment(self, key, feature, attributes=None): - """ - Get the treatment for a feature and key, with an optional dictionary of attributes. - - This method never raises an exception. If there's a problem, the appropriate log message - will be generated and the method will return the CONTROL treatment. - - :param key: The key for which to get the treatment - :type key: str - :param feature: The name of the feature for which to get the treatment - :type feature: str - :param attributes: An optional dictionary of attributes - :type attributes: dict - :return: The treatment for the key and feature - :rtype: str - """ - treatment, _ = self.get_treatment_with_config(key, feature, attributes) - return treatment - - def _evaluate_features_if_ready(self, matching_key, bucketing_key, features, attributes=None): - if not self.ready: - return { - feature: { - 'treatment': CONTROL, - 'configurations': None, - 'impression': {'label': Label.NOT_READY, 'change_number': None} - } - for feature in features - } - - return self._evaluator.evaluate_features( - features, - matching_key, - bucketing_key, - attributes - ) - - def get_treatments_with_config(self, key, features, attributes=None): - """ - Evaluate multiple features and return a dict with feature -> (treatment, config). - - Get the treatments for a list of features considering a key, with an optional dictionary of - attributes. This method never raises an exception. If there's a problem, the appropriate - log message will be generated and the method will return the CONTROL treatment. - :param key: The key for which to get the treatment - :type key: str - :param features: Array of the names of the features for which to get the treatment - :type feature: list - :param attributes: An optional dictionary of attributes - :type attributes: dict - :return: Dictionary with the result of all the features provided - :rtype: dict - """ + def _make_evaluations(self, key, features, attributes, method_name, metric_name): if self.destroyed: self._logger.error("Client has already been destroyed - no calls possible") - return input_validator.generate_control_treatments(features) + return input_validator.generate_control_treatments(features, method_name) start = int(round(time.time() * 1000)) - matching_key, bucketing_key = input_validator.validate_key(key) + matching_key, bucketing_key = input_validator.validate_key(key, method_name) if matching_key is None and bucketing_key is None: - return input_validator.generate_control_treatments(features) + return input_validator.generate_control_treatments(features, method_name) - if input_validator.validate_attributes(attributes) is False: - return input_validator.generate_control_treatments(features) + if input_validator.validate_attributes(attributes, method_name) is False: + return input_validator.generate_control_treatments(features, method_name) features, missing = input_validator.validate_features_get_treatments( + method_name, features, self.ready, self._factory._get_storage('splits') # pylint: disable=protected-access @@ -290,7 +227,83 @@ def get_treatments_with_config(self, key, features, attributes=None): except Exception: # pylint: disable=broad-except self._logger.error('Error getting treatment for features') self._logger.debug('Error: ', exc_info=True) - return input_validator.generate_control_treatments(list(features)) + return input_validator.generate_control_treatments(list(features), method_name) + + def _evaluate_features_if_ready(self, matching_key, bucketing_key, features, attributes=None): + if not self.ready: + return { + feature: { + 'treatment': CONTROL, + 'configurations': None, + 'impression': {'label': Label.NOT_READY, 'change_number': None} + } + for feature in features + } + + return self._evaluator.evaluate_features( + features, + matching_key, + bucketing_key, + attributes + ) + + def get_treatment_with_config(self, key, feature, attributes=None): + """ + Get the treatment and config for a feature and key, with optional dictionary of attributes. + + This method never raises an exception. If there's a problem, the appropriate log message + will be generated and the method will return the CONTROL treatment. + + :param key: The key for which to get the treatment + :type key: str + :param feature: The name of the feature for which to get the treatment + :type feature: str + :param attributes: An optional dictionary of attributes + :type attributes: dict + :return: The treatment for the key and feature + :rtype: tuple(str, str) + """ + return self._make_evaluation(key, feature, attributes, 'get_treatment_with_config', + self._METRIC_GET_TREATMENT_WITH_CONFIG) + + def get_treatment(self, key, feature, attributes=None): + """ + Get the treatment for a feature and key, with an optional dictionary of attributes. + + This method never raises an exception. If there's a problem, the appropriate log message + will be generated and the method will return the CONTROL treatment. + + :param key: The key for which to get the treatment + :type key: str + :param feature: The name of the feature for which to get the treatment + :type feature: str + :param attributes: An optional dictionary of attributes + :type attributes: dict + :return: The treatment for the key and feature + :rtype: str + """ + treatment, _ = self._make_evaluation(key, feature, attributes, 'get_treatment', + self._METRIC_GET_TREATMENT) + return treatment + + def get_treatments_with_config(self, key, features, attributes=None): + """ + Evaluate multiple features and return a dict with feature -> (treatment, config). + + Get the treatments for a list of features considering a key, with an optional dictionary of + attributes. This method never raises an exception. If there's a problem, the appropriate + log message will be generated and the method will return the CONTROL treatment. + :param key: The key for which to get the treatment + :type key: str + :param features: Array of the names of the features for which to get the treatment + :type feature: list + :param attributes: An optional dictionary of attributes + :type attributes: dict + :return: Dictionary with the result of all the features provided + :rtype: dict + """ + return self._make_evaluations(key, features, attributes, 'get_treatments_with_config', + self._METRIC_GET_TREATMENTS_WITH_CONFIG) def get_treatments(self, key, features, attributes=None): """ @@ -308,7 +321,8 @@ def get_treatments(self, key, features, attributes=None): :return: Dictionary with the result of all the features provided :rtype: dict """ - with_config = self.get_treatments_with_config(key, features, attributes) + with_config = self._make_evaluations(key, features, attributes, 'get_treatments', + self._METRIC_GET_TREATMENTS) return {feature: result[0] for (feature, result) in six.iteritems(with_config)} def _build_impression( # pylint: disable=too-many-arguments @@ -346,10 +360,7 @@ def _record_stats(self, impressions, start, operation): """ try: end = int(round(time.time() * 1000)) - if operation == self._METRIC_GET_TREATMENT: - self._impressions_storage.put([impressions]) - else: - self._impressions_storage.put(impressions) + self._impressions_storage.put(impressions) self._telemetry_storage.inc_latency(operation, get_latency_bucket_index(end - start)) except Exception: # pylint: disable=broad-except self._logger.error('Error recording impressions and metrics') diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index 57f7c982..6a12ce87 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -11,7 +11,6 @@ from splitio.api import APIException from splitio.client.key import Key -from splitio.client.util import get_calls from splitio.engine.evaluator import CONTROL @@ -21,23 +20,6 @@ MAX_PROPERTIES_LENGTH_BYTES = 32768 -def _get_first_split_sdk_call(): - """ - Get the method name of the original call on the SplitClient methods. - - :return: Name of the method called by the user. - :rtype: str - """ - unknown_method = 'unknown-method' - try: - calls = get_calls(['Client', 'SplitManager']) - if calls: - return calls[-1] - return unknown_method - except Exception: # pylint: disable=broad-except - return unknown_method - - def _check_not_null(value, name, operation): """ Check if value is null. @@ -217,7 +199,7 @@ def _remove_empty_spaces(value, operation): return strip_value -def validate_key(key): +def validate_key(key, method_name): """ Validate Key parameter for get_treatment/s. @@ -230,31 +212,30 @@ def validate_key(key): :return: The tuple key :rtype: (matching_key,bucketing_key) """ - operation = _get_first_split_sdk_call() matching_key_result = None bucketing_key_result = None if key is None: - _LOGGER.error('%s: you passed a null key, key must be a non-empty string.', operation) + _LOGGER.error('%s: you passed a null key, key must be a non-empty string.', method_name) return None, None if isinstance(key, Key): - matching_key_result = _check_valid_object_key(key.matching_key, 'matching_key', operation) + matching_key_result = _check_valid_object_key(key.matching_key, 'matching_key', method_name) if matching_key_result is None: return None, None bucketing_key_result = _check_valid_object_key(key.bucketing_key, 'bucketing_key', - operation) + method_name) if bucketing_key_result is None: return None, None else: - key_str = _check_can_convert(key, 'key', operation) + key_str = _check_can_convert(key, 'key', method_name) if key_str is not None and \ - _check_string_not_empty(key_str, 'key', operation) and \ - _check_valid_length(key_str, 'key', operation): + _check_string_not_empty(key_str, 'key', method_name) and \ + _check_valid_length(key_str, 'key', method_name): matching_key_result = key_str return matching_key_result, bucketing_key_result -def validate_feature_name(feature_name, should_validate_existance, split_storage): +def validate_feature_name(feature_name, should_validate_existance, split_storage, method_name): """ Check if feature_name is valid for get_treatment. @@ -263,22 +244,21 @@ def validate_feature_name(feature_name, should_validate_existance, split_storage :return: feature_name :rtype: str|None """ - operation = _get_first_split_sdk_call() - if (not _check_not_null(feature_name, 'feature_name', operation)) or \ - (not _check_is_string(feature_name, 'feature_name', operation)) or \ - (not _check_string_not_empty(feature_name, 'feature_name', operation)): + if (not _check_not_null(feature_name, 'feature_name', method_name)) or \ + (not _check_is_string(feature_name, 'feature_name', method_name)) or \ + (not _check_string_not_empty(feature_name, 'feature_name', method_name)): return None if should_validate_existance and split_storage.get(feature_name) is None: _LOGGER.warning( "%s: you passed \"%s\" that does not exist in this environment, " "please double check what Splits exist in the web console.", - operation, + method_name, feature_name ) return None - return _remove_empty_spaces(feature_name, operation) + return _remove_empty_spaces(feature_name, method_name) def validate_track_key(key): @@ -392,7 +372,12 @@ def validate_manager_feature_name(feature_name, should_validate_existance, split return feature_name -def validate_features_get_treatments(features, should_validate_existance=False, split_storage=None): # pylint: disable=invalid-name +def validate_features_get_treatments( # pylint: disable=invalid-name + method_name, + features, + should_validate_existance=False, + split_storage=None +): """ Check if features is valid for get_treatments. @@ -401,21 +386,21 @@ def validate_features_get_treatments(features, should_validate_existance=False, :return: filtered_features :rtype: tuple """ - operation = _get_first_split_sdk_call() if features is None or not isinstance(features, list): - _LOGGER.error("%s: feature_names must be a non-empty array.", operation) + print(features) + _LOGGER.error("%s: feature_names must be a non-empty array.", method_name) return None, None if not features: - _LOGGER.error("%s: feature_names must be a non-empty array.", operation) + _LOGGER.error("%s: feature_names must be a non-empty array.", method_name) return None, None filtered_features = set( - _remove_empty_spaces(feature, operation) for feature in features + _remove_empty_spaces(feature, method_name) for feature in features if feature is not None and - _check_is_string(feature, 'feature_name', operation) and - _check_string_not_empty(feature, 'feature_name', operation) + _check_is_string(feature, 'feature_name', method_name) and + _check_string_not_empty(feature, 'feature_name', method_name) ) if not filtered_features: - _LOGGER.error("%s: feature_names must be a non-empty array.", operation) + _LOGGER.error("%s: feature_names must be a non-empty array.", method_name) return None, None if not should_validate_existance: @@ -426,13 +411,13 @@ def validate_features_get_treatments(features, should_validate_existance=False, _LOGGER.warning( "%s: you passed \"%s\" that does not exist in this environment, " "please double check what Splits exist in the web console.", - operation, + method_name, missing_feature ) return filtered_features - valid_missing_features, valid_missing_features -def generate_control_treatments(features): +def generate_control_treatments(features, method_name): """ Generate valid features to control. @@ -441,10 +426,10 @@ def generate_control_treatments(features): :return: dict :rtype: dict|None """ - return {feature: (CONTROL, None) for feature in validate_features_get_treatments(features)[0]} + return {feature: (CONTROL, None) for feature in validate_features_get_treatments(method_name, features)[0]} -def validate_attributes(attributes): +def validate_attributes(attributes, method_name): """ Check if attributes is valid. @@ -455,11 +440,10 @@ def validate_attributes(attributes): :return: bool :rtype: True|False """ - operation = _get_first_split_sdk_call() if attributes is None: return True if not isinstance(attributes, dict): - _LOGGER.error('%s: attributes must be of type dictionary.', operation) + _LOGGER.error('%s: attributes must be of type dictionary.', method_name) return False return True diff --git a/splitio/client/util.py b/splitio/client/util.py index 62f5b131..81183a68 100644 --- a/splitio/client/util.py +++ b/splitio/client/util.py @@ -51,24 +51,3 @@ def get_metadata(config): version = 'python-%s' % __version__ ip_address, hostname = _get_hostname_and_ip(config) return SdkMetadata(version, hostname, ip_address) - - -def get_calls(classes_filter=None): - """ - Inspect the stack and retrieve an ordered list of caller functions. - - :param class_filter: If not None, only methods from that classes will be returned. - :type class: list(str) - - :return: list of callers ordered by most recent first. - :rtype: list(tuple(str, str)) - """ - try: - return [ - inspect.getframeinfo(frame[0]).function - for frame in inspect.stack() - if classes_filter is None - or 'self' in frame[0].f_locals and frame[0].f_locals['self'].__class__.__name__ in classes_filter # pylint: disable=line-too-long - ] - except Exception: # pylint: disable=broad-except - return [] diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 9f45df02..2546b27d 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -135,7 +135,7 @@ def _get_storage_mock(name): assert mocker.call( [Impression('some_key', 'some_feature', 'on', 'some_label', 123, None, 1000)] ) in impression_storage.put.mock_calls - assert mocker.call('sdk.getTreatment', 5) in telemetry_storage.inc_latency.mock_calls + assert mocker.call('sdk.getTreatmentWithConfig', 5) in telemetry_storage.inc_latency.mock_calls assert client._logger.mock_calls == [] assert mocker.call( Impression('some_key', 'some_feature', 'on', 'some_label', 123, None, 1000), From 26e3695416fc0a154f2f5ac71a44a16a0b665a40 Mon Sep 17 00:00:00 2001 From: Martin Redolatti Date: Wed, 30 Oct 2019 11:28:00 -0300 Subject: [PATCH 2/6] fix error messages --- splitio/client/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 1aa2db08..894503fb 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -206,8 +206,8 @@ def _make_evaluations(self, key, features, attributes, method_name, metric_name) treatments[feature] = (result['treatment'], result['configurations']) except Exception: # pylint: disable=broad-except - self._logger.error('get_treatments: An exception occured when evaluating ' - 'feature ' + feature + ' returning CONTROL.') + self._logger.error('%s: An exception occured when evaluating ' + 'feature %s returning CONTROL.' % (method_name, feature)) treatments[feature] = CONTROL, None self._logger.debug('Error: ', exc_info=True) continue @@ -219,8 +219,8 @@ def _make_evaluations(self, key, features, attributes, method_name, metric_name) for impression in bulk_impressions: self._send_impression_to_listener(impression, attributes) except Exception: # pylint: disable=broad-except - self._logger.error('get_treatments: An exception when trying to store ' - 'impressions.') + self._logger.error('%s: An exception when trying to store ' + 'impressions.' % method_name) self._logger.debug('Error: ', exc_info=True) return treatments From babbdb271fc70c6d92415eeaecbc96651561c696 Mon Sep 17 00:00:00 2001 From: Martin Redolatti Date: Wed, 30 Oct 2019 11:29:18 -0300 Subject: [PATCH 3/6] remove print statement --- splitio/client/input_validator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index 6a12ce87..214a7f95 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -387,7 +387,6 @@ def validate_features_get_treatments( # pylint: disable=invalid-name :rtype: tuple """ if features is None or not isinstance(features, list): - print(features) _LOGGER.error("%s: feature_names must be a non-empty array.", method_name) return None, None if not features: From 4fee7a056a9e9928fcd403322716c10d2098740b Mon Sep 17 00:00:00 2001 From: Martin Redolatti Date: Wed, 30 Oct 2019 17:52:01 -0300 Subject: [PATCH 4/6] fix tests --- tests/client/test_factory.py | 12 +++++++++--- tests/client/test_input_validator.py | 8 ++++++-- tests/integration/test_client_e2e.py | 2 ++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index c0b27231..0f1cc4a2 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -88,6 +88,7 @@ def _segment_task_init_mock(self, api, storage, split_storage, period, event): factory.block_until_ready() time.sleep(1) # give a chance for the bg thread to set the ready status assert factory.ready + factory.destroy() def test_redis_client_creation(self, mocker): """Test that a client with redis storage is created correctly.""" @@ -165,6 +166,7 @@ def test_redis_client_creation(self, mocker): factory.block_until_ready() time.sleep(1) # give a chance for the bg thread to set the ready status assert factory.ready + factory.destroy() def test_uwsgi_client_creation(self): @@ -182,6 +184,7 @@ def test_uwsgi_client_creation(self): factory.block_until_ready() time.sleep(1) # give a chance for the bg thread to set the ready status assert factory.ready + factory.destroy() def test_destroy(self, mocker): """Test that tasks are shutdown and data is flushed when destroy is called.""" @@ -366,7 +369,7 @@ def _make_factory_with_apikey(apikey, *_, **__): assert _INSTANTIATED_FACTORIES['some_api_key'] == 1 assert factory_module_logger.warning.mock_calls == [] - get_factory('some_api_key') + factory2 = get_factory('some_api_key') assert _INSTANTIATED_FACTORIES['some_api_key'] == 2 assert factory_module_logger.warning.mock_calls == [mocker.call( "factory instantiation: You already have %d %s with this API Key. " @@ -377,7 +380,7 @@ def _make_factory_with_apikey(apikey, *_, **__): )] factory_module_logger.reset_mock() - get_factory('some_api_key') + factory3 = get_factory('some_api_key') assert _INSTANTIATED_FACTORIES['some_api_key'] == 3 assert factory_module_logger.warning.mock_calls == [mocker.call( "factory instantiation: You already have %d %s with this API Key. " @@ -388,7 +391,7 @@ def _make_factory_with_apikey(apikey, *_, **__): )] factory_module_logger.reset_mock() - get_factory('some_other_api_key') + factory4 = get_factory('some_other_api_key') assert _INSTANTIATED_FACTORIES['some_api_key'] == 3 assert _INSTANTIATED_FACTORIES['some_other_api_key'] == 1 assert factory_module_logger.warning.mock_calls == [mocker.call( @@ -403,3 +406,6 @@ def _make_factory_with_apikey(apikey, *_, **__): event.wait() assert _INSTANTIATED_FACTORIES['some_other_api_key'] == 1 assert _INSTANTIATED_FACTORIES['some_api_key'] == 2 + factory2.destroy() + factory3.destroy() + factory4.destroy() diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index a7e629a7..e02961d7 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -1108,9 +1108,13 @@ def test_input_validation_factory(self, mocker): ] logger.reset_mock() - assert get_factory(True, config={'uwsgiClient': True}) is not None + f = get_factory(True, config={'uwsgiClient': True}) + assert f is not None assert logger.error.mock_calls == [] + f.destroy() logger.reset_mock() - assert get_factory(True, config={'redisHost': 'some-host'}) is not None + f = get_factory(True, config={'redisHost': 'some-host'}) + assert f is not None assert logger.error.mock_calls == [] + f.destroy() diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index bf95e3c7..0da42da0 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -2,6 +2,7 @@ #pylint: disable=protected-access,line-too-long,no-self-use import json import os +import time from redis import StrictRedis @@ -577,6 +578,7 @@ def test_localhost_e2e(self): filename = os.path.join(os.path.dirname(__file__), 'files', 'file2.yaml') factory = get_factory('localhost', config={'splitFile': filename}) factory.block_until_ready() + time.sleep(1) client = factory.client() assert client.get_treatment_with_config('key', 'my_feature') == ('on', '{"desc" : "this applies only to ON treatment"}') assert client.get_treatment_with_config('only_key', 'my_feature') == ( From 3c2b13cbf439989ec309a52a706127972c9105ba Mon Sep 17 00:00:00 2001 From: Martin Redolatti Date: Thu, 31 Oct 2019 10:33:59 -0300 Subject: [PATCH 5/6] update changes & version --- CHANGES.txt | 3 +++ splitio/version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 83bad4ce..b00a7e87 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +8.1.5 (Oct 31, 2019) + - Fixed input validation performance issue. + 8.1.4 (Oct 14, 2019) - Added logic to fetch multiple splits at once on get_treatments/get_treatments_with_config. - Added flag `ipAddressesEnabled` into config to enable/disable sending machineName and machineIp when data is posted in headers. diff --git a/splitio/version.py b/splitio/version.py index 309633b1..9f5ce37e 100644 --- a/splitio/version.py +++ b/splitio/version.py @@ -1 +1 @@ -__version__ = '8.1.4' +__version__ = '8.1.5' From fa304f5866298674fd2255ec946a1d70f6b88d58 Mon Sep 17 00:00:00 2001 From: Martin Redolatti Date: Thu, 31 Oct 2019 13:26:44 -0300 Subject: [PATCH 6/6] fix version --- CHANGES.txt | 2 +- splitio/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b00a7e87..2b63f930 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -8.1.5 (Oct 31, 2019) +8.1.6 (Oct 31, 2019) - Fixed input validation performance issue. 8.1.4 (Oct 14, 2019) diff --git a/splitio/version.py b/splitio/version.py index 9f5ce37e..99f4cbb4 100644 --- a/splitio/version.py +++ b/splitio/version.py @@ -1 +1 @@ -__version__ = '8.1.5' +__version__ = '8.1.6'