From 89597f8ac22c4ff61d67ff994a398c3bf6575266 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 20 Aug 2025 10:19:20 -0700 Subject: [PATCH 01/26] Reduce log level for asyncio.CancelledError exceptions --- splitio/push/splitsse.py | 2 +- splitio/push/sse.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/splitio/push/splitsse.py b/splitio/push/splitsse.py index 63e24b40..788648d4 100644 --- a/splitio/push/splitsse.py +++ b/splitio/push/splitsse.py @@ -247,7 +247,7 @@ async def stop(self): try: await self._event_source_ended.wait() except asyncio.CancelledError as e: - _LOGGER.error("Exception waiting for event source ended") + _LOGGER.debug("Exception waiting for event source ended") _LOGGER.debug('stack trace: ', exc_info=True) pass diff --git a/splitio/push/sse.py b/splitio/push/sse.py index 84d73224..8cde7f98 100644 --- a/splitio/push/sse.py +++ b/splitio/push/sse.py @@ -205,7 +205,7 @@ async def shutdown(self): try: await self._done.wait() except asyncio.CancelledError: - _LOGGER.error("Exception waiting for SSE connection to end") + _LOGGER.debug("Exception waiting for SSE connection to end") _LOGGER.debug('stack trace: ', exc_info=True) pass From caacb339c562e00752263dbcd962e31aab27a5ad Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Mon, 25 Aug 2025 14:47:44 -0700 Subject: [PATCH 02/26] Updated Config and input validator --- splitio/client/config.py | 28 ++++++++++++++++++-- splitio/client/input_validator.py | 20 +++++++++++++- splitio/models/fallback_config.py | 37 ++++++++++++++++++++++++++ splitio/models/fallback_treatment.py | 30 +++++++++++++++++++++ tests/client/test_config.py | 34 ++++++++++++++++++++++-- tests/client/test_input_validator.py | 39 +++++++++++++++++++++++++--- tests/models/test_fallback.py | 32 +++++++++++++++++++++++ 7 files changed, 212 insertions(+), 8 deletions(-) create mode 100644 splitio/models/fallback_config.py create mode 100644 splitio/models/fallback_treatment.py create mode 100644 tests/models/test_fallback.py diff --git a/splitio/client/config.py b/splitio/client/config.py index 78d08b45..b31cf989 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -4,7 +4,8 @@ from enum import Enum from splitio.engine.impressions import ImpressionsMode -from splitio.client.input_validator import validate_flag_sets +from splitio.client.input_validator import validate_flag_sets, validate_fallback_treatment, validate_regex_name +from splitio.models.fallback_config import FallbackConfig _LOGGER = logging.getLogger(__name__) DEFAULT_DATA_SAMPLING = 1 @@ -69,7 +70,8 @@ class AuthenticateScheme(Enum): 'flagSetsFilter': None, 'httpAuthenticateScheme': AuthenticateScheme.NONE, 'kerberosPrincipalUser': None, - 'kerberosPrincipalPassword': None + 'kerberosPrincipalPassword': None, + 'fallbackConfig': FallbackConfig(None, None) } def _parse_operation_mode(sdk_key, config): @@ -168,4 +170,26 @@ def sanitize(sdk_key, config): ' Defaulting to `none` mode.') processed["httpAuthenticateScheme"] = authenticate_scheme + if config.get('fallbackConfig') is not None: + if not isinstance(config['fallbackConfig'], FallbackConfig): + _LOGGER.warning('Config: fallbackConfig parameter should be of `FallbackConfig` structure.') + processed['fallbackConfig'] = FallbackConfig(None, None) + return processed + + if config['fallbackConfig'].global_fallback_treatment is not None and not validate_fallback_treatment(config['fallbackConfig'].global_fallback_treatment): + _LOGGER.warning('Config: global fallbacktreatment parameter is discarded.') + processed['fallbackConfig'].global_fallback_treatment = None + return processed + + if config['fallbackConfig'].by_flag_fallback_treatment is not None: + sanitized_flag_fallback_treatments = {} + for feature_name in config['fallbackConfig'].by_flag_fallback_treatment.keys(): + if not validate_regex_name(feature_name) or not validate_fallback_treatment(config['fallbackConfig'].by_flag_fallback_treatment[feature_name]): + _LOGGER.warning('Config: fallback treatment parameter for feature flag %s is discarded.', feature_name) + continue + + sanitized_flag_fallback_treatments[feature_name] = config['fallbackConfig'].by_flag_fallback_treatment[feature_name] + + processed['fallbackConfig'] = FallbackConfig(config['fallbackConfig'].global_fallback_treatment, sanitized_flag_fallback_treatments) + return processed diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index 4a2fb8bc..51b8b0d2 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -15,7 +15,8 @@ EVENT_TYPE_PATTERN = r'^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$' MAX_PROPERTIES_LENGTH_BYTES = 32768 _FLAG_SETS_REGEX = '^[a-z0-9][_a-z0-9]{0,49}$' - +_FALLBACK_TREATMENT_REGEX = '^[a-zA-Z][a-zA-Z0-9-_;]+$' +_FALLBACK_TREATMENT_SIZE = 100 def _check_not_null(value, name, operation): """ @@ -712,3 +713,20 @@ def validate_flag_sets(flag_sets, method_name): sanitized_flag_sets.add(flag_set) return list(sanitized_flag_sets) + +def validate_fallback_treatment(fallback_treatment): + if not validate_regex_name(fallback_treatment.treatment): + _LOGGER.warning("Config: Fallback treatment should match regex %s", _FALLBACK_TREATMENT_REGEX) + return False + + if len(fallback_treatment.treatment) > _FALLBACK_TREATMENT_SIZE: + _LOGGER.warning("Config: Fallback treatment size should not exceed %s characters", _FALLBACK_TREATMENT_SIZE) + return False + + return True + +def validate_regex_name(name): + if re.match(_FALLBACK_TREATMENT_REGEX, name) == None: + return False + + return True \ No newline at end of file diff --git a/splitio/models/fallback_config.py b/splitio/models/fallback_config.py new file mode 100644 index 00000000..3d216291 --- /dev/null +++ b/splitio/models/fallback_config.py @@ -0,0 +1,37 @@ +"""Segment module.""" + +class FallbackConfig(object): + """Segment object class.""" + + def __init__(self, global_fallback_treatment=None, by_flag_fallback_treatment=None): + """ + Class constructor. + + :param global_fallback_treatment: global FallbackTreatment. + :type global_fallback_treatment: FallbackTreatment + + :param by_flag_fallback_treatment: Dict of flags and their fallback treatment + :type by_flag_fallback_treatment: {str: FallbackTreatment} + """ + self._global_fallback_treatment = global_fallback_treatment + self._by_flag_fallback_treatment = by_flag_fallback_treatment + + @property + def global_fallback_treatment(self): + """Return global fallback treatment.""" + return self._global_fallback_treatment + + @global_fallback_treatment.setter + def global_fallback_treatment(self, new_value): + """Return global fallback treatment.""" + self._global_fallback_treatment = new_value + + @property + def by_flag_fallback_treatment(self): + """Return by flag fallback treatment.""" + return self._by_flag_fallback_treatment + + @by_flag_fallback_treatment.setter + def by_flag_fallback_treatment(self, new_value): + """Return global fallback treatment.""" + self.by_flag_fallback_treatment = new_value diff --git a/splitio/models/fallback_treatment.py b/splitio/models/fallback_treatment.py new file mode 100644 index 00000000..39fbf9f3 --- /dev/null +++ b/splitio/models/fallback_treatment.py @@ -0,0 +1,30 @@ +"""Segment module.""" +import json + +class FallbackTreatment(object): + """Segment object class.""" + + def __init__(self, treatment, config=None): + """ + Class constructor. + + :param treatment: treatment. + :type treatment: str + + :param config: config. + :type config: json + """ + self._treatment = treatment + self._config = None + if config != None: + self._config = json.dumps(config) + + @property + def treatment(self): + """Return treatment.""" + return self._treatment + + @property + def config(self): + """Return config.""" + return self._config \ No newline at end of file diff --git a/tests/client/test_config.py b/tests/client/test_config.py index 028736b3..5828eefb 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -3,7 +3,8 @@ import pytest from splitio.client import config from splitio.engine.impressions.impressions import ImpressionsMode - +from splitio.models.fallback_treatment import FallbackTreatment +from splitio.models.fallback_config import FallbackConfig class ConfigSanitizationTests(object): """Inmemory storage-based integration tests.""" @@ -62,8 +63,10 @@ def test_sanitize_imp_mode(self): assert mode == ImpressionsMode.DEBUG assert rate == 60 - def test_sanitize(self): + def test_sanitize(self, mocker): """Test sanitization.""" + _logger = mocker.Mock() + mocker.patch('splitio.client.config._LOGGER', new=_logger) configs = {} processed = config.sanitize('some', configs) assert processed['redisLocalCacheEnabled'] # check default is True @@ -87,3 +90,30 @@ def test_sanitize(self): processed = config.sanitize('some', {'httpAuthenticateScheme': 'NONE'}) assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.NONE + + _logger.reset_mock() + processed = config.sanitize('some', {'fallbackConfig': 'NONE'}) + assert processed['fallbackConfig'].global_fallback_treatment == None + assert processed['fallbackConfig'].by_flag_fallback_treatment == None + assert _logger.warning.mock_calls[1] == mocker.call("Config: fallbackConfig parameter should be of `FallbackConfig` structure.") + + _logger.reset_mock() + processed = config.sanitize('some', {'fallbackConfig': FallbackConfig(FallbackTreatment("123"))}) + assert processed['fallbackConfig'].global_fallback_treatment == None + assert _logger.warning.mock_calls[1] == mocker.call("Config: global fallbacktreatment parameter is discarded.") + + fb = FallbackConfig(FallbackTreatment('on')) + processed = config.sanitize('some', {'fallbackConfig': fb}) + assert processed['fallbackConfig'].global_fallback_treatment.treatment == fb.global_fallback_treatment.treatment + + fb = FallbackConfig(FallbackTreatment('on'), {"flag": FallbackTreatment("off")}) + processed = config.sanitize('some', {'fallbackConfig': fb}) + assert processed['fallbackConfig'].global_fallback_treatment.treatment == fb.global_fallback_treatment.treatment + assert processed['fallbackConfig'].by_flag_fallback_treatment["flag"] == fb.by_flag_fallback_treatment["flag"] + + _logger.reset_mock() + fb = FallbackConfig(None, {"flag#%": FallbackTreatment("off"), "flag2": FallbackTreatment("on")}) + processed = config.sanitize('some', {'fallbackConfig': fb}) + assert len(processed['fallbackConfig'].by_flag_fallback_treatment) == 1 + assert processed['fallbackConfig'].by_flag_fallback_treatment.get("flag2") == fb.by_flag_fallback_treatment["flag2"] + assert _logger.warning.mock_calls[1] == mocker.call('Config: fallback treatment parameter for feature flag %s is discarded.', 'flag#%') \ No newline at end of file diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index a5a1c91a..476db45e 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -1,10 +1,8 @@ """Unit tests for the input_validator module.""" -import logging import pytest from splitio.client.factory import SplitFactory, get_factory, SplitFactoryAsync, get_factory_async from splitio.client.client import CONTROL, Client, _LOGGER as _logger, ClientAsync -from splitio.client.manager import SplitManager, SplitManagerAsync from splitio.client.key import Key from splitio.storage import SplitStorage, EventStorage, ImpressionStorage, SegmentStorage, RuleBasedSegmentsStorage from splitio.storage.inmemmory import InMemoryTelemetryStorage, InMemoryTelemetryStorageAsync, \ @@ -14,7 +12,7 @@ from splitio.recorder.recorder import StandardRecorder, StandardRecorderAsync from splitio.engine.telemetry import TelemetryStorageProducer, TelemetryStorageProducerAsync from splitio.engine.impressions.impressions import Manager as ImpressionManager -from splitio.engine.evaluator import EvaluationDataFactory +from splitio.models.fallback_treatment import FallbackTreatment class ClientInputValidationTests(object): """Input validation test cases.""" @@ -1627,7 +1625,42 @@ def test_flag_sets_validation(self): flag_sets = input_validator.validate_flag_sets([12, 33], 'method') assert flag_sets == [] + def test_fallback_treatments(self, mocker): + _logger = mocker.Mock() + mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) + assert input_validator.validate_fallback_treatment(FallbackTreatment("on", {"prop":"val"})) + assert input_validator.validate_fallback_treatment(FallbackTreatment("on")) + + _logger.reset_mock() + assert not input_validator.validate_fallback_treatment(FallbackTreatment("on" * 100)) + assert _logger.warning.mock_calls == [ + mocker.call("Config: Fallback treatment size should not exceed %s characters", 100) + ] + + assert input_validator.validate_fallback_treatment(FallbackTreatment("on", {"prop" * 500:"val" * 500})) + + _logger.reset_mock() + assert not input_validator.validate_fallback_treatment(FallbackTreatment("on/c")) + assert _logger.warning.mock_calls == [ + mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$") + ] + + _logger.reset_mock() + assert not input_validator.validate_fallback_treatment(FallbackTreatment("9on")) + assert _logger.warning.mock_calls == [ + mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$") + ] + + _logger.reset_mock() + assert not input_validator.validate_fallback_treatment(FallbackTreatment("on$as")) + assert _logger.warning.mock_calls == [ + mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$") + ] + + assert input_validator.validate_fallback_treatment(FallbackTreatment("on_c")) + assert input_validator.validate_fallback_treatment(FallbackTreatment("on_45-c")) + class ClientInputValidationAsyncTests(object): """Input validation test cases.""" diff --git a/tests/models/test_fallback.py b/tests/models/test_fallback.py new file mode 100644 index 00000000..b326fd6f --- /dev/null +++ b/tests/models/test_fallback.py @@ -0,0 +1,32 @@ +from splitio.models.fallback_treatment import FallbackTreatment +from splitio.models.fallback_config import FallbackConfig + +class FallbackTreatmentModelTests(object): + """Fallback treatment model tests.""" + + def test_working(self): + fallback_treatment = FallbackTreatment("on", {"prop": "val"}) + assert fallback_treatment.config == '{"prop": "val"}' + assert fallback_treatment.treatment == 'on' + + fallback_treatment = FallbackTreatment("off") + assert fallback_treatment.config == None + assert fallback_treatment.treatment == 'off' + +class FallbackConfigModelTests(object): + """Fallback treatment model tests.""" + + def test_working(self): + global_fb = FallbackTreatment("on") + flag_fb = FallbackTreatment("off") + fallback_config = FallbackConfig(global_fb, {"flag1": flag_fb}) + assert fallback_config.global_fallback_treatment == global_fb + assert fallback_config.by_flag_fallback_treatment == {"flag1": flag_fb} + + fallback_config.global_fallback_treatment = None + assert fallback_config.global_fallback_treatment == None + + fallback_config.by_flag_fallback_treatment["flag2"] = flag_fb + assert fallback_config.by_flag_fallback_treatment == {"flag1": flag_fb, "flag2": flag_fb} + + \ No newline at end of file From d9bbce485fb84e376faa57cde473d37d1b87d4e2 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 26 Aug 2025 10:21:14 -0700 Subject: [PATCH 03/26] Added FallbackTreatmentsConfiguration class --- splitio/client/config.py | 57 ++++++++++++++++++------------- splitio/models/fallback_config.py | 31 +++++++++++++++-- tests/client/test_config.py | 40 ++++++++++++---------- 3 files changed, 84 insertions(+), 44 deletions(-) diff --git a/splitio/client/config.py b/splitio/client/config.py index b31cf989..0d77678e 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -5,7 +5,7 @@ from splitio.engine.impressions import ImpressionsMode from splitio.client.input_validator import validate_flag_sets, validate_fallback_treatment, validate_regex_name -from splitio.models.fallback_config import FallbackConfig +from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration _LOGGER = logging.getLogger(__name__) DEFAULT_DATA_SAMPLING = 1 @@ -71,7 +71,7 @@ class AuthenticateScheme(Enum): 'httpAuthenticateScheme': AuthenticateScheme.NONE, 'kerberosPrincipalUser': None, 'kerberosPrincipalPassword': None, - 'fallbackConfig': FallbackConfig(None, None) + 'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(None) } def _parse_operation_mode(sdk_key, config): @@ -170,26 +170,37 @@ def sanitize(sdk_key, config): ' Defaulting to `none` mode.') processed["httpAuthenticateScheme"] = authenticate_scheme - if config.get('fallbackConfig') is not None: - if not isinstance(config['fallbackConfig'], FallbackConfig): - _LOGGER.warning('Config: fallbackConfig parameter should be of `FallbackConfig` structure.') - processed['fallbackConfig'] = FallbackConfig(None, None) - return processed - - if config['fallbackConfig'].global_fallback_treatment is not None and not validate_fallback_treatment(config['fallbackConfig'].global_fallback_treatment): - _LOGGER.warning('Config: global fallbacktreatment parameter is discarded.') - processed['fallbackConfig'].global_fallback_treatment = None - return processed - - if config['fallbackConfig'].by_flag_fallback_treatment is not None: - sanitized_flag_fallback_treatments = {} - for feature_name in config['fallbackConfig'].by_flag_fallback_treatment.keys(): - if not validate_regex_name(feature_name) or not validate_fallback_treatment(config['fallbackConfig'].by_flag_fallback_treatment[feature_name]): - _LOGGER.warning('Config: fallback treatment parameter for feature flag %s is discarded.', feature_name) - continue - - sanitized_flag_fallback_treatments[feature_name] = config['fallbackConfig'].by_flag_fallback_treatment[feature_name] - - processed['fallbackConfig'] = FallbackConfig(config['fallbackConfig'].global_fallback_treatment, sanitized_flag_fallback_treatments) + processed = _sanitize_fallback_config(config, processed) return processed + +def _sanitize_fallback_config(config, processed): + if config.get('fallbackTreatmentsConfiguration') is not None: + if not isinstance(config['fallbackTreatmentsConfiguration'], FallbackTreatmentsConfiguration): + _LOGGER.warning('Config: fallbackTreatmentsConfiguration parameter should be of `FallbackTreatmentsConfiguration` class.') + processed['fallbackTreatmentsConfiguration'] = FallbackTreatmentsConfiguration(None) + return processed + + if config['fallbackTreatmentsConfiguration'].fallback_config != None: + if not isinstance(config['fallbackTreatmentsConfiguration'].fallback_config, FallbackConfig): + _LOGGER.warning('Config: fallback_config parameter should be of `FallbackConfig` class.') + processed['fallbackTreatmentsConfiguration'].fallback_config = FallbackConfig(None, None) + return processed + + if config['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment is not None and not validate_fallback_treatment(config['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment): + _LOGGER.warning('Config: global fallbacktreatment parameter is discarded.') + processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment = None + return processed + + if config['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment is not None: + sanitized_flag_fallback_treatments = {} + for feature_name in config['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment.keys(): + if not validate_regex_name(feature_name) or not validate_fallback_treatment(config['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment[feature_name]): + _LOGGER.warning('Config: fallback treatment parameter for feature flag %s is discarded.', feature_name) + continue + + sanitized_flag_fallback_treatments[feature_name] = config['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment[feature_name] + + processed['fallbackTreatmentsConfiguration'].fallback_config = FallbackConfig(config['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment, sanitized_flag_fallback_treatments) + + return processed \ No newline at end of file diff --git a/splitio/models/fallback_config.py b/splitio/models/fallback_config.py index 3d216291..6e84d62f 100644 --- a/splitio/models/fallback_config.py +++ b/splitio/models/fallback_config.py @@ -1,7 +1,32 @@ """Segment module.""" +class FallbackTreatmentsConfiguration(object): + """FallbackConfiguration object class.""" + + def __init__(self, fallback_config): + """ + Class constructor. + + :param fallback_config: fallback config object. + :type fallback_config: FallbackConfig + + :param by_flag_fallback_treatment: Dict of flags and their fallback treatment + :type by_flag_fallback_treatment: {str: FallbackTreatment} + """ + self._fallback_config = fallback_config + + @property + def fallback_config(self): + """Return fallback config.""" + return self._fallback_config + + @fallback_config.setter + def fallback_config(self, new_value): + """Set fallback config.""" + self._fallback_config = new_value + class FallbackConfig(object): - """Segment object class.""" + """FallbackConfig object class.""" def __init__(self, global_fallback_treatment=None, by_flag_fallback_treatment=None): """ @@ -23,7 +48,7 @@ def global_fallback_treatment(self): @global_fallback_treatment.setter def global_fallback_treatment(self, new_value): - """Return global fallback treatment.""" + """Set global fallback treatment.""" self._global_fallback_treatment = new_value @property @@ -33,5 +58,5 @@ def by_flag_fallback_treatment(self): @by_flag_fallback_treatment.setter def by_flag_fallback_treatment(self, new_value): - """Return global fallback treatment.""" + """Set global fallback treatment.""" self.by_flag_fallback_treatment = new_value diff --git a/tests/client/test_config.py b/tests/client/test_config.py index 5828eefb..cbe8ffcd 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -4,7 +4,7 @@ from splitio.client import config from splitio.engine.impressions.impressions import ImpressionsMode from splitio.models.fallback_treatment import FallbackTreatment -from splitio.models.fallback_config import FallbackConfig +from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration class ConfigSanitizationTests(object): """Inmemory storage-based integration tests.""" @@ -92,28 +92,32 @@ def test_sanitize(self, mocker): assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.NONE _logger.reset_mock() - processed = config.sanitize('some', {'fallbackConfig': 'NONE'}) - assert processed['fallbackConfig'].global_fallback_treatment == None - assert processed['fallbackConfig'].by_flag_fallback_treatment == None - assert _logger.warning.mock_calls[1] == mocker.call("Config: fallbackConfig parameter should be of `FallbackConfig` structure.") + processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': 'NONE'}) + assert processed['fallbackTreatmentsConfiguration'].fallback_config == None + assert _logger.warning.mock_calls[1] == mocker.call("Config: fallbackTreatmentsConfiguration parameter should be of `FallbackTreatmentsConfiguration` class.") _logger.reset_mock() - processed = config.sanitize('some', {'fallbackConfig': FallbackConfig(FallbackTreatment("123"))}) - assert processed['fallbackConfig'].global_fallback_treatment == None + processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(123)}) + assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment == None + assert _logger.warning.mock_calls[1] == mocker.call("Config: fallback_config parameter should be of `FallbackConfig` class.") + + _logger.reset_mock() + processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("123")))}) + assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment == None assert _logger.warning.mock_calls[1] == mocker.call("Config: global fallbacktreatment parameter is discarded.") - fb = FallbackConfig(FallbackTreatment('on')) - processed = config.sanitize('some', {'fallbackConfig': fb}) - assert processed['fallbackConfig'].global_fallback_treatment.treatment == fb.global_fallback_treatment.treatment + fb = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment('on'))) + processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': fb}) + assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment.treatment == fb.fallback_config.global_fallback_treatment.treatment - fb = FallbackConfig(FallbackTreatment('on'), {"flag": FallbackTreatment("off")}) - processed = config.sanitize('some', {'fallbackConfig': fb}) - assert processed['fallbackConfig'].global_fallback_treatment.treatment == fb.global_fallback_treatment.treatment - assert processed['fallbackConfig'].by_flag_fallback_treatment["flag"] == fb.by_flag_fallback_treatment["flag"] + fb = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment('on'), {"flag": FallbackTreatment("off")})) + processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': fb}) + assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment.treatment == fb.fallback_config.global_fallback_treatment.treatment + assert processed['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment["flag"] == fb.fallback_config.by_flag_fallback_treatment["flag"] _logger.reset_mock() - fb = FallbackConfig(None, {"flag#%": FallbackTreatment("off"), "flag2": FallbackTreatment("on")}) - processed = config.sanitize('some', {'fallbackConfig': fb}) - assert len(processed['fallbackConfig'].by_flag_fallback_treatment) == 1 - assert processed['fallbackConfig'].by_flag_fallback_treatment.get("flag2") == fb.by_flag_fallback_treatment["flag2"] + fb = FallbackTreatmentsConfiguration(FallbackConfig(None, {"flag#%": FallbackTreatment("off"), "flag2": FallbackTreatment("on")})) + processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': fb}) + assert len(processed['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment) == 1 + assert processed['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment.get("flag2") == fb.fallback_config.by_flag_fallback_treatment["flag2"] assert _logger.warning.mock_calls[1] == mocker.call('Config: fallback treatment parameter for feature flag %s is discarded.', 'flag#%') \ No newline at end of file From 5da06df6ac6c55cb30fda13275bc8da33bfa827d Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 26 Aug 2025 11:35:37 -0700 Subject: [PATCH 04/26] added label prefix --- splitio/models/fallback_treatment.py | 8 +++++++- tests/client/test_config.py | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/splitio/models/fallback_treatment.py b/splitio/models/fallback_treatment.py index 39fbf9f3..c8e60001 100644 --- a/splitio/models/fallback_treatment.py +++ b/splitio/models/fallback_treatment.py @@ -18,6 +18,7 @@ def __init__(self, treatment, config=None): self._config = None if config != None: self._config = json.dumps(config) + self._label_prefix = "fallback - " @property def treatment(self): @@ -27,4 +28,9 @@ def treatment(self): @property def config(self): """Return config.""" - return self._config \ No newline at end of file + return self._config + + @property + def label_prefix(self): + """Return label prefix.""" + return self._label_prefix \ No newline at end of file diff --git a/tests/client/test_config.py b/tests/client/test_config.py index cbe8ffcd..7be3c383 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -109,11 +109,13 @@ def test_sanitize(self, mocker): fb = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment('on'))) processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': fb}) assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment.treatment == fb.fallback_config.global_fallback_treatment.treatment + assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment.label_prefix == "fallback - " fb = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment('on'), {"flag": FallbackTreatment("off")})) processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': fb}) assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment.treatment == fb.fallback_config.global_fallback_treatment.treatment assert processed['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment["flag"] == fb.fallback_config.by_flag_fallback_treatment["flag"] + assert processed['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment["flag"].label_prefix == "fallback - " _logger.reset_mock() fb = FallbackTreatmentsConfiguration(FallbackConfig(None, {"flag#%": FallbackTreatment("off"), "flag2": FallbackTreatment("on")})) From 5811d9c854b719676627e5bc90a9432a47a8fb99 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 26 Aug 2025 12:38:41 -0700 Subject: [PATCH 05/26] Updated evaluator --- splitio/client/client.py | 3 +- splitio/engine/evaluator.py | 26 +++++++++++++-- splitio/models/fallback_treatment.py | 8 ++++- tests/engine/test_evaluator.py | 50 ++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 4 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 257c9b97..35383765 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -39,7 +39,7 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes 'impressions_disabled': False } - def __init__(self, factory, recorder, labels_enabled=True): + def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None): """ Construct a Client instance. @@ -64,6 +64,7 @@ def __init__(self, factory, recorder, labels_enabled=True): self._evaluator = Evaluator(self._splitter) self._telemetry_evaluation_producer = self._factory._telemetry_evaluation_producer self._telemetry_init_producer = self._factory._telemetry_init_producer + self._fallback_treatments_configuration = fallback_treatments_configuration @property def ready(self): diff --git a/splitio/engine/evaluator.py b/splitio/engine/evaluator.py index 26875a68..8088b450 100644 --- a/splitio/engine/evaluator.py +++ b/splitio/engine/evaluator.py @@ -20,7 +20,7 @@ class Evaluator(object): # pylint: disable=too-few-public-methods """Split Evaluator class.""" - def __init__(self, splitter): + def __init__(self, splitter, fallback_treatments_configuration=None): """ Construct a Evaluator instance. @@ -28,6 +28,7 @@ def __init__(self, splitter): :type splitter: splitio.engine.splitters.Splitters """ self._splitter = splitter + self._fallback_treatments_configuration = fallback_treatments_configuration def eval_many_with_context(self, key, bucketing, features, attrs, ctx): """ @@ -51,6 +52,7 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx): if not feature: _LOGGER.warning('Unknown or invalid feature: %s', feature) label = Label.SPLIT_NOT_FOUND + label, _treatment, config = self._get_fallback_treatment_and_label(feature_name, _treatment, label) else: _change_number = feature.change_number if feature.killed: @@ -59,10 +61,11 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx): else: label, _treatment = self._check_prerequisites(feature, bucketing, key, attrs, ctx, label, _treatment) label, _treatment = self._get_treatment(feature, bucketing, key, attrs, ctx, label, _treatment) + config = feature.get_configurations_for(_treatment) if feature else None return { 'treatment': _treatment, - 'configurations': feature.get_configurations_for(_treatment) if feature else None, + 'configurations': config, 'impression': { 'label': label, 'change_number': _change_number @@ -70,6 +73,25 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx): 'impressions_disabled': feature.impressions_disabled if feature else None } + def _get_fallback_treatment_and_label(self, feature_name, treatment, label): + if self._fallback_treatments_configuration == None or self._fallback_treatments_configuration.fallback_config == None: + return label, treatment, None + + if self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment != None and \ + self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name) != None: + _LOGGER.debug('Using Fallback Treatment for feature: %s', feature_name) + return self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).label_prefix + label, \ + self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).treatment, \ + self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).config + + if self._fallback_treatments_configuration.fallback_config.global_fallback_treatment != None: + _LOGGER.debug('Using Global Fallback Treatment.') + return self._fallback_treatments_configuration.fallback_config.global_fallback_treatment.label_prefix + label, \ + self._fallback_treatments_configuration.fallback_config.global_fallback_treatment.treatment, \ + self._fallback_treatments_configuration.fallback_config.global_fallback_treatment.config + + return label, treatment, None + def _get_treatment(self, feature, bucketing, key, attrs, ctx, label, _treatment): if _treatment == CONTROL: treatment, label = self._treatment_for_flag(feature, key, bucketing, attrs, ctx) diff --git a/splitio/models/fallback_treatment.py b/splitio/models/fallback_treatment.py index 39fbf9f3..c8e60001 100644 --- a/splitio/models/fallback_treatment.py +++ b/splitio/models/fallback_treatment.py @@ -18,6 +18,7 @@ def __init__(self, treatment, config=None): self._config = None if config != None: self._config = json.dumps(config) + self._label_prefix = "fallback - " @property def treatment(self): @@ -27,4 +28,9 @@ def treatment(self): @property def config(self): """Return config.""" - return self._config \ No newline at end of file + return self._config + + @property + def label_prefix(self): + """Return label prefix.""" + return self._label_prefix \ No newline at end of file diff --git a/tests/engine/test_evaluator.py b/tests/engine/test_evaluator.py index 3ec7e136..e95a8710 100644 --- a/tests/engine/test_evaluator.py +++ b/tests/engine/test_evaluator.py @@ -11,6 +11,8 @@ from splitio.models.impressions import Label from splitio.models.grammar import condition from splitio.models import rule_based_segments +from splitio.models.fallback_treatment import FallbackTreatment +from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration from splitio.engine import evaluator, splitters from splitio.engine.evaluator import EvaluationContext from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, InMemoryRuleBasedSegmentStorage, \ @@ -372,6 +374,54 @@ def test_prerequisites(self): ctx = evaluation_facctory.context_for('mauro@split.io', ['prereq_chain']) assert e.eval_with_context('mauro@split.io', 'mauro@split.io', 'prereq_chain', {'email': 'mauro@split.io'}, ctx)['treatment'] == "on_default" + def test_evaluate_treatment_with_fallback(self, mocker): + """Test that a evaluation return fallback treatment.""" + splitter_mock = mocker.Mock(spec=splitters.Splitter) + logger_mock = mocker.Mock(spec=logging.Logger) + evaluator._LOGGER = logger_mock + mocked_split = mocker.Mock(spec=Split) + ctx = EvaluationContext(flags={'some': mocked_split}, segment_memberships=set(), rbs_segments={}) + + # should use global fallback + logger_mock.reset_mock() + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("off-global", {"prop":"val"})))) + result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx) + assert result['treatment'] == 'off-global' + assert result['configurations'] == '{"prop": "val"}' + assert result['impression']['label'] == "fallback - " + Label.SPLIT_NOT_FOUND + assert logger_mock.debug.mock_calls[0] == mocker.call("Using Global Fallback Treatment.") + + + # should use by flag fallback + logger_mock.reset_mock() + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})}))) + result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx) + assert result['treatment'] == 'off-some2' + assert result['configurations'] == '{"prop2": "val2"}' + assert result['impression']['label'] == "fallback - " + Label.SPLIT_NOT_FOUND + assert logger_mock.debug.mock_calls[0] == mocker.call("Using Fallback Treatment for feature: %s", "some2") + + # should not use any fallback + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})}))) + result = e.eval_with_context('some_key', 'some_bucketing_key', 'some3', {}, ctx) + assert result['treatment'] == 'control' + assert result['configurations'] == None + assert result['impression']['label'] == Label.SPLIT_NOT_FOUND + + # should use by flag fallback + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})}))) + result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx) + assert result['treatment'] == 'off-some2' + assert result['configurations'] == '{"prop2": "val2"}' + assert result['impression']['label'] == "fallback - " + Label.SPLIT_NOT_FOUND + + # should global flag fallback + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})}))) + result = e.eval_with_context('some_key', 'some_bucketing_key', 'some3', {}, ctx) + assert result['treatment'] == 'off-global' + assert result['configurations'] == '{"prop": "val"}' + assert result['impression']['label'] == "fallback - " + Label.SPLIT_NOT_FOUND + @pytest.mark.asyncio async def test_evaluate_treatment_with_rbs_in_condition_async(self): e = evaluator.Evaluator(splitters.Splitter()) From 34f66aeda6cd27c2df9605e046360b3a7a6784ac Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 26 Aug 2025 12:40:31 -0700 Subject: [PATCH 06/26] clean up client --- splitio/client/client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 35383765..257c9b97 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -39,7 +39,7 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes 'impressions_disabled': False } - def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None): + def __init__(self, factory, recorder, labels_enabled=True): """ Construct a Client instance. @@ -64,7 +64,6 @@ def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_c self._evaluator = Evaluator(self._splitter) self._telemetry_evaluation_producer = self._factory._telemetry_evaluation_producer self._telemetry_init_producer = self._factory._telemetry_init_producer - self._fallback_treatments_configuration = fallback_treatments_configuration @property def ready(self): From 50f5b761304febc9d0a231f56cf932d3c1fbf226 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 28 Aug 2025 10:43:20 -0700 Subject: [PATCH 07/26] Update client class --- splitio/client/client.py | 89 +++-- splitio/client/input_validator.py | 11 +- splitio/client/util.py | 19 + splitio/engine/evaluator.py | 23 +- tests/client/test_client.py | 577 ++++++++++++++++++++++++++++++ 5 files changed, 661 insertions(+), 58 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 257c9b97..947da98c 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -2,6 +2,7 @@ import logging import json from collections import namedtuple +import copy from splitio.engine.evaluator import Evaluator, CONTROL, EvaluationDataFactory, AsyncEvaluationDataFactory from splitio.engine.splitters import Splitter @@ -9,6 +10,7 @@ from splitio.models.events import Event, EventWrapper from splitio.models.telemetry import get_latency_bucket_index, MethodExceptionsAndLatencies from splitio.client import input_validator +from splitio.client.util import get_fallback_treatment_and_label from splitio.util.time import get_current_epoch_time_ms, utctime_ms @@ -39,7 +41,7 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes 'impressions_disabled': False } - def __init__(self, factory, recorder, labels_enabled=True): + def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None): """ Construct a Client instance. @@ -64,6 +66,7 @@ def __init__(self, factory, recorder, labels_enabled=True): self._evaluator = Evaluator(self._splitter) self._telemetry_evaluation_producer = self._factory._telemetry_evaluation_producer self._telemetry_init_producer = self._factory._telemetry_init_producer + self._fallback_treatments_configuration = fallback_treatments_configuration @property def ready(self): @@ -203,11 +206,23 @@ def _validate_track(self, key, traffic_type, event_type, value=None, properties= def _get_properties(self, evaluation_options): return evaluation_options.properties if evaluation_options != None else None + def _get_fallback_treatment_with_config(self, treatment, feature): + label = "" + + label, treatment, config = get_fallback_treatment_and_label(self._fallback_treatments_configuration, + feature, treatment, label, _LOGGER) + return treatment, config + + def _get_fallback_eval_results(self, eval_result, feature): + result = copy.deepcopy(eval_result) + result["impression"]["label"], result["treatment"], result["configurations"] = get_fallback_treatment_and_label(self._fallback_treatments_configuration, + feature, result["treatment"], result["impression"]["label"], _LOGGER) + return result class Client(ClientBase): # pylint: disable=too-many-instance-attributes """Entry point for the split sdk.""" - def __init__(self, factory, recorder, labels_enabled=True): + def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None): """ Construct a Client instance. @@ -222,7 +237,7 @@ def __init__(self, factory, recorder, labels_enabled=True): :rtype: Client """ - ClientBase.__init__(self, factory, recorder, labels_enabled) + ClientBase.__init__(self, factory, recorder, labels_enabled, fallback_treatments_configuration) self._context_factory = EvaluationDataFactory(factory._get_storage('splits'), factory._get_storage('segments'), factory._get_storage('rule_based_segments')) def destroy(self): @@ -254,10 +269,11 @@ def get_treatment(self, key, feature_flag_name, attributes=None, evaluation_opti try: treatment, _ = self._get_treatment(MethodExceptionsAndLatencies.TREATMENT, key, feature_flag_name, attributes, evaluation_options) return treatment - + except: _LOGGER.error('get_treatment failed') - return CONTROL + treatment, _ = self._get_fallback_treatment_with_config(CONTROL, feature_flag_name) + return treatment def get_treatment_with_config(self, key, feature_flag_name, attributes=None, evaluation_options=None): """ @@ -282,8 +298,8 @@ def get_treatment_with_config(self, key, feature_flag_name, attributes=None, eva except Exception: _LOGGER.error('get_treatment_with_config failed') - return CONTROL, None - + return self._get_fallback_treatment_with_config(CONTROL, feature_flag_name) + def _get_treatment(self, method, key, feature, attributes=None, evaluation_options=None): """ Validate key, feature flag name and object, and get the treatment and config with an optional dictionary of attributes. @@ -302,7 +318,7 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio :rtype: dict """ if not self._client_is_usable(): # not destroyed & not waiting for a fork - return CONTROL, None + return self._get_fallback_treatment_with_config(CONTROL, feature) start = get_current_epoch_time_ms() if not self.ready: @@ -312,9 +328,10 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio try: key, bucketing, feature, attributes, evaluation_options = self._validate_treatment_input(key, feature, attributes, method, evaluation_options) except _InvalidInputError: - return CONTROL, None + return self._get_fallback_treatment_with_config(CONTROL, feature) - result = self._NON_READY_EVAL_RESULT + result = self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, feature) + if self.ready: try: ctx = self._context_factory.context_for(key, [feature]) @@ -324,15 +341,15 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio _LOGGER.error('Error getting treatment for feature flag') _LOGGER.debug('Error: ', exc_info=True) self._telemetry_evaluation_producer.record_exception(method) - result = self._FAILED_EVAL_RESULT + result = self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, feature) properties = self._get_properties(evaluation_options) - if result['impression']['label'] != Label.SPLIT_NOT_FOUND: + if result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1: impression_decorated = self._build_impression(key, bucketing, feature, result, properties) self._record_stats([(impression_decorated, attributes)], start, method) return result['treatment'], result['configurations'] - + def get_treatments(self, key, feature_flag_names, attributes=None, evaluation_options=None): """ Evaluate multiple feature flags and return a dictionary with all the feature flag/treatments. @@ -356,7 +373,7 @@ def get_treatments(self, key, feature_flag_names, attributes=None, evaluation_op return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} except Exception: - return {feature: CONTROL for feature in feature_flag_names} + return {feature: self._get_fallback_treatment_with_config(CONTROL, feature)[0] for feature in feature_flag_names} def get_treatments_with_config(self, key, feature_flag_names, attributes=None, evaluation_options=None): """ @@ -380,7 +397,7 @@ def get_treatments_with_config(self, key, feature_flag_names, attributes=None, e return self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, attributes, evaluation_options) except Exception: - return {feature: (CONTROL, None) for feature in feature_flag_names} + return {feature: (self._get_fallback_treatment_with_config(CONTROL, feature)) for feature in feature_flag_names} def get_treatments_by_flag_set(self, key, flag_set, attributes=None, evaluation_options=None): """ @@ -604,7 +621,7 @@ def _get_treatments(self, key, features, method, attributes=None, evaluation_opt """ start = get_current_epoch_time_ms() if not self._client_is_usable(): - return input_validator.generate_control_treatments(features) + return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration) if not self.ready: _LOGGER.error("Client is not ready - no calls possible") @@ -613,9 +630,9 @@ def _get_treatments(self, key, features, method, attributes=None, evaluation_opt try: key, bucketing, features, attributes, evaluation_options = self._validate_treatments_input(key, features, attributes, method, evaluation_options) except _InvalidInputError: - return input_validator.generate_control_treatments(features) + return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration) - results = {n: self._NON_READY_EVAL_RESULT for n in features} + results = {n: self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, n) for n in features} if self.ready: try: ctx = self._context_factory.context_for(key, features) @@ -625,12 +642,12 @@ def _get_treatments(self, key, features, method, attributes=None, evaluation_opt _LOGGER.error('Error getting treatment for feature flag') _LOGGER.debug('Error: ', exc_info=True) self._telemetry_evaluation_producer.record_exception(method) - results = {n: self._FAILED_EVAL_RESULT for n in features} + results = {n: self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, n) for n in features} properties = self._get_properties(evaluation_options) imp_decorated_attrs = [ (i, attributes) for i in self._build_impressions(key, bucketing, results, properties) - if i.Impression.label != Label.SPLIT_NOT_FOUND + if i.Impression.label.find(Label.SPLIT_NOT_FOUND) == -1 ] self._record_stats(imp_decorated_attrs, start, method) @@ -706,7 +723,7 @@ def track(self, key, traffic_type, event_type, value=None, properties=None): class ClientAsync(ClientBase): # pylint: disable=too-many-instance-attributes """Entry point for the split sdk.""" - def __init__(self, factory, recorder, labels_enabled=True): + def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None): """ Construct a Client instance. @@ -721,7 +738,7 @@ def __init__(self, factory, recorder, labels_enabled=True): :rtype: Client """ - ClientBase.__init__(self, factory, recorder, labels_enabled) + ClientBase.__init__(self, factory, recorder, labels_enabled, fallback_treatments_configuration) self._context_factory = AsyncEvaluationDataFactory(factory._get_storage('splits'), factory._get_storage('segments'), factory._get_storage('rule_based_segments')) async def destroy(self): @@ -756,7 +773,8 @@ async def get_treatment(self, key, feature_flag_name, attributes=None, evaluatio except: _LOGGER.error('get_treatment failed') - return CONTROL + treatment, _ = self._get_fallback_treatment_with_config(CONTROL, feature_flag_name) + return treatment async def get_treatment_with_config(self, key, feature_flag_name, attributes=None, evaluation_options=None): """ @@ -781,7 +799,7 @@ async def get_treatment_with_config(self, key, feature_flag_name, attributes=Non except Exception: _LOGGER.error('get_treatment_with_config failed') - return CONTROL, None + return self._get_fallback_treatment_with_config(CONTROL, feature_flag_name) async def _get_treatment(self, method, key, feature, attributes=None, evaluation_options=None): """ @@ -801,7 +819,7 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation :rtype: dict """ if not self._client_is_usable(): # not destroyed & not waiting for a fork - return CONTROL, None + return self._get_fallback_treatment_with_config(CONTROL, feature) start = get_current_epoch_time_ms() if not self.ready: @@ -811,9 +829,9 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation try: key, bucketing, feature, attributes, evaluation_options = self._validate_treatment_input(key, feature, attributes, method, evaluation_options) except _InvalidInputError: - return CONTROL, None + return self._get_fallback_treatment_with_config(CONTROL, feature) - result = self._NON_READY_EVAL_RESULT + result = self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, feature) if self.ready: try: ctx = await self._context_factory.context_for(key, [feature]) @@ -823,10 +841,10 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation _LOGGER.error('Error getting treatment for feature flag') _LOGGER.debug('Error: ', exc_info=True) await self._telemetry_evaluation_producer.record_exception(method) - result = self._FAILED_EVAL_RESULT + result = self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, feature) properties = self._get_properties(evaluation_options) - if result['impression']['label'] != Label.SPLIT_NOT_FOUND: + if result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1: impression_decorated = self._build_impression(key, bucketing, feature, result, properties) await self._record_stats([(impression_decorated, attributes)], start, method) return result['treatment'], result['configurations'] @@ -854,7 +872,7 @@ async def get_treatments(self, key, feature_flag_names, attributes=None, evaluat return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} except Exception: - return {feature: CONTROL for feature in feature_flag_names} + return {feature: self._get_fallback_treatment_with_config(CONTROL, feature)[0] for feature in feature_flag_names} async def get_treatments_with_config(self, key, feature_flag_names, attributes=None, evaluation_options=None): """ @@ -878,8 +896,7 @@ async def get_treatments_with_config(self, key, feature_flag_names, attributes=N return await self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, attributes, evaluation_options) except Exception: - _LOGGER.error("AA", exc_info=True) - return {feature: (CONTROL, None) for feature in feature_flag_names} + return {feature: (self._get_fallback_treatment_with_config(CONTROL, feature)) for feature in feature_flag_names} async def get_treatments_by_flag_set(self, key, flag_set, attributes=None, evaluation_options=None): """ @@ -1017,7 +1034,7 @@ async def _get_treatments(self, key, features, method, attributes=None, evaluati """ start = get_current_epoch_time_ms() if not self._client_is_usable(): - return input_validator.generate_control_treatments(features) + return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration) if not self.ready: _LOGGER.error("Client is not ready - no calls possible") @@ -1026,9 +1043,9 @@ async def _get_treatments(self, key, features, method, attributes=None, evaluati try: key, bucketing, features, attributes, evaluation_options = self._validate_treatments_input(key, features, attributes, method, evaluation_options) except _InvalidInputError: - return input_validator.generate_control_treatments(features) + return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration) - results = {n: self._NON_READY_EVAL_RESULT for n in features} + results = {n: self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, n) for n in features} if self.ready: try: ctx = await self._context_factory.context_for(key, features) @@ -1038,7 +1055,7 @@ async def _get_treatments(self, key, features, method, attributes=None, evaluati _LOGGER.error('Error getting treatment for feature flag') _LOGGER.debug('Error: ', exc_info=True) await self._telemetry_evaluation_producer.record_exception(method) - results = {n: self._FAILED_EVAL_RESULT for n in features} + results = {n: self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, n) for n in features} properties = self._get_properties(evaluation_options) imp_decorated_attrs = [ diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index 51b8b0d2..2367816d 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -7,6 +7,7 @@ from splitio.client.key import Key from splitio.client import client +from splitio.client.util import get_fallback_treatment_and_label from splitio.engine.evaluator import CONTROL @@ -501,7 +502,7 @@ def validate_feature_flags_get_treatments( # pylint: disable=invalid-name valid_feature_flags.append(ff) return valid_feature_flags -def generate_control_treatments(feature_flags): +def generate_control_treatments(feature_flags, fallback_treatments_configuration): """ Generate valid feature flags to control. @@ -516,7 +517,13 @@ def generate_control_treatments(feature_flags): to_return = {} for feature_flag in feature_flags: if isinstance(feature_flag, str) and len(feature_flag.strip())> 0: - to_return[feature_flag] = (CONTROL, None) + treatment = CONTROL + config = None + label = "" + label, treatment, config = get_fallback_treatment_and_label(fallback_treatments_configuration, + feature_flag, treatment, label, _LOGGER) + + to_return[feature_flag] = (treatment, config) return to_return diff --git a/splitio/client/util.py b/splitio/client/util.py index e4892512..6541b9df 100644 --- a/splitio/client/util.py +++ b/splitio/client/util.py @@ -51,3 +51,22 @@ def get_metadata(config): version = 'python-%s' % __version__ ip_address, hostname = _get_hostname_and_ip(config) return SdkMetadata(version, hostname, ip_address) + +def get_fallback_treatment_and_label(fallback_treatments_configuration, feature_name, treatment, label, _logger): + if fallback_treatments_configuration == None or fallback_treatments_configuration.fallback_config == None: + return label, treatment, None + + if fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment != None and \ + fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name) != None: + _logger.debug('Using Fallback Treatment for feature: %s', feature_name) + return fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).label_prefix + label, \ + fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).treatment, \ + fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).config + + if fallback_treatments_configuration.fallback_config.global_fallback_treatment != None: + _logger.debug('Using Global Fallback Treatment.') + return fallback_treatments_configuration.fallback_config.global_fallback_treatment.label_prefix + label, \ + fallback_treatments_configuration.fallback_config.global_fallback_treatment.treatment, \ + fallback_treatments_configuration.fallback_config.global_fallback_treatment.config + + return label, treatment, None diff --git a/splitio/engine/evaluator.py b/splitio/engine/evaluator.py index 8088b450..2a564d3a 100644 --- a/splitio/engine/evaluator.py +++ b/splitio/engine/evaluator.py @@ -2,6 +2,7 @@ import logging from collections import namedtuple +from splitio.client.util import get_fallback_treatment_and_label from splitio.models.impressions import Label from splitio.models.grammar.condition import ConditionType from splitio.models.grammar.matchers.misc import DependencyMatcher @@ -52,7 +53,8 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx): if not feature: _LOGGER.warning('Unknown or invalid feature: %s', feature) label = Label.SPLIT_NOT_FOUND - label, _treatment, config = self._get_fallback_treatment_and_label(feature_name, _treatment, label) + label, _treatment, config = get_fallback_treatment_and_label(self._fallback_treatments_configuration, + feature_name, _treatment, label, _LOGGER) else: _change_number = feature.change_number if feature.killed: @@ -72,25 +74,6 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx): }, 'impressions_disabled': feature.impressions_disabled if feature else None } - - def _get_fallback_treatment_and_label(self, feature_name, treatment, label): - if self._fallback_treatments_configuration == None or self._fallback_treatments_configuration.fallback_config == None: - return label, treatment, None - - if self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment != None and \ - self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name) != None: - _LOGGER.debug('Using Fallback Treatment for feature: %s', feature_name) - return self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).label_prefix + label, \ - self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).treatment, \ - self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).config - - if self._fallback_treatments_configuration.fallback_config.global_fallback_treatment != None: - _LOGGER.debug('Using Global Fallback Treatment.') - return self._fallback_treatments_configuration.fallback_config.global_fallback_treatment.label_prefix + label, \ - self._fallback_treatments_configuration.fallback_config.global_fallback_treatment.treatment, \ - self._fallback_treatments_configuration.fallback_config.global_fallback_treatment.config - - return label, treatment, None def _get_treatment(self, feature, bucketing, key, attrs, ctx, label, _treatment): if _treatment == CONTROL: diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 9a6848eb..ab790214 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -9,6 +9,8 @@ from splitio.client.client import Client, _LOGGER as _logger, CONTROL, ClientAsync, EvaluationOptions from splitio.client.factory import SplitFactory, Status as FactoryStatus, SplitFactoryAsync +from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_treatment import FallbackTreatment from splitio.models.impressions import Impression, Label from splitio.models.events import Event, EventWrapper from splitio.storage import EventStorage, ImpressionStorage, SegmentStorage, SplitStorage, RuleBasedSegmentsStorage @@ -1375,6 +1377,287 @@ def synchronize_config(*_): assert client.get_treatments_with_config_by_flag_sets('some_key', ['set_1'], evaluation_options=EvaluationOptions({"prop": "value"})) == {'SPLIT_2': ('on', None)} assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + @mock.patch('splitio.engine.evaluator.Evaluator.eval_with_context', side_effect=RuntimeError()) + def test_fallback_treatment_eval_exception(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + self.imps = None + def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global", {"prop":"val"})))) + + def get_feature_flag_names_by_flag_sets(*_): + return ["some", "some2"] + client._get_feature_flag_names_by_flag_sets = get_feature_flag_names_by_flag_sets + + treatment = client.get_treatment("key", "some") + assert(treatment == "on-global") + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = client.get_treatments("key_m", ["some", "some2"]) + assert(treatment == {"some": "on-global", "some2": "on-global"}) + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - exception") + assert(self.imps[1].treatment == "on-global") + assert(self.imps[1].label == "fallback - exception") + + assert(client.get_treatment_with_config("key", "some") == ("on-global", '{"prop": "val"}')) + assert(client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + assert(client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-global", "some2": "on-global"}) + assert(client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-global", "some2": "on-global"}) + assert(client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global", {"prop":"val"}), {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = client.get_treatments("key2_m", ["some", "some2"]) + assert(treatment == {"some": "on-local", "some2": "on-global"}) + assert_both = 0 + for imp in self.imps: + if imp.feature_name == "some": + assert_both += 1 + assert(imp.treatment == "on-local") + assert(imp.label == "fallback - exception") + else: + assert_both += 1 + assert(imp.treatment == "on-global") + assert(imp.label == "fallback - exception") + assert assert_both == 2 + + assert(client.get_treatment_with_config("key", "some") == ("on-local", None)) + assert(client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + assert(client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-local", "some2": "on-global"}) + assert(client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-local", "some2": "on-global"}) + assert(client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local", {"prop":"val"})})) + treatment = client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = client.get_treatments("key3_m", ["some", "some2"]) + assert(treatment == {"some": "on-local", "some2": "control"}) + assert_both = 0 + for imp in self.imps: + if imp.feature_name == "some": + assert_both += 1 + assert(imp.treatment == "on-local") + assert(imp.label == "fallback - exception") + else: + assert_both += 1 + assert(imp.treatment == "control") + assert(imp.label == "exception") + assert assert_both == 2 + + assert(client.get_treatment_with_config("key", "some") == ("on-local", '{"prop": "val"}')) + assert(client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + assert(client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-local", "some2": "control"}) + assert(client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-local", "some2": "control"}) + assert(client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps[0].treatment == "control") + assert(self.imps[0].label == "exception") + + try: + factory.destroy() + except: + pass + + @mock.patch('splitio.engine.evaluator.Evaluator.eval_with_context', side_effect=Exception()) + def test_fallback_treatment_exception_no_impressions(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + self.imps = None + def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global")))) + treatment = client.get_treatment("key", "some") + assert(treatment == "on-global") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps == None) + + try: + factory.destroy() + except: + pass + + @mock.patch('splitio.client.client.Client.ready', side_effect=None) + def test_fallback_treatment_not_ready_impressions(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + self.imps = None + def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global")))) + client.ready = False + + treatment = client.get_treatment("key", "some") + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps[0].treatment == "control") + assert(self.imps[0].label == "not ready") + + try: + factory.destroy() + except: + pass + class ClientAsyncTests(object): # pylint: disable=too-few-public-methods """Split client async test cases.""" @@ -2585,3 +2868,297 @@ async def synchronize_config(*_): assert await client.get_treatments_with_config_by_flag_sets('some_key', ['set_1'], evaluation_options=EvaluationOptions({"prop": "value"})) == {'SPLIT_2': ('on', None)} assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + + @pytest.mark.asyncio + @mock.patch('splitio.engine.evaluator.Evaluator.eval_with_context', side_effect=RuntimeError()) + async def test_fallback_treatment_eval_exception(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = await InMemoryTelemetryStorageAsync.create() + telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) + telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() + impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) + recorder = StandardRecorderAsync(impmanager, event_storage, impression_storage, telemetry_evaluation_producer, telemetry_producer.get_telemetry_runtime_producer()) + + factory = SplitFactoryAsync(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock(), + mocker.Mock() + ) + + self.imps = None + async def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + async def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = ClientAsync(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global", {"prop":"val"})))) + + async def get_feature_flag_names_by_flag_sets(*_): + return ["some", "some2"] + client._get_feature_flag_names_by_flag_sets = get_feature_flag_names_by_flag_sets + + async def fetch_many(*_): + return {"some": from_raw(splits_json['splitChange1_1']['ff']['d'][0])} + split_storage.fetch_many = fetch_many + + async def fetch_many_rbs(*_): + return {} + rb_segment_storage.fetch_many = fetch_many_rbs + + treatment = await client.get_treatment("key", "some") + assert(treatment == "on-global") + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = await client.get_treatments("key_m", ["some", "some2"]) + assert(treatment == {"some": "on-global", "some2": "on-global"}) + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - exception") + assert(self.imps[1].treatment == "on-global") + assert(self.imps[1].label == "fallback - exception") + + assert(await client.get_treatment_with_config("key", "some") == ("on-global", '{"prop": "val"}')) + assert(await client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + assert(await client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-global", "some2": "on-global"}) + assert(await client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-global", "some2": "on-global"}) + assert(await client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global", {"prop":"val"}), {'some': FallbackTreatment("on-local")})) + treatment = await client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = await client.get_treatments("key2_m", ["some", "some2"]) + assert(treatment == {"some": "on-local", "some2": "on-global"}) + assert_both = 0 + for imp in self.imps: + if imp.feature_name == "some": + assert_both += 1 + assert(imp.treatment == "on-local") + assert(imp.label == "fallback - exception") + else: + assert_both += 1 + assert(imp.treatment == "on-global") + assert(imp.label == "fallback - exception") + assert assert_both == 2 + + assert(await client.get_treatment_with_config("key", "some") == ("on-local", None)) + assert(await client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + assert(await client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-local", "some2": "on-global"}) + assert(await client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-local", "some2": "on-global"}) + assert(await client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local", {"prop":"val"})})) + treatment = await client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - exception") + + self.imps = None + treatment = await client.get_treatments("key3_m", ["some", "some2"]) + assert(treatment == {"some": "on-local", "some2": "control"}) + assert_both = 0 + for imp in self.imps: + if imp.feature_name == "some": + assert_both += 1 + assert(imp.treatment == "on-local") + assert(imp.label == "fallback - exception") + else: + assert_both += 1 + assert(imp.treatment == "control") + assert(imp.label == "exception") + assert assert_both == 2 + + assert(await client.get_treatment_with_config("key", "some") == ("on-local", '{"prop": "val"}')) + assert(await client.get_treatments_with_config("key_m", ["some", "some2"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + assert(await client.get_treatments_by_flag_set("key_m", "set") == {"some": "on-local", "some2": "control"}) + assert(await client.get_treatments_by_flag_set("key_m", ["set"]) == {"some": "on-local", "some2": "control"}) + assert(await client.get_treatments_with_config_by_flag_set("key_m", "set") == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + treatment = await client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps[0].treatment == "control") + assert(self.imps[0].label == "exception") + + try: + await factory.destroy() + except: + pass + + @pytest.mark.asyncio + @mock.patch('splitio.engine.evaluator.Evaluator.eval_with_context', side_effect=Exception()) + def test_fallback_treatment_exception_no_impressions(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + self.imps = None + def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global")))) + treatment = client.get_treatment("key", "some") + assert(treatment == "on-global") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps == None) + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps == None) + + try: + factory.destroy() + except: + pass + + @pytest.mark.asyncio + @mock.patch('splitio.client.client.Client.ready', side_effect=None) + def test_fallback_treatment_not_ready_impressions(self, mocker): + # using fallback when the evaluator has RuntimeError exception + split_storage = mocker.Mock(spec=SplitStorage) + segment_storage = mocker.Mock(spec=SegmentStorage) + rb_segment_storage = mocker.Mock(spec=RuleBasedSegmentsStorage) + impression_storage = mocker.Mock(spec=ImpressionStorage) + event_storage = mocker.Mock(spec=EventStorage) + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + impmanager, + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + mocker.Mock() + ) + + self.imps = None + def put(impressions): + self.imps = impressions + impression_storage.put = put + + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + factory._telemetry_submitter = TelemetrySubmitterMock() + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global")))) + client.ready = False + + treatment = client.get_treatment("key", "some") + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key2", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key3", "some") + assert(treatment == "on-local") + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - not ready") + + self.imps = None + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + treatment = client.get_treatment("key4", "some") + assert(treatment == "control") + assert(self.imps[0].treatment == "control") + assert(self.imps[0].label == "not ready") + + try: + factory.destroy() + except: + pass \ No newline at end of file From b7391b43434bfb06511099faae298a50e6c21bc3 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 28 Aug 2025 21:07:00 -0700 Subject: [PATCH 08/26] Update factory and tests --- splitio/client/client.py | 10 +-- splitio/client/factory.py | 35 +++++--- splitio/storage/adapters/redis.py | 4 +- tests/client/test_factory.py | 15 +++- tests/client/test_input_validator.py | 2 + tests/integration/test_client_e2e.py | 94 ++++++++++++++++++-- tests/push/test_manager.py | 10 ++- tests/storage/adapters/test_redis_adapter.py | 6 +- 8 files changed, 140 insertions(+), 36 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 947da98c..ec3c9260 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -63,7 +63,7 @@ def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_c self._feature_flag_storage = factory._get_storage('splits') # pylint: disable=protected-access self._segment_storage = factory._get_storage('segments') # pylint: disable=protected-access self._events_storage = factory._get_storage('events') # pylint: disable=protected-access - self._evaluator = Evaluator(self._splitter) + self._evaluator = Evaluator(self._splitter, fallback_treatments_configuration) self._telemetry_evaluation_producer = self._factory._telemetry_evaluation_producer self._telemetry_init_producer = self._factory._telemetry_init_producer self._fallback_treatments_configuration = fallback_treatments_configuration @@ -344,7 +344,7 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio result = self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, feature) properties = self._get_properties(evaluation_options) - if result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1: + if result['impression']['label'] == None or (result['impression']['label'] != None and result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1): impression_decorated = self._build_impression(key, bucketing, feature, result, properties) self._record_stats([(impression_decorated, attributes)], start, method) @@ -647,7 +647,7 @@ def _get_treatments(self, key, features, method, attributes=None, evaluation_opt properties = self._get_properties(evaluation_options) imp_decorated_attrs = [ (i, attributes) for i in self._build_impressions(key, bucketing, results, properties) - if i.Impression.label.find(Label.SPLIT_NOT_FOUND) == -1 + if i.Impression.label == None or (i.Impression.label != None and i.Impression.label.find(Label.SPLIT_NOT_FOUND)) == -1 ] self._record_stats(imp_decorated_attrs, start, method) @@ -844,7 +844,7 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation result = self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, feature) properties = self._get_properties(evaluation_options) - if result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1: + if result['impression']['label'] == None or (result['impression']['label'] != None and result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1): impression_decorated = self._build_impression(key, bucketing, feature, result, properties) await self._record_stats([(impression_decorated, attributes)], start, method) return result['treatment'], result['configurations'] @@ -1060,7 +1060,7 @@ async def _get_treatments(self, key, features, method, attributes=None, evaluati properties = self._get_properties(evaluation_options) imp_decorated_attrs = [ (i, attributes) for i in self._build_impressions(key, bucketing, results, properties) - if i.Impression.label != Label.SPLIT_NOT_FOUND + if i.Impression.label == None or (i.Impression.label != None and i.Impression.label.find(Label.SPLIT_NOT_FOUND)) == -1 ] await self._record_stats(imp_decorated_attrs, start, method) diff --git a/splitio/client/factory.py b/splitio/client/factory.py index f6070243..57d194ab 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -170,7 +170,8 @@ def __init__( # pylint: disable=too-many-arguments telemetry_producer=None, telemetry_init_producer=None, telemetry_submitter=None, - preforked_initialization=False + preforked_initialization=False, + fallback_treatments_configuration=None ): """ Class constructor. @@ -201,6 +202,7 @@ def __init__( # pylint: disable=too-many-arguments self._ready_time = get_current_epoch_time_ms() _LOGGER.debug("Running in threading mode") self._sdk_internal_ready_flag = sdk_ready_flag + self._fallback_treatments_configuration = fallback_treatments_configuration self._start_status_updater() def _start_status_updater(self): @@ -242,7 +244,7 @@ def client(self): This client is only a set of references to structures hold by the factory. Creating one a fast operation and safe to be used anywhere. """ - return Client(self, self._recorder, self._labels_enabled) + return Client(self, self._recorder, self._labels_enabled, self._fallback_treatments_configuration) def manager(self): """ @@ -338,7 +340,8 @@ def __init__( # pylint: disable=too-many-arguments telemetry_init_producer=None, telemetry_submitter=None, manager_start_task=None, - api_client=None + api_client=None, + fallback_treatments_configuration=None ): """ Class constructor. @@ -372,6 +375,7 @@ def __init__( # pylint: disable=too-many-arguments self._sdk_ready_flag = asyncio.Event() self._ready_task = asyncio.get_running_loop().create_task(self._update_status_when_ready_async()) self._api_client = api_client + self._fallback_treatments_configuration = fallback_treatments_configuration async def _update_status_when_ready_async(self): """Wait until the sdk is ready and update the status for async mode.""" @@ -460,7 +464,7 @@ def client(self): This client is only a set of references to structures hold by the factory. Creating one a fast operation and safe to be used anywhere. """ - return ClientAsync(self, self._recorder, self._labels_enabled) + return ClientAsync(self, self._recorder, self._labels_enabled, self._fallback_treatments_configuration) def _wrap_impression_listener(listener, metadata): """ @@ -623,7 +627,8 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl synchronizer._split_synchronizers._segment_sync.shutdown() return SplitFactory(api_key, storages, cfg['labelsEnabled'], - recorder, manager, None, telemetry_producer, telemetry_init_producer, telemetry_submitter, preforked_initialization=preforked_initialization) + recorder, manager, None, telemetry_producer, telemetry_init_producer, telemetry_submitter, preforked_initialization=preforked_initialization, + fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration']) initialization_thread = threading.Thread(target=manager.start, name="SDKInitializer", daemon=True) initialization_thread.start() @@ -631,7 +636,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl return SplitFactory(api_key, storages, cfg['labelsEnabled'], recorder, manager, sdk_ready_flag, telemetry_producer, telemetry_init_producer, - telemetry_submitter) + telemetry_submitter, fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration']) async def _build_in_memory_factory_async(api_key, cfg, sdk_url=None, events_url=None, # pylint:disable=too-many-arguments,too-many-localsa auth_api_base_url=None, streaming_api_base_url=None, telemetry_api_base_url=None, @@ -750,7 +755,7 @@ async def _build_in_memory_factory_async(api_key, cfg, sdk_url=None, events_url= recorder, manager, telemetry_producer, telemetry_init_producer, telemetry_submitter, manager_start_task=manager_start_task, - api_client=http_client) + api_client=http_client, fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration']) def _build_redis_factory(api_key, cfg): """Build and return a split factory with redis-based storage.""" @@ -828,7 +833,8 @@ def _build_redis_factory(api_key, cfg): manager, sdk_ready_flag=None, telemetry_producer=telemetry_producer, - telemetry_init_producer=telemetry_init_producer + telemetry_init_producer=telemetry_init_producer, + fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -910,7 +916,8 @@ async def _build_redis_factory_async(api_key, cfg): manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() await storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -992,7 +999,8 @@ def _build_pluggable_factory(api_key, cfg): manager, sdk_ready_flag=None, telemetry_producer=telemetry_producer, - telemetry_init_producer=telemetry_init_producer + telemetry_init_producer=telemetry_init_producer, + fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -1072,7 +1080,8 @@ async def _build_pluggable_factory_async(api_key, cfg): manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() await storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -1150,6 +1159,7 @@ def _build_localhost_factory(cfg): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=LocalhostTelemetrySubmitter(), + fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] ) async def _build_localhost_factory_async(cfg): @@ -1220,7 +1230,8 @@ async def _build_localhost_factory_async(cfg): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=LocalhostTelemetrySubmitterAsync(), - manager_start_task=manager_start_task + manager_start_task=manager_start_task, + fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] ) def get_factory(api_key, **kwargs): diff --git a/splitio/storage/adapters/redis.py b/splitio/storage/adapters/redis.py index 78d88487..4cf87b5e 100644 --- a/splitio/storage/adapters/redis.py +++ b/splitio/storage/adapters/redis.py @@ -715,7 +715,7 @@ def _build_default_client(config): # pylint: disable=too-many-locals unix_socket_path = config.get('redisUnixSocketPath', None) encoding = config.get('redisEncoding', 'utf-8') encoding_errors = config.get('redisEncodingErrors', 'strict') - errors = config.get('redisErrors', None) +# errors = config.get('redisErrors', None) decode_responses = config.get('redisDecodeResponses', True) retry_on_timeout = config.get('redisRetryOnTimeout', False) ssl = config.get('redisSsl', False) @@ -740,7 +740,7 @@ def _build_default_client(config): # pylint: disable=too-many-locals unix_socket_path=unix_socket_path, encoding=encoding, encoding_errors=encoding_errors, - errors=errors, +# errors=errors, Starting from redis 6.0.0 errors argument is removed decode_responses=decode_responses, retry_on_timeout=retry_on_timeout, ssl=ssl, diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index fbe499d6..5f5224e0 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -13,6 +13,8 @@ from splitio.storage import redis, inmemmory, pluggable from splitio.tasks.util import asynctask from splitio.engine.impressions.impressions import Manager as ImpressionsManager +from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_treatment import FallbackTreatment from splitio.sync.manager import Manager, ManagerAsync from splitio.sync.synchronizer import Synchronizer, SynchronizerAsync, SplitSynchronizers, SplitTasks from splitio.sync.split import SplitSynchronizer, SplitSynchronizerAsync @@ -94,7 +96,7 @@ def test_redis_client_creation(self, mocker): """Test that a client with redis storage is created correctly.""" strict_redis_mock = mocker.Mock() mocker.patch('splitio.storage.adapters.redis.StrictRedis', new=strict_redis_mock) - + fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on"))) config = { 'labelsEnabled': False, 'impressionListener': 123, @@ -119,7 +121,8 @@ def test_redis_client_creation(self, mocker): 'redisSslCertReqs': 'some_cert_req', 'redisSslCaCerts': 'some_ca_cert', 'redisMaxConnections': 999, - 'flagSetsFilter': ['set_1'] + 'flagSetsFilter': ['set_1'], + 'fallbackTreatmentsConfiguration': fallback_treatments_configuration } factory = get_factory('some_api_key', config=config) class TelemetrySubmitterMock(): @@ -133,6 +136,7 @@ def synchronize_config(*_): assert isinstance(factory._get_storage('events'), redis.RedisEventsStorage) assert factory._get_storage('splits').flag_set_filter.flag_sets == set([]) + assert factory._fallback_treatments_configuration == fallback_treatments_configuration adapter = factory._get_storage('splits')._redis assert adapter == factory._get_storage('segments')._redis @@ -153,7 +157,7 @@ def synchronize_config(*_): unix_socket_path='/some_path', encoding='utf-8', encoding_errors='non-strict', - errors=True, +# errors=True, decode_responses=True, retry_on_timeout=True, ssl=True, @@ -705,10 +709,13 @@ class SplitFactoryAsyncTests(object): @pytest.mark.asyncio async def test_flag_sets_counts(self): + fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on"))) factory = await get_factory_async("none", config={ 'flagSetsFilter': ['set1', 'set2', 'set3'], - 'streamEnabled': False + 'streamEnabled': False, + 'fallbackTreatmentsConfiguration': fallback_treatments_configuration }) + assert factory._fallback_treatments_configuration == fallback_treatments_configuration assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets == 3 assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets_invalid == 0 await factory.destroy() diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index 476db45e..3923ffbf 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -1,5 +1,6 @@ """Unit tests for the input_validator module.""" import pytest +import logging from splitio.client.factory import SplitFactory, get_factory, SplitFactoryAsync, get_factory_async from splitio.client.client import CONTROL, Client, _LOGGER as _logger, ClientAsync @@ -9,6 +10,7 @@ InMemorySplitStorage, InMemorySplitStorageAsync, InMemoryRuleBasedSegmentStorage, InMemoryRuleBasedSegmentStorageAsync from splitio.models.splits import Split from splitio.client import input_validator +from splitio.client.manager import SplitManager, SplitManagerAsync from splitio.recorder.recorder import StandardRecorder, StandardRecorderAsync from splitio.engine.telemetry import TelemetryStorageProducer, TelemetryStorageProducerAsync from splitio.engine.impressions.impressions import Manager as ImpressionManager diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index f8625f6a..894bba8d 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -29,6 +29,8 @@ PluggableRuleBasedSegmentsStorage, PluggableRuleBasedSegmentsStorageAsync from splitio.storage.adapters.redis import build, RedisAdapter, RedisAdapterAsync, build_async from splitio.models import splits, segments, rule_based_segments +from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_treatment import FallbackTreatment from splitio.engine.impressions.impressions import Manager as ImpressionsManager, ImpressionsMode from splitio.engine.impressions import set_classes, set_classes_async from splitio.engine.impressions.strategies import StrategyDebugMode, StrategyOptimizedMode, StrategyNoneMode @@ -196,6 +198,11 @@ def _get_treatment(factory, skip_rbs=False): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): _validate_last_impressions(client, ('prereq_feature', 'user1234', 'off_default')) + # test fallback treatment + assert client.get_treatment('user4321', 'fallback_feature') == 'on-local' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present + def _get_treatment_with_config(factory): """Test client.get_treatment_with_config().""" try: @@ -229,6 +236,11 @@ def _get_treatment_with_config(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert client.get_treatment_with_config('user4321', 'fallback_feature') == ('on-local', '{"prop": "val"}') + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present + def _get_treatments(factory): """Test client.get_treatments().""" try: @@ -267,6 +279,11 @@ def _get_treatments(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert client.get_treatments('user4321', ['fallback_feature']) == {'fallback_feature': 'on-local'} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present + def _get_treatments_with_config(factory): """Test client.get_treatments_with_config().""" try: @@ -306,6 +323,11 @@ def _get_treatments_with_config(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): _validate_last_impressions(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert client.get_treatments_with_config('user4321', ['fallback_feature']) == {'fallback_feature': ('on-local', '{"prop": "val"}')} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + _validate_last_impressions(client) # No impressions should be present + def _get_treatments_by_flag_set(factory): """Test client.get_treatments_by_flag_set().""" try: @@ -539,6 +561,7 @@ def setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -697,6 +720,7 @@ def setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init def test_get_treatment(self): @@ -819,7 +843,11 @@ def setup_method(self): 'sdk_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), 'events_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), 'auth_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), - 'config': {'connectTimeout': 10000, 'streamingEnabled': False, 'impressionsMode': 'debug'} + 'config': {'connectTimeout': 10000, + 'streamingEnabled': False, + 'impressionsMode': 'debug', + 'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + } } self.factory = get_factory('some_apikey', **kwargs) @@ -989,6 +1017,7 @@ def setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init def test_get_treatment(self): @@ -1177,6 +1206,7 @@ def setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init class LocalhostIntegrationTests(object): # pylint: disable=too-few-public-methods @@ -1400,6 +1430,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1595,6 +1626,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1789,6 +1821,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1942,6 +1975,7 @@ def test_optimized(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -1964,6 +1998,8 @@ def test_optimized(self): assert len(imps_count) == 1 assert imps_count[0].feature == 'SPLIT_3' assert imps_count[0].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' def test_debug(self): split_storage = InMemorySplitStorage() @@ -1997,6 +2033,7 @@ def test_debug(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2019,6 +2056,8 @@ def test_debug(self): assert len(imps_count) == 1 assert imps_count[0].feature == 'SPLIT_3' assert imps_count[0].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' def test_none(self): split_storage = InMemorySplitStorage() @@ -2052,6 +2091,7 @@ def test_none(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2076,6 +2116,8 @@ def test_none(self): assert imps_count[1].count == 1 assert imps_count[2].feature == 'SPLIT_3' assert imps_count[2].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' class RedisImpressionsToggleIntegrationTests(object): """Run impression toggle tests for Redis.""" @@ -2113,6 +2155,7 @@ def test_optimized(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init try: @@ -2141,6 +2184,8 @@ def test_optimized(self): assert len(imps_count) == 1 assert imps_count[0].feature == 'SPLIT_3' assert imps_count[0].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' self.clear_cache() client.destroy() @@ -2177,6 +2222,7 @@ def test_debug(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init try: @@ -2205,6 +2251,8 @@ def test_debug(self): assert len(imps_count) == 1 assert imps_count[0].feature == 'SPLIT_3' assert imps_count[0].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' self.clear_cache() client.destroy() @@ -2241,6 +2289,7 @@ def test_none(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init try: @@ -2271,6 +2320,8 @@ def test_none(self): assert imps_count[1].count == 1 assert imps_count[2].feature == 'SPLIT_3' assert imps_count[2].count == 1 + assert client.get_treatment('user1', 'incorrect_feature') == 'on-global' + assert client.get_treatment('user1', 'fallback_feature') == 'on-local' self.clear_cache() client.destroy() @@ -2342,6 +2393,7 @@ async def _setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2513,6 +2565,7 @@ async def _setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2653,7 +2706,11 @@ async def _setup_method(self): 'sdk_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), 'events_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), 'auth_api_base_url': 'http://localhost:%d/api' % self.split_backend.port(), - 'config': {'connectTimeout': 10000, 'streamingEnabled': False, 'impressionsMode': 'debug'} + 'config': {'connectTimeout': 10000, + 'streamingEnabled': False, + 'impressionsMode': 'debug', + 'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + } } self.factory = await get_factory_async('some_apikey', **kwargs) @@ -2861,7 +2918,8 @@ async def _setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3083,7 +3141,8 @@ async def _setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3317,7 +3376,8 @@ async def _setup_method(self): RedisManagerAsync(PluggableSynchronizerAsync()), telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3546,7 +3606,8 @@ async def _setup_method(self): RedisManagerAsync(PluggableSynchronizerAsync()), telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - telemetry_submitter=telemetry_submitter + telemetry_submitter=telemetry_submitter, + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() @@ -3781,6 +3842,7 @@ async def _setup_method(self): manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -4481,6 +4543,11 @@ async def _get_treatment_async(factory, skip_rbs=False): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): await _validate_last_impressions_async(client, ('regex_test', 'abc4', 'on')) + # test fallback treatment + assert await client.get_treatment('user4321', 'fallback_feature') == 'on-local' + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + await _validate_last_impressions_async(client) # No impressions should be present + if skip_rbs: return @@ -4537,6 +4604,11 @@ async def _get_treatment_with_config_async(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): await _validate_last_impressions_async(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert await client.get_treatment_with_config('user4321', 'fallback_feature') == ('on-local', '{"prop": "val"}') + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + await _validate_last_impressions_async(client) # No impressions should be present + async def _get_treatments_async(factory): """Test client.get_treatments().""" try: @@ -4575,6 +4647,11 @@ async def _get_treatments_async(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): await _validate_last_impressions_async(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert await client.get_treatments('user4321', ['fallback_feature']) == {'fallback_feature': 'on-local'} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + await _validate_last_impressions_async(client) # No impressions should be present + async def _get_treatments_with_config_async(factory): """Test client.get_treatments_with_config().""" try: @@ -4614,6 +4691,11 @@ async def _get_treatments_with_config_async(factory): if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): await _validate_last_impressions_async(client, ('all_feature', 'invalidKey', 'on')) + # test fallback treatment + assert await client.get_treatments_with_config('user4321', ['fallback_feature']) == {'fallback_feature': ('on-local', '{"prop": "val"}')} + if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): + await _validate_last_impressions_async(client) # No impressions should be present + async def _get_treatments_by_flag_set_async(factory): """Test client.get_treatments_by_flag_set().""" try: diff --git a/tests/push/test_manager.py b/tests/push/test_manager.py index c85301d8..3525baf3 100644 --- a/tests/push/test_manager.py +++ b/tests/push/test_manager.py @@ -259,7 +259,6 @@ class PushManagerAsyncTests(object): async def test_connection_success(self, mocker): """Test the initial status is ok and reset() works as expected.""" api_mock = mocker.Mock() - async def authenticate(): return Token(True, 'abc', {}, 2000000, 1000000) api_mock.authenticate.side_effect = authenticate @@ -274,8 +273,8 @@ async def coro(): t = 0 try: while t < 3: - yield SSEEvent('1', EventType.MESSAGE, '', '{}') await asyncio.sleep(1) + yield SSEEvent('1', EventType.MESSAGE, '', '{}') t += 1 except Exception: pass @@ -295,7 +294,7 @@ async def stop(): manager._sse_client = sse_mock async def deferred_shutdown(): - await asyncio.sleep(1) + await asyncio.sleep(2) await manager.stop(True) manager.start() @@ -309,7 +308,10 @@ async def deferred_shutdown(): assert self.token.exp == 2000000 assert self.token.iat == 1000000 - await shutdown_task + try: + await shutdown_task + except: + pass assert not manager._running assert(telemetry_storage._streaming_events._streaming_events[0]._type == StreamingEventTypes.TOKEN_REFRESH.value) assert(telemetry_storage._streaming_events._streaming_events[1]._type == StreamingEventTypes.CONNECTION_ESTABLISHED.value) diff --git a/tests/storage/adapters/test_redis_adapter.py b/tests/storage/adapters/test_redis_adapter.py index a6bc72dc..78d28bbc 100644 --- a/tests/storage/adapters/test_redis_adapter.py +++ b/tests/storage/adapters/test_redis_adapter.py @@ -99,7 +99,7 @@ def test_adapter_building(self, mocker): 'redisUnixSocketPath': '/tmp/socket', 'redisEncoding': 'utf-8', 'redisEncodingErrors': 'strict', - 'redisErrors': 'abc', +# 'redisErrors': 'abc', 'redisDecodeResponses': True, 'redisRetryOnTimeout': True, 'redisSsl': True, @@ -126,7 +126,7 @@ def test_adapter_building(self, mocker): unix_socket_path='/tmp/socket', encoding='utf-8', encoding_errors='strict', - errors='abc', +# errors='abc', decode_responses=True, retry_on_timeout=True, ssl=True, @@ -151,7 +151,7 @@ def test_adapter_building(self, mocker): 'redisUnixSocketPath': '/tmp/socket', 'redisEncoding': 'utf-8', 'redisEncodingErrors': 'strict', - 'redisErrors': 'abc', +# 'redisErrors': 'abc', 'redisDecodeResponses': True, 'redisRetryOnTimeout': True, 'redisSsl': False, From 0cfb0ef7a7ac691d83d6ce127bb48ac5333d5222 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 2 Sep 2025 08:32:40 -0700 Subject: [PATCH 09/26] updated regex --- splitio/client/input_validator.py | 2 +- tests/client/test_input_validator.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index 2367816d..d732ba21 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -16,7 +16,7 @@ EVENT_TYPE_PATTERN = r'^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$' MAX_PROPERTIES_LENGTH_BYTES = 32768 _FLAG_SETS_REGEX = '^[a-z0-9][_a-z0-9]{0,49}$' -_FALLBACK_TREATMENT_REGEX = '^[a-zA-Z][a-zA-Z0-9-_;]+$' +_FALLBACK_TREATMENT_REGEX = '^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$' _FALLBACK_TREATMENT_SIZE = 100 def _check_not_null(value, name, operation): diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index 3923ffbf..144f2160 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -1645,19 +1645,19 @@ def test_fallback_treatments(self, mocker): _logger.reset_mock() assert not input_validator.validate_fallback_treatment(FallbackTreatment("on/c")) assert _logger.warning.mock_calls == [ - mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$") + mocker.call("Config: Fallback treatment should match regex %s", "^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$") ] _logger.reset_mock() assert not input_validator.validate_fallback_treatment(FallbackTreatment("9on")) assert _logger.warning.mock_calls == [ - mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$") + mocker.call("Config: Fallback treatment should match regex %s", "^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$") ] _logger.reset_mock() assert not input_validator.validate_fallback_treatment(FallbackTreatment("on$as")) assert _logger.warning.mock_calls == [ - mocker.call("Config: Fallback treatment should match regex %s", "^[a-zA-Z][a-zA-Z0-9-_;]+$") + mocker.call("Config: Fallback treatment should match regex %s", "^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$") ] assert input_validator.validate_fallback_treatment(FallbackTreatment("on_c")) From 52a29672c77064047a14993359bccb060e068c02 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 2 Sep 2025 13:16:20 -0700 Subject: [PATCH 10/26] Removed FallbackConfig object and updated config parameter name --- splitio/client/config.py | 52 ++++++++++++---------------- splitio/client/input_validator.py | 5 +++ splitio/models/fallback_config.py | 27 +-------------- splitio/models/fallback_treatment.py | 2 +- tests/client/test_config.py | 44 +++++++++++------------ tests/models/test_fallback.py | 6 ++-- 6 files changed, 55 insertions(+), 81 deletions(-) diff --git a/splitio/client/config.py b/splitio/client/config.py index 0d77678e..316ac96f 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -5,7 +5,7 @@ from splitio.engine.impressions import ImpressionsMode from splitio.client.input_validator import validate_flag_sets, validate_fallback_treatment, validate_regex_name -from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration _LOGGER = logging.getLogger(__name__) DEFAULT_DATA_SAMPLING = 1 @@ -71,7 +71,7 @@ class AuthenticateScheme(Enum): 'httpAuthenticateScheme': AuthenticateScheme.NONE, 'kerberosPrincipalUser': None, 'kerberosPrincipalPassword': None, - 'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(None) + 'fallbackTreatments': FallbackTreatmentsConfiguration(None) } def _parse_operation_mode(sdk_key, config): @@ -175,32 +175,26 @@ def sanitize(sdk_key, config): return processed def _sanitize_fallback_config(config, processed): - if config.get('fallbackTreatmentsConfiguration') is not None: - if not isinstance(config['fallbackTreatmentsConfiguration'], FallbackTreatmentsConfiguration): - _LOGGER.warning('Config: fallbackTreatmentsConfiguration parameter should be of `FallbackTreatmentsConfiguration` class.') - processed['fallbackTreatmentsConfiguration'] = FallbackTreatmentsConfiguration(None) + if config.get('fallbackTreatments') is not None: + if not isinstance(config['fallbackTreatments'], FallbackTreatmentsConfiguration): + _LOGGER.warning('Config: fallbackTreatments parameter should be of `FallbackTreatmentsConfiguration` class.') + processed['fallbackTreatments'] = None return processed - - if config['fallbackTreatmentsConfiguration'].fallback_config != None: - if not isinstance(config['fallbackTreatmentsConfiguration'].fallback_config, FallbackConfig): - _LOGGER.warning('Config: fallback_config parameter should be of `FallbackConfig` class.') - processed['fallbackTreatmentsConfiguration'].fallback_config = FallbackConfig(None, None) - return processed - - if config['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment is not None and not validate_fallback_treatment(config['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment): - _LOGGER.warning('Config: global fallbacktreatment parameter is discarded.') - processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment = None - return processed - - if config['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment is not None: - sanitized_flag_fallback_treatments = {} - for feature_name in config['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment.keys(): - if not validate_regex_name(feature_name) or not validate_fallback_treatment(config['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment[feature_name]): - _LOGGER.warning('Config: fallback treatment parameter for feature flag %s is discarded.', feature_name) - continue - - sanitized_flag_fallback_treatments[feature_name] = config['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment[feature_name] - - processed['fallbackTreatmentsConfiguration'].fallback_config = FallbackConfig(config['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment, sanitized_flag_fallback_treatments) - + + sanitized_global_fallback_treatment = config['fallbackTreatments'].global_fallback_treatment + if config['fallbackTreatments'].global_fallback_treatment is not None and not validate_fallback_treatment(config['fallbackTreatments'].global_fallback_treatment): + _LOGGER.warning('Config: global fallbacktreatment parameter is discarded.') + sanitized_global_fallback_treatment = None + + sanitized_flag_fallback_treatments = {} + if config['fallbackTreatments'].by_flag_fallback_treatment is not None: + for feature_name in config['fallbackTreatments'].by_flag_fallback_treatment.keys(): + if not validate_regex_name(feature_name) or not validate_fallback_treatment(config['fallbackTreatments'].by_flag_fallback_treatment[feature_name]): + _LOGGER.warning('Config: fallback treatment parameter for feature flag %s is discarded.', feature_name) + continue + + sanitized_flag_fallback_treatments[feature_name] = config['fallbackTreatments'].by_flag_fallback_treatment[feature_name] + + processed['fallbackTreatments'] = FallbackTreatmentsConfiguration(sanitized_global_fallback_treatment, sanitized_flag_fallback_treatments) + return processed \ No newline at end of file diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index 51b8b0d2..90d1028f 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -8,6 +8,7 @@ from splitio.client.key import Key from splitio.client import client from splitio.engine.evaluator import CONTROL +from splitio.models.fallback_treatment import FallbackTreatment _LOGGER = logging.getLogger(__name__) @@ -715,6 +716,10 @@ def validate_flag_sets(flag_sets, method_name): return list(sanitized_flag_sets) def validate_fallback_treatment(fallback_treatment): + if not isinstance(fallback_treatment, FallbackTreatment): + _LOGGER.warning("Config: Fallback treatment instance should be FallbackTreatment, input is discarded") + return False + if not validate_regex_name(fallback_treatment.treatment): _LOGGER.warning("Config: Fallback treatment should match regex %s", _FALLBACK_TREATMENT_REGEX) return False diff --git a/splitio/models/fallback_config.py b/splitio/models/fallback_config.py index 6e84d62f..14b00dda 100644 --- a/splitio/models/fallback_config.py +++ b/splitio/models/fallback_config.py @@ -1,32 +1,7 @@ """Segment module.""" class FallbackTreatmentsConfiguration(object): - """FallbackConfiguration object class.""" - - def __init__(self, fallback_config): - """ - Class constructor. - - :param fallback_config: fallback config object. - :type fallback_config: FallbackConfig - - :param by_flag_fallback_treatment: Dict of flags and their fallback treatment - :type by_flag_fallback_treatment: {str: FallbackTreatment} - """ - self._fallback_config = fallback_config - - @property - def fallback_config(self): - """Return fallback config.""" - return self._fallback_config - - @fallback_config.setter - def fallback_config(self, new_value): - """Set fallback config.""" - self._fallback_config = new_value - -class FallbackConfig(object): - """FallbackConfig object class.""" + """FallbackTreatmentsConfiguration object class.""" def __init__(self, global_fallback_treatment=None, by_flag_fallback_treatment=None): """ diff --git a/splitio/models/fallback_treatment.py b/splitio/models/fallback_treatment.py index c8e60001..c8374f09 100644 --- a/splitio/models/fallback_treatment.py +++ b/splitio/models/fallback_treatment.py @@ -2,7 +2,7 @@ import json class FallbackTreatment(object): - """Segment object class.""" + """FallbackTreatment object class.""" def __init__(self, treatment, config=None): """ diff --git a/tests/client/test_config.py b/tests/client/test_config.py index 7be3c383..76164016 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -4,7 +4,7 @@ from splitio.client import config from splitio.engine.impressions.impressions import ImpressionsMode from splitio.models.fallback_treatment import FallbackTreatment -from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration class ConfigSanitizationTests(object): """Inmemory storage-based integration tests.""" @@ -92,34 +92,34 @@ def test_sanitize(self, mocker): assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.NONE _logger.reset_mock() - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': 'NONE'}) - assert processed['fallbackTreatmentsConfiguration'].fallback_config == None - assert _logger.warning.mock_calls[1] == mocker.call("Config: fallbackTreatmentsConfiguration parameter should be of `FallbackTreatmentsConfiguration` class.") + processed = config.sanitize('some', {'fallbackTreatments': 'NONE'}) + assert processed['fallbackTreatments'] == None + assert _logger.warning.mock_calls[1] == mocker.call("Config: fallbackTreatments parameter should be of `FallbackTreatmentsConfiguration` class.") _logger.reset_mock() - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(123)}) - assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment == None - assert _logger.warning.mock_calls[1] == mocker.call("Config: fallback_config parameter should be of `FallbackConfig` class.") + processed = config.sanitize('some', {'fallbackTreatments': FallbackTreatmentsConfiguration(123)}) + assert processed['fallbackTreatments'].global_fallback_treatment == None + assert _logger.warning.mock_calls[1] == mocker.call("Config: global fallbacktreatment parameter is discarded.") _logger.reset_mock() - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("123")))}) - assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment == None + processed = config.sanitize('some', {'fallbackTreatments': FallbackTreatmentsConfiguration(FallbackTreatment("123"))}) + assert processed['fallbackTreatments'].global_fallback_treatment == None assert _logger.warning.mock_calls[1] == mocker.call("Config: global fallbacktreatment parameter is discarded.") - fb = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment('on'))) - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': fb}) - assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment.treatment == fb.fallback_config.global_fallback_treatment.treatment - assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment.label_prefix == "fallback - " + fb = FallbackTreatmentsConfiguration(FallbackTreatment('on')) + processed = config.sanitize('some', {'fallbackTreatments': fb}) + assert processed['fallbackTreatments'].global_fallback_treatment.treatment == fb.global_fallback_treatment.treatment + assert processed['fallbackTreatments'].global_fallback_treatment.label_prefix == "fallback - " - fb = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment('on'), {"flag": FallbackTreatment("off")})) - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': fb}) - assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment.treatment == fb.fallback_config.global_fallback_treatment.treatment - assert processed['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment["flag"] == fb.fallback_config.by_flag_fallback_treatment["flag"] - assert processed['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment["flag"].label_prefix == "fallback - " + fb = FallbackTreatmentsConfiguration(FallbackTreatment('on'), {"flag": FallbackTreatment("off")}) + processed = config.sanitize('some', {'fallbackTreatments': fb}) + assert processed['fallbackTreatments'].global_fallback_treatment.treatment == fb.global_fallback_treatment.treatment + assert processed['fallbackTreatments'].by_flag_fallback_treatment["flag"] == fb.by_flag_fallback_treatment["flag"] + assert processed['fallbackTreatments'].by_flag_fallback_treatment["flag"].label_prefix == "fallback - " _logger.reset_mock() - fb = FallbackTreatmentsConfiguration(FallbackConfig(None, {"flag#%": FallbackTreatment("off"), "flag2": FallbackTreatment("on")})) - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': fb}) - assert len(processed['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment) == 1 - assert processed['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment.get("flag2") == fb.fallback_config.by_flag_fallback_treatment["flag2"] + fb = FallbackTreatmentsConfiguration(None, {"flag#%": FallbackTreatment("off"), "flag2": FallbackTreatment("on")}) + processed = config.sanitize('some', {'fallbackTreatments': fb}) + assert len(processed['fallbackTreatments'].by_flag_fallback_treatment) == 1 + assert processed['fallbackTreatments'].by_flag_fallback_treatment.get("flag2") == fb.by_flag_fallback_treatment["flag2"] assert _logger.warning.mock_calls[1] == mocker.call('Config: fallback treatment parameter for feature flag %s is discarded.', 'flag#%') \ No newline at end of file diff --git a/tests/models/test_fallback.py b/tests/models/test_fallback.py index b326fd6f..a3111277 100644 --- a/tests/models/test_fallback.py +++ b/tests/models/test_fallback.py @@ -1,5 +1,5 @@ from splitio.models.fallback_treatment import FallbackTreatment -from splitio.models.fallback_config import FallbackConfig +from splitio.models.fallback_config import FallbackTreatmentsConfiguration class FallbackTreatmentModelTests(object): """Fallback treatment model tests.""" @@ -13,13 +13,13 @@ def test_working(self): assert fallback_treatment.config == None assert fallback_treatment.treatment == 'off' -class FallbackConfigModelTests(object): +class FallbackTreatmentsConfigModelTests(object): """Fallback treatment model tests.""" def test_working(self): global_fb = FallbackTreatment("on") flag_fb = FallbackTreatment("off") - fallback_config = FallbackConfig(global_fb, {"flag1": flag_fb}) + fallback_config = FallbackTreatmentsConfiguration(global_fb, {"flag1": flag_fb}) assert fallback_config.global_fallback_treatment == global_fb assert fallback_config.by_flag_fallback_treatment == {"flag1": flag_fb} From 714b4ba0df85583fad4fe25569ff1c26df8dd654 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 2 Sep 2025 13:29:17 -0700 Subject: [PATCH 11/26] updated evaluator --- splitio/engine/evaluator.py | 20 ++++++++++---------- tests/engine/test_evaluator.py | 12 ++++++------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/splitio/engine/evaluator.py b/splitio/engine/evaluator.py index 8088b450..4b37229c 100644 --- a/splitio/engine/evaluator.py +++ b/splitio/engine/evaluator.py @@ -74,21 +74,21 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx): } def _get_fallback_treatment_and_label(self, feature_name, treatment, label): - if self._fallback_treatments_configuration == None or self._fallback_treatments_configuration.fallback_config == None: + if self._fallback_treatments_configuration == None: return label, treatment, None - if self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment != None and \ - self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name) != None: + if self._fallback_treatments_configuration.by_flag_fallback_treatment != None and \ + self._fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name) != None: _LOGGER.debug('Using Fallback Treatment for feature: %s', feature_name) - return self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).label_prefix + label, \ - self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).treatment, \ - self._fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).config + return self._fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).label_prefix + label, \ + self._fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).treatment, \ + self._fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).config - if self._fallback_treatments_configuration.fallback_config.global_fallback_treatment != None: + if self._fallback_treatments_configuration.global_fallback_treatment != None: _LOGGER.debug('Using Global Fallback Treatment.') - return self._fallback_treatments_configuration.fallback_config.global_fallback_treatment.label_prefix + label, \ - self._fallback_treatments_configuration.fallback_config.global_fallback_treatment.treatment, \ - self._fallback_treatments_configuration.fallback_config.global_fallback_treatment.config + return self._fallback_treatments_configuration.global_fallback_treatment.label_prefix + label, \ + self._fallback_treatments_configuration.global_fallback_treatment.treatment, \ + self._fallback_treatments_configuration.global_fallback_treatment.config return label, treatment, None diff --git a/tests/engine/test_evaluator.py b/tests/engine/test_evaluator.py index e95a8710..ba51f901 100644 --- a/tests/engine/test_evaluator.py +++ b/tests/engine/test_evaluator.py @@ -12,7 +12,7 @@ from splitio.models.grammar import condition from splitio.models import rule_based_segments from splitio.models.fallback_treatment import FallbackTreatment -from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration from splitio.engine import evaluator, splitters from splitio.engine.evaluator import EvaluationContext from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, InMemoryRuleBasedSegmentStorage, \ @@ -384,7 +384,7 @@ def test_evaluate_treatment_with_fallback(self, mocker): # should use global fallback logger_mock.reset_mock() - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("off-global", {"prop":"val"})))) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackTreatment("off-global", {"prop":"val"}))) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx) assert result['treatment'] == 'off-global' assert result['configurations'] == '{"prop": "val"}' @@ -394,7 +394,7 @@ def test_evaluate_treatment_with_fallback(self, mocker): # should use by flag fallback logger_mock.reset_mock() - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})}))) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx) assert result['treatment'] == 'off-some2' assert result['configurations'] == '{"prop2": "val2"}' @@ -402,21 +402,21 @@ def test_evaluate_treatment_with_fallback(self, mocker): assert logger_mock.debug.mock_calls[0] == mocker.call("Using Fallback Treatment for feature: %s", "some2") # should not use any fallback - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})}))) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some3', {}, ctx) assert result['treatment'] == 'control' assert result['configurations'] == None assert result['impression']['label'] == Label.SPLIT_NOT_FOUND # should use by flag fallback - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})}))) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx) assert result['treatment'] == 'off-some2' assert result['configurations'] == '{"prop2": "val2"}' assert result['impression']['label'] == "fallback - " + Label.SPLIT_NOT_FOUND # should global flag fallback - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})}))) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some3', {}, ctx) assert result['treatment'] == 'off-global' assert result['configurations'] == '{"prop": "val"}' From 44110b031e1623d6c45f79789d5280da6a3ded50 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 2 Sep 2025 14:40:24 -0700 Subject: [PATCH 12/26] polishing --- splitio/client/client.py | 7 ++-- splitio/client/config.py | 52 ++++++++++++---------------- splitio/client/factory.py | 18 +++++----- splitio/client/input_validator.py | 9 +++++ splitio/client/util.py | 20 +++++------ splitio/models/fallback_config.py | 27 +-------------- tests/client/test_client.py | 50 +++++++++++++------------- tests/client/test_config.py | 42 +++++++++++----------- tests/client/test_factory.py | 14 ++++---- tests/client/test_input_validator.py | 6 ---- tests/engine/test_evaluator.py | 12 +++---- tests/integration/test_client_e2e.py | 46 ++++++++++++------------ tests/models/test_fallback.py | 6 ++-- 13 files changed, 143 insertions(+), 166 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index ec3c9260..9a33e67c 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -219,6 +219,9 @@ def _get_fallback_eval_results(self, eval_result, feature): feature, result["treatment"], result["impression"]["label"], _LOGGER) return result + def _check_impression_label(self, result): + return result['impression']['label'] == None or (result['impression']['label'] != None and result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1) + class Client(ClientBase): # pylint: disable=too-many-instance-attributes """Entry point for the split sdk.""" @@ -344,7 +347,7 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio result = self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, feature) properties = self._get_properties(evaluation_options) - if result['impression']['label'] == None or (result['impression']['label'] != None and result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1): + if self._check_impression_label(result): impression_decorated = self._build_impression(key, bucketing, feature, result, properties) self._record_stats([(impression_decorated, attributes)], start, method) @@ -844,7 +847,7 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation result = self._get_fallback_eval_results(self._FAILED_EVAL_RESULT, feature) properties = self._get_properties(evaluation_options) - if result['impression']['label'] == None or (result['impression']['label'] != None and result['impression']['label'].find(Label.SPLIT_NOT_FOUND) == -1): + if self._check_impression_label(result): impression_decorated = self._build_impression(key, bucketing, feature, result, properties) await self._record_stats([(impression_decorated, attributes)], start, method) return result['treatment'], result['configurations'] diff --git a/splitio/client/config.py b/splitio/client/config.py index 0d77678e..316ac96f 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -5,7 +5,7 @@ from splitio.engine.impressions import ImpressionsMode from splitio.client.input_validator import validate_flag_sets, validate_fallback_treatment, validate_regex_name -from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration _LOGGER = logging.getLogger(__name__) DEFAULT_DATA_SAMPLING = 1 @@ -71,7 +71,7 @@ class AuthenticateScheme(Enum): 'httpAuthenticateScheme': AuthenticateScheme.NONE, 'kerberosPrincipalUser': None, 'kerberosPrincipalPassword': None, - 'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(None) + 'fallbackTreatments': FallbackTreatmentsConfiguration(None) } def _parse_operation_mode(sdk_key, config): @@ -175,32 +175,26 @@ def sanitize(sdk_key, config): return processed def _sanitize_fallback_config(config, processed): - if config.get('fallbackTreatmentsConfiguration') is not None: - if not isinstance(config['fallbackTreatmentsConfiguration'], FallbackTreatmentsConfiguration): - _LOGGER.warning('Config: fallbackTreatmentsConfiguration parameter should be of `FallbackTreatmentsConfiguration` class.') - processed['fallbackTreatmentsConfiguration'] = FallbackTreatmentsConfiguration(None) + if config.get('fallbackTreatments') is not None: + if not isinstance(config['fallbackTreatments'], FallbackTreatmentsConfiguration): + _LOGGER.warning('Config: fallbackTreatments parameter should be of `FallbackTreatmentsConfiguration` class.') + processed['fallbackTreatments'] = None return processed - - if config['fallbackTreatmentsConfiguration'].fallback_config != None: - if not isinstance(config['fallbackTreatmentsConfiguration'].fallback_config, FallbackConfig): - _LOGGER.warning('Config: fallback_config parameter should be of `FallbackConfig` class.') - processed['fallbackTreatmentsConfiguration'].fallback_config = FallbackConfig(None, None) - return processed - - if config['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment is not None and not validate_fallback_treatment(config['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment): - _LOGGER.warning('Config: global fallbacktreatment parameter is discarded.') - processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment = None - return processed - - if config['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment is not None: - sanitized_flag_fallback_treatments = {} - for feature_name in config['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment.keys(): - if not validate_regex_name(feature_name) or not validate_fallback_treatment(config['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment[feature_name]): - _LOGGER.warning('Config: fallback treatment parameter for feature flag %s is discarded.', feature_name) - continue - - sanitized_flag_fallback_treatments[feature_name] = config['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment[feature_name] - - processed['fallbackTreatmentsConfiguration'].fallback_config = FallbackConfig(config['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment, sanitized_flag_fallback_treatments) - + + sanitized_global_fallback_treatment = config['fallbackTreatments'].global_fallback_treatment + if config['fallbackTreatments'].global_fallback_treatment is not None and not validate_fallback_treatment(config['fallbackTreatments'].global_fallback_treatment): + _LOGGER.warning('Config: global fallbacktreatment parameter is discarded.') + sanitized_global_fallback_treatment = None + + sanitized_flag_fallback_treatments = {} + if config['fallbackTreatments'].by_flag_fallback_treatment is not None: + for feature_name in config['fallbackTreatments'].by_flag_fallback_treatment.keys(): + if not validate_regex_name(feature_name) or not validate_fallback_treatment(config['fallbackTreatments'].by_flag_fallback_treatment[feature_name]): + _LOGGER.warning('Config: fallback treatment parameter for feature flag %s is discarded.', feature_name) + continue + + sanitized_flag_fallback_treatments[feature_name] = config['fallbackTreatments'].by_flag_fallback_treatment[feature_name] + + processed['fallbackTreatments'] = FallbackTreatmentsConfiguration(sanitized_global_fallback_treatment, sanitized_flag_fallback_treatments) + return processed \ No newline at end of file diff --git a/splitio/client/factory.py b/splitio/client/factory.py index 57d194ab..e06d6cf9 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -628,7 +628,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl return SplitFactory(api_key, storages, cfg['labelsEnabled'], recorder, manager, None, telemetry_producer, telemetry_init_producer, telemetry_submitter, preforked_initialization=preforked_initialization, - fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration']) + fallback_treatments_configuration=cfg['fallbackTreatments']) initialization_thread = threading.Thread(target=manager.start, name="SDKInitializer", daemon=True) initialization_thread.start() @@ -636,7 +636,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl return SplitFactory(api_key, storages, cfg['labelsEnabled'], recorder, manager, sdk_ready_flag, telemetry_producer, telemetry_init_producer, - telemetry_submitter, fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration']) + telemetry_submitter, fallback_treatments_configuration=cfg['fallbackTreatments']) async def _build_in_memory_factory_async(api_key, cfg, sdk_url=None, events_url=None, # pylint:disable=too-many-arguments,too-many-localsa auth_api_base_url=None, streaming_api_base_url=None, telemetry_api_base_url=None, @@ -755,7 +755,7 @@ async def _build_in_memory_factory_async(api_key, cfg, sdk_url=None, events_url= recorder, manager, telemetry_producer, telemetry_init_producer, telemetry_submitter, manager_start_task=manager_start_task, - api_client=http_client, fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration']) + api_client=http_client, fallback_treatments_configuration=cfg['fallbackTreatments']) def _build_redis_factory(api_key, cfg): """Build and return a split factory with redis-based storage.""" @@ -834,7 +834,7 @@ def _build_redis_factory(api_key, cfg): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, - fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] + fallback_treatments_configuration=cfg['fallbackTreatments'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -917,7 +917,7 @@ async def _build_redis_factory_async(api_key, cfg): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] + fallback_treatments_configuration=cfg['fallbackTreatments'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() await storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -1000,7 +1000,7 @@ def _build_pluggable_factory(api_key, cfg): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, - fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] + fallback_treatments_configuration=cfg['fallbackTreatments'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -1081,7 +1081,7 @@ async def _build_pluggable_factory_async(api_key, cfg): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] + fallback_treatments_configuration=cfg['fallbackTreatments'] ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() await storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -1159,7 +1159,7 @@ def _build_localhost_factory(cfg): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=LocalhostTelemetrySubmitter(), - fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] + fallback_treatments_configuration=cfg['fallbackTreatments'] ) async def _build_localhost_factory_async(cfg): @@ -1231,7 +1231,7 @@ async def _build_localhost_factory_async(cfg): telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=LocalhostTelemetrySubmitterAsync(), manager_start_task=manager_start_task, - fallback_treatments_configuration=cfg['fallbackTreatmentsConfiguration'] + fallback_treatments_configuration=cfg['fallbackTreatments'] ) def get_factory(api_key, **kwargs): diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index d732ba21..aaaf8026 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -9,6 +9,7 @@ from splitio.client import client from splitio.client.util import get_fallback_treatment_and_label from splitio.engine.evaluator import CONTROL +from splitio.models.fallback_treatment import FallbackTreatment _LOGGER = logging.getLogger(__name__) @@ -722,6 +723,14 @@ def validate_flag_sets(flag_sets, method_name): return list(sanitized_flag_sets) def validate_fallback_treatment(fallback_treatment): + if not isinstance(fallback_treatment, FallbackTreatment): + _LOGGER.warning("Config: Fallback treatment instance should be FallbackTreatment, input is discarded") + return False + + if not isinstance(fallback_treatment.treatment, str): + _LOGGER.warning("Config: Fallback treatment value should be str type, input is discarded") + return False + if not validate_regex_name(fallback_treatment.treatment): _LOGGER.warning("Config: Fallback treatment should match regex %s", _FALLBACK_TREATMENT_REGEX) return False diff --git a/splitio/client/util.py b/splitio/client/util.py index 6541b9df..1f01de3f 100644 --- a/splitio/client/util.py +++ b/splitio/client/util.py @@ -53,20 +53,20 @@ def get_metadata(config): return SdkMetadata(version, hostname, ip_address) def get_fallback_treatment_and_label(fallback_treatments_configuration, feature_name, treatment, label, _logger): - if fallback_treatments_configuration == None or fallback_treatments_configuration.fallback_config == None: + if fallback_treatments_configuration == None: return label, treatment, None - if fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment != None and \ - fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name) != None: + if fallback_treatments_configuration.by_flag_fallback_treatment != None and \ + fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name) != None: _logger.debug('Using Fallback Treatment for feature: %s', feature_name) - return fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).label_prefix + label, \ - fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).treatment, \ - fallback_treatments_configuration.fallback_config.by_flag_fallback_treatment.get(feature_name).config + return fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).label_prefix + label, \ + fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).treatment, \ + fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).config - if fallback_treatments_configuration.fallback_config.global_fallback_treatment != None: + if fallback_treatments_configuration.global_fallback_treatment != None: _logger.debug('Using Global Fallback Treatment.') - return fallback_treatments_configuration.fallback_config.global_fallback_treatment.label_prefix + label, \ - fallback_treatments_configuration.fallback_config.global_fallback_treatment.treatment, \ - fallback_treatments_configuration.fallback_config.global_fallback_treatment.config + return fallback_treatments_configuration.global_fallback_treatment.label_prefix + label, \ + fallback_treatments_configuration.global_fallback_treatment.treatment, \ + fallback_treatments_configuration.global_fallback_treatment.config return label, treatment, None diff --git a/splitio/models/fallback_config.py b/splitio/models/fallback_config.py index 6e84d62f..14b00dda 100644 --- a/splitio/models/fallback_config.py +++ b/splitio/models/fallback_config.py @@ -1,32 +1,7 @@ """Segment module.""" class FallbackTreatmentsConfiguration(object): - """FallbackConfiguration object class.""" - - def __init__(self, fallback_config): - """ - Class constructor. - - :param fallback_config: fallback config object. - :type fallback_config: FallbackConfig - - :param by_flag_fallback_treatment: Dict of flags and their fallback treatment - :type by_flag_fallback_treatment: {str: FallbackTreatment} - """ - self._fallback_config = fallback_config - - @property - def fallback_config(self): - """Return fallback config.""" - return self._fallback_config - - @fallback_config.setter - def fallback_config(self, new_value): - """Set fallback config.""" - self._fallback_config = new_value - -class FallbackConfig(object): - """FallbackConfig object class.""" + """FallbackTreatmentsConfiguration object class.""" def __init__(self, global_fallback_treatment=None, by_flag_fallback_treatment=None): """ diff --git a/tests/client/test_client.py b/tests/client/test_client.py index ab790214..75f46464 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -9,7 +9,7 @@ from splitio.client.client import Client, _LOGGER as _logger, CONTROL, ClientAsync, EvaluationOptions from splitio.client.factory import SplitFactory, Status as FactoryStatus, SplitFactoryAsync -from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration from splitio.models.fallback_treatment import FallbackTreatment from splitio.models.impressions import Impression, Label from splitio.models.events import Event, EventWrapper @@ -1419,7 +1419,7 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global", {"prop":"val"})))) + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global", {"prop":"val"}))) def get_feature_flag_names_by_flag_sets(*_): return ["some", "some2"] @@ -1446,7 +1446,7 @@ def get_feature_flag_names_by_flag_sets(*_): assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global", {"prop":"val"}), {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global", {"prop":"val"}), {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") @@ -1475,7 +1475,7 @@ def get_feature_flag_names_by_flag_sets(*_): assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local", {"prop":"val"})})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local", {"prop":"val"})}) treatment = client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") @@ -1504,7 +1504,7 @@ def get_feature_flag_names_by_flag_sets(*_): assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) treatment = client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps[0].treatment == "control") @@ -1557,25 +1557,25 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global")))) + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global"))) treatment = client.get_treatment("key", "some") assert(treatment == "on-global") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) treatment = client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps == None) @@ -1625,7 +1625,7 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global")))) + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global"))) client.ready = False treatment = client.get_treatment("key", "some") @@ -1633,21 +1633,21 @@ def synchronize_config(*_): assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) treatment = client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps[0].treatment == "control") @@ -2914,7 +2914,7 @@ class TelemetrySubmitterMock(): async def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = ClientAsync(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global", {"prop":"val"})))) + client = ClientAsync(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global", {"prop":"val"}))) async def get_feature_flag_names_by_flag_sets(*_): return ["some", "some2"] @@ -2949,7 +2949,7 @@ async def fetch_many_rbs(*_): assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global", {"prop":"val"}), {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global", {"prop":"val"}), {'some': FallbackTreatment("on-local")}) treatment = await client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") @@ -2978,7 +2978,7 @@ async def fetch_many_rbs(*_): assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local", {"prop":"val"})})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local", {"prop":"val"})}) treatment = await client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") @@ -3007,7 +3007,7 @@ async def fetch_many_rbs(*_): assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) treatment = await client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps[0].treatment == "control") @@ -3061,25 +3061,25 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global")))) + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global"))) treatment = client.get_treatment("key", "some") assert(treatment == "on-global") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) treatment = client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps == None) @@ -3130,7 +3130,7 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global")))) + client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global"))) client.ready = False treatment = client.get_treatment("key", "some") @@ -3138,21 +3138,21 @@ def synchronize_config(*_): assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")}) treatment = client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(None, {'some2': FallbackTreatment("on-local")})) + client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) treatment = client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps[0].treatment == "control") diff --git a/tests/client/test_config.py b/tests/client/test_config.py index cbe8ffcd..0017938c 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -4,7 +4,7 @@ from splitio.client import config from splitio.engine.impressions.impressions import ImpressionsMode from splitio.models.fallback_treatment import FallbackTreatment -from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration class ConfigSanitizationTests(object): """Inmemory storage-based integration tests.""" @@ -92,32 +92,34 @@ def test_sanitize(self, mocker): assert processed['httpAuthenticateScheme'] is config.AuthenticateScheme.NONE _logger.reset_mock() - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': 'NONE'}) - assert processed['fallbackTreatmentsConfiguration'].fallback_config == None - assert _logger.warning.mock_calls[1] == mocker.call("Config: fallbackTreatmentsConfiguration parameter should be of `FallbackTreatmentsConfiguration` class.") + processed = config.sanitize('some', {'fallbackTreatments': 'NONE'}) + assert processed['fallbackTreatments'] == None + assert _logger.warning.mock_calls[1] == mocker.call("Config: fallbackTreatments parameter should be of `FallbackTreatmentsConfiguration` class.") _logger.reset_mock() - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(123)}) - assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment == None - assert _logger.warning.mock_calls[1] == mocker.call("Config: fallback_config parameter should be of `FallbackConfig` class.") + processed = config.sanitize('some', {'fallbackTreatments': FallbackTreatmentsConfiguration(123)}) + assert processed['fallbackTreatments'].global_fallback_treatment == None + assert _logger.warning.mock_calls[1] == mocker.call("Config: global fallbacktreatment parameter is discarded.") _logger.reset_mock() - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("123")))}) - assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment == None + processed = config.sanitize('some', {'fallbackTreatments': FallbackTreatmentsConfiguration(FallbackTreatment(123))}) + assert processed['fallbackTreatments'].global_fallback_treatment == None assert _logger.warning.mock_calls[1] == mocker.call("Config: global fallbacktreatment parameter is discarded.") - fb = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment('on'))) - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': fb}) - assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment.treatment == fb.fallback_config.global_fallback_treatment.treatment + fb = FallbackTreatmentsConfiguration(FallbackTreatment('on')) + processed = config.sanitize('some', {'fallbackTreatments': fb}) + assert processed['fallbackTreatments'].global_fallback_treatment.treatment == fb.global_fallback_treatment.treatment + assert processed['fallbackTreatments'].global_fallback_treatment.label_prefix == "fallback - " - fb = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment('on'), {"flag": FallbackTreatment("off")})) - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': fb}) - assert processed['fallbackTreatmentsConfiguration'].fallback_config.global_fallback_treatment.treatment == fb.fallback_config.global_fallback_treatment.treatment - assert processed['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment["flag"] == fb.fallback_config.by_flag_fallback_treatment["flag"] + fb = FallbackTreatmentsConfiguration(FallbackTreatment('on'), {"flag": FallbackTreatment("off")}) + processed = config.sanitize('some', {'fallbackTreatments': fb}) + assert processed['fallbackTreatments'].global_fallback_treatment.treatment == fb.global_fallback_treatment.treatment + assert processed['fallbackTreatments'].by_flag_fallback_treatment["flag"] == fb.by_flag_fallback_treatment["flag"] + assert processed['fallbackTreatments'].by_flag_fallback_treatment["flag"].label_prefix == "fallback - " _logger.reset_mock() - fb = FallbackTreatmentsConfiguration(FallbackConfig(None, {"flag#%": FallbackTreatment("off"), "flag2": FallbackTreatment("on")})) - processed = config.sanitize('some', {'fallbackTreatmentsConfiguration': fb}) - assert len(processed['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment) == 1 - assert processed['fallbackTreatmentsConfiguration'].fallback_config.by_flag_fallback_treatment.get("flag2") == fb.fallback_config.by_flag_fallback_treatment["flag2"] + fb = FallbackTreatmentsConfiguration(None, {"flag#%": FallbackTreatment("off"), "flag2": FallbackTreatment("on")}) + processed = config.sanitize('some', {'fallbackTreatments': fb}) + assert len(processed['fallbackTreatments'].by_flag_fallback_treatment) == 1 + assert processed['fallbackTreatments'].by_flag_fallback_treatment.get("flag2") == fb.by_flag_fallback_treatment["flag2"] assert _logger.warning.mock_calls[1] == mocker.call('Config: fallback treatment parameter for feature flag %s is discarded.', 'flag#%') \ No newline at end of file diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index 5f5224e0..86e13088 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -13,7 +13,7 @@ from splitio.storage import redis, inmemmory, pluggable from splitio.tasks.util import asynctask from splitio.engine.impressions.impressions import Manager as ImpressionsManager -from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration from splitio.models.fallback_treatment import FallbackTreatment from splitio.sync.manager import Manager, ManagerAsync from splitio.sync.synchronizer import Synchronizer, SynchronizerAsync, SplitSynchronizers, SplitTasks @@ -96,7 +96,7 @@ def test_redis_client_creation(self, mocker): """Test that a client with redis storage is created correctly.""" strict_redis_mock = mocker.Mock() mocker.patch('splitio.storage.adapters.redis.StrictRedis', new=strict_redis_mock) - fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on"))) + fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on")) config = { 'labelsEnabled': False, 'impressionListener': 123, @@ -122,7 +122,7 @@ def test_redis_client_creation(self, mocker): 'redisSslCaCerts': 'some_ca_cert', 'redisMaxConnections': 999, 'flagSetsFilter': ['set_1'], - 'fallbackTreatmentsConfiguration': fallback_treatments_configuration + 'fallbackTreatments': fallback_treatments_configuration } factory = get_factory('some_api_key', config=config) class TelemetrySubmitterMock(): @@ -136,7 +136,7 @@ def synchronize_config(*_): assert isinstance(factory._get_storage('events'), redis.RedisEventsStorage) assert factory._get_storage('splits').flag_set_filter.flag_sets == set([]) - assert factory._fallback_treatments_configuration == fallback_treatments_configuration + assert factory._fallback_treatments_configuration.global_fallback_treatment.treatment == fallback_treatments_configuration.global_fallback_treatment.treatment adapter = factory._get_storage('splits')._redis assert adapter == factory._get_storage('segments')._redis @@ -709,13 +709,13 @@ class SplitFactoryAsyncTests(object): @pytest.mark.asyncio async def test_flag_sets_counts(self): - fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on"))) + fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on")) factory = await get_factory_async("none", config={ 'flagSetsFilter': ['set1', 'set2', 'set3'], 'streamEnabled': False, - 'fallbackTreatmentsConfiguration': fallback_treatments_configuration + 'fallbackTreatments': fallback_treatments_configuration }) - assert factory._fallback_treatments_configuration == fallback_treatments_configuration + assert factory._fallback_treatments_configuration.global_fallback_treatment.treatment == fallback_treatments_configuration.global_fallback_treatment.treatment assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets == 3 assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets_invalid == 0 await factory.destroy() diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index 144f2160..85afb248 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -1648,12 +1648,6 @@ def test_fallback_treatments(self, mocker): mocker.call("Config: Fallback treatment should match regex %s", "^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$") ] - _logger.reset_mock() - assert not input_validator.validate_fallback_treatment(FallbackTreatment("9on")) - assert _logger.warning.mock_calls == [ - mocker.call("Config: Fallback treatment should match regex %s", "^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$") - ] - _logger.reset_mock() assert not input_validator.validate_fallback_treatment(FallbackTreatment("on$as")) assert _logger.warning.mock_calls == [ diff --git a/tests/engine/test_evaluator.py b/tests/engine/test_evaluator.py index e95a8710..ba51f901 100644 --- a/tests/engine/test_evaluator.py +++ b/tests/engine/test_evaluator.py @@ -12,7 +12,7 @@ from splitio.models.grammar import condition from splitio.models import rule_based_segments from splitio.models.fallback_treatment import FallbackTreatment -from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration from splitio.engine import evaluator, splitters from splitio.engine.evaluator import EvaluationContext from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, InMemoryRuleBasedSegmentStorage, \ @@ -384,7 +384,7 @@ def test_evaluate_treatment_with_fallback(self, mocker): # should use global fallback logger_mock.reset_mock() - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("off-global", {"prop":"val"})))) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackTreatment("off-global", {"prop":"val"}))) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx) assert result['treatment'] == 'off-global' assert result['configurations'] == '{"prop": "val"}' @@ -394,7 +394,7 @@ def test_evaluate_treatment_with_fallback(self, mocker): # should use by flag fallback logger_mock.reset_mock() - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})}))) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx) assert result['treatment'] == 'off-some2' assert result['configurations'] == '{"prop2": "val2"}' @@ -402,21 +402,21 @@ def test_evaluate_treatment_with_fallback(self, mocker): assert logger_mock.debug.mock_calls[0] == mocker.call("Using Fallback Treatment for feature: %s", "some2") # should not use any fallback - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})}))) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some3', {}, ctx) assert result['treatment'] == 'control' assert result['configurations'] == None assert result['impression']['label'] == Label.SPLIT_NOT_FOUND # should use by flag fallback - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})}))) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx) assert result['treatment'] == 'off-some2' assert result['configurations'] == '{"prop2": "val2"}' assert result['impression']['label'] == "fallback - " + Label.SPLIT_NOT_FOUND # should global flag fallback - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})}))) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some3', {}, ctx) assert result['treatment'] == 'off-global' assert result['configurations'] == '{"prop": "val"}' diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index 894bba8d..257d9099 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -29,7 +29,7 @@ PluggableRuleBasedSegmentsStorage, PluggableRuleBasedSegmentsStorageAsync from splitio.storage.adapters.redis import build, RedisAdapter, RedisAdapterAsync, build_async from splitio.models import splits, segments, rule_based_segments -from splitio.models.fallback_config import FallbackConfig, FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration from splitio.models.fallback_treatment import FallbackTreatment from splitio.engine.impressions.impressions import Manager as ImpressionsManager, ImpressionsMode from splitio.engine.impressions import set_classes, set_classes_async @@ -561,7 +561,7 @@ def setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -720,7 +720,7 @@ def setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init def test_get_treatment(self): @@ -846,7 +846,7 @@ def setup_method(self): 'config': {'connectTimeout': 10000, 'streamingEnabled': False, 'impressionsMode': 'debug', - 'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + 'fallbackTreatments': FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) } } @@ -1017,7 +1017,7 @@ def setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init def test_get_treatment(self): @@ -1206,7 +1206,7 @@ def setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init class LocalhostIntegrationTests(object): # pylint: disable=too-few-public-methods @@ -1430,7 +1430,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1626,7 +1626,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1821,7 +1821,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1975,7 +1975,7 @@ def test_optimized(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2033,7 +2033,7 @@ def test_debug(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2091,7 +2091,7 @@ def test_none(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2155,7 +2155,7 @@ def test_optimized(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init try: @@ -2222,7 +2222,7 @@ def test_debug(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init try: @@ -2289,7 +2289,7 @@ def test_none(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init try: @@ -2393,7 +2393,7 @@ async def _setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2565,7 +2565,7 @@ async def _setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2709,7 +2709,7 @@ async def _setup_method(self): 'config': {'connectTimeout': 10000, 'streamingEnabled': False, 'impressionsMode': 'debug', - 'fallbackTreatmentsConfiguration': FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + 'fallbackTreatments': FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) } } @@ -2919,7 +2919,7 @@ async def _setup_method(self): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3142,7 +3142,7 @@ async def _setup_method(self): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3377,7 +3377,7 @@ async def _setup_method(self): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3607,7 +3607,7 @@ async def _setup_method(self): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() @@ -3842,7 +3842,7 @@ async def _setup_method(self): manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackConfig(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})})) + fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage diff --git a/tests/models/test_fallback.py b/tests/models/test_fallback.py index b326fd6f..a3111277 100644 --- a/tests/models/test_fallback.py +++ b/tests/models/test_fallback.py @@ -1,5 +1,5 @@ from splitio.models.fallback_treatment import FallbackTreatment -from splitio.models.fallback_config import FallbackConfig +from splitio.models.fallback_config import FallbackTreatmentsConfiguration class FallbackTreatmentModelTests(object): """Fallback treatment model tests.""" @@ -13,13 +13,13 @@ def test_working(self): assert fallback_treatment.config == None assert fallback_treatment.treatment == 'off' -class FallbackConfigModelTests(object): +class FallbackTreatmentsConfigModelTests(object): """Fallback treatment model tests.""" def test_working(self): global_fb = FallbackTreatment("on") flag_fb = FallbackTreatment("off") - fallback_config = FallbackConfig(global_fb, {"flag1": flag_fb}) + fallback_config = FallbackTreatmentsConfiguration(global_fb, {"flag1": flag_fb}) assert fallback_config.global_fallback_treatment == global_fb assert fallback_config.by_flag_fallback_treatment == {"flag1": flag_fb} From 48c30847ca55a49b24dbda918008a09255c76395 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 9 Sep 2025 20:26:44 -0700 Subject: [PATCH 13/26] Added fallback calculator --- splitio/client/client.py | 63 +++++++------- splitio/client/factory.py | 32 +++---- splitio/client/input_validator.py | 11 +-- splitio/client/util.py | 21 +---- splitio/engine/evaluator.py | 11 +-- splitio/models/fallback_config.py | 44 ++++++++++ splitio/models/fallback_treatment.py | 12 ++- tests/client/test_client.py | 122 +++++++++++++-------------- tests/client/test_config.py | 4 +- tests/client/test_factory.py | 6 +- tests/client/test_input_validator.py | 37 ++++---- tests/client/test_utils.py | 1 - tests/engine/test_evaluator.py | 19 ++--- tests/integration/test_client_e2e.py | 52 +++++++----- tests/models/test_fallback.py | 32 ++++++- 15 files changed, 256 insertions(+), 211 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 9a33e67c..9e1ddffc 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -10,7 +10,6 @@ from splitio.models.events import Event, EventWrapper from splitio.models.telemetry import get_latency_bucket_index, MethodExceptionsAndLatencies from splitio.client import input_validator -from splitio.client.util import get_fallback_treatment_and_label from splitio.util.time import get_current_epoch_time_ms, utctime_ms @@ -41,7 +40,7 @@ class ClientBase(object): # pylint: disable=too-many-instance-attributes 'impressions_disabled': False } - def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None): + def __init__(self, factory, recorder, labels_enabled=True, fallback_treatment_calculator=None): """ Construct a Client instance. @@ -63,10 +62,10 @@ def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_c self._feature_flag_storage = factory._get_storage('splits') # pylint: disable=protected-access self._segment_storage = factory._get_storage('segments') # pylint: disable=protected-access self._events_storage = factory._get_storage('events') # pylint: disable=protected-access - self._evaluator = Evaluator(self._splitter, fallback_treatments_configuration) + self._evaluator = Evaluator(self._splitter, fallback_treatment_calculator) self._telemetry_evaluation_producer = self._factory._telemetry_evaluation_producer self._telemetry_init_producer = self._factory._telemetry_init_producer - self._fallback_treatments_configuration = fallback_treatments_configuration + self._fallback_treatment_calculator = fallback_treatment_calculator @property def ready(self): @@ -206,17 +205,17 @@ def _validate_track(self, key, traffic_type, event_type, value=None, properties= def _get_properties(self, evaluation_options): return evaluation_options.properties if evaluation_options != None else None - def _get_fallback_treatment_with_config(self, treatment, feature): - label = "" - - label, treatment, config = get_fallback_treatment_and_label(self._fallback_treatments_configuration, - feature, treatment, label, _LOGGER) - return treatment, config + def _get_fallback_treatment_with_config(self, feature): + fallback_treatment = self._fallback_treatment_calculator.resolve(feature, "") + return fallback_treatment.treatment, fallback_treatment.config def _get_fallback_eval_results(self, eval_result, feature): result = copy.deepcopy(eval_result) - result["impression"]["label"], result["treatment"], result["configurations"] = get_fallback_treatment_and_label(self._fallback_treatments_configuration, - feature, result["treatment"], result["impression"]["label"], _LOGGER) + fallback_treatment = self._fallback_treatment_calculator.resolve(feature, result["impression"]["label"]) + result["impression"]["label"] = fallback_treatment.label + result["treatment"] = fallback_treatment.treatment + result["configurations"] = fallback_treatment.config + return result def _check_impression_label(self, result): @@ -225,7 +224,7 @@ def _check_impression_label(self, result): class Client(ClientBase): # pylint: disable=too-many-instance-attributes """Entry point for the split sdk.""" - def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None): + def __init__(self, factory, recorder, labels_enabled=True, fallback_treatment_calculator=None): """ Construct a Client instance. @@ -240,7 +239,7 @@ def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_c :rtype: Client """ - ClientBase.__init__(self, factory, recorder, labels_enabled, fallback_treatments_configuration) + ClientBase.__init__(self, factory, recorder, labels_enabled, fallback_treatment_calculator) self._context_factory = EvaluationDataFactory(factory._get_storage('splits'), factory._get_storage('segments'), factory._get_storage('rule_based_segments')) def destroy(self): @@ -275,7 +274,7 @@ def get_treatment(self, key, feature_flag_name, attributes=None, evaluation_opti except: _LOGGER.error('get_treatment failed') - treatment, _ = self._get_fallback_treatment_with_config(CONTROL, feature_flag_name) + treatment, _ = self._get_fallback_treatment_with_config(feature_flag_name) return treatment def get_treatment_with_config(self, key, feature_flag_name, attributes=None, evaluation_options=None): @@ -301,7 +300,7 @@ def get_treatment_with_config(self, key, feature_flag_name, attributes=None, eva except Exception: _LOGGER.error('get_treatment_with_config failed') - return self._get_fallback_treatment_with_config(CONTROL, feature_flag_name) + return self._get_fallback_treatment_with_config(feature_flag_name) def _get_treatment(self, method, key, feature, attributes=None, evaluation_options=None): """ @@ -321,7 +320,7 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio :rtype: dict """ if not self._client_is_usable(): # not destroyed & not waiting for a fork - return self._get_fallback_treatment_with_config(CONTROL, feature) + return self._get_fallback_treatment_with_config(feature) start = get_current_epoch_time_ms() if not self.ready: @@ -331,7 +330,7 @@ def _get_treatment(self, method, key, feature, attributes=None, evaluation_optio try: key, bucketing, feature, attributes, evaluation_options = self._validate_treatment_input(key, feature, attributes, method, evaluation_options) except _InvalidInputError: - return self._get_fallback_treatment_with_config(CONTROL, feature) + return self._get_fallback_treatment_with_config(feature) result = self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, feature) @@ -376,7 +375,7 @@ def get_treatments(self, key, feature_flag_names, attributes=None, evaluation_op return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} except Exception: - return {feature: self._get_fallback_treatment_with_config(CONTROL, feature)[0] for feature in feature_flag_names} + return {feature: self._get_fallback_treatment_with_config(feature)[0] for feature in feature_flag_names} def get_treatments_with_config(self, key, feature_flag_names, attributes=None, evaluation_options=None): """ @@ -400,7 +399,7 @@ def get_treatments_with_config(self, key, feature_flag_names, attributes=None, e return self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, attributes, evaluation_options) except Exception: - return {feature: (self._get_fallback_treatment_with_config(CONTROL, feature)) for feature in feature_flag_names} + return {feature: (self._get_fallback_treatment_with_config(feature)) for feature in feature_flag_names} def get_treatments_by_flag_set(self, key, flag_set, attributes=None, evaluation_options=None): """ @@ -624,7 +623,7 @@ def _get_treatments(self, key, features, method, attributes=None, evaluation_opt """ start = get_current_epoch_time_ms() if not self._client_is_usable(): - return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration) + return input_validator.generate_control_treatments(features, self._fallback_treatment_calculator) if not self.ready: _LOGGER.error("Client is not ready - no calls possible") @@ -633,7 +632,7 @@ def _get_treatments(self, key, features, method, attributes=None, evaluation_opt try: key, bucketing, features, attributes, evaluation_options = self._validate_treatments_input(key, features, attributes, method, evaluation_options) except _InvalidInputError: - return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration) + return input_validator.generate_control_treatments(features, self._fallback_treatment_calculator) results = {n: self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, n) for n in features} if self.ready: @@ -726,7 +725,7 @@ def track(self, key, traffic_type, event_type, value=None, properties=None): class ClientAsync(ClientBase): # pylint: disable=too-many-instance-attributes """Entry point for the split sdk.""" - def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_configuration=None): + def __init__(self, factory, recorder, labels_enabled=True, fallback_treatment_calculator=None): """ Construct a Client instance. @@ -741,7 +740,7 @@ def __init__(self, factory, recorder, labels_enabled=True, fallback_treatments_c :rtype: Client """ - ClientBase.__init__(self, factory, recorder, labels_enabled, fallback_treatments_configuration) + ClientBase.__init__(self, factory, recorder, labels_enabled, fallback_treatment_calculator) self._context_factory = AsyncEvaluationDataFactory(factory._get_storage('splits'), factory._get_storage('segments'), factory._get_storage('rule_based_segments')) async def destroy(self): @@ -776,7 +775,7 @@ async def get_treatment(self, key, feature_flag_name, attributes=None, evaluatio except: _LOGGER.error('get_treatment failed') - treatment, _ = self._get_fallback_treatment_with_config(CONTROL, feature_flag_name) + treatment, _ = self._get_fallback_treatment_with_config(feature_flag_name) return treatment async def get_treatment_with_config(self, key, feature_flag_name, attributes=None, evaluation_options=None): @@ -802,7 +801,7 @@ async def get_treatment_with_config(self, key, feature_flag_name, attributes=Non except Exception: _LOGGER.error('get_treatment_with_config failed') - return self._get_fallback_treatment_with_config(CONTROL, feature_flag_name) + return self._get_fallback_treatment_with_config(feature_flag_name) async def _get_treatment(self, method, key, feature, attributes=None, evaluation_options=None): """ @@ -822,7 +821,7 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation :rtype: dict """ if not self._client_is_usable(): # not destroyed & not waiting for a fork - return self._get_fallback_treatment_with_config(CONTROL, feature) + return self._get_fallback_treatment_with_config(feature) start = get_current_epoch_time_ms() if not self.ready: @@ -832,7 +831,7 @@ async def _get_treatment(self, method, key, feature, attributes=None, evaluation try: key, bucketing, feature, attributes, evaluation_options = self._validate_treatment_input(key, feature, attributes, method, evaluation_options) except _InvalidInputError: - return self._get_fallback_treatment_with_config(CONTROL, feature) + return self._get_fallback_treatment_with_config(feature) result = self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, feature) if self.ready: @@ -875,7 +874,7 @@ async def get_treatments(self, key, feature_flag_names, attributes=None, evaluat return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} except Exception: - return {feature: self._get_fallback_treatment_with_config(CONTROL, feature)[0] for feature in feature_flag_names} + return {feature: self._get_fallback_treatment_with_config(feature)[0] for feature in feature_flag_names} async def get_treatments_with_config(self, key, feature_flag_names, attributes=None, evaluation_options=None): """ @@ -899,7 +898,7 @@ async def get_treatments_with_config(self, key, feature_flag_names, attributes=N return await self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, attributes, evaluation_options) except Exception: - return {feature: (self._get_fallback_treatment_with_config(CONTROL, feature)) for feature in feature_flag_names} + return {feature: (self._get_fallback_treatment_with_config(feature)) for feature in feature_flag_names} async def get_treatments_by_flag_set(self, key, flag_set, attributes=None, evaluation_options=None): """ @@ -1037,7 +1036,7 @@ async def _get_treatments(self, key, features, method, attributes=None, evaluati """ start = get_current_epoch_time_ms() if not self._client_is_usable(): - return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration) + return input_validator.generate_control_treatments(features, self._fallback_treatment_calculator) if not self.ready: _LOGGER.error("Client is not ready - no calls possible") @@ -1046,7 +1045,7 @@ async def _get_treatments(self, key, features, method, attributes=None, evaluati try: key, bucketing, features, attributes, evaluation_options = self._validate_treatments_input(key, features, attributes, method, evaluation_options) except _InvalidInputError: - return input_validator.generate_control_treatments(features, self._fallback_treatments_configuration) + return input_validator.generate_control_treatments(features, self._fallback_treatment_calculator) results = {n: self._get_fallback_eval_results(self._NON_READY_EVAL_RESULT, n) for n in features} if self.ready: diff --git a/splitio/client/factory.py b/splitio/client/factory.py index e06d6cf9..6c7ce990 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -18,7 +18,7 @@ TelemetryStorageProducerAsync, TelemetryStorageConsumerAsync from splitio.engine.impressions.manager import Counter as ImpressionsCounter from splitio.engine.impressions.unique_keys_tracker import UniqueKeysTracker, UniqueKeysTrackerAsync - +from splitio.models.fallback_config import FallbackTreatmentCalculator # Storage from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, \ InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage, LocalhostTelemetryStorage, \ @@ -171,7 +171,7 @@ def __init__( # pylint: disable=too-many-arguments telemetry_init_producer=None, telemetry_submitter=None, preforked_initialization=False, - fallback_treatments_configuration=None + fallback_treatment_calculator=None ): """ Class constructor. @@ -202,7 +202,7 @@ def __init__( # pylint: disable=too-many-arguments self._ready_time = get_current_epoch_time_ms() _LOGGER.debug("Running in threading mode") self._sdk_internal_ready_flag = sdk_ready_flag - self._fallback_treatments_configuration = fallback_treatments_configuration + self._fallback_treatment_calculator = fallback_treatment_calculator self._start_status_updater() def _start_status_updater(self): @@ -244,7 +244,7 @@ def client(self): This client is only a set of references to structures hold by the factory. Creating one a fast operation and safe to be used anywhere. """ - return Client(self, self._recorder, self._labels_enabled, self._fallback_treatments_configuration) + return Client(self, self._recorder, self._labels_enabled, self._fallback_treatment_calculator) def manager(self): """ @@ -341,7 +341,7 @@ def __init__( # pylint: disable=too-many-arguments telemetry_submitter=None, manager_start_task=None, api_client=None, - fallback_treatments_configuration=None + fallback_treatment_calculator=None ): """ Class constructor. @@ -375,7 +375,7 @@ def __init__( # pylint: disable=too-many-arguments self._sdk_ready_flag = asyncio.Event() self._ready_task = asyncio.get_running_loop().create_task(self._update_status_when_ready_async()) self._api_client = api_client - self._fallback_treatments_configuration = fallback_treatments_configuration + self._fallback_treatment_calculator = fallback_treatment_calculator async def _update_status_when_ready_async(self): """Wait until the sdk is ready and update the status for async mode.""" @@ -464,7 +464,7 @@ def client(self): This client is only a set of references to structures hold by the factory. Creating one a fast operation and safe to be used anywhere. """ - return ClientAsync(self, self._recorder, self._labels_enabled, self._fallback_treatments_configuration) + return ClientAsync(self, self._recorder, self._labels_enabled, self._fallback_treatment_calculator) def _wrap_impression_listener(listener, metadata): """ @@ -628,7 +628,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl return SplitFactory(api_key, storages, cfg['labelsEnabled'], recorder, manager, None, telemetry_producer, telemetry_init_producer, telemetry_submitter, preforked_initialization=preforked_initialization, - fallback_treatments_configuration=cfg['fallbackTreatments']) + fallback_treatment_calculator=FallbackTreatmentCalculator(cfg['fallbackTreatments'])) initialization_thread = threading.Thread(target=manager.start, name="SDKInitializer", daemon=True) initialization_thread.start() @@ -636,7 +636,7 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl return SplitFactory(api_key, storages, cfg['labelsEnabled'], recorder, manager, sdk_ready_flag, telemetry_producer, telemetry_init_producer, - telemetry_submitter, fallback_treatments_configuration=cfg['fallbackTreatments']) + telemetry_submitter, fallback_treatment_calculator = FallbackTreatmentCalculator(cfg['fallbackTreatments'])) async def _build_in_memory_factory_async(api_key, cfg, sdk_url=None, events_url=None, # pylint:disable=too-many-arguments,too-many-localsa auth_api_base_url=None, streaming_api_base_url=None, telemetry_api_base_url=None, @@ -755,7 +755,7 @@ async def _build_in_memory_factory_async(api_key, cfg, sdk_url=None, events_url= recorder, manager, telemetry_producer, telemetry_init_producer, telemetry_submitter, manager_start_task=manager_start_task, - api_client=http_client, fallback_treatments_configuration=cfg['fallbackTreatments']) + api_client=http_client, fallback_treatment_calculator=FallbackTreatmentCalculator(cfg['fallbackTreatments'])) def _build_redis_factory(api_key, cfg): """Build and return a split factory with redis-based storage.""" @@ -834,7 +834,7 @@ def _build_redis_factory(api_key, cfg): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, - fallback_treatments_configuration=cfg['fallbackTreatments'] + fallback_treatment_calculator=FallbackTreatmentCalculator(cfg['fallbackTreatments']) ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -917,7 +917,7 @@ async def _build_redis_factory_async(api_key, cfg): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=cfg['fallbackTreatments'] + fallback_treatment_calculator=FallbackTreatmentCalculator(cfg['fallbackTreatments']) ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() await storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -1000,7 +1000,7 @@ def _build_pluggable_factory(api_key, cfg): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, - fallback_treatments_configuration=cfg['fallbackTreatments'] + fallback_treatment_calculator=FallbackTreatmentCalculator(cfg['fallbackTreatments']) ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -1081,7 +1081,7 @@ async def _build_pluggable_factory_async(api_key, cfg): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_init_producer, telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=cfg['fallbackTreatments'] + fallback_treatment_calculator=FallbackTreatmentCalculator(cfg['fallbackTreatments']) ) redundant_factory_count, active_factory_count = _get_active_and_redundant_count() await storages['telemetry'].record_active_and_redundant_factories(active_factory_count, redundant_factory_count) @@ -1159,7 +1159,7 @@ def _build_localhost_factory(cfg): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=LocalhostTelemetrySubmitter(), - fallback_treatments_configuration=cfg['fallbackTreatments'] + fallback_treatment_calculator=FallbackTreatmentCalculator(cfg['fallbackTreatments']) ) async def _build_localhost_factory_async(cfg): @@ -1231,7 +1231,7 @@ async def _build_localhost_factory_async(cfg): telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=LocalhostTelemetrySubmitterAsync(), manager_start_task=manager_start_task, - fallback_treatments_configuration=cfg['fallbackTreatments'] + fallback_treatment_calculator=FallbackTreatmentCalculator(cfg['fallbackTreatments']) ) def get_factory(api_key, **kwargs): diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index aaaf8026..dfded942 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -7,7 +7,6 @@ from splitio.client.key import Key from splitio.client import client -from splitio.client.util import get_fallback_treatment_and_label from splitio.engine.evaluator import CONTROL from splitio.models.fallback_treatment import FallbackTreatment @@ -503,7 +502,7 @@ def validate_feature_flags_get_treatments( # pylint: disable=invalid-name valid_feature_flags.append(ff) return valid_feature_flags -def generate_control_treatments(feature_flags, fallback_treatments_configuration): +def generate_control_treatments(feature_flags, fallback_treatment_calculator): """ Generate valid feature flags to control. @@ -518,11 +517,9 @@ def generate_control_treatments(feature_flags, fallback_treatments_configuration to_return = {} for feature_flag in feature_flags: if isinstance(feature_flag, str) and len(feature_flag.strip())> 0: - treatment = CONTROL - config = None - label = "" - label, treatment, config = get_fallback_treatment_and_label(fallback_treatments_configuration, - feature_flag, treatment, label, _LOGGER) + fallback_treatment = fallback_treatment_calculator.resolve(feature_flag, "") + treatment = fallback_treatment.treatment + config = fallback_treatment.config to_return[feature_flag] = (treatment, config) return to_return diff --git a/splitio/client/util.py b/splitio/client/util.py index 1f01de3f..b5b693cb 100644 --- a/splitio/client/util.py +++ b/splitio/client/util.py @@ -50,23 +50,4 @@ def get_metadata(config): """ version = 'python-%s' % __version__ ip_address, hostname = _get_hostname_and_ip(config) - return SdkMetadata(version, hostname, ip_address) - -def get_fallback_treatment_and_label(fallback_treatments_configuration, feature_name, treatment, label, _logger): - if fallback_treatments_configuration == None: - return label, treatment, None - - if fallback_treatments_configuration.by_flag_fallback_treatment != None and \ - fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name) != None: - _logger.debug('Using Fallback Treatment for feature: %s', feature_name) - return fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).label_prefix + label, \ - fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).treatment, \ - fallback_treatments_configuration.by_flag_fallback_treatment.get(feature_name).config - - if fallback_treatments_configuration.global_fallback_treatment != None: - _logger.debug('Using Global Fallback Treatment.') - return fallback_treatments_configuration.global_fallback_treatment.label_prefix + label, \ - fallback_treatments_configuration.global_fallback_treatment.treatment, \ - fallback_treatments_configuration.global_fallback_treatment.config - - return label, treatment, None + return SdkMetadata(version, hostname, ip_address) \ No newline at end of file diff --git a/splitio/engine/evaluator.py b/splitio/engine/evaluator.py index 2a564d3a..017b5e74 100644 --- a/splitio/engine/evaluator.py +++ b/splitio/engine/evaluator.py @@ -2,7 +2,6 @@ import logging from collections import namedtuple -from splitio.client.util import get_fallback_treatment_and_label from splitio.models.impressions import Label from splitio.models.grammar.condition import ConditionType from splitio.models.grammar.matchers.misc import DependencyMatcher @@ -21,7 +20,7 @@ class Evaluator(object): # pylint: disable=too-few-public-methods """Split Evaluator class.""" - def __init__(self, splitter, fallback_treatments_configuration=None): + def __init__(self, splitter, fallback_treatment_calculator=None): """ Construct a Evaluator instance. @@ -29,7 +28,7 @@ def __init__(self, splitter, fallback_treatments_configuration=None): :type splitter: splitio.engine.splitters.Splitters """ self._splitter = splitter - self._fallback_treatments_configuration = fallback_treatments_configuration + self._fallback_treatment_calculator = fallback_treatment_calculator def eval_many_with_context(self, key, bucketing, features, attrs, ctx): """ @@ -53,8 +52,10 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx): if not feature: _LOGGER.warning('Unknown or invalid feature: %s', feature) label = Label.SPLIT_NOT_FOUND - label, _treatment, config = get_fallback_treatment_and_label(self._fallback_treatments_configuration, - feature_name, _treatment, label, _LOGGER) + fallback_treatment = self._fallback_treatment_calculator.resolve(feature_name, label) + label = fallback_treatment.label + _treatment = fallback_treatment.treatment + config = fallback_treatment.config else: _change_number = feature.change_number if feature.killed: diff --git a/splitio/models/fallback_config.py b/splitio/models/fallback_config.py index 14b00dda..aba7ad7b 100644 --- a/splitio/models/fallback_config.py +++ b/splitio/models/fallback_config.py @@ -1,4 +1,6 @@ """Segment module.""" +from splitio.models.fallback_treatment import FallbackTreatment +from splitio.client.client import CONTROL class FallbackTreatmentsConfiguration(object): """FallbackTreatmentsConfiguration object class.""" @@ -35,3 +37,45 @@ def by_flag_fallback_treatment(self): def by_flag_fallback_treatment(self, new_value): """Set global fallback treatment.""" self.by_flag_fallback_treatment = new_value + +class FallbackTreatmentCalculator(object): + """FallbackTreatmentCalculator object class.""" + + def __init__(self, fallback_treatment_configuration): + """ + Class constructor. + + :param fallback_treatment_configuration: fallback treatment configuration + :type fallback_treatment_configuration: FallbackTreatmentsConfiguration + """ + self._label_prefix = "fallback - " + self._fallback_treatments_configuration = fallback_treatment_configuration + + @property + def fallback_treatments_configuration(self): + """Return fallback treatment configuration.""" + return self._fallback_treatments_configuration + + def resolve(self, flag_name, label): + if self._fallback_treatments_configuration != None: + if self._fallback_treatments_configuration.by_flag_fallback_treatment != None \ + and self._fallback_treatments_configuration.by_flag_fallback_treatment.get(flag_name) != None: + return self._copy_with_label(self._fallback_treatments_configuration.by_flag_fallback_treatment.get(flag_name), \ + self._resolve_label(label)) + + if self._fallback_treatments_configuration.global_fallback_treatment != None: + return self._copy_with_label(self._fallback_treatments_configuration.global_fallback_treatment, \ + self._resolve_label(label)) + + return FallbackTreatment(CONTROL, None, label) + + def _resolve_label(self, label): + if label == None: + return None + + return self._label_prefix + label + + def _copy_with_label(self, fallback_treatment, label): + return FallbackTreatment(fallback_treatment.treatment, fallback_treatment.config, label) + + \ No newline at end of file diff --git a/splitio/models/fallback_treatment.py b/splitio/models/fallback_treatment.py index c8e60001..19b58665 100644 --- a/splitio/models/fallback_treatment.py +++ b/splitio/models/fallback_treatment.py @@ -4,7 +4,7 @@ class FallbackTreatment(object): """Segment object class.""" - def __init__(self, treatment, config=None): + def __init__(self, treatment, config=None, label=None): """ Class constructor. @@ -15,10 +15,8 @@ def __init__(self, treatment, config=None): :type config: json """ self._treatment = treatment - self._config = None - if config != None: - self._config = json.dumps(config) - self._label_prefix = "fallback - " + self._config = config + self._label = label @property def treatment(self): @@ -31,6 +29,6 @@ def config(self): return self._config @property - def label_prefix(self): + def label(self): """Return label prefix.""" - return self._label_prefix \ No newline at end of file + return self._label \ No newline at end of file diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 75f46464..15c2b96b 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -9,7 +9,7 @@ from splitio.client.client import Client, _LOGGER as _logger, CONTROL, ClientAsync, EvaluationOptions from splitio.client.factory import SplitFactory, Status as FactoryStatus, SplitFactoryAsync -from splitio.models.fallback_config import FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration, FallbackTreatmentCalculator from splitio.models.fallback_treatment import FallbackTreatment from splitio.models.impressions import Impression, Label from splitio.models.events import Event, EventWrapper @@ -76,7 +76,7 @@ def synchronize_config(*_): factory.block_until_ready(5) split_storage.update([from_raw(splits_json['splitChange1_1']['ff']['d'][0])], [], -1) - client = Client(factory, recorder, True) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) client._evaluator.eval_with_context.return_value = { 'treatment': 'on', @@ -148,7 +148,7 @@ def synchronize_config(*_): mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) split_storage.update([from_raw(splits_json['splitChange1_1']['ff']['d'][0])], [], -1) - client = Client(factory, recorder, True) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) client._evaluator.eval_with_context.return_value = { 'treatment': 'on', @@ -225,7 +225,7 @@ def synchronize_config(*_): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - client = Client(factory, recorder, True) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -305,7 +305,7 @@ def synchronize_config(*_): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - client = Client(factory, recorder, True) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -384,7 +384,7 @@ def synchronize_config(*_): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - client = Client(factory, recorder, True) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -462,7 +462,7 @@ def synchronize_config(*_): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - client = Client(factory, recorder, True) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -545,7 +545,7 @@ def synchronize_config(*_): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - client = Client(factory, recorder, True) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -625,7 +625,7 @@ def synchronize_config(*_): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - client = Client(factory, recorder, True) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -712,7 +712,7 @@ def synchronize_config(*_): from_raw(splits_json['splitChange1_1']['ff']['d'][1]), from_raw(splits_json['splitChange1_1']['ff']['d'][2]) ], [], -1) - client = Client(factory, recorder, True) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) assert client.get_treatment('some_key', 'SPLIT_1') == 'off' assert client.get_treatment('some_key', 'SPLIT_2') == 'on' assert client.get_treatment('some_key', 'SPLIT_3') == 'on' @@ -776,7 +776,7 @@ def synchronize_config(*_): from_raw(splits_json['splitChange1_1']['ff']['d'][1]), from_raw(splits_json['splitChange1_1']['ff']['d'][2]) ], [], -1) - client = Client(factory, recorder, True) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) assert client.get_treatment('some_key', 'SPLIT_1') == 'off' assert client.get_treatment('some_key', 'SPLIT_2') == 'on' assert client.get_treatment('some_key', 'SPLIT_3') == 'on' @@ -840,7 +840,7 @@ def synchronize_config(*_): from_raw(splits_json['splitChange1_1']['ff']['d'][1]), from_raw(splits_json['splitChange1_1']['ff']['d'][2]) ], [], -1) - client = Client(factory, recorder, True) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) assert client.get_treatment('some_key', 'SPLIT_1') == 'off' assert client.get_treatment('some_key', 'SPLIT_2') == 'on' assert client.get_treatment('some_key', 'SPLIT_3') == 'on' @@ -881,7 +881,7 @@ def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) client.destroy() assert client.destroyed is not None assert(mocker.called) @@ -923,7 +923,7 @@ def synchronize_config(*_): factory._apikey = 'test' mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) - client = Client(factory, recorder, True) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) assert client.track('key', 'user', 'purchase', 12) is True assert mocker.call([ EventWrapper( @@ -972,7 +972,7 @@ def synchronize_config(*_): mocker.call('Client is not ready - no calls possible') ] - client = Client(factory, mocker.Mock()) + client = Client(factory, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.client._LOGGER', new=_logger) @@ -1044,7 +1044,7 @@ def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, mocker.Mock()) + client = Client(factory, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) client.ready = False assert client.get_treatment('some_key', 'SPLIT_2') == CONTROL assert(telemetry_storage._tel_config._not_ready == 1) @@ -1096,7 +1096,7 @@ def stop(*_): ready_property = mocker.PropertyMock() ready_property.return_value = True type(factory).ready = ready_property - client = Client(factory, recorder, True) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) def _raise(*_): raise RuntimeError('something') client._evaluator.eval_many_with_context = _raise @@ -1192,7 +1192,7 @@ def stop(*_): pass factory._sync_manager.stop = stop - client = Client(factory, recorder, True) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) assert client.get_treatment('key', 'SPLIT_2') == 'on' assert(telemetry_storage._method_latencies._treatment[0] == 1) @@ -1258,7 +1258,7 @@ def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) try: client.track('key', 'tt', 'ev') except: @@ -1311,7 +1311,7 @@ def synchronize_config(*_): factory.block_until_ready(5) split_storage.update([from_raw(splits_json['splitChange1_1']['ff']['d'][0])], [], -1) - client = Client(factory, recorder, True) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -1419,7 +1419,7 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global", {"prop":"val"}))) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global", '{"prop": "val"}')))) def get_feature_flag_names_by_flag_sets(*_): return ["some", "some2"] @@ -1446,7 +1446,7 @@ def get_feature_flag_names_by_flag_sets(*_): assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global", {"prop":"val"}), {'some': FallbackTreatment("on-local")}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global", '{"prop": "val"}'), {'some': FallbackTreatment("on-local")})) treatment = client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") @@ -1475,7 +1475,7 @@ def get_feature_flag_names_by_flag_sets(*_): assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local", {"prop":"val"})}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local", '{"prop": "val"}')})) treatment = client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") @@ -1504,7 +1504,7 @@ def get_feature_flag_names_by_flag_sets(*_): assert(client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")})) treatment = client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps[0].treatment == "control") @@ -1557,25 +1557,25 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global"))) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) treatment = client.get_treatment("key", "some") assert(treatment == "on-global") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) treatment = client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")})) treatment = client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")})) treatment = client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps == None) @@ -1625,7 +1625,7 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global"))) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) client.ready = False treatment = client.get_treatment("key", "some") @@ -1633,21 +1633,21 @@ def synchronize_config(*_): assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) treatment = client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")})) treatment = client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")})) treatment = client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps[0].treatment == "control") @@ -1700,7 +1700,7 @@ async def synchronize_config(*_): ) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) client._evaluator.eval_with_context.return_value = { 'treatment': 'on', @@ -1772,7 +1772,7 @@ async def synchronize_config(*_): mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) client._evaluator.eval_with_context.return_value = { 'treatment': 'on', @@ -1849,7 +1849,7 @@ async def synchronize_config(*_): mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -1929,7 +1929,7 @@ async def synchronize_config(*_): mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -2009,7 +2009,7 @@ async def synchronize_config(*_): mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -2088,7 +2088,7 @@ async def synchronize_config(*_): mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -2172,7 +2172,7 @@ async def synchronize_config(*_): mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -2256,7 +2256,7 @@ async def synchronize_config(*_): mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -2342,7 +2342,7 @@ async def test_impression_toggle_optimized(self, mocker): from_raw(splits_json['splitChange1_1']['ff']['d'][1]), from_raw(splits_json['splitChange1_1']['ff']['d'][2]) ], [], -1) - client = ClientAsync(factory, recorder, True) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) treatment = await client.get_treatment('some_key', 'SPLIT_1') assert treatment == 'off' treatment = await client.get_treatment('some_key', 'SPLIT_2') @@ -2405,7 +2405,7 @@ async def test_impression_toggle_debug(self, mocker): from_raw(splits_json['splitChange1_1']['ff']['d'][1]), from_raw(splits_json['splitChange1_1']['ff']['d'][2]) ], [], -1) - client = ClientAsync(factory, recorder, True) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) assert await client.get_treatment('some_key', 'SPLIT_1') == 'off' assert await client.get_treatment('some_key', 'SPLIT_2') == 'on' assert await client.get_treatment('some_key', 'SPLIT_3') == 'on' @@ -2465,7 +2465,7 @@ async def test_impression_toggle_none(self, mocker): from_raw(splits_json['splitChange1_1']['ff']['d'][1]), from_raw(splits_json['splitChange1_1']['ff']['d'][2]) ], [], -1) - client = ClientAsync(factory, recorder, True) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) assert await client.get_treatment('some_key', 'SPLIT_1') == 'off' assert await client.get_treatment('some_key', 'SPLIT_2') == 'on' assert await client.get_treatment('some_key', 'SPLIT_3') == 'on' @@ -2516,7 +2516,7 @@ async def synchronize_config(*_): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) assert await client.track('key', 'user', 'purchase', 12) is True assert self.events[0] == [EventWrapper( event=Event('key', 'user', 'purchase', 12, 1000, None), @@ -2560,7 +2560,7 @@ async def synchronize_config(*_): type(factory).ready = ready_property await factory.block_until_ready(1) - client = ClientAsync(factory, recorder) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) assert await client.get_treatment('some_key', 'SPLIT_2') == CONTROL assert(telemetry_storage._tel_config._not_ready == 1) await client.track('key', 'tt', 'ev') @@ -2608,7 +2608,7 @@ async def synchronize_config(*_): ready_property.return_value = True type(factory).ready = ready_property - client = ClientAsync(factory, recorder, True) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock() def _raise(*_): raise RuntimeError('something') @@ -2686,7 +2686,7 @@ async def synchronize_config(*_): await factory.block_until_ready(1) except: pass - client = ClientAsync(factory, recorder, True) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) assert await client.get_treatment('key', 'SPLIT_2') == 'on' assert(telemetry_storage._method_latencies._treatment[0] == 1) @@ -2756,7 +2756,7 @@ async def exc(*_): recorder.record_track_stats = exc await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) try: await client.track('key', 'tt', 'ev') except: @@ -2803,7 +2803,7 @@ async def synchronize_config(*_): ) await factory.block_until_ready(1) - client = ClientAsync(factory, recorder, True) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(None)) client._evaluator = mocker.Mock(spec=Evaluator) evaluation = { 'treatment': 'on', @@ -2914,7 +2914,7 @@ class TelemetrySubmitterMock(): async def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = ClientAsync(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global", {"prop":"val"}))) + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global", '{"prop": "val"}')))) async def get_feature_flag_names_by_flag_sets(*_): return ["some", "some2"] @@ -2949,7 +2949,7 @@ async def fetch_many_rbs(*_): assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-global", '{"prop": "val"}'), "some2": ("on-global", '{"prop": "val"}')}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global", {"prop":"val"}), {'some': FallbackTreatment("on-local")}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global", '{"prop": "val"}'), {'some': FallbackTreatment("on-local")})) treatment = await client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") @@ -2978,7 +2978,7 @@ async def fetch_many_rbs(*_): assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", None), "some2": ("on-global", '{"prop": "val"}')}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local", {"prop":"val"})}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local", '{"prop": "val"}')})) treatment = await client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") @@ -3007,7 +3007,7 @@ async def fetch_many_rbs(*_): assert(await client.get_treatments_with_config_by_flag_sets("key_m", ["set"]) == {"some": ("on-local", '{"prop": "val"}'), "some2": ("control", None)}) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")})) treatment = await client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps[0].treatment == "control") @@ -3061,25 +3061,25 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global"))) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) treatment = client.get_treatment("key", "some") assert(treatment == "on-global") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) treatment = client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")})) treatment = client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps == None) self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")})) treatment = client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps == None) @@ -3130,7 +3130,7 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentsConfiguration(FallbackTreatment("on-global"))) + client = Client(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) client.ready = False treatment = client.get_treatment("key", "some") @@ -3138,21 +3138,21 @@ def synchronize_config(*_): assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) treatment = client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")})) treatment = client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") assert(self.imps[0].label == "fallback - not ready") self.imps = None - client._fallback_treatments_configuration = FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")}) + client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")})) treatment = client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps[0].treatment == "control") diff --git a/tests/client/test_config.py b/tests/client/test_config.py index 0017938c..e08a1d4b 100644 --- a/tests/client/test_config.py +++ b/tests/client/test_config.py @@ -109,13 +109,13 @@ def test_sanitize(self, mocker): fb = FallbackTreatmentsConfiguration(FallbackTreatment('on')) processed = config.sanitize('some', {'fallbackTreatments': fb}) assert processed['fallbackTreatments'].global_fallback_treatment.treatment == fb.global_fallback_treatment.treatment - assert processed['fallbackTreatments'].global_fallback_treatment.label_prefix == "fallback - " + assert processed['fallbackTreatments'].global_fallback_treatment.label == None fb = FallbackTreatmentsConfiguration(FallbackTreatment('on'), {"flag": FallbackTreatment("off")}) processed = config.sanitize('some', {'fallbackTreatments': fb}) assert processed['fallbackTreatments'].global_fallback_treatment.treatment == fb.global_fallback_treatment.treatment assert processed['fallbackTreatments'].by_flag_fallback_treatment["flag"] == fb.by_flag_fallback_treatment["flag"] - assert processed['fallbackTreatments'].by_flag_fallback_treatment["flag"].label_prefix == "fallback - " + assert processed['fallbackTreatments'].by_flag_fallback_treatment["flag"].label == None _logger.reset_mock() fb = FallbackTreatmentsConfiguration(None, {"flag#%": FallbackTreatment("off"), "flag2": FallbackTreatment("on")}) diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index 86e13088..9a5ad992 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -13,7 +13,7 @@ from splitio.storage import redis, inmemmory, pluggable from splitio.tasks.util import asynctask from splitio.engine.impressions.impressions import Manager as ImpressionsManager -from splitio.models.fallback_config import FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration, FallbackTreatmentCalculator from splitio.models.fallback_treatment import FallbackTreatment from splitio.sync.manager import Manager, ManagerAsync from splitio.sync.synchronizer import Synchronizer, SynchronizerAsync, SplitSynchronizers, SplitTasks @@ -136,7 +136,7 @@ def synchronize_config(*_): assert isinstance(factory._get_storage('events'), redis.RedisEventsStorage) assert factory._get_storage('splits').flag_set_filter.flag_sets == set([]) - assert factory._fallback_treatments_configuration.global_fallback_treatment.treatment == fallback_treatments_configuration.global_fallback_treatment.treatment + assert factory._fallback_treatment_calculator.fallback_treatments_configuration.global_fallback_treatment.treatment == fallback_treatments_configuration.global_fallback_treatment.treatment adapter = factory._get_storage('splits')._redis assert adapter == factory._get_storage('segments')._redis @@ -715,7 +715,7 @@ async def test_flag_sets_counts(self): 'streamEnabled': False, 'fallbackTreatments': fallback_treatments_configuration }) - assert factory._fallback_treatments_configuration.global_fallback_treatment.treatment == fallback_treatments_configuration.global_fallback_treatment.treatment + assert factory._fallback_treatment_calculator.fallback_treatments_configuration.global_fallback_treatment.treatment == fallback_treatments_configuration.global_fallback_treatment.treatment assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets == 3 assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets_invalid == 0 await factory.destroy() diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index 85afb248..06ae5b60 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -9,6 +9,7 @@ from splitio.storage.inmemmory import InMemoryTelemetryStorage, InMemoryTelemetryStorageAsync, \ InMemorySplitStorage, InMemorySplitStorageAsync, InMemoryRuleBasedSegmentStorage, InMemoryRuleBasedSegmentStorageAsync from splitio.models.splits import Split +from splitio.models.fallback_config import FallbackTreatmentCalculator from splitio.client import input_validator from splitio.client.manager import SplitManager, SplitManagerAsync from splitio.recorder.recorder import StandardRecorder, StandardRecorderAsync @@ -56,7 +57,7 @@ def test_get_treatment(self, mocker): mocker.Mock() ) - client = Client(factory, mocker.Mock()) + client = Client(factory, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -297,7 +298,7 @@ def _configs(treatment): mocker.Mock() ) - client = Client(factory, mocker.Mock()) + client = Client(factory, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -573,7 +574,7 @@ def test_track(self, mocker): ) factory._sdk_key = 'some-test' - client = Client(factory, recorder) + client = Client(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) client._event_storage = event_storage _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -855,7 +856,7 @@ def test_get_treatments(self, mocker): ready_mock.return_value = True type(factory).ready = ready_mock - client = Client(factory, recorder) + client = Client(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -1005,7 +1006,7 @@ def _configs(treatment): return '{"some": "property"}' if treatment == 'default_treatment' else None split_mock.get_configurations_for.side_effect = _configs - client = Client(factory, mocker.Mock()) + client = Client(factory, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -1151,7 +1152,7 @@ def test_get_treatments_by_flag_set(self, mocker): ready_mock.return_value = True type(factory).ready = ready_mock - client = Client(factory, recorder) + client = Client(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -1270,7 +1271,7 @@ def test_get_treatments_by_flag_sets(self, mocker): ready_mock.return_value = True type(factory).ready = ready_mock - client = Client(factory, recorder) + client = Client(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -1400,7 +1401,7 @@ def _configs(treatment): ready_mock.return_value = True type(factory).ready = ready_mock - client = Client(factory, recorder) + client = Client(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -1524,7 +1525,7 @@ def _configs(treatment): ready_mock.return_value = True type(factory).ready = ready_mock - client = Client(factory, recorder) + client = Client(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -1710,7 +1711,7 @@ async def get_change_number(*_): ready_mock.return_value = True type(factory).ready = ready_mock - client = ClientAsync(factory, mocker.Mock()) + client = ClientAsync(factory, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) async def record_treatment_stats(*_): pass @@ -1972,7 +1973,7 @@ async def get_change_number(*_): ready_mock.return_value = True type(factory).ready = ready_mock - client = ClientAsync(factory, mocker.Mock()) + client = ClientAsync(factory, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) async def record_treatment_stats(*_): pass client._recorder.record_treatment_stats = record_treatment_stats @@ -2214,7 +2215,7 @@ async def put(*_): ) factory._sdk_key = 'some-test' - client = ClientAsync(factory, recorder) + client = ClientAsync(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) client._event_storage = event_storage _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) @@ -2506,7 +2507,7 @@ async def fetch_many_rbs(*_): ready_mock.return_value = True type(factory).ready = ready_mock - client = ClientAsync(factory, recorder) + client = ClientAsync(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) async def record_treatment_stats(*_): pass client._recorder.record_treatment_stats = record_treatment_stats @@ -2672,7 +2673,7 @@ def _configs(treatment): return '{"some": "property"}' if treatment == 'default_treatment' else None split_mock.get_configurations_for.side_effect = _configs - client = ClientAsync(factory, mocker.Mock()) + client = ClientAsync(factory, mocker.Mock(), mocker.Mock(), FallbackTreatmentCalculator(None)) async def record_treatment_stats(*_): pass client._recorder.record_treatment_stats = record_treatment_stats @@ -2837,7 +2838,7 @@ async def fetch_many_rbs(*_): ready_mock.return_value = True type(factory).ready = ready_mock - client = ClientAsync(factory, recorder) + client = ClientAsync(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) async def record_treatment_stats(*_): pass client._recorder.record_treatment_stats = record_treatment_stats @@ -2983,7 +2984,7 @@ async def get_feature_flags_by_sets(*_): ready_mock.return_value = True type(factory).ready = ready_mock - client = ClientAsync(factory, recorder) + client = ClientAsync(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) async def record_treatment_stats(*_): pass client._recorder.record_treatment_stats = record_treatment_stats @@ -3138,7 +3139,7 @@ async def get_feature_flags_by_sets(*_): ready_mock.return_value = True type(factory).ready = ready_mock - client = ClientAsync(factory, recorder) + client = ClientAsync(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) async def record_treatment_stats(*_): pass client._recorder.record_treatment_stats = record_treatment_stats @@ -3287,7 +3288,7 @@ async def get_feature_flags_by_sets(*_): ready_mock.return_value = True type(factory).ready = ready_mock - client = ClientAsync(factory, recorder) + client = ClientAsync(factory, recorder, mocker.Mock(), FallbackTreatmentCalculator(None)) async def record_treatment_stats(*_): pass client._recorder.record_treatment_stats = record_treatment_stats diff --git a/tests/client/test_utils.py b/tests/client/test_utils.py index 64edb076..98d9d8f6 100644 --- a/tests/client/test_utils.py +++ b/tests/client/test_utils.py @@ -14,7 +14,6 @@ class ClientUtilsTests(object): def test_get_metadata(self, mocker): """Test the get_metadata function.""" meta = util.get_metadata({'machineIp': 'some_ip', 'machineName': 'some_machine_name'}) - # assert _get_hostname_and_ip.mock_calls == [] assert meta.instance_ip == 'some_ip' assert meta.instance_name == 'some_machine_name' assert meta.sdk_version == 'python-' + __version__ diff --git a/tests/engine/test_evaluator.py b/tests/engine/test_evaluator.py index ba51f901..07f79a80 100644 --- a/tests/engine/test_evaluator.py +++ b/tests/engine/test_evaluator.py @@ -12,7 +12,7 @@ from splitio.models.grammar import condition from splitio.models import rule_based_segments from splitio.models.fallback_treatment import FallbackTreatment -from splitio.models.fallback_config import FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration, FallbackTreatmentCalculator from splitio.engine import evaluator, splitters from splitio.engine.evaluator import EvaluationContext from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, InMemoryRuleBasedSegmentStorage, \ @@ -383,40 +383,35 @@ def test_evaluate_treatment_with_fallback(self, mocker): ctx = EvaluationContext(flags={'some': mocked_split}, segment_memberships=set(), rbs_segments={}) # should use global fallback - logger_mock.reset_mock() - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackTreatment("off-global", {"prop":"val"}))) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("off-global", '{"prop": "val"}')))) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx) assert result['treatment'] == 'off-global' assert result['configurations'] == '{"prop": "val"}' - assert result['impression']['label'] == "fallback - " + Label.SPLIT_NOT_FOUND - assert logger_mock.debug.mock_calls[0] == mocker.call("Using Global Fallback Treatment.") - + assert result['impression']['label'] == "fallback - " + Label.SPLIT_NOT_FOUND # should use by flag fallback - logger_mock.reset_mock() - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {"some2": FallbackTreatment("off-some2", '{"prop2": "val2"}')}))) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx) assert result['treatment'] == 'off-some2' assert result['configurations'] == '{"prop2": "val2"}' assert result['impression']['label'] == "fallback - " + Label.SPLIT_NOT_FOUND - assert logger_mock.debug.mock_calls[0] == mocker.call("Using Fallback Treatment for feature: %s", "some2") # should not use any fallback - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(None, {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {"some2": FallbackTreatment("off-some2", '{"prop2": "val2"}')}))) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some3', {}, ctx) assert result['treatment'] == 'control' assert result['configurations'] == None assert result['impression']['label'] == Label.SPLIT_NOT_FOUND # should use by flag fallback - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("off-global", '{"prop": "val"}'), {"some2": FallbackTreatment("off-some2", '{"prop2": "val2"}')}))) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some2', {}, ctx) assert result['treatment'] == 'off-some2' assert result['configurations'] == '{"prop2": "val2"}' assert result['impression']['label'] == "fallback - " + Label.SPLIT_NOT_FOUND # should global flag fallback - e = evaluator.Evaluator(splitter_mock, FallbackTreatmentsConfiguration(FallbackTreatment("off-global", {"prop":"val"}), {"some2": FallbackTreatment("off-some2", {"prop2":"val2"})})) + e = evaluator.Evaluator(splitter_mock, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("off-global", '{"prop": "val"}'), {"some2": FallbackTreatment("off-some2", '{"prop2": "val2"}')}))) result = e.eval_with_context('some_key', 'some_bucketing_key', 'some3', {}, ctx) assert result['treatment'] == 'off-global' assert result['configurations'] == '{"prop": "val"}' diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index 257d9099..9e7c614e 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -29,7 +29,7 @@ PluggableRuleBasedSegmentsStorage, PluggableRuleBasedSegmentsStorageAsync from splitio.storage.adapters.redis import build, RedisAdapter, RedisAdapterAsync, build_async from splitio.models import splits, segments, rule_based_segments -from splitio.models.fallback_config import FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration, FallbackTreatmentCalculator from splitio.models.fallback_treatment import FallbackTreatment from splitio.engine.impressions.impressions import Manager as ImpressionsManager, ImpressionsMode from splitio.engine.impressions import set_classes, set_classes_async @@ -561,7 +561,7 @@ def setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -720,7 +720,7 @@ def setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init def test_get_treatment(self): @@ -846,7 +846,7 @@ def setup_method(self): 'config': {'connectTimeout': 10000, 'streamingEnabled': False, 'impressionsMode': 'debug', - 'fallbackTreatments': FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + 'fallbackTreatments': FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')}) } } @@ -1017,7 +1017,7 @@ def setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init def test_get_treatment(self): @@ -1206,7 +1206,7 @@ def setup_method(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init class LocalhostIntegrationTests(object): # pylint: disable=too-few-public-methods @@ -1430,7 +1430,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1626,7 +1626,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1821,7 +1821,7 @@ def setup_method(self): sdk_ready_flag=None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -1975,7 +1975,7 @@ def test_optimized(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2033,7 +2033,7 @@ def test_debug(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2091,7 +2091,7 @@ def test_none(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2155,7 +2155,7 @@ def test_optimized(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init try: @@ -2222,7 +2222,7 @@ def test_debug(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init try: @@ -2289,7 +2289,7 @@ def test_none(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'fallback_feature': FallbackTreatment("on-local", '{"prop":"val"}')})) ) # pylint:disable=attribute-defined-outside-init try: @@ -2393,7 +2393,7 @@ async def _setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2565,7 +2565,7 @@ async def _setup_method(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -2709,7 +2709,7 @@ async def _setup_method(self): 'config': {'connectTimeout': 10000, 'streamingEnabled': False, 'impressionsMode': 'debug', - 'fallbackTreatments': FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + 'fallbackTreatments': FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')}) } } @@ -2919,7 +2919,7 @@ async def _setup_method(self): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3142,7 +3142,7 @@ async def _setup_method(self): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3377,7 +3377,7 @@ async def _setup_method(self): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -3607,7 +3607,7 @@ async def _setup_method(self): telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), telemetry_submitter=telemetry_submitter, - fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() @@ -3842,7 +3842,7 @@ async def _setup_method(self): manager, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), - fallback_treatments_configuration=FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", {"prop":"val"})}) + fallback_treatment_calculator=FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'fallback_feature': FallbackTreatment("on-local", '{"prop": "val"}')})) ) # pylint:disable=attribute-defined-outside-init # Adding data to storage @@ -4056,6 +4056,7 @@ async def test_optimized(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatment_calculator=FallbackTreatmentCalculator(None) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -4116,6 +4117,7 @@ async def test_debug(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatment_calculator=FallbackTreatmentCalculator(None) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -4176,6 +4178,7 @@ async def test_none(self): None, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatment_calculator=FallbackTreatmentCalculator(None) ) # pylint:disable=attribute-defined-outside-init except: pass @@ -4243,6 +4246,7 @@ async def test_optimized(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatment_calculator=FallbackTreatmentCalculator(None) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -4312,6 +4316,7 @@ async def test_debug(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatment_calculator=FallbackTreatmentCalculator(None) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True @@ -4381,6 +4386,7 @@ async def test_none(self): recorder, telemetry_producer=telemetry_producer, telemetry_init_producer=telemetry_producer.get_telemetry_init_producer(), + fallback_treatment_calculator=FallbackTreatmentCalculator(None) ) # pylint:disable=attribute-defined-outside-init ready_property = mocker.PropertyMock() ready_property.return_value = True diff --git a/tests/models/test_fallback.py b/tests/models/test_fallback.py index a3111277..4dfdf79e 100644 --- a/tests/models/test_fallback.py +++ b/tests/models/test_fallback.py @@ -1,11 +1,11 @@ from splitio.models.fallback_treatment import FallbackTreatment -from splitio.models.fallback_config import FallbackTreatmentsConfiguration +from splitio.models.fallback_config import FallbackTreatmentsConfiguration, FallbackTreatmentCalculator class FallbackTreatmentModelTests(object): """Fallback treatment model tests.""" def test_working(self): - fallback_treatment = FallbackTreatment("on", {"prop": "val"}) + fallback_treatment = FallbackTreatment("on", '{"prop": "val"}') assert fallback_treatment.config == '{"prop": "val"}' assert fallback_treatment.treatment == 'on' @@ -14,7 +14,7 @@ def test_working(self): assert fallback_treatment.treatment == 'off' class FallbackTreatmentsConfigModelTests(object): - """Fallback treatment model tests.""" + """Fallback treatment configuration model tests.""" def test_working(self): global_fb = FallbackTreatment("on") @@ -29,4 +29,28 @@ def test_working(self): fallback_config.by_flag_fallback_treatment["flag2"] = flag_fb assert fallback_config.by_flag_fallback_treatment == {"flag1": flag_fb, "flag2": flag_fb} - \ No newline at end of file + +class FallbackTreatmentCalculatorTests(object): + """Fallback treatment calculator model tests.""" + + def test_working(self): + fallback_config = FallbackTreatmentsConfiguration(FallbackTreatment("on" ,"{}"), None) + fallback_calculator = FallbackTreatmentCalculator(fallback_config) + assert fallback_calculator.fallback_treatments_configuration == fallback_config + assert fallback_calculator._label_prefix == "fallback - " + + fallback_treatment = fallback_calculator.resolve("feature", "not ready") + assert fallback_treatment.treatment == "on" + assert fallback_treatment.label == "fallback - not ready" + assert fallback_treatment.config == "{}" + + fallback_calculator._fallback_treatments_configuration = FallbackTreatmentsConfiguration(FallbackTreatment("on" ,"{}"), {'feature': FallbackTreatment("off" , '{"prop": "val"}')}) + fallback_treatment = fallback_calculator.resolve("feature", "not ready") + assert fallback_treatment.treatment == "off" + assert fallback_treatment.label == "fallback - not ready" + assert fallback_treatment.config == '{"prop": "val"}' + + fallback_treatment = fallback_calculator.resolve("feature2", "not ready") + assert fallback_treatment.treatment == "on" + assert fallback_treatment.label == "fallback - not ready" + assert fallback_treatment.config == "{}" From f3d1065ff3b1a35b4c0a3661286e1155d2ec5dd7 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 10 Sep 2025 09:03:03 -0700 Subject: [PATCH 14/26] deprecate redis errors param --- splitio/client/config.py | 5 +++++ splitio/storage/adapters/redis.py | 1 - tests/client/test_factory.py | 1 - tests/storage/adapters/test_redis_adapter.py | 3 --- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/splitio/client/config.py b/splitio/client/config.py index 316ac96f..1c055c64 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -171,7 +171,12 @@ def sanitize(sdk_key, config): processed["httpAuthenticateScheme"] = authenticate_scheme processed = _sanitize_fallback_config(config, processed) + + if config.get("redisErrors") is not None: + _LOGGER.warning('Parameter `redisErrors` is deprecated as it is no longer supported in redis lib.' \ + ' Will ignore this value.') + processed["redisErrors"] = None return processed def _sanitize_fallback_config(config, processed): diff --git a/splitio/storage/adapters/redis.py b/splitio/storage/adapters/redis.py index 4cf87b5e..8b8c5e5c 100644 --- a/splitio/storage/adapters/redis.py +++ b/splitio/storage/adapters/redis.py @@ -715,7 +715,6 @@ def _build_default_client(config): # pylint: disable=too-many-locals unix_socket_path = config.get('redisUnixSocketPath', None) encoding = config.get('redisEncoding', 'utf-8') encoding_errors = config.get('redisEncodingErrors', 'strict') -# errors = config.get('redisErrors', None) decode_responses = config.get('redisDecodeResponses', True) retry_on_timeout = config.get('redisRetryOnTimeout', False) ssl = config.get('redisSsl', False) diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index 9a5ad992..f8aa159d 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -112,7 +112,6 @@ def test_redis_client_creation(self, mocker): 'redisConnectionPool': False, 'redisUnixSocketPath': '/some_path', 'redisEncodingErrors': 'non-strict', - 'redisErrors': True, 'redisDecodeResponses': True, 'redisRetryOnTimeout': True, 'redisSsl': True, diff --git a/tests/storage/adapters/test_redis_adapter.py b/tests/storage/adapters/test_redis_adapter.py index 78d28bbc..ac25d03b 100644 --- a/tests/storage/adapters/test_redis_adapter.py +++ b/tests/storage/adapters/test_redis_adapter.py @@ -99,7 +99,6 @@ def test_adapter_building(self, mocker): 'redisUnixSocketPath': '/tmp/socket', 'redisEncoding': 'utf-8', 'redisEncodingErrors': 'strict', -# 'redisErrors': 'abc', 'redisDecodeResponses': True, 'redisRetryOnTimeout': True, 'redisSsl': True, @@ -151,7 +150,6 @@ def test_adapter_building(self, mocker): 'redisUnixSocketPath': '/tmp/socket', 'redisEncoding': 'utf-8', 'redisEncodingErrors': 'strict', -# 'redisErrors': 'abc', 'redisDecodeResponses': True, 'redisRetryOnTimeout': True, 'redisSsl': False, @@ -529,7 +527,6 @@ def master_for(se, 'redisUnixSocketPath': '/tmp/socket', 'redisEncoding': 'utf-8', 'redisEncodingErrors': 'strict', - 'redisErrors': 'abc', 'redisDecodeResponses': True, 'redisRetryOnTimeout': True, 'redisSsl': False, From 5e17009dbeae61d13376d958de0c29f035e5472a Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 10 Sep 2025 09:13:48 -0700 Subject: [PATCH 15/26] polish --- splitio/storage/adapters/redis.py | 1 - 1 file changed, 1 deletion(-) diff --git a/splitio/storage/adapters/redis.py b/splitio/storage/adapters/redis.py index 8b8c5e5c..92aa2544 100644 --- a/splitio/storage/adapters/redis.py +++ b/splitio/storage/adapters/redis.py @@ -739,7 +739,6 @@ def _build_default_client(config): # pylint: disable=too-many-locals unix_socket_path=unix_socket_path, encoding=encoding, encoding_errors=encoding_errors, -# errors=errors, Starting from redis 6.0.0 errors argument is removed decode_responses=decode_responses, retry_on_timeout=retry_on_timeout, ssl=ssl, From 6b02164f0f0960aded594b3f74a75ce13f50e9a4 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 10 Sep 2025 09:14:51 -0700 Subject: [PATCH 16/26] polish --- tests/client/test_factory.py | 1 - tests/storage/adapters/test_redis_adapter.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index f8aa159d..255d4296 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -156,7 +156,6 @@ def synchronize_config(*_): unix_socket_path='/some_path', encoding='utf-8', encoding_errors='non-strict', -# errors=True, decode_responses=True, retry_on_timeout=True, ssl=True, diff --git a/tests/storage/adapters/test_redis_adapter.py b/tests/storage/adapters/test_redis_adapter.py index ac25d03b..9888c853 100644 --- a/tests/storage/adapters/test_redis_adapter.py +++ b/tests/storage/adapters/test_redis_adapter.py @@ -125,7 +125,6 @@ def test_adapter_building(self, mocker): unix_socket_path='/tmp/socket', encoding='utf-8', encoding_errors='strict', -# errors='abc', decode_responses=True, retry_on_timeout=True, ssl=True, From 4fe9854021c5f975a163c610425beaff57e22f92 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 10 Sep 2025 13:02:03 -0700 Subject: [PATCH 17/26] Restrict redis lib to below 7.0.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1e1928fc..e2b4c74a 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ tests_require=TESTS_REQUIRES, extras_require={ 'test': TESTS_REQUIRES, - 'redis': ['redis>=2.10.5'], + 'redis': ['redis>=2.10.5,<7.0.0'], 'uwsgi': ['uwsgi>=2.0.0'], 'cpphash': ['mmh3cffi==0.2.1'], 'asyncio': ['aiohttp>=3.8.4', 'aiofiles>=23.1.0'], From 9685012f58577143b47e1905e2250c89d62d3fb1 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 12 Sep 2025 08:04:42 -0700 Subject: [PATCH 18/26] Updated version and changes --- CHANGES.txt | 4 ++++ splitio/version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index e66834b4..33c29af1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +10.5.0 (Sep 12, 2025) +- Changed the log level from error to debug when renewing the token for Streaming service in asyncio mode. +- Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs. + 10.4.0 (Aug 4, 2025) - Added a new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impressions sent to Split backend. Read more in our docs. diff --git a/splitio/version.py b/splitio/version.py index 9858bdcf..780d6251 100644 --- a/splitio/version.py +++ b/splitio/version.py @@ -1 +1 @@ -__version__ = '10.4.0' \ No newline at end of file +__version__ = '10.5.0' \ No newline at end of file From 0451cb17f5da93f5ec2eb1c07a7c790f9847c40d Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 12 Sep 2025 12:20:40 -0700 Subject: [PATCH 19/26] polish tests --- tests/client/test_client.py | 105 +++++++++++++++++++++++------------- 1 file changed, 68 insertions(+), 37 deletions(-) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 15c2b96b..70b147ff 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -23,7 +23,7 @@ from splitio.engine.impressions.manager import Counter as ImpressionsCounter from splitio.engine.impressions.unique_keys_tracker import UniqueKeysTracker, UniqueKeysTrackerAsync from splitio.engine.telemetry import TelemetryStorageConsumer, TelemetryStorageProducer, TelemetryStorageProducerAsync -from splitio.engine.evaluator import Evaluator +from splitio.engine.evaluator import Evaluator, EvaluationContext from splitio.recorder.recorder import StandardRecorder, StandardRecorderAsync from splitio.engine.impressions.strategies import StrategyDebugMode, StrategyNoneMode, StrategyOptimizedMode from tests.integration import splits_json @@ -1516,7 +1516,7 @@ def get_feature_flag_names_by_flag_sets(*_): pass @mock.patch('splitio.engine.evaluator.Evaluator.eval_with_context', side_effect=Exception()) - def test_fallback_treatment_exception_no_impressions(self, mocker): + def test_fallback_treatment_exception(self, mocker): # using fallback when the evaluator has RuntimeError exception split_storage = mocker.Mock(spec=SplitStorage) segment_storage = mocker.Mock(spec=SegmentStorage) @@ -2890,6 +2890,9 @@ async def test_fallback_treatment_eval_exception(self, mocker): impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) recorder = StandardRecorderAsync(impmanager, event_storage, impression_storage, telemetry_evaluation_producer, telemetry_producer.get_telemetry_runtime_producer()) + async def manager_start_task(): + pass + factory = SplitFactoryAsync(mocker.Mock(), {'splits': split_storage, 'segments': segment_storage, @@ -2902,7 +2905,7 @@ async def test_fallback_treatment_eval_exception(self, mocker): telemetry_producer, telemetry_producer.get_telemetry_init_producer(), mocker.Mock(), - mocker.Mock() + manager_start_task ) self.imps = None @@ -3020,7 +3023,7 @@ async def fetch_many_rbs(*_): @pytest.mark.asyncio @mock.patch('splitio.engine.evaluator.Evaluator.eval_with_context', side_effect=Exception()) - def test_fallback_treatment_exception_no_impressions(self, mocker): + async def test_fallback_treatment_exception(self, mocker): # using fallback when the evaluator has RuntimeError exception split_storage = mocker.Mock(spec=SplitStorage) segment_storage = mocker.Mock(spec=SegmentStorage) @@ -3033,11 +3036,14 @@ def test_fallback_treatment_exception_no_impressions(self, mocker): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - telemetry_storage = InMemoryTelemetryStorage() - telemetry_producer = TelemetryStorageProducer(telemetry_storage) + telemetry_storage = await InMemoryTelemetryStorageAsync.create() + telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) - recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) - factory = SplitFactory(mocker.Mock(), + recorder = StandardRecorderAsync(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + async def manager_start_task(): + pass + + factory = SplitFactoryAsync(mocker.Mock(), {'splits': split_storage, 'segments': segment_storage, 'rule_based_segments': rb_segment_storage, @@ -3046,14 +3052,14 @@ def test_fallback_treatment_exception_no_impressions(self, mocker): mocker.Mock(), recorder, impmanager, - mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), - mocker.Mock() + mocker.Mock(), + manager_start_task ) self.imps = None - def put(impressions): + async def put(impressions): self.imps = impressions impression_storage.put = put @@ -3061,37 +3067,49 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) - treatment = client.get_treatment("key", "some") + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) + + async def context_for(*_): + return EvaluationContext( + {}, + {}, + {} + ) + client._context_factory.context_for = context_for + + treatment = await client.get_treatment("key", "some") assert(treatment == "on-global") - assert(self.imps == None) + assert(self.imps[0].treatment == "on-global") + assert(self.imps[0].label == "fallback - exception") self.imps = None client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) - treatment = client.get_treatment("key2", "some") + treatment = await client.get_treatment("key2", "some") assert(treatment == "on-local") - assert(self.imps == None) + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - exception") self.imps = None client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")})) - treatment = client.get_treatment("key3", "some") + treatment = await client.get_treatment("key3", "some") assert(treatment == "on-local") - assert(self.imps == None) + assert(self.imps[0].treatment == "on-local") + assert(self.imps[0].label == "fallback - exception") self.imps = None client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")})) - treatment = client.get_treatment("key4", "some") + treatment = await client.get_treatment("key4", "some") assert(treatment == "control") - assert(self.imps == None) + assert(self.imps[0].treatment == "control") + assert(self.imps[0].label == "exception") try: - factory.destroy() + await factory.destroy() except: pass @pytest.mark.asyncio - @mock.patch('splitio.client.client.Client.ready', side_effect=None) - def test_fallback_treatment_not_ready_impressions(self, mocker): + async def test_fallback_treatment_not_ready_impressions(self, mocker): # using fallback when the evaluator has RuntimeError exception split_storage = mocker.Mock(spec=SplitStorage) segment_storage = mocker.Mock(spec=SegmentStorage) @@ -3102,11 +3120,14 @@ def test_fallback_treatment_not_ready_impressions(self, mocker): mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) - telemetry_storage = InMemoryTelemetryStorage() - telemetry_producer = TelemetryStorageProducer(telemetry_storage) + telemetry_storage = await InMemoryTelemetryStorageAsync.create() + telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) - recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) - factory = SplitFactory(mocker.Mock(), + recorder = StandardRecorderAsync(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + async def manager_start_task(): + pass + + factory = SplitFactoryAsync(mocker.Mock(), {'splits': split_storage, 'segments': segment_storage, 'rule_based_segments': rb_segment_storage, @@ -3115,14 +3136,14 @@ def test_fallback_treatment_not_ready_impressions(self, mocker): mocker.Mock(), recorder, impmanager, - mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), - mocker.Mock() + mocker.Mock(), + manager_start_task ) self.imps = None - def put(impressions): + async def put(impressions): self.imps = impressions impression_storage.put = put @@ -3130,35 +3151,45 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() - client = Client(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) - client.ready = False + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) + ready_property = mocker.PropertyMock() + ready_property.return_value = False + type(factory).ready = ready_property - treatment = client.get_treatment("key", "some") + async def context_for(*_): + return EvaluationContext( + {"some": {}}, + {}, + {} + ) + client._context_factory.context_for = context_for + + treatment = await client.get_treatment("key", "some") assert(self.imps[0].treatment == "on-global") assert(self.imps[0].label == "fallback - not ready") self.imps = None client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global"), {'some': FallbackTreatment("on-local")})) - treatment = client.get_treatment("key2", "some") + treatment = await client.get_treatment("key2", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") assert(self.imps[0].label == "fallback - not ready") self.imps = None client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'some': FallbackTreatment("on-local")})) - treatment = client.get_treatment("key3", "some") + treatment = await client.get_treatment("key3", "some") assert(treatment == "on-local") assert(self.imps[0].treatment == "on-local") assert(self.imps[0].label == "fallback - not ready") self.imps = None client._fallback_treatment_calculator = FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(None, {'some2': FallbackTreatment("on-local")})) - treatment = client.get_treatment("key4", "some") + treatment = await client.get_treatment("key4", "some") assert(treatment == "control") assert(self.imps[0].treatment == "control") assert(self.imps[0].label == "not ready") try: - factory.destroy() + await factory.destroy() except: pass \ No newline at end of file From bfe6ee7299b048c975346fc3a3ada6922b0e3661 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 12 Sep 2025 13:05:26 -0700 Subject: [PATCH 20/26] update tests --- tests/client/test_client.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 70b147ff..452a565f 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -2889,9 +2889,10 @@ async def test_fallback_treatment_eval_exception(self, mocker): telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) recorder = StandardRecorderAsync(impmanager, event_storage, impression_storage, telemetry_evaluation_producer, telemetry_producer.get_telemetry_runtime_producer()) - - async def manager_start_task(): - pass + + class TelemetrySubmitterMock(): + async def synchronize_config(*_): + pass factory = SplitFactoryAsync(mocker.Mock(), {'splits': split_storage, @@ -2904,19 +2905,19 @@ async def manager_start_task(): impmanager, telemetry_producer, telemetry_producer.get_telemetry_init_producer(), - mocker.Mock(), - manager_start_task + TelemetrySubmitterMock(), + None ) + ready_property = mocker.PropertyMock() + ready_property.return_value = True + type(factory).ready = ready_property + self.imps = None async def put(impressions): self.imps = impressions impression_storage.put = put - - class TelemetrySubmitterMock(): - async def synchronize_config(*_): - pass - factory._telemetry_submitter = TelemetrySubmitterMock() + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global", '{"prop": "val"}')))) async def get_feature_flag_names_by_flag_sets(*_): @@ -3040,8 +3041,6 @@ async def test_fallback_treatment_exception(self, mocker): telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) recorder = StandardRecorderAsync(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) - async def manager_start_task(): - pass factory = SplitFactoryAsync(mocker.Mock(), {'splits': split_storage, @@ -3055,8 +3054,11 @@ async def manager_start_task(): telemetry_producer, telemetry_producer.get_telemetry_init_producer(), mocker.Mock(), - manager_start_task + None ) + ready_property = mocker.PropertyMock() + ready_property.return_value = True + type(factory).ready = ready_property self.imps = None async def put(impressions): @@ -3067,6 +3069,7 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) async def context_for(*_): @@ -3151,6 +3154,7 @@ class TelemetrySubmitterMock(): def synchronize_config(*_): pass factory._telemetry_submitter = TelemetrySubmitterMock() + client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) ready_property = mocker.PropertyMock() ready_property.return_value = False From 13edab5e7ed6ac3cc0e9a9628d031dfee723ec20 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 12 Sep 2025 13:46:53 -0700 Subject: [PATCH 21/26] update test --- tests/client/test_factory.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index 255d4296..e6096344 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -583,6 +583,8 @@ def synchronize_config(*_): assert clear_impressions._called == 1 assert clear_events._called == 1 factory.destroy() + time.sleep(0.1) + assert factory.destroyed def test_error_prefork(self, mocker): """Test not handling fork.""" @@ -645,6 +647,8 @@ def synchronize_config(*_): pass assert factory.ready factory.destroy() + time.sleep(0.1) + assert factory.destroyed def test_destroy_with_event_pluggable(self, mocker): config = { @@ -700,7 +704,8 @@ def synchronize_config(*_): assert factory._status == Status.WAITING_FORK factory.destroy() - + time.sleep(0.1) + assert factory.destroyed class SplitFactoryAsyncTests(object): """Split factory async test cases.""" From bcd67de0b9c12b82188843f00bea740d78ddfbf8 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Sun, 14 Sep 2025 19:13:16 -0700 Subject: [PATCH 22/26] fix tests --- CHANGES.txt | 1 + tests/client/test_factory.py | 76 +++++++++++++++++----------- tests/client/test_input_validator.py | 27 ++++++---- 3 files changed, 66 insertions(+), 38 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 33c29af1..c9edfb33 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,7 @@ 10.5.0 (Sep 12, 2025) - Changed the log level from error to debug when renewing the token for Streaming service in asyncio mode. - Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs. +- Deprecated config parameter `redisErrors` as it is removed in redis lib since 6.0.0 version (https://github.com/redis/redis-py/releases/tag/v6.0.0). 10.4.0 (Aug 4, 2025) - Added a new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impressions sent to Split backend. Read more in our docs. diff --git a/tests/client/test_factory.py b/tests/client/test_factory.py index e6096344..3a43e29f 100644 --- a/tests/client/test_factory.py +++ b/tests/client/test_factory.py @@ -27,32 +27,37 @@ class SplitFactoryTests(object): """Split factory test cases.""" - def test_flag_sets_counts(self): + def test_flag_sets_counts(self): factory = get_factory("none", config={ 'flagSetsFilter': ['set1', 'set2', 'set3'] }) assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets == 3 assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets_invalid == 0 - factory.destroy() - + event = threading.Event() + factory.destroy(event) + event.wait() + factory = get_factory("none", config={ 'flagSetsFilter': ['s#et1', 'set2', 'set3'] }) assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets == 3 assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets_invalid == 1 - factory.destroy() + event = threading.Event() + factory.destroy(event) + event.wait() factory = get_factory("none", config={ 'flagSetsFilter': ['s#et1', 22, 'set3'] }) assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets == 3 assert factory._telemetry_init_producer._telemetry_storage._tel_config._flag_sets_invalid == 2 - factory.destroy() + event = threading.Event() + factory.destroy(event) + event.wait() def test_inmemory_client_creation_streaming_false(self, mocker): """Test that a client with in-memory storage is created correctly.""" - # Setup synchronizer def _split_synchronizer(self, ready_flag, some, auth_api, streaming_enabled, sdk_matadata, telemetry_runtime_producer, sse_url=None, client_key=None): synchronizer = mocker.Mock(spec=Synchronizer) @@ -518,9 +523,15 @@ def synchronize_config(*_): event.wait() assert _INSTANTIATED_FACTORIES['some_other_api_key'] == 1 assert _INSTANTIATED_FACTORIES['some_api_key'] == 2 - factory2.destroy() - factory3.destroy() - factory4.destroy() + event = threading.Event() + factory2.destroy(event) + event.wait() + event = threading.Event() + factory3.destroy(event) + event.wait() + event = threading.Event() + factory4.destroy(event) + event.wait() def test_uwsgi_preforked(self, mocker): """Test preforked initializations.""" @@ -740,17 +751,22 @@ async def test_flag_sets_counts(self): @pytest.mark.asyncio async def test_inmemory_client_creation_streaming_false_async(self, mocker): """Test that a client with in-memory storage is created correctly for async.""" - # Setup synchronizer def _split_synchronizer(self, ready_flag, some, auth_api, streaming_enabled, sdk_matadata, telemetry_runtime_producer, sse_url=None, client_key=None): synchronizer = mocker.Mock(spec=SynchronizerAsync) async def sync_all(*_): return None synchronizer.sync_all = sync_all + + def start_periodic_fetching(): + pass + synchronizer.start_periodic_fetching = start_periodic_fetching + self._ready_flag = ready_flag self._synchronizer = synchronizer self._streaming_enabled = False self._telemetry_runtime_producer = telemetry_runtime_producer + mocker.patch('splitio.sync.manager.ManagerAsync.__init__', new=_split_synchronizer) async def synchronize_config(*_): @@ -758,29 +774,30 @@ async def synchronize_config(*_): mocker.patch('splitio.sync.telemetry.InMemoryTelemetrySubmitterAsync.synchronize_config', new=synchronize_config) # Start factory and make assertions - factory = await get_factory_async('some_api_key', config={'streamingEmabled': False}) - assert isinstance(factory, SplitFactoryAsync) - assert isinstance(factory._storages['splits'], inmemmory.InMemorySplitStorageAsync) - assert isinstance(factory._storages['segments'], inmemmory.InMemorySegmentStorageAsync) - assert isinstance(factory._storages['impressions'], inmemmory.InMemoryImpressionStorageAsync) - assert factory._storages['impressions']._impressions.maxsize == 10000 - assert isinstance(factory._storages['events'], inmemmory.InMemoryEventStorageAsync) - assert factory._storages['events']._events.maxsize == 10000 + factory2 = await get_factory_async('some_api_key', config={'streamingEmabled': False}) - assert isinstance(factory._sync_manager, ManagerAsync) + assert isinstance(factory2, SplitFactoryAsync) + assert isinstance(factory2._storages['splits'], inmemmory.InMemorySplitStorageAsync) + assert isinstance(factory2._storages['segments'], inmemmory.InMemorySegmentStorageAsync) + assert isinstance(factory2._storages['impressions'], inmemmory.InMemoryImpressionStorageAsync) + assert factory2._storages['impressions']._impressions.maxsize == 10000 + assert isinstance(factory2._storages['events'], inmemmory.InMemoryEventStorageAsync) + assert factory2._storages['events']._events.maxsize == 10000 - assert isinstance(factory._recorder, StandardRecorderAsync) - assert isinstance(factory._recorder._impressions_manager, ImpressionsManager) - assert isinstance(factory._recorder._event_sotrage, inmemmory.EventStorage) - assert isinstance(factory._recorder._impression_storage, inmemmory.ImpressionStorage) + assert isinstance(factory2._sync_manager, ManagerAsync) - assert factory._labels_enabled is True + assert isinstance(factory2._recorder, StandardRecorderAsync) + assert isinstance(factory2._recorder._impressions_manager, ImpressionsManager) + assert isinstance(factory2._recorder._event_sotrage, inmemmory.EventStorage) + assert isinstance(factory2._recorder._impression_storage, inmemmory.ImpressionStorage) + + assert factory2._labels_enabled is True try: - await factory.block_until_ready(1) + await factory2.block_until_ready(1) except: pass - assert factory.ready - await factory.destroy() + assert factory2._status == Status.READY + await factory2.destroy() @pytest.mark.asyncio async def test_destroy_async(self, mocker): @@ -884,7 +901,7 @@ async def start(*_): await factory.block_until_ready(1) except: pass - assert factory.ready + assert factory._status == Status.READY assert factory.destroyed is False await factory.destroy() @@ -925,7 +942,7 @@ async def test_pluggable_client_creation_async(self, mocker): await factory.block_until_ready(1) except: pass - assert factory.ready + assert factory._status == Status.READY await factory.destroy() @pytest.mark.asyncio @@ -954,3 +971,4 @@ async def _make_factory_with_apikey(apikey, *_, **__): await asyncio.sleep(0.5) assert factory.destroyed assert len(build_redis.mock_calls) == 2 + \ No newline at end of file diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index 06ae5b60..be2ec574 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -1705,7 +1705,8 @@ async def get_change_number(*_): impmanager, telemetry_producer, telemetry_producer.get_telemetry_init_producer(), - mocker.Mock() + mocker.Mock(), + None ) ready_mock = mocker.PropertyMock() ready_mock.return_value = True @@ -1967,7 +1968,8 @@ async def get_change_number(*_): impmanager, telemetry_producer, telemetry_producer.get_telemetry_init_producer(), - mocker.Mock() + mocker.Mock(), + None ) ready_mock = mocker.PropertyMock() ready_mock.return_value = True @@ -2211,7 +2213,8 @@ async def put(*_): impmanager, telemetry_producer, telemetry_producer.get_telemetry_init_producer(), - mocker.Mock() + mocker.Mock(), + None ) factory._sdk_key = 'some-test' @@ -2501,7 +2504,8 @@ async def fetch_many_rbs(*_): impmanager, telemetry_producer, telemetry_producer.get_telemetry_init_producer(), - mocker.Mock() + mocker.Mock(), + None ) ready_mock = mocker.PropertyMock() ready_mock.return_value = True @@ -2665,7 +2669,8 @@ async def fetch_many_rbs(*_): impmanager, telemetry_producer, telemetry_producer.get_telemetry_init_producer(), - mocker.Mock() + mocker.Mock(), + None ) split_mock.name = 'some_feature' @@ -2832,7 +2837,8 @@ async def fetch_many_rbs(*_): mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), - mocker.Mock() + mocker.Mock(), + None ) ready_mock = mocker.PropertyMock() ready_mock.return_value = True @@ -2978,7 +2984,8 @@ async def get_feature_flags_by_sets(*_): mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), - mocker.Mock() + mocker.Mock(), + None ) ready_mock = mocker.PropertyMock() ready_mock.return_value = True @@ -3133,7 +3140,8 @@ async def get_feature_flags_by_sets(*_): mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), - mocker.Mock() + mocker.Mock(), + None ) ready_mock = mocker.PropertyMock() ready_mock.return_value = True @@ -3282,7 +3290,8 @@ async def get_feature_flags_by_sets(*_): mocker.Mock(), telemetry_producer, telemetry_producer.get_telemetry_init_producer(), - mocker.Mock() + mocker.Mock(), + None ) ready_mock = mocker.PropertyMock() ready_mock.return_value = True From 6f2d22490f3b17861c9c094d6c86f353926501eb Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Sun, 14 Sep 2025 19:31:28 -0700 Subject: [PATCH 23/26] update test --- tests/client/test_client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 452a565f..ae26e099 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -2868,6 +2868,10 @@ async def synchronize_config(*_): assert await client.get_treatments_with_config_by_flag_sets('some_key', ['set_1'], evaluation_options=EvaluationOptions({"prop": "value"})) == {'SPLIT_2': ('on', None)} assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + try: + await factory.destroy() + except: + pass @pytest.mark.asyncio @mock.patch('splitio.engine.evaluator.Evaluator.eval_with_context', side_effect=RuntimeError()) From c838083b7c9534d28cb89472b1d24cbe77a92503 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Sun, 14 Sep 2025 19:41:14 -0700 Subject: [PATCH 24/26] update test --- tests/client/test_client.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index ae26e099..27ed399d 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -2874,7 +2874,6 @@ async def synchronize_config(*_): pass @pytest.mark.asyncio - @mock.patch('splitio.engine.evaluator.Evaluator.eval_with_context', side_effect=RuntimeError()) async def test_fallback_treatment_eval_exception(self, mocker): # using fallback when the evaluator has RuntimeError exception split_storage = mocker.Mock(spec=SplitStorage) @@ -2891,7 +2890,7 @@ async def test_fallback_treatment_eval_exception(self, mocker): telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) telemetry_evaluation_producer = telemetry_producer.get_telemetry_evaluation_producer() - impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) + impmanager = ImpressionManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) recorder = StandardRecorderAsync(impmanager, event_storage, impression_storage, telemetry_evaluation_producer, telemetry_producer.get_telemetry_runtime_producer()) class TelemetrySubmitterMock(): @@ -2924,6 +2923,10 @@ async def put(impressions): client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global", '{"prop": "val"}')))) + def eval_with_context(*_): + raise RuntimeError() + client._evaluator.eval_with_context = eval_with_context + async def get_feature_flag_names_by_flag_sets(*_): return ["some", "some2"] client._get_feature_flag_names_by_flag_sets = get_feature_flag_names_by_flag_sets @@ -3027,7 +3030,6 @@ async def fetch_many_rbs(*_): pass @pytest.mark.asyncio - @mock.patch('splitio.engine.evaluator.Evaluator.eval_with_context', side_effect=Exception()) async def test_fallback_treatment_exception(self, mocker): # using fallback when the evaluator has RuntimeError exception split_storage = mocker.Mock(spec=SplitStorage) @@ -3043,7 +3045,7 @@ async def test_fallback_treatment_exception(self, mocker): telemetry_storage = await InMemoryTelemetryStorageAsync.create() telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) - impmanager = ImpressionManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) + impmanager = ImpressionManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_producer.get_telemetry_runtime_producer()) recorder = StandardRecorderAsync(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) factory = SplitFactoryAsync(mocker.Mock(), @@ -3075,6 +3077,10 @@ def synchronize_config(*_): factory._telemetry_submitter = TelemetrySubmitterMock() client = ClientAsync(factory, recorder, True, FallbackTreatmentCalculator(FallbackTreatmentsConfiguration(FallbackTreatment("on-global")))) + + def eval_with_context(*_): + raise Exception() + client._evaluator.eval_with_context = eval_with_context async def context_for(*_): return EvaluationContext( From 0e2f13cfe2c1912aafe0bcdf4d34b4331eac7109 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Sun, 14 Sep 2025 19:52:43 -0700 Subject: [PATCH 25/26] updated changes --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index c9edfb33..58205457 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -10.5.0 (Sep 12, 2025) +10.5.0 (Sep 15, 2025) - Changed the log level from error to debug when renewing the token for Streaming service in asyncio mode. - Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs. - Deprecated config parameter `redisErrors` as it is removed in redis lib since 6.0.0 version (https://github.com/redis/redis-py/releases/tag/v6.0.0). From 1226d2bc5ce73bd4aae00b269def7dabd2b714d6 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Sun, 14 Sep 2025 20:11:15 -0700 Subject: [PATCH 26/26] polishing --- splitio/client/config.py | 44 +++++++++++++++++++------------------ splitio/engine/evaluator.py | 2 +- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/splitio/client/config.py b/splitio/client/config.py index 1c055c64..25b1bc31 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -180,26 +180,28 @@ def sanitize(sdk_key, config): return processed def _sanitize_fallback_config(config, processed): - if config.get('fallbackTreatments') is not None: - if not isinstance(config['fallbackTreatments'], FallbackTreatmentsConfiguration): - _LOGGER.warning('Config: fallbackTreatments parameter should be of `FallbackTreatmentsConfiguration` class.') - processed['fallbackTreatments'] = None - return processed - - sanitized_global_fallback_treatment = config['fallbackTreatments'].global_fallback_treatment - if config['fallbackTreatments'].global_fallback_treatment is not None and not validate_fallback_treatment(config['fallbackTreatments'].global_fallback_treatment): - _LOGGER.warning('Config: global fallbacktreatment parameter is discarded.') - sanitized_global_fallback_treatment = None - - sanitized_flag_fallback_treatments = {} - if config['fallbackTreatments'].by_flag_fallback_treatment is not None: - for feature_name in config['fallbackTreatments'].by_flag_fallback_treatment.keys(): - if not validate_regex_name(feature_name) or not validate_fallback_treatment(config['fallbackTreatments'].by_flag_fallback_treatment[feature_name]): - _LOGGER.warning('Config: fallback treatment parameter for feature flag %s is discarded.', feature_name) - continue - - sanitized_flag_fallback_treatments[feature_name] = config['fallbackTreatments'].by_flag_fallback_treatment[feature_name] - - processed['fallbackTreatments'] = FallbackTreatmentsConfiguration(sanitized_global_fallback_treatment, sanitized_flag_fallback_treatments) + if config.get('fallbackTreatments') is None: + return processed + + if not isinstance(config['fallbackTreatments'], FallbackTreatmentsConfiguration): + _LOGGER.warning('Config: fallbackTreatments parameter should be of `FallbackTreatmentsConfiguration` class.') + processed['fallbackTreatments'] = None + return processed + + sanitized_global_fallback_treatment = config['fallbackTreatments'].global_fallback_treatment + if config['fallbackTreatments'].global_fallback_treatment is not None and not validate_fallback_treatment(config['fallbackTreatments'].global_fallback_treatment): + _LOGGER.warning('Config: global fallbacktreatment parameter is discarded.') + sanitized_global_fallback_treatment = None + + sanitized_flag_fallback_treatments = {} + if config['fallbackTreatments'].by_flag_fallback_treatment is not None: + for feature_name in config['fallbackTreatments'].by_flag_fallback_treatment.keys(): + if not validate_regex_name(feature_name) or not validate_fallback_treatment(config['fallbackTreatments'].by_flag_fallback_treatment[feature_name]): + _LOGGER.warning('Config: fallback treatment parameter for feature flag %s is discarded.', feature_name) + continue + + sanitized_flag_fallback_treatments[feature_name] = config['fallbackTreatments'].by_flag_fallback_treatment[feature_name] + + processed['fallbackTreatments'] = FallbackTreatmentsConfiguration(sanitized_global_fallback_treatment, sanitized_flag_fallback_treatments) return processed \ No newline at end of file diff --git a/splitio/engine/evaluator.py b/splitio/engine/evaluator.py index 017b5e74..b47db5c5 100644 --- a/splitio/engine/evaluator.py +++ b/splitio/engine/evaluator.py @@ -64,7 +64,7 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx): else: label, _treatment = self._check_prerequisites(feature, bucketing, key, attrs, ctx, label, _treatment) label, _treatment = self._get_treatment(feature, bucketing, key, attrs, ctx, label, _treatment) - config = feature.get_configurations_for(_treatment) if feature else None + config = feature.get_configurations_for(_treatment) return { 'treatment': _treatment,