From 81780a4f5f91411152f8b934af25743486e4284b Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Mon, 7 Jul 2025 21:45:13 -0700 Subject: [PATCH 1/6] Added evaluation options --- splitio/client/client.py | 260 +++++++++++++++++---------- splitio/client/input_validator.py | 13 ++ tests/client/test_client.py | 74 ++++---- tests/integration/test_client_e2e.py | 2 +- 4 files changed, 217 insertions(+), 132 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 94413289..b695a1b1 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -85,7 +85,7 @@ def _client_is_usable(self): return True @staticmethod - def _validate_treatment_input(key, feature, attributes, method, impressions_properties=None): + def _validate_treatment_input(key, feature, attributes, method, evaluation_options=None): """Perform all static validations on user supplied input.""" matching_key, bucketing_key = input_validator.validate_key(key, 'get_' + method.value) if not matching_key: @@ -98,11 +98,11 @@ def _validate_treatment_input(key, feature, attributes, method, impressions_prop if not input_validator.validate_attributes(attributes, 'get_' + method.value): raise _InvalidInputError() - impressions_properties = ClientBase._validate_treatment_properties(method, impressions_properties) - return matching_key, bucketing_key, feature, attributes, impressions_properties + evaluation_options = ClientBase._validate_treatment_options('get_' + method.value, evaluation_options) + return matching_key, bucketing_key, feature, attributes, evaluation_options @staticmethod - def _validate_treatments_input(key, features, attributes, method, impressions_properties=None): + def _validate_treatments_input(key, features, attributes, method, evaluation_options=None): """Perform all static validations on user supplied input.""" matching_key, bucketing_key = input_validator.validate_key(key, 'get_' + method.value) if not matching_key: @@ -112,19 +112,23 @@ def _validate_treatments_input(key, features, attributes, method, impressions_pr if not features: raise _InvalidInputError() - if not input_validator.validate_attributes(attributes, method): + if not input_validator.validate_attributes(attributes, 'get_' + method.value): raise _InvalidInputError() - impressions_properties = ClientBase._validate_treatment_properties(method, impressions_properties) - return matching_key, bucketing_key, features, attributes, impressions_properties + evaluation_options = ClientBase._validate_treatment_options('get_' + method.value, evaluation_options) + return matching_key, bucketing_key, features, attributes, evaluation_options @staticmethod - def _validate_treatment_properties(method, properties=None): - if properties is not None: - valid, properties, size = input_validator.valid_properties(properties, 'get_' + method.value) + def _validate_treatment_options(method_name, evaluation_options=None): + evaluation_options = input_validator.validate_evaluation_options(evaluation_options, method_name) + if evaluation_options == None: + return None + + if evaluation_options["properties"] is not None: + valid, evaluation_options["properties"], size = input_validator.valid_properties(evaluation_options["properties"], method_name) if not valid: - properties = None - return properties + evaluation_options["properties"] = None + return evaluation_options def _build_impression(self, key, bucketing, feature, result, properties=None): """Build an impression based on evaluation data & it's result.""" @@ -193,6 +197,9 @@ def _validate_track(self, key, traffic_type, event_type, value=None, properties= return True, event, size + def _get_properties(self, evaluation_options): + return evaluation_options["properties"] if evaluation_options != None and evaluation_options.get("properties") != None else None + class Client(ClientBase): # pylint: disable=too-many-instance-attributes """Entry point for the split sdk.""" @@ -223,7 +230,7 @@ def destroy(self): """ self._factory.destroy() - def get_treatment(self, key, feature_flag_name, attributes=None, impressions_properties=None): + def get_treatment(self, key, feature_flag_name, attributes=None, evaluation_options=None): """ Get the treatment for a feature flag and key, with an optional dictionary of attributes. @@ -236,18 +243,20 @@ def get_treatment(self, key, feature_flag_name, attributes=None, impressions_pro :type feature_flag_name: str :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: The treatment for the key and feature flag :rtype: str """ try: - treatment, _ = self._get_treatment(MethodExceptionsAndLatencies.TREATMENT, key, feature_flag_name, attributes, impressions_properties) + treatment, _ = self._get_treatment(MethodExceptionsAndLatencies.TREATMENT, key, feature_flag_name, attributes, evaluation_options) return treatment except: _LOGGER.error('get_treatment failed') return CONTROL - def get_treatment_with_config(self, key, feature_flag_name, attributes=None, impressions_properties=None): + def get_treatment_with_config(self, key, feature_flag_name, attributes=None, evaluation_options=None): """ Get the treatment and config for a feature flag and key, with optional dictionary of attributes. @@ -260,17 +269,19 @@ def get_treatment_with_config(self, key, feature_flag_name, attributes=None, imp :type feature: str :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: The treatment for the key and feature flag :rtype: tuple(str, str) """ try: - return self._get_treatment(MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG, key, feature_flag_name, attributes, impressions_properties) + return self._get_treatment(MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG, key, feature_flag_name, attributes, evaluation_options) except Exception: _LOGGER.error('get_treatment_with_config failed') return CONTROL, None - def _get_treatment(self, method, key, feature, attributes=None, impressions_properties=None): + 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. @@ -282,6 +293,8 @@ def _get_treatment(self, method, key, feature, attributes=None, impressions_prop :type attributes: dict :param method: The method calling this function :type method: splitio.models.telemetry.MethodExceptionsAndLatencies + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: The treatment and config for the key and feature flag :rtype: dict """ @@ -294,7 +307,7 @@ def _get_treatment(self, method, key, feature, attributes=None, impressions_prop self._telemetry_init_producer.record_not_ready_usage() try: - key, bucketing, feature, attributes, impressions_properties = self._validate_treatment_input(key, feature, attributes, method, impressions_properties) + key, bucketing, feature, attributes, evaluation_options = self._validate_treatment_input(key, feature, attributes, method, evaluation_options) except _InvalidInputError: return CONTROL, None @@ -310,13 +323,14 @@ def _get_treatment(self, method, key, feature, attributes=None, impressions_prop self._telemetry_evaluation_producer.record_exception(method) result = self._FAILED_EVAL_RESULT + properties = self._get_properties(evaluation_options) if result['impression']['label'] != Label.SPLIT_NOT_FOUND: - impression_decorated = self._build_impression(key, bucketing, feature, result, impressions_properties) + 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, impressions_properties=None): + 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. @@ -329,17 +343,19 @@ def get_treatments(self, key, feature_flag_names, attributes=None, impressions_p :type feature: list :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: Dictionary with the result of all the feature flags provided :rtype: dict """ try: - with_config = self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS, attributes, impressions_properties) + with_config = self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS, attributes, evaluation_options) return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} except Exception: return {feature: CONTROL for feature in feature_flag_names} - def get_treatments_with_config(self, key, feature_flag_names, attributes=None, impressions_properties=None): + def get_treatments_with_config(self, key, feature_flag_names, attributes=None, evaluation_options=None): """ Evaluate multiple feature flags and return a dict with feature flag -> (treatment, config). @@ -352,32 +368,36 @@ def get_treatments_with_config(self, key, feature_flag_names, attributes=None, i :type feature: list :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: Dictionary with the result of all the feature flags provided :rtype: dict """ try: - return self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, attributes, impressions_properties) + 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} - def get_treatments_by_flag_set(self, key, flag_set, attributes=None, impressions_properties=None): - """ - Get treatments for feature flags that contain given flag set. - This method never raises an exception. If there's a problem, the appropriate log message - will be generated and the method will return the CONTROL treatment. - :param key: The key for which to get the treatment - :type key: str - :param flag_set: flag set - :type flag_sets: str - :param attributes: An optional dictionary of attributes - :type attributes: dict - :return: Dictionary with the result of all the feature flags provided - :rtype: dict - """ - return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, attributes, impressions_properties) - - def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None, impressions_properties=None): + def get_treatments_by_flag_set(self, key, flag_set, attributes=None, evaluation_options=None): + """ + Get treatments for feature flags that contain given flag set. + This method never raises an exception. If there's a problem, the appropriate log message + will be generated and the method will return the CONTROL treatment. + :param key: The key for which to get the treatment + :type key: str + :param flag_set: flag set + :type flag_sets: str + :param attributes: An optional dictionary of attributes + :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict + :return: Dictionary with the result of all the feature flags provided + :rtype: dict + """ + return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, attributes, evaluation_options) + + def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None, evaluation_options=None): """ Get treatments for feature flags that contain given flag sets. This method never raises an exception. If there's a problem, the appropriate log message @@ -388,12 +408,14 @@ def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None, impressio :type flag_sets: list :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, attributes, impressions_properties) + return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, attributes, evaluation_options) - def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None, impressions_properties=None): + def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None, evaluation_options=None): """ Get treatments for feature flags that contain given flag set. This method never raises an exception. If there's a problem, the appropriate log message @@ -404,12 +426,14 @@ def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None, :type flag_sets: str :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, attributes, impressions_properties) + return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, attributes, evaluation_options) - def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=None, impressions_properties=None): + def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=None, evaluation_options=None): """ Get treatments for feature flags that contain given flag set. This method never raises an exception. If there's a problem, the appropriate log message @@ -420,12 +444,14 @@ def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=Non :type flag_sets: str :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, attributes, impressions_properties) + return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, attributes, evaluation_options) - def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None, impressions_properties=None): + def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None, evaluation_options=None): """ Get treatments for feature flags that contain given flag sets. This method never raises an exception. If there's a problem, the appropriate log message @@ -438,6 +464,8 @@ def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None, :type method: splitio.models.telemetry.MethodExceptionsAndLatencies :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: Dictionary with the result of all the feature flags provided :rtype: dict """ @@ -447,12 +475,12 @@ def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None, return {} if 'config' in method.value: - return self._get_treatments(key, feature_flags_names, method, attributes, impressions_properties) + return self._get_treatments(key, feature_flags_names, method, attributes, evaluation_options) - with_config = self._get_treatments(key, feature_flags_names, method, attributes, impressions_properties) + with_config = self._get_treatments(key, feature_flags_names, method, attributes, evaluation_options) return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} - def get_treatments_by_flag_set(self, key, flag_set, attributes=None, impressions_properties=None): + def get_treatments_by_flag_set(self, key, flag_set, attributes=None, evaluation_options=None): """ Get treatments for feature flags that contain given flag set. @@ -465,13 +493,15 @@ def get_treatments_by_flag_set(self, key, flag_set, attributes=None, impressions :type flag_sets: str :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, attributes, impressions_properties) + return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, attributes, evaluation_options) - def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None, impressions_properties=None): + def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None, evaluation_options=None): """ Get treatments for feature flags that contain given flag sets. @@ -484,13 +514,15 @@ def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None, impressio :type flag_sets: list :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, attributes, impressions_properties) + return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, attributes, evaluation_options) - def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None, impressions_properties=None): + def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None, evaluation_options=None): """ Get treatments for feature flags that contain given flag set. @@ -503,13 +535,15 @@ def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None, :type flag_sets: str :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, attributes, impressions_properties) + return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, attributes, evaluation_options) - def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=None, impressions_properties=None): + def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=None, evaluation_options=None): """ Get treatments for feature flags that contain given flag set. @@ -522,11 +556,13 @@ def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=Non :type flag_sets: str :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, attributes, impressions_properties) + return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, attributes, evaluation_options) def _get_feature_flag_names_by_flag_sets(self, flag_sets, method_name): """ @@ -545,7 +581,7 @@ def _get_feature_flag_names_by_flag_sets(self, flag_sets, method_name): return feature_flags_by_set - def _get_treatments(self, key, features, method, attributes=None, impressions_properties=None): + def _get_treatments(self, key, features, method, attributes=None, evaluation_options=None): """ Validate key, feature flag names and objects, and get the treatments and configs with an optional dictionary of attributes. @@ -557,6 +593,8 @@ def _get_treatments(self, key, features, method, attributes=None, impressions_pr :type method: splitio.models.telemetry.MethodExceptionsAndLatencies :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: The treatments and configs for the key and feature flags :rtype: dict @@ -570,7 +608,7 @@ def _get_treatments(self, key, features, method, attributes=None, impressions_pr self._telemetry_init_producer.record_not_ready_usage() try: - key, bucketing, features, attributes, impressions_properties = self._validate_treatments_input(key, features, attributes, method, impressions_properties) + 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) @@ -586,8 +624,9 @@ def _get_treatments(self, key, features, method, attributes=None, impressions_pr self._telemetry_evaluation_producer.record_exception(method) results = {n: self._FAILED_EVAL_RESULT for n in features} + properties = self._get_properties(evaluation_options) imp_decorated_attrs = [ - (i, attributes) for i in self._build_impressions(key, bucketing, results, impressions_properties) + (i, attributes) for i in self._build_impressions(key, bucketing, results, properties) if i.Impression.label != Label.SPLIT_NOT_FOUND ] self._record_stats(imp_decorated_attrs, start, method) @@ -690,7 +729,7 @@ async def destroy(self): """ await self._factory.destroy() - async def get_treatment(self, key, feature_flag_name, attributes=None, impressions_properties=None): + async def get_treatment(self, key, feature_flag_name, attributes=None, evaluation_options=None): """ Get the treatment for a feature and key, with an optional dictionary of attributes, for async calls @@ -703,18 +742,20 @@ async def get_treatment(self, key, feature_flag_name, attributes=None, impressio :type feature: str :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: The treatment for the key and feature :rtype: str """ try: - treatment, _ = await self._get_treatment(MethodExceptionsAndLatencies.TREATMENT, key, feature_flag_name, attributes, impressions_properties) + treatment, _ = await self._get_treatment(MethodExceptionsAndLatencies.TREATMENT, key, feature_flag_name, attributes, evaluation_options) return treatment except: _LOGGER.error('get_treatment failed') return CONTROL - async def get_treatment_with_config(self, key, feature_flag_name, attributes=None, impressions_properties=None): + async def get_treatment_with_config(self, key, feature_flag_name, attributes=None, evaluation_options=None): """ Get the treatment for a feature and key, with an optional dictionary of attributes, for async calls @@ -727,17 +768,19 @@ async def get_treatment_with_config(self, key, feature_flag_name, attributes=Non :type feature: str :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: The treatment for the key and feature :rtype: str """ try: - return await self._get_treatment(MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG, key, feature_flag_name, attributes, impressions_properties) + return await self._get_treatment(MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG, key, feature_flag_name, attributes, evaluation_options) except Exception: _LOGGER.error('get_treatment_with_config failed') return CONTROL, None - async def _get_treatment(self, method, key, feature, attributes=None, impressions_properties=None): + async 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, for async calls @@ -749,6 +792,8 @@ async def _get_treatment(self, method, key, feature, attributes=None, impression :type attributes: dict :param method: The method calling this function :type method: splitio.models.telemetry.MethodExceptionsAndLatencies + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: The treatment and config for the key and feature flag :rtype: dict """ @@ -761,7 +806,7 @@ async def _get_treatment(self, method, key, feature, attributes=None, impression await self._telemetry_init_producer.record_not_ready_usage() try: - key, bucketing, feature, attributes, impressions_properties = self._validate_treatment_input(key, feature, attributes, method, impressions_properties) + key, bucketing, feature, attributes, evaluation_options = self._validate_treatment_input(key, feature, attributes, method, evaluation_options) except _InvalidInputError: return CONTROL, None @@ -777,12 +822,13 @@ async def _get_treatment(self, method, key, feature, attributes=None, impression await self._telemetry_evaluation_producer.record_exception(method) result = self._FAILED_EVAL_RESULT + properties = self._get_properties(evaluation_options) if result['impression']['label'] != Label.SPLIT_NOT_FOUND: - impression_decorated = self._build_impression(key, bucketing, feature, result, impressions_properties) + impression_decorated = self._build_impression(key, bucketing, feature, result, properties) await self._record_stats([(impression_decorated, attributes)], start, method) return result['treatment'], result['configurations'] - async def get_treatments(self, key, feature_flag_names, attributes=None, impressions_properties=None): + async 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, for async calls @@ -795,17 +841,19 @@ async def get_treatments(self, key, feature_flag_names, attributes=None, impress :type feature: list :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: Dictionary with the result of all the feature flags provided :rtype: dict """ try: - with_config = await self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS, attributes, impressions_properties) + with_config = await self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS, attributes, evaluation_options) return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} except Exception: return {feature: CONTROL for feature in feature_flag_names} - async def get_treatments_with_config(self, key, feature_flag_names, attributes=None, impressions_properties=None): + async def get_treatments_with_config(self, key, feature_flag_names, attributes=None, evaluation_options=None): """ Evaluate multiple feature flags and return a dict with feature flag -> (treatment, config), for async calls @@ -818,33 +866,37 @@ async def get_treatments_with_config(self, key, feature_flag_names, attributes=N :type feature: list :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: Dictionary with the result of all the feature flags provided :rtype: dict """ try: - return await self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, attributes, impressions_properties) + 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} - async def get_treatments_by_flag_set(self, key, flag_set, attributes=None, impressions_properties=None): - """ - Get treatments for feature flags that contain given flag set. - This method never raises an exception. If there's a problem, the appropriate log message - will be generated and the method will return the CONTROL treatment. - :param key: The key for which to get the treatment - :type key: str - :param flag_set: flag set - :type flag_sets: str - :param attributes: An optional dictionary of attributes - :type attributes: dict - :return: Dictionary with the result of all the feature flags provided - :rtype: dict - """ - return await self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, attributes, impressions_properties) - - async def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None, impressions_properties=None): + async def get_treatments_by_flag_set(self, key, flag_set, attributes=None, evaluation_options=None): + """ + Get treatments for feature flags that contain given flag set. + This method never raises an exception. If there's a problem, the appropriate log message + will be generated and the method will return the CONTROL treatment. + :param key: The key for which to get the treatment + :type key: str + :param flag_set: flag set + :type flag_sets: str + :param attributes: An optional dictionary of attributes + :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict + :return: Dictionary with the result of all the feature flags provided + :rtype: dict + """ + return await self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, attributes, evaluation_options) + + async def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None, evaluation_options=None): """ Get treatments for feature flags that contain given flag sets. This method never raises an exception. If there's a problem, the appropriate log message @@ -855,12 +907,14 @@ async def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None, imp :type flag_sets: list :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return await self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, attributes, impressions_properties) + return await self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, attributes, evaluation_options) - async def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None, impressions_properties=None): + async def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None, evaluation_options=None): """ Get treatments for feature flags that contain given flag set. This method never raises an exception. If there's a problem, the appropriate log message @@ -871,12 +925,14 @@ async def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes :type flag_sets: str :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return await self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, attributes, impressions_properties) + return await self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, attributes, evaluation_options) - async def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=None, impressions_properties=None): + async def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=None, evaluation_options=None): """ Get treatments for feature flags that contain given flag set. This method never raises an exception. If there's a problem, the appropriate log message @@ -887,12 +943,14 @@ async def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attribut :type flag_sets: str :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return await self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, attributes, impressions_properties) + return await self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, attributes, evaluation_options) - async def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None, impressions_properties=None): + async def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None, evaluation_options=None): """ Get treatments for feature flags that contain given flag sets. This method never raises an exception. If there's a problem, the appropriate log message @@ -905,6 +963,8 @@ async def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes= :type method: splitio.models.telemetry.MethodExceptionsAndLatencies :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: Dictionary with the result of all the feature flags provided :rtype: dict """ @@ -914,9 +974,9 @@ async def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes= return {} if 'config' in method.value: - return await self._get_treatments(key, feature_flags_names, method, attributes, impressions_properties) + return await self._get_treatments(key, feature_flags_names, method, attributes, evaluation_options) - with_config = await self._get_treatments(key, feature_flags_names, method, attributes, impressions_properties) + with_config = await self._get_treatments(key, feature_flags_names, method, attributes, evaluation_options) return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} async def _get_feature_flag_names_by_flag_sets(self, flag_sets, method_name): @@ -935,7 +995,7 @@ async def _get_feature_flag_names_by_flag_sets(self, flag_sets, method_name): return feature_flags_by_set - async def _get_treatments(self, key, features, method, attributes=None, impressions_properties=None): + async def _get_treatments(self, key, features, method, attributes=None, evaluation_options=None): """ Validate key, feature flag names and objects, and get the treatments and configs with an optional dictionary of attributes, for async calls @@ -947,6 +1007,8 @@ async def _get_treatments(self, key, features, method, attributes=None, impressi :type method: splitio.models.telemetry.MethodExceptionsAndLatencies :param attributes: An optional dictionary of attributes :type attributes: dict + :param evaluation_options: An optional dictionary of options + :type evaluation_options: dict :return: The treatments and configs for the key and feature flags :rtype: dict """ @@ -959,7 +1021,7 @@ async def _get_treatments(self, key, features, method, attributes=None, impressi await self._telemetry_init_producer.record_not_ready_usage() try: - key, bucketing, features, attributes, impressions_properties = self._validate_treatments_input(key, features, attributes, method, impressions_properties) + 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) @@ -975,8 +1037,9 @@ async def _get_treatments(self, key, features, method, attributes=None, impressi await self._telemetry_evaluation_producer.record_exception(method) results = {n: self._FAILED_EVAL_RESULT for n in features} + properties = self._get_properties(evaluation_options) imp_decorated_attrs = [ - (i, attributes) for i in self._build_impressions(key, bucketing, results, impressions_properties) + (i, attributes) for i in self._build_impressions(key, bucketing, results, properties) if i.Impression.label != Label.SPLIT_NOT_FOUND ] await self._record_stats(imp_decorated_attrs, start, method) @@ -1049,6 +1112,5 @@ async def track(self, key, traffic_type, event_type, value=None, properties=None _LOGGER.debug('Error: ', exc_info=True) return False - class _InvalidInputError(Exception): pass diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index f2ad03a5..5b1233f9 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -538,6 +538,19 @@ def validate_attributes(attributes, method_name): return True +def validate_evaluation_options(evaluation_options, method_name): + if evaluation_options == None: + return None + + if not isinstance(evaluation_options, dict): + _LOGGER.error("%s: evaluaiton option should be dictionary, setting its value to None.", method_name) + return None + + if evaluation_options.get("properties") == None: + _LOGGER.error("%s: evaluaiton option must have `properties` key, setting its value to None.", method_name) + return None + + return evaluation_options class _ApiLogFilter(logging.Filter): # pylint: disable=too-few-public-methods def filter(self, record): diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 66c7c195..8c5774c7 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -1327,47 +1327,52 @@ def synchronize_config(*_): _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) - assert client.get_treatment('some_key', 'SPLIT_2', impressions_properties={"prop": "value"}) == 'on' + assert client.get_treatment('some_key', 'SPLIT_2', evaluation_options={"properties":{"prop": "value"}}) == 'on' assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] - assert client.get_treatment('some_key', 'SPLIT_2', impressions_properties=12) == 'on' + assert client.get_treatment('some_key', 'SPLIT_2', evaluation_options=12) == 'on' assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, None)] - assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatment')] + assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be dictionary, setting its value to None.', 'get_treatment')] _logger.reset_mock() - assert client.get_treatment('some_key', 'SPLIT_2', impressions_properties='12') == 'on' + assert client.get_treatment('some_key', 'SPLIT_2', evaluation_options='12') == 'on' assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] - assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatment')] + assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be dictionary, setting its value to None.', 'get_treatment')] - assert client.get_treatment_with_config('some_key', 'SPLIT_2', impressions_properties={"prop": "value"}) == ('on', None) + _logger.reset_mock() + assert client.get_treatment('some_key', 'SPLIT_2', evaluation_options={"property":{"prop": "value"}}) == 'on' + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] + assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option must have `properties` key, setting its value to None.', 'get_treatment')] + + assert client.get_treatment_with_config('some_key', 'SPLIT_2', evaluation_options={"properties":{"prop": "value"}}) == ('on', None) assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] - assert client.get_treatments('some_key', ['SPLIT_2'], impressions_properties={"prop": "value"}) == {'SPLIT_2': 'on'} + assert client.get_treatments('some_key', ['SPLIT_2'], evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': 'on'} assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] _logger.reset_mock() - assert client.get_treatments('some_key', ['SPLIT_2'], impressions_properties="prop") == {'SPLIT_2': 'on'} + assert client.get_treatments('some_key', ['SPLIT_2'], evaluation_options="prop") == {'SPLIT_2': 'on'} assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] - assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatments')] + assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be dictionary, setting its value to None.', 'get_treatments')] _logger.reset_mock() - assert client.get_treatments('some_key', ['SPLIT_2'], impressions_properties=123) == {'SPLIT_2': 'on'} + assert client.get_treatments('some_key', ['SPLIT_2'], evaluation_options=123) == {'SPLIT_2': 'on'} assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] - assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatments')] + assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be dictionary, setting its value to None.', 'get_treatments')] - assert client.get_treatments_with_config('some_key', ['SPLIT_2'], impressions_properties={"prop": "value"}) == {'SPLIT_2': ('on', None)} + assert client.get_treatments_with_config('some_key', ['SPLIT_2'], evaluation_options={"properties":{"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"}')] - assert client.get_treatments_by_flag_set('some_key', 'set_1', impressions_properties={"prop": "value"}) == {'SPLIT_2': 'on'} + assert client.get_treatments_by_flag_set('some_key', 'set_1', evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': 'on'} assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] - assert client.get_treatments_by_flag_sets('some_key', ['set_1'], impressions_properties={"prop": "value"}) == {'SPLIT_2': 'on'} + assert client.get_treatments_by_flag_sets('some_key', ['set_1'], evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': 'on'} assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] - assert client.get_treatments_with_config_by_flag_set('some_key', 'set_1', impressions_properties={"prop": "value"}) == {'SPLIT_2': ('on', None)} + assert client.get_treatments_with_config_by_flag_set('some_key', 'set_1', evaluation_options={"properties":{"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"}')] - assert client.get_treatments_with_config_by_flag_sets('some_key', ['set_1'], impressions_properties={"prop": "value"}) == {'SPLIT_2': ('on', None)} + assert client.get_treatments_with_config_by_flag_sets('some_key', ['set_1'], evaluation_options={"properties":{"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"}')] class ClientAsyncTests(object): # pylint: disable=too-few-public-methods @@ -2533,45 +2538,50 @@ async def synchronize_config(*_): _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) - assert await client.get_treatment('some_key', 'SPLIT_2', impressions_properties={"prop": "value"}) == 'on' + assert await client.get_treatment('some_key', 'SPLIT_2', evaluation_options={"properties":{"prop": "value"}}) == 'on' assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] - assert await client.get_treatment('some_key', 'SPLIT_2', impressions_properties=12) == 'on' + assert await client.get_treatment('some_key', 'SPLIT_2', evaluation_options=12) == 'on' assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, None)] - assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatment')] + assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be dictionary, setting its value to None.', 'get_treatment')] _logger.reset_mock() - assert await client.get_treatment('some_key', 'SPLIT_2', impressions_properties='12') == 'on' + assert await client.get_treatment('some_key', 'SPLIT_2', evaluation_options='12') == 'on' + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] + assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be dictionary, setting its value to None.', 'get_treatment')] + + _logger.reset_mock() + assert await client.get_treatment('some_key', 'SPLIT_2', evaluation_options={"property":{"prop": "value"}}) == 'on' assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] - assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatment')] + assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option must have `properties` key, setting its value to None.', 'get_treatment')] - assert await client.get_treatment_with_config('some_key', 'SPLIT_2', impressions_properties={"prop": "value"}) == ('on', None) + assert await client.get_treatment_with_config('some_key', 'SPLIT_2', evaluation_options={"properties":{"prop": "value"}}) == ('on', None) assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] - assert await client.get_treatments('some_key', ['SPLIT_2'], impressions_properties={"prop": "value"}) == {'SPLIT_2': 'on'} + assert await client.get_treatments('some_key', ['SPLIT_2'], evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': 'on'} assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] _logger.reset_mock() - assert await client.get_treatments('some_key', ['SPLIT_2'], impressions_properties="prop") == {'SPLIT_2': 'on'} + assert await client.get_treatments('some_key', ['SPLIT_2'], evaluation_options="prop") == {'SPLIT_2': 'on'} assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] - assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatments')] + assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be dictionary, setting its value to None.', 'get_treatments')] _logger.reset_mock() - assert await client.get_treatments('some_key', ['SPLIT_2'], impressions_properties=123) == {'SPLIT_2': 'on'} + assert await client.get_treatments('some_key', ['SPLIT_2'], evaluation_options=123) == {'SPLIT_2': 'on'} assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] - assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatments')] + assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be dictionary, setting its value to None.', 'get_treatments')] - assert await client.get_treatments_with_config('some_key', ['SPLIT_2'], impressions_properties={"prop": "value"}) == {'SPLIT_2': ('on', None)} + assert await client.get_treatments_with_config('some_key', ['SPLIT_2'], evaluation_options={"properties":{"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"}')] - assert await client.get_treatments_by_flag_set('some_key', 'set_1', impressions_properties={"prop": "value"}) == {'SPLIT_2': 'on'} + assert await client.get_treatments_by_flag_set('some_key', 'set_1', evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': 'on'} assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] - assert await client.get_treatments_by_flag_sets('some_key', ['set_1'], impressions_properties={"prop": "value"}) == {'SPLIT_2': 'on'} + assert await client.get_treatments_by_flag_sets('some_key', ['set_1'], evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': 'on'} assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] - assert await client.get_treatments_with_config_by_flag_set('some_key', 'set_1', impressions_properties={"prop": "value"}) == {'SPLIT_2': ('on', None)} + assert await client.get_treatments_with_config_by_flag_set('some_key', 'set_1', evaluation_options={"properties":{"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"}')] - assert await client.get_treatments_with_config_by_flag_sets('some_key', ['set_1'], impressions_properties={"prop": "value"}) == {'SPLIT_2': ('on', None)} + assert await client.get_treatments_with_config_by_flag_sets('some_key', ['set_1'], evaluation_options={"properties":{"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"}')] diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index 96384f55..86a47575 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -122,7 +122,7 @@ def _get_treatment(factory, skip_rbs=False): except: pass - assert client.get_treatment('user1', 'sample_feature', impressions_properties={'prop':'value'}) == 'on' + assert client.get_treatment('user1', 'sample_feature', evaluation_options={"properties":{"prop": "value"}}) == 'on' if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): _validate_last_impressions(client, ('sample_feature', 'user1', 'on', '{"prop": "value"}')) From 90ef85fe27d73e685c1b8b345d12253e549d1397 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 8 Jul 2025 21:33:23 -0700 Subject: [PATCH 2/6] Added EvaluationOption tuple class --- splitio/client/client.py | 11 ++-- splitio/client/input_validator.py | 9 +-- tests/client/test_client.py | 82 ++++++++++++++-------------- tests/integration/test_client_e2e.py | 5 +- 4 files changed, 54 insertions(+), 53 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index b695a1b1..3d20556a 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -1,6 +1,7 @@ """A module for Split.io SDK API clients.""" import logging import json +from collections import namedtuple from splitio.engine.evaluator import Evaluator, CONTROL, EvaluationDataFactory, AsyncEvaluationDataFactory from splitio.engine.splitters import Splitter @@ -12,6 +13,7 @@ _LOGGER = logging.getLogger(__name__) +EvaluationOptions = namedtuple('EvaluationOptions', ['properties']) class ClientBase(object): # pylint: disable=too-many-instance-attributes @@ -124,10 +126,11 @@ def _validate_treatment_options(method_name, evaluation_options=None): if evaluation_options == None: return None - if evaluation_options["properties"] is not None: - valid, evaluation_options["properties"], size = input_validator.valid_properties(evaluation_options["properties"], method_name) + if evaluation_options.properties is not None: + valid, properties, size = input_validator.valid_properties(evaluation_options.properties, method_name) if not valid: - evaluation_options["properties"] = None + evaluation_options = EvaluationOptions(None) + evaluation_options = EvaluationOptions(properties) return evaluation_options def _build_impression(self, key, bucketing, feature, result, properties=None): @@ -198,7 +201,7 @@ def _validate_track(self, key, traffic_type, event_type, value=None, properties= return True, event, size def _get_properties(self, evaluation_options): - return evaluation_options["properties"] if evaluation_options != None and evaluation_options.get("properties") != None else None + return evaluation_options.properties if evaluation_options != None else None class Client(ClientBase): # pylint: disable=too-many-instance-attributes diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index 5b1233f9..38cba70d 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -6,6 +6,7 @@ import inspect from splitio.client.key import Key +from splitio.client import client from splitio.engine.evaluator import CONTROL @@ -542,12 +543,8 @@ def validate_evaluation_options(evaluation_options, method_name): if evaluation_options == None: return None - if not isinstance(evaluation_options, dict): - _LOGGER.error("%s: evaluaiton option should be dictionary, setting its value to None.", method_name) - return None - - if evaluation_options.get("properties") == None: - _LOGGER.error("%s: evaluaiton option must have `properties` key, setting its value to None.", method_name) + if not isinstance(evaluation_options, client.EvaluationOptions): + _LOGGER.error("%s: evaluaiton option should be an instance of EvaluationOptions, setting its value to None.", method_name) return None return evaluation_options diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 8c5774c7..923eb504 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -7,7 +7,7 @@ import time import pytest -from splitio.client.client import Client, _LOGGER as _logger, CONTROL, ClientAsync +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.impressions import Impression, Label from splitio.models.events import Event, EventWrapper @@ -1327,52 +1327,52 @@ def synchronize_config(*_): _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) - assert client.get_treatment('some_key', 'SPLIT_2', evaluation_options={"properties":{"prop": "value"}}) == 'on' + assert client.get_treatment('some_key', 'SPLIT_2', evaluation_options=EvaluationOptions({"prop": "value"})) == 'on' assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] - assert client.get_treatment('some_key', 'SPLIT_2', evaluation_options=12) == 'on' + assert client.get_treatment('some_key', 'SPLIT_2', evaluation_options=EvaluationOptions(12)) == 'on' assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, None)] - assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be dictionary, setting its value to None.', 'get_treatment')] + assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatment')] _logger.reset_mock() - assert client.get_treatment('some_key', 'SPLIT_2', evaluation_options='12') == 'on' + assert client.get_treatment('some_key', 'SPLIT_2', evaluation_options=EvaluationOptions('12')) == 'on' assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] - assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be dictionary, setting its value to None.', 'get_treatment')] + assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatment')] - _logger.reset_mock() - assert client.get_treatment('some_key', 'SPLIT_2', evaluation_options={"property":{"prop": "value"}}) == 'on' - assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] - assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option must have `properties` key, setting its value to None.', 'get_treatment')] - - assert client.get_treatment_with_config('some_key', 'SPLIT_2', evaluation_options={"properties":{"prop": "value"}}) == ('on', None) + assert client.get_treatment_with_config('some_key', 'SPLIT_2', evaluation_options=EvaluationOptions({"prop": "value"})) == ('on', None) assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] - assert client.get_treatments('some_key', ['SPLIT_2'], evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': 'on'} + assert client.get_treatments('some_key', ['SPLIT_2'], evaluation_options=EvaluationOptions({"prop": "value"})) == {'SPLIT_2': 'on'} assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] _logger.reset_mock() - assert client.get_treatments('some_key', ['SPLIT_2'], evaluation_options="prop") == {'SPLIT_2': 'on'} + assert client.get_treatments('some_key', ['SPLIT_2'], evaluation_options=EvaluationOptions("prop")) == {'SPLIT_2': 'on'} + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] + assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatments')] + + _logger.reset_mock() + assert client.get_treatments('some_key', ['SPLIT_2'], evaluation_options=EvaluationOptions(123)) == {'SPLIT_2': 'on'} assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] - assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be dictionary, setting its value to None.', 'get_treatments')] + assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatments')] _logger.reset_mock() assert client.get_treatments('some_key', ['SPLIT_2'], evaluation_options=123) == {'SPLIT_2': 'on'} assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] - assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be dictionary, setting its value to None.', 'get_treatments')] + assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be an instance of EvaluationOptions, setting its value to None.', 'get_treatments')] - assert client.get_treatments_with_config('some_key', ['SPLIT_2'], evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': ('on', None)} + assert client.get_treatments_with_config('some_key', ['SPLIT_2'], 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"}')] - assert client.get_treatments_by_flag_set('some_key', 'set_1', evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': 'on'} + assert client.get_treatments_by_flag_set('some_key', 'set_1', evaluation_options=EvaluationOptions({"prop": "value"})) == {'SPLIT_2': 'on'} assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] - assert client.get_treatments_by_flag_sets('some_key', ['set_1'], evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': 'on'} + assert client.get_treatments_by_flag_sets('some_key', ['set_1'], evaluation_options=EvaluationOptions({"prop": "value"})) == {'SPLIT_2': 'on'} assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] - assert client.get_treatments_with_config_by_flag_set('some_key', 'set_1', evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': ('on', None)} + assert client.get_treatments_with_config_by_flag_set('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"}')] - assert client.get_treatments_with_config_by_flag_sets('some_key', ['set_1'], evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': ('on', None)} + 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"}')] class ClientAsyncTests(object): # pylint: disable=too-few-public-methods @@ -2538,50 +2538,50 @@ async def synchronize_config(*_): _logger = mocker.Mock() mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) - assert await client.get_treatment('some_key', 'SPLIT_2', evaluation_options={"properties":{"prop": "value"}}) == 'on' + assert await client.get_treatment('some_key', 'SPLIT_2', evaluation_options=EvaluationOptions({"prop": "value"})) == 'on' assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] - assert await client.get_treatment('some_key', 'SPLIT_2', evaluation_options=12) == 'on' + assert await client.get_treatment('some_key', 'SPLIT_2', evaluation_options=EvaluationOptions(12)) == 'on' assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, None)] - assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be dictionary, setting its value to None.', 'get_treatment')] + assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatment')] _logger.reset_mock() - assert await client.get_treatment('some_key', 'SPLIT_2', evaluation_options='12') == 'on' + assert await client.get_treatment('some_key', 'SPLIT_2', evaluation_options=EvaluationOptions('12')) == 'on' assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] - assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be dictionary, setting its value to None.', 'get_treatment')] + assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatment')] - _logger.reset_mock() - assert await client.get_treatment('some_key', 'SPLIT_2', evaluation_options={"property":{"prop": "value"}}) == 'on' - assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] - assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option must have `properties` key, setting its value to None.', 'get_treatment')] - - assert await client.get_treatment_with_config('some_key', 'SPLIT_2', evaluation_options={"properties":{"prop": "value"}}) == ('on', None) + assert await client.get_treatment_with_config('some_key', 'SPLIT_2', evaluation_options=EvaluationOptions({"prop": "value"})) == ('on', None) assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] - assert await client.get_treatments('some_key', ['SPLIT_2'], evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': 'on'} + assert await client.get_treatments('some_key', ['SPLIT_2'], evaluation_options=EvaluationOptions({"prop": "value"})) == {'SPLIT_2': 'on'} assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] _logger.reset_mock() - assert await client.get_treatments('some_key', ['SPLIT_2'], evaluation_options="prop") == {'SPLIT_2': 'on'} + assert await client.get_treatments('some_key', ['SPLIT_2'], evaluation_options=EvaluationOptions("prop")) == {'SPLIT_2': 'on'} + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] + assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatments')] + + _logger.reset_mock() + assert await client.get_treatments('some_key', ['SPLIT_2'], evaluation_options=EvaluationOptions(123)) == {'SPLIT_2': 'on'} assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] - assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be dictionary, setting its value to None.', 'get_treatments')] + assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatments')] _logger.reset_mock() assert await client.get_treatments('some_key', ['SPLIT_2'], evaluation_options=123) == {'SPLIT_2': 'on'} assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] - assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be dictionary, setting its value to None.', 'get_treatments')] + assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be an instance of EvaluationOptions, setting its value to None.', 'get_treatments')] - assert await client.get_treatments_with_config('some_key', ['SPLIT_2'], evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': ('on', None)} + assert await client.get_treatments_with_config('some_key', ['SPLIT_2'], 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"}')] - assert await client.get_treatments_by_flag_set('some_key', 'set_1', evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': 'on'} + assert await client.get_treatments_by_flag_set('some_key', 'set_1', evaluation_options=EvaluationOptions({"prop": "value"})) == {'SPLIT_2': 'on'} assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] - assert await client.get_treatments_by_flag_sets('some_key', ['set_1'], evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': 'on'} + assert await client.get_treatments_by_flag_sets('some_key', ['set_1'], evaluation_options=EvaluationOptions({"prop": "value"})) == {'SPLIT_2': 'on'} assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] - assert await client.get_treatments_with_config_by_flag_set('some_key', 'set_1', evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': ('on', None)} + assert await client.get_treatments_with_config_by_flag_set('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"}')] - assert await client.get_treatments_with_config_by_flag_sets('some_key', ['set_1'], evaluation_options={"properties":{"prop": "value"}}) == {'SPLIT_2': ('on', None)} + 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"}')] diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index 86a47575..f8625f6a 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -13,6 +13,8 @@ from splitio.exceptions import TimeoutException from splitio.client.factory import get_factory, SplitFactory, get_factory_async, SplitFactoryAsync from splitio.client.util import SdkMetadata +from splitio.client.config import DEFAULT_CONFIG +from splitio.client.client import EvaluationOptions from splitio.storage.inmemmory import InMemoryEventStorage, InMemoryImpressionStorage, \ InMemorySegmentStorage, InMemorySplitStorage, InMemoryTelemetryStorage, InMemorySplitStorageAsync,\ InMemoryEventStorageAsync, InMemoryImpressionStorageAsync, InMemorySegmentStorageAsync, \ @@ -35,7 +37,6 @@ from splitio.engine.impressions.manager import Counter as ImpressionsCounter from splitio.engine.impressions.unique_keys_tracker import UniqueKeysTracker, UniqueKeysTrackerAsync from splitio.recorder.recorder import StandardRecorder, PipelinedRecorder, StandardRecorderAsync, PipelinedRecorderAsync -from splitio.client.config import DEFAULT_CONFIG from splitio.sync.synchronizer import SplitTasks, SplitSynchronizers, Synchronizer, RedisSynchronizer, SynchronizerAsync,\ RedisSynchronizerAsync from splitio.sync.manager import Manager, RedisManager, ManagerAsync, RedisManagerAsync @@ -122,7 +123,7 @@ def _get_treatment(factory, skip_rbs=False): except: pass - assert client.get_treatment('user1', 'sample_feature', evaluation_options={"properties":{"prop": "value"}}) == 'on' + assert client.get_treatment('user1', 'sample_feature', evaluation_options=EvaluationOptions({"prop": "value"})) == 'on' if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): _validate_last_impressions(client, ('sample_feature', 'user1', 'on', '{"prop": "value"}')) From 1ebc3f14421912ec5fe9f963e69ad3ceeed98aca Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 8 Jul 2025 22:00:14 -0700 Subject: [PATCH 3/6] polish --- splitio/client/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 3d20556a..257c9b97 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -128,9 +128,9 @@ def _validate_treatment_options(method_name, evaluation_options=None): if evaluation_options.properties is not None: valid, properties, size = input_validator.valid_properties(evaluation_options.properties, method_name) + evaluation_options = EvaluationOptions(properties) if not valid: evaluation_options = EvaluationOptions(None) - evaluation_options = EvaluationOptions(properties) return evaluation_options def _build_impression(self, key, bucketing, feature, result, properties=None): From bedcbce3af10b78fca0b561abfb77c9e38ae80fe Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany <41021307+chillaq@users.noreply.github.com> Date: Thu, 10 Jul 2025 08:11:42 -0700 Subject: [PATCH 4/6] Update splitio/client/input_validator.py Co-authored-by: Emiliano Sanchez --- splitio/client/input_validator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index 38cba70d..4a2fb8bc 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -544,7 +544,7 @@ def validate_evaluation_options(evaluation_options, method_name): return None if not isinstance(evaluation_options, client.EvaluationOptions): - _LOGGER.error("%s: evaluaiton option should be an instance of EvaluationOptions, setting its value to None.", method_name) + _LOGGER.error("%s: evaluation options should be an instance of EvaluationOptions. Setting its value to None.", method_name) return None return evaluation_options From 4f85231a10d98333715770acd3677d855b23082a Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany <41021307+chillaq@users.noreply.github.com> Date: Thu, 10 Jul 2025 08:11:54 -0700 Subject: [PATCH 5/6] Update tests/client/test_client.py Co-authored-by: Emiliano Sanchez --- tests/client/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 923eb504..8a33ba16 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -1358,7 +1358,7 @@ def synchronize_config(*_): _logger.reset_mock() assert client.get_treatments('some_key', ['SPLIT_2'], evaluation_options=123) == {'SPLIT_2': 'on'} assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] - assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be an instance of EvaluationOptions, setting its value to None.', 'get_treatments')] + assert _logger.error.mock_calls == [mocker.call('%s: evaluation options should be an instance of EvaluationOptions. Setting its value to None.', 'get_treatments')] assert client.get_treatments_with_config('some_key', ['SPLIT_2'], 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"}')] From 72d2e6adcd4ddf2d70add26db3e24aabec1c2853 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany <41021307+chillaq@users.noreply.github.com> Date: Thu, 10 Jul 2025 08:12:02 -0700 Subject: [PATCH 6/6] Update tests/client/test_client.py Co-authored-by: Emiliano Sanchez --- tests/client/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 8a33ba16..9a6848eb 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -2569,7 +2569,7 @@ async def synchronize_config(*_): _logger.reset_mock() assert await client.get_treatments('some_key', ['SPLIT_2'], evaluation_options=123) == {'SPLIT_2': 'on'} assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] - assert _logger.error.mock_calls == [mocker.call('%s: evaluaiton option should be an instance of EvaluationOptions, setting its value to None.', 'get_treatments')] + assert _logger.error.mock_calls == [mocker.call('%s: evaluation options should be an instance of EvaluationOptions. Setting its value to None.', 'get_treatments')] assert await client.get_treatments_with_config('some_key', ['SPLIT_2'], 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"}')]