diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml
index d4eab45..cc1abe9 100644
--- a/.github/workflows/python-ci.yml
+++ b/.github/workflows/python-ci.yml
@@ -14,10 +14,11 @@ on:
 jobs:
   test:
 
-    runs-on: ubuntu-20.04
+    runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11"]
+        python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
+        os: [ windows-latest, macos-latest, ubuntu-20.04 ]
 
     steps:
     - uses: actions/checkout@v3
@@ -36,7 +37,7 @@ jobs:
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip 
-        pip install pytest pytest-cov mock flake8
+        pip install pytest pytest-cov parameterized mock flake8
         pip install -r requirements.txt
 
     - name: Lint with flake8
diff --git a/configcatclient/config.py b/configcatclient/config.py
new file mode 100644
index 0000000..68eb08e
--- /dev/null
+++ b/configcatclient/config.py
@@ -0,0 +1,293 @@
+from enum import IntEnum
+
+CONFIG_FILE_NAME = 'config_v6'
+SERIALIZATION_FORMAT_VERSION = 'v2'
+
+# Config
+PREFERENCES = 'p'
+SEGMENTS = 's'
+FEATURE_FLAGS = 'f'
+
+# Preferences
+BASE_URL = 'u'
+REDIRECT = 'r'
+SALT = 's'
+
+# Segment
+SEGMENT_NAME = 'n'  # The first 4 characters of the Segment's name
+SEGMENT_CONDITIONS = 'r'  # The list of segment rule conditions (has a logical AND relation between the items).
+
+# Segment Condition (User Condition)
+COMPARISON_ATTRIBUTE = 'a'  # The attribute of the user object that should be used to evaluate this rule
+COMPARATOR = 'c'
+
+# Feature flag (Evaluation Formula)
+SETTING_TYPE = 't'  # 0 = bool, 1 = string, 2 = int, 3 = double
+PERCENTAGE_RULE_ATTRIBUTE = 'a'  # Percentage rule evaluation hashes this attribute of the User object to calculate the buckets
+TARGETING_RULES = 'r'  # Targeting Rules (Logically connected by OR)
+PERCENTAGE_OPTIONS = 'p'  # Percentage Options without conditions
+VALUE = 'v'
+VARIATION_ID = 'i'
+INLINE_SALT = 'inline_salt'
+
+# Targeting Rule (Evaluation Rule)
+CONDITIONS = 'c'
+SERVED_VALUE = 's'  # Value and Variation ID
+TARGETING_RULE_PERCENTAGE_OPTIONS = 'p'
+
+# Condition
+USER_CONDITION = 'u'
+SEGMENT_CONDITION = 's'  # Segment targeting rule
+PREREQUISITE_FLAG_CONDITION = 'p'  # Prerequisite flag targeting rule
+
+# Segment Condition
+SEGMENT_INDEX = 's'
+SEGMENT_COMPARATOR = 'c'
+INLINE_SEGMENT = 'inline_segment'
+
+# Prerequisite Flag Condition
+PREREQUISITE_FLAG_KEY = 'f'
+PREREQUISITE_COMPARATOR = 'c'
+
+# Percentage Option
+PERCENTAGE = 'p'
+
+# Value
+BOOL_VALUE = 'b'
+STRING_VALUE = 's'
+INT_VALUE = 'i'
+DOUBLE_VALUE = 'd'
+STRING_LIST_VALUE = 'l'
+UNSUPPORTED_VALUE = 'unsupported_value'
+
+
+def get_value(dictionary, setting_type):
+    value_descriptor = dictionary.get(VALUE)
+    if value_descriptor is None:
+        raise ValueError('Value is missing')
+
+    if setting_type not in list(map(int, SettingType)):
+        raise ValueError('Unsupported setting type')
+
+    expected_value_type, expected_py_type = SettingType.get_type_info(setting_type)
+    if expected_value_type is None:
+        raise ValueError('Unsupported setting type')
+
+    value = value_descriptor.get(expected_value_type)
+    if value is None:
+        raise ValueError('Setting value is not of the expected type %s' % expected_py_type)
+
+    return value
+
+
+def get_value_type(dictionary):
+    value = dictionary.get(VALUE)
+    if value is not None:
+        if value.get(BOOL_VALUE) is not None:
+            return bool
+        if value.get(STRING_VALUE) is not None:
+            return str
+        if value.get(INT_VALUE) is not None:
+            return int
+        if value.get(DOUBLE_VALUE) is not None:
+            return float
+
+    return None
+
+
+class SettingType(IntEnum):
+    BOOL = 0
+    STRING = 1
+    INT = 2
+    DOUBLE = 3
+
+    @staticmethod
+    def get_type_info(setting_type):
+        return setting_type_mapping.get(setting_type, (None, None))
+
+    @staticmethod
+    def from_type(object_type):
+        if object_type is bool:
+            return SettingType.BOOL
+        if object_type is str:
+            return SettingType.STRING
+        if object_type is int:
+            return SettingType.INT
+        if object_type is float:
+            return SettingType.DOUBLE
+
+        return None
+
+    @staticmethod
+    def to_type(setting_type):
+        return SettingType.get_type_info(setting_type)[1]
+
+    @staticmethod
+    def to_value_type(setting_type):
+        return SettingType.get_type_info(setting_type)[0]
+
+
+setting_type_mapping = {
+    SettingType.BOOL: (BOOL_VALUE, bool),
+    SettingType.STRING: (STRING_VALUE, str),
+    SettingType.INT: (INT_VALUE, int),
+    SettingType.DOUBLE: (DOUBLE_VALUE, float)
+}
+
+
+class PrerequisiteComparator(IntEnum):
+    EQUALS = 0
+    NOT_EQUALS = 1
+
+
+class SegmentComparator(IntEnum):
+    IS_IN = 0
+    IS_NOT_IN = 1
+
+
+class Comparator(IntEnum):
+    IS_ONE_OF = 0
+    IS_NOT_ONE_OF = 1
+    CONTAINS_ANY_OF = 2
+    NOT_CONTAINS_ANY_OF = 3
+    IS_ONE_OF_SEMVER = 4
+    IS_NOT_ONE_OF_SEMVER = 5
+    LESS_THAN_SEMVER = 6
+    LESS_THAN_OR_EQUAL_SEMVER = 7
+    GREATER_THAN_SEMVER = 8
+    GREATER_THAN_OR_EQUAL_SEMVER = 9
+    EQUALS_NUMBER = 10
+    NOT_EQUALS_NUMBER = 11
+    LESS_THAN_NUMBER = 12
+    LESS_THAN_OR_EQUAL_NUMBER = 13
+    GREATER_THAN_NUMBER = 14
+    GREATER_THAN_OR_EQUAL_NUMBER = 15
+    IS_ONE_OF_HASHED = 16
+    IS_NOT_ONE_OF_HASHED = 17
+    BEFORE_DATETIME = 18
+    AFTER_DATETIME = 19
+    EQUALS_HASHED = 20
+    NOT_EQUALS_HASHED = 21
+    STARTS_WITH_ANY_OF_HASHED = 22
+    NOT_STARTS_WITH_ANY_OF_HASHED = 23
+    ENDS_WITH_ANY_OF_HASHED = 24
+    NOT_ENDS_WITH_ANY_OF_HASHED = 25
+    ARRAY_CONTAINS_ANY_OF_HASHED = 26
+    ARRAY_NOT_CONTAINS_ANY_OF_HASHED = 27
+    EQUALS = 28
+    NOT_EQUALS = 29
+    STARTS_WITH_ANY_OF = 30
+    NOT_STARTS_WITH_ANY_OF = 31
+    ENDS_WITH_ANY_OF = 32
+    NOT_ENDS_WITH_ANY_OF = 33
+    ARRAY_CONTAINS_ANY_OF = 34
+    ARRAY_NOT_CONTAINS_ANY_OF = 35
+
+
+COMPARATOR_TEXTS = [
+    'IS ONE OF',                  # IS_ONE_OF
+    'IS NOT ONE OF',              # IS_NOT_ONE_OF
+    'CONTAINS ANY OF',            # CONTAINS_ANY_OF
+    'NOT CONTAINS ANY OF',        # NOT_CONTAINS_ANY_OF
+    'IS ONE OF',                  # IS_ONE_OF_SEMVER
+    'IS NOT ONE OF',              # IS_NOT_ONE_OF_SEMVER
+    '<',                          # LESS_THAN_SEMVER
+    '<=',                         # LESS_THAN_OR_EQUAL_SEMVER
+    '>',                          # GREATER_THAN_SEMVER
+    '>=',                         # GREATER_THAN_OR_EQUAL_SEMVER
+    '=',                          # EQUALS_NUMBER
+    '!=',                         # NOT_EQUALS_NUMBER
+    '<',                          # LESS_THAN_NUMBER
+    '<=',                         # LESS_THAN_OR_EQUAL_NUMBER
+    '>',                          # GREATER_THAN_NUMBER
+    '>=',                         # GREATER_THAN_OR_EQUAL_NUMBER
+    'IS ONE OF',                  # IS_ONE_OF_HASHED
+    'IS NOT ONE OF',              # IS_NOT_ONE_OF_HASHED
+    'BEFORE',                     # BEFORE_DATETIME
+    'AFTER',                      # AFTER_DATETIME
+    'EQUALS',                     # EQUALS_HASHED
+    'NOT EQUALS',                 # NOT_EQUALS_HASHED
+    'STARTS WITH ANY OF',         # STARTS_WITH_ANY_OF_HASHED
+    'NOT STARTS WITH ANY OF',     # NOT_STARTS_WITH_ANY_OF_HASHED
+    'ENDS WITH ANY OF',           # ENDS_WITH_ANY_OF_HASHED
+    'NOT ENDS WITH ANY OF',       # NOT_ENDS_WITH_ANY_OF_HASHED
+    'ARRAY CONTAINS ANY OF',      # ARRAY_CONTAINS_ANY_OF_HASHED
+    'ARRAY NOT CONTAINS ANY OF',  # ARRAY_NOT_CONTAINS_ANY_OF_HASHED
+    'EQUALS',                     # EQUALS
+    'NOT EQUALS',                 # NOT_EQUALS
+    'STARTS WITH ANY OF',         # STARTS_WITH_ANY_OF
+    'NOT STARTS WITH ANY OF',     # NOT_STARTS_WITH_ANY_OF
+    'ENDS WITH ANY OF',           # ENDS_WITH_ANY_OF
+    'NOT ENDS WITH ANY OF',       # NOT_ENDS_WITH_ANY_OF
+    'ARRAY CONTAINS ANY OF',      # ARRAY_CONTAINS_ANY_OF
+    'ARRAY NOT CONTAINS ANY OF'   # ARRAY_NOT_CONTAINS_ANY_OF
+]
+COMPARISON_VALUES = [
+    STRING_LIST_VALUE,  # IS_ONE_OF
+    STRING_LIST_VALUE,  # IS_NOT_ONE_OF
+    STRING_LIST_VALUE,  # CONTAINS_ANY_OF
+    STRING_LIST_VALUE,  # NOT_CONTAINS_ANY_OF
+    STRING_LIST_VALUE,  # IS_ONE_OF_SEMVER
+    STRING_LIST_VALUE,  # IS_NOT_ONE_OF_SEMVER
+    STRING_VALUE,       # LESS_THAN_SEMVER
+    STRING_VALUE,       # LESS_THAN_OR_EQUAL_SEMVER
+    STRING_VALUE,       # GREATER_THAN_SEMVER
+    STRING_VALUE,       # GREATER_THAN_OR_EQUAL_SEMVER
+    DOUBLE_VALUE,       # EQUALS_NUMBER
+    DOUBLE_VALUE,       # NOT_EQUALS_NUMBER
+    DOUBLE_VALUE,       # LESS_THAN_NUMBER
+    DOUBLE_VALUE,       # LESS_THAN_OR_EQUAL_NUMBER
+    DOUBLE_VALUE,       # GREATER_THAN_NUMBER
+    DOUBLE_VALUE,       # GREATER_THAN_OR_EQUAL_NUMBER
+    STRING_LIST_VALUE,  # IS_ONE_OF_HASHED
+    STRING_LIST_VALUE,  # IS_NOT_ONE_OF_HASHED
+    DOUBLE_VALUE,       # BEFORE_DATETIME
+    DOUBLE_VALUE,       # AFTER_DATETIME
+    STRING_VALUE,       # EQUALS_HASHED
+    STRING_VALUE,       # NOT_EQUALS_HASHED
+    STRING_LIST_VALUE,  # STARTS_WITH_ANY_OF_HASHED
+    STRING_LIST_VALUE,  # NOT_STARTS_WITH_ANY_OF_HASHED
+    STRING_LIST_VALUE,  # ENDS_WITH_ANY_OF_HASHED
+    STRING_LIST_VALUE,  # NOT_ENDS_WITH_ANY_OF_HASHED
+    STRING_LIST_VALUE,  # ARRAY_CONTAINS_ANY_OF_HASHED
+    STRING_LIST_VALUE,  # ARRAY_NOT_CONTAINS_ANY_OF_HASHED
+    STRING_VALUE,       # EQUALS
+    STRING_VALUE,       # NOT_EQUALS
+    STRING_LIST_VALUE,  # STARTS_WITH_ANY_OF
+    STRING_LIST_VALUE,  # NOT_STARTS_WITH_ANY_OF
+    STRING_LIST_VALUE,  # ENDS_WITH_ANY_OF
+    STRING_LIST_VALUE,  # NOT_ENDS_WITH_ANY_OF
+    STRING_LIST_VALUE,  # ARRAY_CONTAINS_ANY_OF
+    STRING_LIST_VALUE   # ARRAY_NOT_CONTAINS_ANY_OF
+]
+SEGMENT_COMPARATOR_TEXTS = ['IS IN SEGMENT', 'IS NOT IN SEGMENT']
+PREREQUISITE_COMPARATOR_TEXTS = ['EQUALS', 'DOES NOT EQUAL']
+
+
+def extend_config_with_inline_salt_and_segment(config):
+    """
+    Adds the inline salt and segment to the config.
+    When using flag overrides, the original salt and segment indexes may become invalid. Therefore, we copy the
+    object references to the locations where they are referenced and use these references instead of the indexes.
+    """
+    salt = config.get(PREFERENCES, {}).get(SALT, '')
+    segments = config.get(SEGMENTS, [])
+    settings = config.get(FEATURE_FLAGS, {})
+    for setting in settings.values():
+        if not isinstance(setting, dict):
+            continue
+
+        # add salt
+        setting[INLINE_SALT] = salt
+
+        # add segment to the segment conditions
+        targeting_rules = setting.get(TARGETING_RULES, [])
+        for targeting_rule in targeting_rules:
+            conditions = targeting_rule.get(CONDITIONS, [])
+            for condition in conditions:
+
+                segment_condition = condition.get(SEGMENT_CONDITION)
+                if segment_condition:
+                    segment_index = segment_condition.get(SEGMENT_INDEX)
+                    segment = segments[segment_index]
+                    segment_condition[INLINE_SEGMENT] = segment
diff --git a/configcatclient/configcatclient.py b/configcatclient/configcatclient.py
index a6d405c..5d3e989 100644
--- a/configcatclient/configcatclient.py
+++ b/configcatclient/configcatclient.py
@@ -1,9 +1,12 @@
+import logging
+import sys
 from threading import Lock
 
 from . import utils
 from .configservice import ConfigService
-from .constants import ROLLOUT_RULES, VARIATION_ID, VALUE, ROLLOUT_PERCENTAGE_ITEMS
+from .config import TARGETING_RULES, VARIATION_ID, PERCENTAGE_OPTIONS, FEATURE_FLAGS, SERVED_VALUE, SETTING_TYPE
 from .evaluationdetails import EvaluationDetails
+from .evaluationlogbuilder import EvaluationLogBuilder
 from .interfaces import ConfigCatClientException
 from .logger import Logger
 from .configfetcher import ConfigFetcher
@@ -11,10 +14,11 @@
 from .configcatoptions import ConfigCatOptions, Hooks
 from .overridedatasource import OverrideBehaviour
 from .refreshresult import RefreshResult
-from .rolloutevaluator import RolloutEvaluator
+from .rolloutevaluator import RolloutEvaluator, get_value
 from collections import namedtuple
 import copy
 from .utils import method_is_called_from, get_date_time
+import re
 
 KeyValue = namedtuple('KeyValue', 'key value')
 
@@ -57,7 +61,7 @@ def close_all(cls):
         """
         with cls._lock:
             for key, value in list(cls._instances.items()):
-                value.__close_resources()
+                value._close_resources()
             cls._instances.clear()
 
     def __init__(self,
@@ -74,14 +78,22 @@ def __init__(self,
         if sdk_key is None:
             raise ConfigCatClientException('SDK Key is required.')
 
-        self._sdk_key = sdk_key
-        self._default_user = options.default_user
-        self._rollout_evaluator = RolloutEvaluator(self.log)
         if options.flag_overrides:
             self._override_data_source = options.flag_overrides.create_data_source(self.log)
         else:
             self._override_data_source = None
 
+        # In case of local only flag overrides mode, we accept any SDK Key format.
+        if self._override_data_source is None or self._override_data_source.get_behaviour() != OverrideBehaviour.LocalOnly:
+            is_valid_sdk_key = re.match('^.{22}/.{22}$', sdk_key) is not None or \
+                re.match('^configcat-sdk-1/.{22}/.{22}$', sdk_key) is not None or \
+                (options.base_url and re.match('^configcat-proxy/.+$', sdk_key) is not None)
+            if not is_valid_sdk_key:
+                raise ConfigCatClientException('SDK Key `%s` is invalid.' % sdk_key)
+
+        self._sdk_key = sdk_key
+        self._default_user = options.default_user
+        self._rollout_evaluator = RolloutEvaluator(self.log)
         config_cache = options.config_cache if options.config_cache is not None else NullConfigCache()
 
         if self._override_data_source and self._override_data_source.get_behaviour() == OverrideBehaviour.LocalOnly:
@@ -112,8 +124,8 @@ def get_value(self, key, default_value, user=None):
         :param user: the user object to identify the caller.
         :return: the value.
         """
-        settings, fetch_time = self.__get_settings()
-        if settings is None:
+        config, fetch_time = self._get_config()
+        if config is None or config.get(FEATURE_FLAGS) is None:
             message = 'Config JSON is not present when evaluating setting \'%s\'. ' \
                       'Returning the `%s` parameter that you specified in your application: \'%s\'.'
             message_args = (key, 'default_value', str(default_value))
@@ -122,12 +134,12 @@ def get_value(self, key, default_value, user=None):
                 EvaluationDetails.from_error(key, default_value, Logger.format(message, message_args)))
             return default_value
 
-        details = self.__evaluate(key=key,
-                                  user=user,
-                                  default_value=default_value,
-                                  default_variation_id=None,
-                                  settings=settings,
-                                  fetch_time=fetch_time)
+        details = self._evaluate(key=key,
+                                 user=user,
+                                 default_value=default_value,
+                                 default_variation_id=None,
+                                 config=config,
+                                 fetch_time=fetch_time)
 
         return details.value
 
@@ -140,8 +152,8 @@ def get_value_details(self, key, default_value, user=None):
         :param user: the user object to identify the caller.
         :return: the evaluation details.
         """
-        settings, fetch_time = self.__get_settings()
-        if settings is None:
+        config, fetch_time = self._get_config()
+        if config is None or config.get(FEATURE_FLAGS) is None:
             message = 'Config JSON is not present when evaluating setting \'%s\'. ' \
                       'Returning the `%s` parameter that you specified in your application: \'%s\'.'
             message_args = (key, 'default_value', str(default_value))
@@ -150,12 +162,12 @@ def get_value_details(self, key, default_value, user=None):
             self._hooks.invoke_on_flag_evaluated(details)
             return details
 
-        details = self.__evaluate(key=key,
-                                  user=user,
-                                  default_value=default_value,
-                                  default_variation_id=None,
-                                  settings=settings,
-                                  fetch_time=fetch_time)
+        details = self._evaluate(key=key,
+                                 user=user,
+                                 default_value=default_value,
+                                 default_variation_id=None,
+                                 config=config,
+                                 fetch_time=fetch_time)
 
         return details
 
@@ -165,11 +177,12 @@ def get_all_keys(self):
 
         :return: list of keys.
         """
-        settings, _ = self.__get_settings()
-        if settings is None:
+        config, _ = self._get_config()
+        if config is None:
             self.log.error('Config JSON is not present. Returning %s.', 'empty list', event_id=1000)
             return []
 
+        settings = config.get(FEATURE_FLAGS, {})
         return list(settings)
 
     def get_key_and_value(self, variation_id):
@@ -179,24 +192,31 @@ def get_key_and_value(self, variation_id):
         :param variation_id: variation ID
         :return: key and value
         """
-        settings, _ = self.__get_settings()
-        if settings is None:
+        config, _ = self._get_config()
+        if config is None:
             self.log.error('Config JSON is not present. Returning %s.', 'None', event_id=1000)
             return None
 
-        for key, value in list(settings.items()):
-            if variation_id == value.get(VARIATION_ID):
-                return KeyValue(key, value[VALUE])
-
-            rollout_rules = value.get(ROLLOUT_RULES, [])
-            for rollout_rule in rollout_rules:
-                if variation_id == rollout_rule.get(VARIATION_ID):
-                    return KeyValue(key, rollout_rule[VALUE])
-
-            rollout_percentage_items = value.get(ROLLOUT_PERCENTAGE_ITEMS, [])
-            for rollout_percentage_item in rollout_percentage_items:
-                if variation_id == rollout_percentage_item.get(VARIATION_ID):
-                    return KeyValue(key, rollout_percentage_item[VALUE])
+        settings = config.get(FEATURE_FLAGS, {})
+        try:
+            for key, value in list(settings.items()):
+                setting_type = value.get(SETTING_TYPE)
+                if variation_id == value.get(VARIATION_ID):
+                    return KeyValue(key, get_value(value, setting_type))
+
+                targeting_rules = value.get(TARGETING_RULES, [])
+                for targeting_rule in targeting_rules:
+                    served_value = targeting_rule.get(SERVED_VALUE)
+                    if served_value is not None and variation_id == served_value.get(VARIATION_ID):
+                        return KeyValue(key, get_value(served_value, setting_type))
+
+                    rollout_percentage_items = targeting_rule.get(PERCENTAGE_OPTIONS, [])
+                    for rollout_percentage_item in rollout_percentage_items:
+                        if variation_id == rollout_percentage_item.get(VARIATION_ID):
+                            return KeyValue(key, get_value(rollout_percentage_item, setting_type))
+        except Exception:
+            self.log.exception('Error occurred in the `' + __name__ + '` method. Returning None.', event_id=1002)
+            return None
 
         self.log.error('Could not find the setting for the specified variation ID: \'%s\'.', variation_id, event_id=2011)
         return None
@@ -208,11 +228,12 @@ def get_all_values(self, user=None):
         :param user: the user object to identify the caller.
         :return: dictionary of values
         """
-        settings, _ = self.__get_settings()
-        if settings is None:
+        config, _ = self._get_config()
+        if config is None:
             self.log.error('Config JSON is not present. Returning %s.', 'empty dictionary', event_id=1000)
             return {}
 
+        settings = config.get(FEATURE_FLAGS, {})
         all_values = {}
         for key in list(settings):
             value = self.get_value(key, None, user)
@@ -228,19 +249,20 @@ def get_all_value_details(self, user=None):
         :param user: the user object to identify the caller.
         :return: list of all evaluation details
         """
-        settings, fetch_time = self.__get_settings()
-        if settings is None:
+        config, fetch_time = self._get_config()
+        if config is None:
             self.log.error('Config JSON is not present. Returning %s.', 'empty list', event_id=1000)
             return []
 
         details_result = []
+        settings = config.get(FEATURE_FLAGS, {})
         for key in list(settings):
-            details = self.__evaluate(key=key,
-                                      user=user,
-                                      default_value=None,
-                                      default_variation_id=None,
-                                      settings=settings,
-                                      fetch_time=fetch_time)
+            details = self._evaluate(key=key,
+                                     user=user,
+                                     default_value=None,
+                                     default_variation_id=None,
+                                     config=config,
+                                     fetch_time=fetch_time)
             details_result.append(details)
 
         return details_result
@@ -310,52 +332,79 @@ def close(self):
         Closes the underlying resources.
         """
         with ConfigCatClient._lock:
-            self.__close_resources()
+            self._close_resources()
             ConfigCatClient._instances.pop(self._sdk_key)
 
-    def __close_resources(self):
+    def _close_resources(self):
         if self._config_service:
             self._config_service.close()
         self._hooks.clear()
 
-    def __get_settings(self):
+    def _get_config(self):
         if self._override_data_source:
             behaviour = self._override_data_source.get_behaviour()
 
             if behaviour == OverrideBehaviour.LocalOnly:
                 return self._override_data_source.get_overrides(), utils.distant_past
             elif behaviour == OverrideBehaviour.RemoteOverLocal:
-                remote_settings, fetch_time = self._config_service.get_settings()
-                local_settings = self._override_data_source.get_overrides()
-                if not remote_settings:
-                    remote_settings = {}
-                if not local_settings:
-                    local_settings = {}
-                result = copy.deepcopy(local_settings)
-                if result:
-                    result.update(remote_settings)
+                remote_config, fetch_time = self._config_service.get_config()
+                local_config = self._override_data_source.get_overrides()
+                if not remote_config:
+                    remote_config = {FEATURE_FLAGS: {}}
+                if not local_config:
+                    local_config = {FEATURE_FLAGS: {}}
+                result = copy.deepcopy(local_config)
+                result[FEATURE_FLAGS].update(remote_config[FEATURE_FLAGS])
                 return result, fetch_time
             elif behaviour == OverrideBehaviour.LocalOverRemote:
-                remote_settings, fetch_time = self._config_service.get_settings()
-                local_settings = self._override_data_source.get_overrides()
-                if not remote_settings:
-                    remote_settings = {}
-                if not local_settings:
-                    local_settings = {}
-                result = copy.deepcopy(remote_settings)
-                result.update(local_settings)
+                remote_config, fetch_time = self._config_service.get_config()
+                local_config = self._override_data_source.get_overrides()
+                if not remote_config:
+                    remote_config = {FEATURE_FLAGS: {}}
+                if not local_config:
+                    local_config = {FEATURE_FLAGS: {}}
+                result = copy.deepcopy(remote_config)
+                result[FEATURE_FLAGS].update(local_config[FEATURE_FLAGS])
                 return result, fetch_time
 
-        return self._config_service.get_settings()
+        return self._config_service.get_config()
+
+    def _check_type_missmatch(self, value, default_value):
+        is_float_int_missmatch = \
+            (type(value) is float and type(default_value) is int) or \
+            (type(value) is int and type(default_value) is float)
+
+        # On Python 2.7, do not log a warning if the type missmatch is between str and unicode.
+        # (ignore warning: unicode is undefined in Python 3)
+        is_str_unicode_missmatch = \
+            (sys.version_info[0] == 2 and type(value) is unicode and type(default_value) is str) or \
+            (sys.version_info[0] == 2 and type(value) is str and type(default_value) is unicode)  # noqa: F821
+
+        if default_value is not None and type(value) is not type(default_value):
+            if not is_float_int_missmatch and not is_str_unicode_missmatch:
+                self.log.warning("The type of a setting does not match the type of the specified default value (%s). "
+                                 "Setting's type was %s but the default value's type was %s. "
+                                 "Please make sure that using a default value not matching the setting's type was intended." %
+                                 (default_value, type(value), type(default_value)), event_id=4002)
 
-    def __evaluate(self, key, user, default_value, default_variation_id, settings, fetch_time):
+    def _evaluate(self, key, user, default_value, default_variation_id, config, fetch_time):
         user = user if user is not None else self._default_user
+
+        # Skip building the evaluation log if it won't be logged.
+        log_builder = EvaluationLogBuilder() if self.log.isEnabledFor(logging.INFO) else None
+
         value, variation_id, rule, percentage_rule, error = self._rollout_evaluator.evaluate(
             key=key,
             user=user,
             default_value=default_value,
             default_variation_id=default_variation_id,
-            settings=settings)
+            config=config,
+            log_builder=log_builder)
+
+        self._check_type_missmatch(value, default_value)
+
+        if log_builder:
+            self.log.info(str(log_builder), event_id=5000)
 
         details = EvaluationDetails(key=key,
                                     value=value,
@@ -364,7 +413,7 @@ def __evaluate(self, key, user, default_value, default_variation_id, settings, f
                                     user=user,
                                     is_default_value=True if error else False,
                                     error=error,
-                                    matched_evaluation_rule=rule,
-                                    matched_evaluation_percentage_rule=percentage_rule)
+                                    matched_targeting_rule=rule,
+                                    matched_percentage_option=percentage_rule)
         self._hooks.invoke_on_flag_evaluated(details)
         return details
diff --git a/configcatclient/configentry.py b/configcatclient/configentry.py
index 3fc8774..bfb7c24 100644
--- a/configcatclient/configentry.py
+++ b/configcatclient/configentry.py
@@ -1,14 +1,13 @@
 import json
+import sys
 from math import floor
 
 from . import utils
+from .config import extend_config_with_inline_salt_and_segment
+from .utils import unicode_to_utf8
 
 
 class ConfigEntry(object):
-    CONFIG = 'config'
-    ETAG = 'etag'
-    FETCH_TIME = 'fetch_time'
-
     def __init__(self, config=None, etag='', config_json_string='{}', fetch_time=utils.distant_past):
         self.config = config if config is not None else {}
         self.etag = etag
@@ -42,6 +41,9 @@ def create_from_string(cls, string):
         try:
             config_json = string[etag_index + 1:]
             config = json.loads(config_json)
+            if sys.version_info[0] == 2:
+                config = unicode_to_utf8(config)  # On Python 2.7, convert unicode to utf-8
+            extend_config_with_inline_salt_and_segment(config)
         except ValueError as e:
             raise ValueError('Invalid config JSON: {}. {}'.format(config_json, str(e)))
 
diff --git a/configcatclient/configfetcher.py b/configcatclient/configfetcher.py
index 031db5e..fe9b9a1 100644
--- a/configcatclient/configfetcher.py
+++ b/configcatclient/configfetcher.py
@@ -5,11 +5,12 @@
 from requests import HTTPError
 from requests import Timeout
 
+from .config import extend_config_with_inline_salt_and_segment
 from .configentry import ConfigEntry
-from .constants import CONFIG_FILE_NAME, PREFERENCES, BASE_URL, REDIRECT
+from .config import CONFIG_FILE_NAME, PREFERENCES, BASE_URL, REDIRECT
 from .datagovernance import DataGovernance
 from .logger import Logger
-from .utils import get_utc_now_seconds_since_epoch
+from .utils import get_utc_now_seconds_since_epoch, unicode_to_utf8
 from .version import CONFIGCATCLIENT_VERSION
 
 if sys.version_info < (2, 7, 9):
@@ -151,7 +152,7 @@ def get_configuration(self, etag='', retries=0):
         # Retry the config download with the new base_url
         return self.get_configuration(etag, retries + 1)
 
-    def _fetch(self, etag):
+    def _fetch(self, etag):  # noqa: C901
         uri = self._base_url + '/' + BASE_PATH + self._sdk_key + BASE_EXTENSION
         headers = self._headers
         if etag:
@@ -165,12 +166,19 @@ def _fetch(self, etag):
             response.raise_for_status()
 
             if response.status_code in [200, 201, 202, 203, 204]:
-                response_etag = response.headers.get('Etag')
+                response_etag = response.headers.get('ETag')
                 if response_etag is None:
                     response_etag = ''
                 config = response.json()
+                extend_config_with_inline_salt_and_segment(config)
+                if sys.version_info[0] == 2:
+                    config = unicode_to_utf8(config)  # On Python 2.7, convert unicode to utf-8
+                    config_json_string = response.text.encode('utf-8')
+                else:
+                    config_json_string = response.text
+
                 return FetchResponse.success(
-                    ConfigEntry(config, response_etag, response.text, get_utc_now_seconds_since_epoch()))
+                    ConfigEntry(config, response_etag, config_json_string, get_utc_now_seconds_since_epoch()))
             elif response.status_code == 304:
                 return FetchResponse.not_modified()
             elif response.status_code in [404, 403]:
diff --git a/configcatclient/configservice.py b/configcatclient/configservice.py
index 1212194..addd181 100644
--- a/configcatclient/configservice.py
+++ b/configcatclient/configservice.py
@@ -2,8 +2,8 @@
 from threading import Thread, Event, Lock
 
 from . import utils
+from .config import FEATURE_FLAGS, CONFIG_FILE_NAME, SERIALIZATION_FORMAT_VERSION
 from .configentry import ConfigEntry
-from .constants import CONFIG_FILE_NAME, FEATURE_FLAGS, SERIALIZATION_FORMAT_VERSION
 from .pollingmode import AutoPollingMode, LazyLoadingMode
 from .refreshresult import RefreshResult
 
@@ -31,11 +31,11 @@ def __init__(self, sdk_key, polling_mode, hooks, config_fetcher, log, config_cac
         else:
             self._set_initialized()
 
-    def get_settings(self):
+    def get_config(self):
         if isinstance(self._polling_mode, LazyLoadingMode):
             entry, _ = self._fetch_if_older(
                 utils.get_utc_now_seconds_since_epoch() - self._polling_mode.cache_refresh_interval_seconds)
-            return (entry.config.get(FEATURE_FLAGS, {}), entry.fetch_time) \
+            return (entry.config, entry.fetch_time) \
                 if not entry.is_empty() \
                 else (None, utils.distant_past)
         elif isinstance(self._polling_mode, AutoPollingMode) and not self._initialized.is_set():
@@ -46,12 +46,13 @@ def get_settings(self):
                 # Max wait time expired without result, notify subscribers with the cached config.
                 if not self._initialized.is_set():
                     self._set_initialized()
-                    return (self._cached_entry.config.get(FEATURE_FLAGS, {}), self._cached_entry.fetch_time) \
+                    return (self._cached_entry.config, self._cached_entry.fetch_time) \
                         if not self._cached_entry.is_empty() \
                         else (None, utils.distant_past)
 
-        entry, _ = self._fetch_if_older(utils.distant_past, prefer_cache=True)
-        return (entry.config.get(FEATURE_FLAGS, {}), entry.fetch_time) \
+        # If we are initialized, we prefer the cached results
+        entry, _ = self._fetch_if_older(utils.distant_past, prefer_cache=self._initialized.is_set())
+        return (entry.config, entry.fetch_time) \
             if not entry.is_empty() \
             else (None, utils.distant_past)
 
@@ -59,6 +60,11 @@ def refresh(self):
         """
         :return: RefreshResult object
         """
+        if self.is_offline():
+            offline_warning = 'Client is in offline mode, it cannot initiate HTTP calls.'
+            self.log.warning(offline_warning, event_id=3200)
+            return RefreshResult(is_success=False, error=offline_warning)
+
         _, error = self._fetch_if_older(utils.distant_future)
         return RefreshResult(is_success=error is None, error=error)
 
@@ -92,35 +98,26 @@ def close(self):
         if isinstance(self._polling_mode, AutoPollingMode):
             self._stopped.set()
 
-    def _fetch_if_older(self, time, prefer_cache=False):
+    def _fetch_if_older(self, threshold, prefer_cache=False):
         """
         :return: Returns the ConfigEntry object and error message in case of any error.
         """
 
         with self._lock:
             # Sync up with the cache and use it when it's not expired.
-            if self._cached_entry.is_empty() or self._cached_entry.fetch_time > time:
-                entry = self._read_cache()
-                if not entry.is_empty() and entry.etag != self._cached_entry.etag:
-                    self._cached_entry = entry
-                    self._hooks.invoke_on_config_changed(entry.config.get(FEATURE_FLAGS))
-
-                # Cache isn't expired
-                if self._cached_entry.fetch_time > time:
-                    self._set_initialized()
-                    return self._cached_entry, None
+            from_cache = self._read_cache()
+            if not from_cache.is_empty() and from_cache.etag != self._cached_entry.etag:
+                self._cached_entry = from_cache
+                self._hooks.invoke_on_config_changed(from_cache.config.get(FEATURE_FLAGS))
 
-            # Use cache anyway (get calls on auto & manual poll must not initiate fetch).
-            # The initialized check ensures that we subscribe for the ongoing fetch during the
-            # max init wait time window in case of auto poll.
-            if prefer_cache and self._initialized.is_set():
+            # Cache isn't expired
+            if self._cached_entry.fetch_time > threshold:
+                self._set_initialized()
                 return self._cached_entry, None
 
-            # If we are in offline mode we are not allowed to initiate fetch.
-            if self._is_offline:
-                offline_warning = 'Client is in offline mode, it cannot initiate HTTP calls.'
-                self.log.warning(offline_warning, event_id=3200)
-                return self._cached_entry, offline_warning
+            # If we are in offline mode or the caller prefers cached values, do not initiate fetch.
+            if self._is_offline or prefer_cache:
+                return self._cached_entry, None
 
         # No fetch is running, initiate a new one.
         # Ensure only one fetch request is running at a time.
diff --git a/configcatclient/constants.py b/configcatclient/constants.py
deleted file mode 100644
index e1adaf8..0000000
--- a/configcatclient/constants.py
+++ /dev/null
@@ -1,16 +0,0 @@
-CONFIG_FILE_NAME = 'config_v5'
-SERIALIZATION_FORMAT_VERSION = 'v2'
-
-PREFERENCES = 'p'
-BASE_URL = 'u'
-REDIRECT = 'r'
-
-FEATURE_FLAGS = 'f'
-VALUE = 'v'
-COMPARATOR = 't'
-COMPARISON_ATTRIBUTE = 'a'
-COMPARISON_VALUE = 'c'
-ROLLOUT_PERCENTAGE_ITEMS = 'p'
-PERCENTAGE = 'p'
-ROLLOUT_RULES = 'r'
-VARIATION_ID = 'i'
diff --git a/configcatclient/evaluationcontext.py b/configcatclient/evaluationcontext.py
new file mode 100644
index 0000000..2b38cae
--- /dev/null
+++ b/configcatclient/evaluationcontext.py
@@ -0,0 +1,14 @@
+class EvaluationContext(object):
+    def __init__(self,
+                 key,
+                 setting_type,
+                 user,
+                 visited_keys=None,
+                 is_missing_user_object_logged=False,
+                 is_missing_user_object_attribute_logged=False):
+        self.key = key
+        self.setting_type = setting_type
+        self.user = user
+        self.visited_keys = visited_keys
+        self.is_missing_user_object_logged = is_missing_user_object_logged
+        self.is_missing_user_object_attribute_logged = is_missing_user_object_attribute_logged
diff --git a/configcatclient/evaluationdetails.py b/configcatclient/evaluationdetails.py
index 7517ca9..44a1a39 100644
--- a/configcatclient/evaluationdetails.py
+++ b/configcatclient/evaluationdetails.py
@@ -7,17 +7,35 @@ def __init__(self,
                  user=None,
                  is_default_value=False,
                  error=None,
-                 matched_evaluation_rule=None,
-                 matched_evaluation_percentage_rule=None):
+                 matched_targeting_rule=None,
+                 matched_percentage_option=None):
+        # Key of the feature flag or setting.
         self.key = key
+
+        # Evaluated value of the feature flag or setting.
         self.value = value
+
+        # Variation ID of the feature flag or setting (if available).
         self.variation_id = variation_id
+
+        # Time of last successful config download.
         self.fetch_time = fetch_time
+
+        # The User Object used for the evaluation (if available).
         self.user = user
+
+        # Indicates whether the default value passed to the setting evaluation methods like ConfigCatClient.get_value,
+        # ConfigCatClient.get_value_details, etc. is used as the result of the evaluation.
         self.is_default_value = is_default_value
+
+        # Error message in case evaluation failed.
         self.error = error
-        self.matched_evaluation_rule = matched_evaluation_rule
-        self.matched_evaluation_percentage_rule = matched_evaluation_percentage_rule
+
+        # The targeting rule (if any) that matched during the evaluation and was used to return the evaluated value.
+        self.matched_targeting_rule = matched_targeting_rule
+
+        # The percentage option (if any) that was used to select the evaluated value.
+        self.matched_percentage_option = matched_percentage_option
 
     @staticmethod
     def from_error(key, value, error, variation_id=None):
diff --git a/configcatclient/evaluationlogbuilder.py b/configcatclient/evaluationlogbuilder.py
new file mode 100644
index 0000000..585114a
--- /dev/null
+++ b/configcatclient/evaluationlogbuilder.py
@@ -0,0 +1,69 @@
+from .config import Comparator
+from .utils import get_date_time
+
+
+class EvaluationLogBuilder(object):
+    def __init__(self):
+        self.indent_level = 0
+        self.text = ''
+
+    @staticmethod
+    def trunc_comparison_value_if_needed(comparator, comparison_value):
+        if comparator in [Comparator.IS_ONE_OF_HASHED,
+                          Comparator.IS_NOT_ONE_OF_HASHED,
+                          Comparator.EQUALS_HASHED,
+                          Comparator.NOT_EQUALS_HASHED,
+                          Comparator.STARTS_WITH_ANY_OF_HASHED,
+                          Comparator.NOT_STARTS_WITH_ANY_OF_HASHED,
+                          Comparator.ENDS_WITH_ANY_OF_HASHED,
+                          Comparator.NOT_ENDS_WITH_ANY_OF_HASHED,
+                          Comparator.ARRAY_CONTAINS_ANY_OF_HASHED,
+                          Comparator.ARRAY_NOT_CONTAINS_ANY_OF_HASHED]:
+            if isinstance(comparison_value, list):
+                length = len(comparison_value)
+                if length > 1:
+                    return '[<{} hashed values>]'.format(length)
+                return '[<{} hashed value>]'.format(length)
+
+            return "'<hashed value>'"
+
+        if isinstance(comparison_value, list):
+            length_limit = 10
+            length = len(comparison_value)
+            if length > length_limit:
+                remaining = length - length_limit
+                if remaining == 1:
+                    more_text = "<1 more value>"
+                else:
+                    more_text = "<{} more values>".format(remaining)
+
+                return str(comparison_value[:length_limit])[:-1] + ', ... ' + more_text + ']'
+
+            return str(comparison_value)
+
+        if comparator in [Comparator.BEFORE_DATETIME, Comparator.AFTER_DATETIME]:
+            time = get_date_time(comparison_value)
+            return "'%s' (%sZ UTC)" % (str(comparison_value), time.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3])
+
+        return "'%s'" % str(comparison_value)
+
+    def increase_indent(self):
+        self.indent_level += 1
+        return self
+
+    def decrease_indent(self):
+        self.indent_level = max(0, self.indent_level - 1)
+        return self
+
+    def append(self, text):
+        self.text += text
+        return self
+
+    def new_line(self, text=None):
+        self.text += '\n' + '  ' * self.indent_level
+        if text:
+            self.text += text
+        return self
+
+    def __str__(self):
+        return self.text
diff --git a/configcatclient/localdictionarydatasource.py b/configcatclient/localdictionarydatasource.py
index c2f8600..4ff67cd 100644
--- a/configcatclient/localdictionarydatasource.py
+++ b/configcatclient/localdictionarydatasource.py
@@ -1,5 +1,9 @@
-from .constants import VALUE
+import sys
+
+from .config import VALUE, FEATURE_FLAGS, BOOL_VALUE, STRING_VALUE, INT_VALUE, DOUBLE_VALUE, SettingType, SETTING_TYPE, \
+    UNSUPPORTED_VALUE
 from .overridedatasource import OverrideDataSource, FlagOverrides
+from .utils import unicode_to_utf8
 
 
 class LocalDictionaryFlagOverrides(FlagOverrides):
@@ -7,6 +11,9 @@ def __init__(self, source, override_behaviour):
         self.source = source
         self.override_behaviour = override_behaviour
 
+        if sys.version_info[0] == 2:
+            self.source = unicode_to_utf8(self.source)  # On Python 2.7, convert unicode to utf-8
+
     def create_data_source(self, log):
         return LocalDictionaryDataSource(self.source, self.override_behaviour, log)
 
@@ -15,9 +22,26 @@ class LocalDictionaryDataSource(OverrideDataSource):
     def __init__(self, source, override_behaviour, log):
         OverrideDataSource.__init__(self, override_behaviour=override_behaviour)
         self.log = log
-        self._settings = {}
+        self._config = {}
         for key, value in source.items():
-            self._settings[key] = {VALUE: value}
+            if isinstance(value, bool):
+                value_type = BOOL_VALUE
+            elif isinstance(value, str):
+                value_type = STRING_VALUE
+            elif isinstance(value, int):
+                value_type = INT_VALUE
+            elif isinstance(value, float):
+                value_type = DOUBLE_VALUE
+            else:
+                value_type = UNSUPPORTED_VALUE
+
+            if FEATURE_FLAGS not in self._config:
+                self._config[FEATURE_FLAGS] = {}
+
+            self._config[FEATURE_FLAGS][key] = {VALUE: {value_type: value}}
+            setting_type = SettingType.from_type(type(value))
+            if setting_type is not None:
+                self._config[FEATURE_FLAGS][key][SETTING_TYPE] = int(setting_type)
 
     def get_overrides(self):
-        return self._settings
+        return self._config
diff --git a/configcatclient/localfiledatasource.py b/configcatclient/localfiledatasource.py
index 7d340db..e199af4 100644
--- a/configcatclient/localfiledatasource.py
+++ b/configcatclient/localfiledatasource.py
@@ -1,8 +1,13 @@
-from .constants import VALUE, FEATURE_FLAGS
+import sys
+
+from .config import extend_config_with_inline_salt_and_segment, VALUE, FEATURE_FLAGS, BOOL_VALUE, STRING_VALUE, \
+    INT_VALUE, DOUBLE_VALUE, SettingType, SETTING_TYPE, UNSUPPORTED_VALUE
 from .overridedatasource import OverrideDataSource, FlagOverrides
 import json
 import os
 
+from .utils import unicode_to_utf8
+
 
 class LocalFileFlagOverrides(FlagOverrides):
     def __init__(self, file_path, override_behaviour):
@@ -25,27 +30,46 @@ def __init__(self, file_path, override_behaviour, log):
                            file_path, event_id=1300)
 
         self._file_path = file_path
-        self._settings = None
+        self._config = None
         self._cached_file_stamp = 0
 
     def get_overrides(self):
         self._reload_file_content()
-        return self._settings
+        return self._config
 
-    def _reload_file_content(self):
+    def _reload_file_content(self):  # noqa: C901
         try:
             stamp = os.stat(self._file_path).st_mtime
             if stamp != self._cached_file_stamp:
                 self._cached_file_stamp = stamp
                 with open(self._file_path) as file:
                     data = json.load(file)
+
+                    if sys.version_info[0] == 2:
+                        data = unicode_to_utf8(data)  # On Python 2.7, convert unicode to utf-8
+
                     if 'flags' in data:
-                        self._settings = {}
+                        self._config = {FEATURE_FLAGS: {}}
                         source = data['flags']
                         for key, value in source.items():
-                            self._settings[key] = {VALUE: value}
+                            if isinstance(value, bool):
+                                value_type = BOOL_VALUE
+                            elif isinstance(value, str):
+                                value_type = STRING_VALUE
+                            elif isinstance(value, int):
+                                value_type = INT_VALUE
+                            elif isinstance(value, float):
+                                value_type = DOUBLE_VALUE
+                            else:
+                                value_type = UNSUPPORTED_VALUE
+
+                            self._config[FEATURE_FLAGS][key] = {VALUE: {value_type: value}}
+                            setting_type = SettingType.from_type(type(value))
+                            if setting_type is not None:
+                                self._config[FEATURE_FLAGS][key][SETTING_TYPE] = int(setting_type)
                     else:
-                        self._settings = data[FEATURE_FLAGS]
+                        extend_config_with_inline_salt_and_segment(data)
+                        self._config = data
         except OSError:
             self.log.exception('Failed to read the local config file \'%s\'.', self._file_path, event_id=1302)
         except ValueError:
diff --git a/configcatclient/rolloutevaluator.py b/configcatclient/rolloutevaluator.py
index d7bf898..f233f26 100644
--- a/configcatclient/rolloutevaluator.py
+++ b/configcatclient/rolloutevaluator.py
@@ -1,43 +1,45 @@
+import json
+
 import hashlib
+import sys
 import semver
 
+from .config import FEATURE_FLAGS, INLINE_SALT, TARGETING_RULES, PERCENTAGE_RULE_ATTRIBUTE, CONDITIONS, SERVED_VALUE, \
+    get_value, VARIATION_ID, TARGETING_RULE_PERCENTAGE_OPTIONS, PERCENTAGE_OPTIONS, PERCENTAGE, USER_CONDITION, \
+    SEGMENT_CONDITION, PREREQUISITE_FLAG_CONDITION, PREREQUISITE_FLAG_KEY, PREREQUISITE_COMPARATOR, \
+    PrerequisiteComparator, INLINE_SEGMENT, SEGMENT_NAME, SEGMENT_COMPARATOR, SEGMENT_CONDITIONS, SegmentComparator, \
+    COMPARISON_ATTRIBUTE, COMPARATOR, Comparator, COMPARATOR_TEXTS, PREREQUISITE_COMPARATOR_TEXTS, \
+    SEGMENT_COMPARATOR_TEXTS, COMPARISON_VALUES, get_value_type, SETTING_TYPE, SettingType
+from .evaluationcontext import EvaluationContext
+from .evaluationlogbuilder import EvaluationLogBuilder
 from .logger import Logger
+from datetime import datetime
 
-from .constants import ROLLOUT_RULES, ROLLOUT_PERCENTAGE_ITEMS, VALUE, VARIATION_ID, COMPARISON_ATTRIBUTE, \
-    COMPARISON_VALUE, COMPARATOR, PERCENTAGE
 from .user import User
+from .utils import unicode_to_utf8, encode_utf8, get_seconds_since_epoch
 
 
-class RolloutEvaluator(object):
-    SEMANTIC_VERSION_COMPARATORS = ['<', '<=', '>', '>=']
-    COMPARATOR_TEXTS = [
-        'IS ONE OF',
-        'IS NOT ONE OF',
-        'CONTAINS',
-        'DOES NOT CONTAIN',
-        'IS ONE OF (SemVer)',
-        'IS NOT ONE OF (SemVer)',
-        '< (SemVer)',
-        '<= (SemVer)',
-        '> (SemVer)',
-        '>= (SemVer)',
-        '= (Number)',
-        '<> (Number)',
-        '< (Number)',
-        '<= (Number)',
-        '> (Number)',
-        '>= (Number)',
-        'IS ONE OF (Sensitive)',
-        'IS NOT ONE OF (Sensitive)'
-    ]
+def sha256(value_utf8, salt, context_salt):
+    """
+    Calculates the SHA256 hash of the given value with the given salt and context_salt.
+    """
+    return hashlib.sha256(value_utf8 + salt.encode('utf-8') + context_salt.encode('utf-8')).hexdigest()
 
+
+class RolloutEvaluator(object):
     def __init__(self, log):
         self.log = log
 
-    def evaluate(self, key, user, default_value, default_variation_id, settings):  # noqa: C901
+    def evaluate(self, key, user, default_value, default_variation_id, config, log_builder, visited_keys=None):  # noqa: C901
         """
-        returns value, variation_id, matched_evaluation_rule, matched_evaluation_percentage_rule, error
+        returns value, variation_id, matched_targeting_rule, matched_percentage_option, error
         """
+
+        if visited_keys is None:
+            visited_keys = []
+        is_root_flag_evaluation = len(visited_keys) == 0
+
+        settings = config.get(FEATURE_FLAGS, {})
         setting_descriptor = settings.get(key)
 
         if setting_descriptor is None:
@@ -48,174 +50,676 @@ def evaluate(self, key, user, default_value, default_variation_id, settings):  #
             self.log.error(error, *error_args, event_id=1001)
             return default_value, default_variation_id, None, None, Logger.format(error, error_args)
 
-        rollout_rules = setting_descriptor.get(ROLLOUT_RULES, [])
-        rollout_percentage_items = setting_descriptor.get(ROLLOUT_PERCENTAGE_ITEMS, [])
+        setting_type = setting_descriptor.get(SETTING_TYPE)
+        salt = setting_descriptor.get(INLINE_SALT, '')
+        targeting_rules = setting_descriptor.get(TARGETING_RULES, [])
+        percentage_rule_attribute = setting_descriptor.get(PERCENTAGE_RULE_ATTRIBUTE)
 
-        user_has_invalid_type = user is not None and type(user) is not User
+        context = EvaluationContext(key, setting_type, user, visited_keys)
+
+        user_has_invalid_type = context.user is not None and not isinstance(context.user, User)
         if user_has_invalid_type:
             self.log.warning('Cannot evaluate targeting rules and %% options for setting \'%s\' '
-                             '(User Object is not an instance of User type).',
+                             '(User Object is not an instance of User type). '
+                             'You should pass a User Object to the evaluation methods like `get_value()` '
+                             'in order to make targeting work properly. '
+                             'Read more: https://configcat.com/docs/advanced/user-object/',
                              key, event_id=4001)
-            user = None
+            # We set the user to None and won't log further missing user object warnings
+            context.user = None
+            context.is_missing_user_object_logged = True
+
+        try:
+            if log_builder and is_root_flag_evaluation:
+                log_builder.append("Evaluating '{}'".format(key))
+                log_builder.append(" for User '{}'".format(context.user) if context.user is not None else '')
+                log_builder.increase_indent()
+
+            # Evaluate targeting rules (logically connected by OR)
+            if log_builder and len(targeting_rules) > 0:
+                log_builder.new_line('Evaluating targeting rules and applying the first match if any:')
+            for targeting_rule in targeting_rules:
+                conditions = targeting_rule.get(CONDITIONS, [])
+
+                if len(conditions) > 0:
+                    served_value = targeting_rule.get(SERVED_VALUE)
+                    value = get_value(served_value, setting_type) if served_value is not None else None
+
+                    # Evaluate targeting rule conditions (logically connected by AND)
+                    if self._evaluate_conditions(conditions, context, salt, config, log_builder, value):
+                        if served_value is not None:
+                            variation_id = served_value.get(VARIATION_ID, default_variation_id)
+                            log_builder and is_root_flag_evaluation and log_builder.new_line("Returning '%s'." % value)
+                            return value, variation_id, targeting_rule, None, None
+                    else:
+                        continue
+
+                # Evaluate percentage options of the targeting rule
+                log_builder and log_builder.increase_indent()
+                percentage_options = targeting_rule.get(TARGETING_RULE_PERCENTAGE_OPTIONS, [])
+                percentage_evaluation_result, percentage_value, percentage_variation_id, percentage_option = \
+                    self._evaluate_percentage_options(percentage_options, context, percentage_rule_attribute,
+                                                      default_variation_id, log_builder)
+
+                if percentage_evaluation_result:
+                    if log_builder:
+                        log_builder.decrease_indent()
+                        is_root_flag_evaluation and log_builder.new_line("Returning '%s'." % percentage_value)
+                    return percentage_value, percentage_variation_id, targeting_rule, percentage_option, None
+                else:
+                    if log_builder:
+                        log_builder.new_line(
+                            'The current targeting rule is ignored and the evaluation continues with the next rule.')
+                        log_builder.decrease_indent()
+                    continue
+
+            # Evaluate percentage options
+            percentage_options = setting_descriptor.get(PERCENTAGE_OPTIONS, [])
+            percentage_evaluation_result, percentage_value, percentage_variation_id, percentage_option = \
+                self._evaluate_percentage_options(percentage_options, context, percentage_rule_attribute,
+                                                  default_variation_id, log_builder)
+            if percentage_evaluation_result:
+                log_builder and is_root_flag_evaluation and log_builder.new_line("Returning '%s'." % percentage_value)
+                return percentage_value, percentage_variation_id, None, percentage_option, None
+
+            return_value = get_value(setting_descriptor, setting_type)
+            return_variation_id = setting_descriptor.get(VARIATION_ID, default_variation_id)
+            log_builder and is_root_flag_evaluation and log_builder.new_line("Returning '%s'." % return_value)
+            return return_value, return_variation_id, None, None, None
+        except Exception as e:
+            # During the recursive evaluation of a prerequisite flag, we propagate the exceptions
+            # and let the root flag's evaluation code handle them.
+            if not is_root_flag_evaluation:
+                raise e
+
+            error = 'Failed to evaluate setting \'%s\'. (%s). ' \
+                    'Returning the `%s` parameter that you specified in your application: \'%s\'. '
+            error_args = (key, str(e), 'default_value', str(default_value))
+            self.log.error(error, *error_args, event_id=2001)
+            return default_value, default_variation_id, None, None, Logger.format(error, error_args)
+
+    def _format_rule(self, comparison_attribute, comparator, comparison_value):
+        comparator_text = COMPARATOR_TEXTS[comparator]
+        return "User.%s %s %s" \
+               % (comparison_attribute, comparator_text,
+                  EvaluationLogBuilder.trunc_comparison_value_if_needed(comparator, comparison_value))
+
+    def _user_attribute_value_to_string(self, value):
+        if value is None:
+            return None
+
+        if isinstance(value, datetime):
+            value = self._get_user_attribute_value_as_seconds_since_epoch(value)
+        elif isinstance(value, list):
+            value = self._get_user_attribute_value_as_string_list(value)
+
+        return str(value)
+
+    def _get_user_attribute_value_as_text(self, attribute_name, attribute_value, condition, key):
+        if isinstance(attribute_value, str):
+            return attribute_value
+
+        if sys.version_info[0] == 2 and isinstance(attribute_value, unicode):  # noqa: F821
+            return attribute_value  # Handle unicode strings on Python 2.7
+
+        self.log.warning('Evaluation of condition (%s) for setting \'%s\' may not produce the expected result '
+                         '(the User.%s attribute is not a string value, thus it was automatically converted to '
+                         'the string value \'%s\'). Please make sure that using a non-string value was intended.',
+                         condition, key, attribute_name, attribute_value, event_id=3005)
+        return self._user_attribute_value_to_string(attribute_value)
+
+    def _convert_numeric_to_float(self, value):
+        if isinstance(value, str):
+            return float(value.replace(",", "."))
+
+        if sys.version_info[0] == 2 and isinstance(value, unicode):  # noqa: F821
+            return float(value.replace(",", "."))  # Handle unicode strings on Python 2.7
+
+        return float(value)
+
+    def _get_user_attribute_value_as_seconds_since_epoch(self, attribute_value):
+        if isinstance(attribute_value, datetime):
+            return get_seconds_since_epoch(attribute_value)
+
+        return self._convert_numeric_to_float(attribute_value)
+
+    def _get_user_attribute_value_as_string_list(self, attribute_value):
+        if not isinstance(attribute_value, list):
+            attribute_value_list = json.loads(attribute_value)
+        else:
+            attribute_value_list = attribute_value
+        if not isinstance(attribute_value_list, list):
+            raise ValueError()
+
+        return attribute_value_list
+
+    def _handle_invalid_user_attribute(self, comparison_attribute, comparator, comparison_value, key, validation_error):
+        """
+        returns: evaluation error message
+        """
+        error = 'cannot evaluate, the User.%s attribute is invalid (%s)' % (comparison_attribute, validation_error)
+        self.log.warning('Cannot evaluate condition (%s) for setting \'%s\' '
+                         '(%s). Please check the User.%s attribute and make sure that its value corresponds to the '
+                         'comparison operator.',
+                         self._format_rule(comparison_attribute, comparator, comparison_value), key,
+                         validation_error, comparison_attribute, event_id=3004)
+        return error
+
+    def _evaluate_percentage_options(self, percentage_options, context, percentage_rule_attribute, default_variation_id, log_builder):  # noqa: C901, E501
+        """
+        returns: evaluation_result, percentage_value, percentage_variation_id, percentage_option
+        """
+        if len(percentage_options) == 0:
+            return False, None, None, None
+
+        user = context.user
+        key = context.key
 
         if user is None:
-            if not user_has_invalid_type and (len(rollout_rules) > 0 or len(rollout_percentage_items) > 0):
+            if not context.is_missing_user_object_logged:
                 self.log.warning('Cannot evaluate targeting rules and %% options for setting \'%s\' '
                                  '(User Object is missing). '
                                  'You should pass a User Object to the evaluation methods like `get_value()` '
                                  'in order to make targeting work properly. '
                                  'Read more: https://configcat.com/docs/advanced/user-object/',
                                  key, event_id=3001)
-            return_value = setting_descriptor.get(VALUE, default_value)
-            return_variation_id = setting_descriptor.get(VARIATION_ID, default_variation_id)
-            self.log.info('%s', 'Returning [%s]' % str(return_value), event_id=5000)
-            return return_value, return_variation_id, None, None, None
+                context.is_missing_user_object_logged = True
 
-        log_entries = ['Evaluating get_value(\'%s\').' % key, 'User object:\n%s' % str(user)]
+            if log_builder:
+                log_builder.new_line('Skipping % options because the User Object is missing.')
+            return False, None, None, None
 
-        try:
-            # Evaluate targeting rules
-            for rollout_rule in rollout_rules:
-                comparison_attribute = rollout_rule.get(COMPARISON_ATTRIBUTE)
-                comparison_value = rollout_rule.get(COMPARISON_VALUE)
-                comparator = rollout_rule.get(COMPARATOR)
-
-                user_value = user.get_attribute(comparison_attribute)
-                if user_value is None or not user_value:
-                    log_entries.append(
-                        self._format_no_match_rule(comparison_attribute, user_value, comparator, comparison_value)
-                    )
-                    continue
+        user_attribute_name = percentage_rule_attribute if percentage_rule_attribute is not None else 'Identifier'
+        if percentage_rule_attribute is not None:
+            user_key = user.get_attribute(percentage_rule_attribute)
+        else:
+            user_key = user.get_identifier()
+        if percentage_rule_attribute is not None and user_key is None:
+            if not context.is_missing_user_object_attribute_logged:
+                self.log.warning('Cannot evaluate %% options for setting \'%s\' '
+                                 '(the User.%s attribute is missing). You should set the User.%s attribute in order to make '
+                                 'targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/',
+                                 key, percentage_rule_attribute, percentage_rule_attribute,
+                                 event_id=3003)
+                context.is_missing_user_object_attribute_logged = True
 
-                value = rollout_rule.get(VALUE)
-                variation_id = rollout_rule.get(VARIATION_ID, default_variation_id)
-
-                # IS ONE OF
-                if comparator == 0:
-                    if str(user_value) in [x.strip() for x in str(comparison_value).split(',')]:
-                        log_entries.append(self._format_match_rule(comparison_attribute, user_value, comparator,
-                                                                   comparison_value, value))
-                        return value, variation_id, rollout_rule, None, None
-                # IS NOT ONE OF
-                elif comparator == 1:
-                    if str(user_value) not in [x.strip() for x in str(comparison_value).split(',')]:
-                        log_entries.append(self._format_match_rule(comparison_attribute, user_value, comparator,
-                                                                   comparison_value, value))
-                        return value, variation_id, rollout_rule, None, None
-                # CONTAINS
-                elif comparator == 2:
-                    if str(user_value).__contains__(str(comparison_value)):
-                        log_entries.append(self._format_match_rule(comparison_attribute, user_value, comparator,
-                                                                   comparison_value, value))
-                        return value, variation_id, rollout_rule, None, None
-                # DOES NOT CONTAIN
-                elif comparator == 3:
-                    if not str(user_value).__contains__(str(comparison_value)):
-                        log_entries.append(self._format_match_rule(comparison_attribute, user_value, comparator,
-                                                                   comparison_value, value))
-                        return value, variation_id, rollout_rule, None, None
-
-                # IS ONE OF, IS NOT ONE OF (Semantic version)
-                elif 4 <= comparator <= 5:
-                    try:
-                        match = False
-                        for x in filter(None, [x.strip() for x in str(comparison_value).split(',')]):
-                            match = semver.VersionInfo.parse(str(user_value).strip()).match('==' + x) or match
-                        if (match and comparator == 4) or (not match and comparator == 5):
-                            log_entries.append(self._format_match_rule(comparison_attribute, user_value, comparator,
-                                                                       comparison_value, value))
-                            return value, variation_id, rollout_rule, None, None
-                    except ValueError as e:
-                        message = self._format_validation_error_rule(comparison_attribute, user_value, comparator,
-                                                                     comparison_value, str(e))
-                        self.log.warning(message)
-                        log_entries.append(message)
-                        continue
+            if log_builder:
+                log_builder.new_line(
+                    'Skipping %% options because the User.%s attribute is missing.' % user_attribute_name)
+            return False, None, None, None
 
-                # LESS THAN, LESS THAN OR EQUALS TO, GREATER THAN, GREATER THAN OR EQUALS TO (Semantic version)
-                elif 6 <= comparator <= 9:
-                    try:
-                        if semver.VersionInfo.parse(str(user_value).strip()).match(
-                            self.SEMANTIC_VERSION_COMPARATORS[comparator - 6] + str(comparison_value).strip()
-                        ):
-                            log_entries.append(self._format_match_rule(comparison_attribute, user_value, comparator,
-                                                                       comparison_value, value))
-                            return value, variation_id, rollout_rule, None, None
-                    except ValueError as e:
-                        message = self._format_validation_error_rule(comparison_attribute, user_value, comparator,
-                                                                     comparison_value, str(e))
-                        self.log.warning(message)
-                        log_entries.append(message)
-                        continue
-                elif 10 <= comparator <= 15:
-                    try:
-                        user_value_float = float(str(user_value).replace(",", "."))
-                        comparison_value_float = float(str(comparison_value).replace(",", "."))
-
-                        if (comparator == 10 and user_value_float == comparison_value_float) \
-                                or (comparator == 11 and user_value_float != comparison_value_float) \
-                                or (comparator == 12 and user_value_float < comparison_value_float) \
-                                or (comparator == 13 and user_value_float <= comparison_value_float) \
-                                or (comparator == 14 and user_value_float > comparison_value_float) \
-                                or (comparator == 15 and user_value_float >= comparison_value_float):
-                            log_entries.append(self._format_match_rule(comparison_attribute, user_value, comparator,
-                                                                       comparison_value, value))
-                            return value, variation_id, rollout_rule, None, None
-                    except Exception as e:
-                        message = self._format_validation_error_rule(comparison_attribute, user_value, comparator,
-                                                                     comparison_value, str(e))
-                        self.log.warning(message)
-                        log_entries.append(message)
-                        continue
-                # IS ONE OF (Sensitive)
-                elif comparator == 16:
-                    if str(hashlib.sha1(user_value.encode('utf8')).hexdigest()) in [  # NOSONAR python:S4790
-                        x.strip() for x in str(comparison_value).split(',')
-                    ]:
-                        log_entries.append(self._format_match_rule(comparison_attribute, user_value, comparator,
-                                                                   comparison_value, value))
-                        return value, variation_id, rollout_rule, None, None
-                # IS NOT ONE OF (Sensitive)
-                elif comparator == 17:
-                    if str(hashlib.sha1(user_value.encode('utf8')).hexdigest()) not in [  # NOSONAR python:S4790
-                        x.strip() for x in str(comparison_value).split(',')
-                    ]:
-                        log_entries.append(self._format_match_rule(comparison_attribute, user_value, comparator,
-                                                                   comparison_value, value))
-                        return value, variation_id, rollout_rule, None, None
-
-                log_entries.append(self._format_no_match_rule(comparison_attribute, user_value, comparator, comparison_value))
-
-            # Evaluate variations
-            if len(rollout_percentage_items) > 0:
-                user_key = user.get_identifier()
-                hash_candidate = ('%s%s' % (key, user_key)).encode('utf-8')
-                hash_val = int(hashlib.sha1(hash_candidate).hexdigest()[:7], 16) % 100
-
-                bucket = 0
-                for rollout_percentage_item in rollout_percentage_items or []:
-                    bucket += rollout_percentage_item.get(PERCENTAGE, 0)
-                    if hash_val < bucket:
-                        percentage_value = rollout_percentage_item.get(VALUE)
-                        variation_id = rollout_percentage_item.get(VARIATION_ID, default_variation_id)
-                        log_entries.append('Evaluating %% options. Returning %s' % percentage_value)
-                        return percentage_value, variation_id, None, rollout_percentage_item, None
-
-            return_value = setting_descriptor.get(VALUE, default_value)
-            return_variation_id = setting_descriptor.get(VARIATION_ID, default_variation_id)
-            log_entries.append('Returning %s' % return_value)
-            return return_value, return_variation_id, None, None, None
-        finally:
-            self.log.info('%s', '\n'.join(log_entries), event_id=5000)
+        hash_candidate = ('%s%s' % (key, self._user_attribute_value_to_string(user_key))).encode('utf-8')
+        hash_val = int(hashlib.sha1(hash_candidate).hexdigest()[:7], 16) % 100
+
+        bucket = 0
+        index = 1
+        for percentage_option in percentage_options or []:
+            percentage = percentage_option.get(PERCENTAGE, 0)
+            bucket += percentage
+            if hash_val < bucket:
+                percentage_value = get_value(percentage_option, context.setting_type)
+                variation_id = percentage_option.get(VARIATION_ID, default_variation_id)
+                if log_builder:
+                    log_builder.new_line('Evaluating %% options based on the User.%s attribute:' %
+                                         user_attribute_name)
+                    log_builder.new_line('- Computing hash in the [0..99] range from User.%s => %s '
+                                         '(this value is sticky and consistent across all SDKs)' %
+                                         (user_attribute_name, hash_val))
+                    log_builder.new_line("- Hash value %s selects %% option %s (%s%%), '%s'." %
+                                         (hash_val, index, percentage, percentage_value))
+                return True, percentage_value, variation_id, percentage_option
+            index += 1
+
+        return False, None, None, None
+
+    def _evaluate_conditions(self, conditions, context, salt, config, log_builder, value):  # noqa: C901
+        first_condition = True
+        condition_result = True
+        error = None
+        for condition in conditions:
+            user_condition = condition.get(USER_CONDITION)
+            segment_condition = condition.get(SEGMENT_CONDITION)
+            prerequisite_flag_condition = condition.get(PREREQUISITE_FLAG_CONDITION)
+
+            if first_condition:
+                if log_builder:
+                    log_builder.new_line('- IF ')
+                    log_builder.increase_indent()
+                first_condition = False
+            else:
+                if log_builder:
+                    log_builder.new_line('AND ')
+
+            if user_condition is not None:
+                result, error = self._evaluate_user_condition(user_condition, context, context.key, salt,
+                                                              log_builder)
+                if log_builder and len(conditions) > 1:
+                    log_builder.append('=> {}'.format('true' if result else 'false'))
+                    if not result:
+                        log_builder.append(', skipping the remaining AND conditions')
+
+                if not result or error:
+                    condition_result = False
+                    break
+            elif segment_condition is not None:
+                result, error = self._evaluate_segment_condition(segment_condition, context, salt, log_builder)
+                if log_builder:
+                    if len(conditions) > 1:
+                        log_builder.append(' => {}'.format('true' if result else 'false'))
+                        if not result:
+                            log_builder.append(', skipping the remaining AND conditions')
+                    elif error is None:
+                        log_builder.new_line()
+
+                if not result or error:
+                    condition_result = False
+                    break
+            elif prerequisite_flag_condition is not None:
+                result = self._evaluate_prerequisite_flag_condition(prerequisite_flag_condition, context, config, log_builder)
+                if not result:
+                    condition_result = False
+                    break
+
+        if log_builder:
+            if len(conditions) > 1:
+                log_builder.new_line()
+            if error:
+                log_builder.append('THEN %s => %s' % (
+                    "'" + str(value) + "'" if value is not None else '% options',
+                    error))
+                log_builder.new_line(
+                    'The current targeting rule is ignored and the evaluation continues with the next rule.')
+            else:
+                log_builder.append('THEN %s => %s' % (
+                    "'" + str(value) + "'" if value is not None else '% options',
+                    'MATCH, applying rule' if condition_result else 'no match'))
+            if len(conditions) > 0:
+                log_builder.decrease_indent()
+
+        return condition_result
+
+    def _evaluate_prerequisite_flag_condition(self, prerequisite_flag_condition, context, config, log_builder):  # noqa: C901, E501
+        prerequisite_key = prerequisite_flag_condition.get(PREREQUISITE_FLAG_KEY)
+        prerequisite_comparator = prerequisite_flag_condition.get(PREREQUISITE_COMPARATOR)
+
+        # Check if the prerequisite key exists
+        settings = config.get(FEATURE_FLAGS, {})
+        if prerequisite_key is None or settings.get(prerequisite_key) is None:
+            raise ValueError('Prerequisite flag key is missing or invalid.')
+
+        prerequisite_condition_result = False
+        prerequisite_flag_setting_type = settings[prerequisite_key].get(SETTING_TYPE)
+        prerequisite_comparison_value_type = get_value_type(prerequisite_flag_condition)
+
+        # Type mismatch check
+        if prerequisite_comparison_value_type != SettingType.to_type(prerequisite_flag_setting_type):
+            raise ValueError("Type mismatch between comparison value type %s and type %s of prerequisite flag '%s'" %
+                             (prerequisite_comparison_value_type, SettingType.to_type(prerequisite_flag_setting_type),
+                              prerequisite_key))
+
+        prerequisite_comparison_value = get_value(prerequisite_flag_condition, prerequisite_flag_setting_type)
+
+        prerequisite_condition = ("Flag '%s' %s '%s'" %
+                                  (prerequisite_key, PREREQUISITE_COMPARATOR_TEXTS[prerequisite_comparator],
+                                   str(prerequisite_comparison_value)))
+
+        # Circular dependency check
+        visited_keys = context.visited_keys
+        visited_keys.append(context.key)
+        if prerequisite_key in visited_keys:
+            depending_flags = ' -> '.join("'{}'".format(s) for s in list(visited_keys) + [prerequisite_key])
+            raise ValueError('Circular dependency detected between the following depending flags: %s.' % depending_flags)
+
+        if log_builder:
+            log_builder.append(prerequisite_condition)
+            log_builder.new_line('(').increase_indent()
+            log_builder.new_line("Evaluating prerequisite flag '%s':" % prerequisite_key)
+
+        prerequisite_value, _, _, _, _ = self.evaluate(prerequisite_key, context.user, None, None, config,
+                                                       log_builder, context.visited_keys)
+
+        if visited_keys:
+            visited_keys.pop()
+
+        if log_builder:
+            log_builder.new_line("Prerequisite flag evaluation result: '%s'." % str(prerequisite_value))
+            log_builder.new_line("Condition (Flag '%s' %s '%s') evaluates to " %
+                                 (prerequisite_key, PREREQUISITE_COMPARATOR_TEXTS[prerequisite_comparator],
+                                  str(prerequisite_comparison_value)))
+
+        # EQUALS
+        if prerequisite_comparator == PrerequisiteComparator.EQUALS:
+            if prerequisite_value == prerequisite_comparison_value:
+                prerequisite_condition_result = True
+        # DOES NOT EQUAL
+        elif prerequisite_comparator == PrerequisiteComparator.NOT_EQUALS:
+            if prerequisite_value != prerequisite_comparison_value:
+                prerequisite_condition_result = True
+
+        if log_builder:
+            log_builder.append('%s.' % ('true' if prerequisite_condition_result else 'false'))
+            log_builder.decrease_indent().new_line(')').new_line()
+
+        return prerequisite_condition_result
+
+    def _evaluate_segment_condition(self, segment_condition, context, salt, log_builder):  # noqa: C901
+        user = context.user
+        key = context.key
+
+        segment = segment_condition[INLINE_SEGMENT]
+        if segment is None:
+            raise ValueError('Segment reference is invalid.')
+
+        segment_name = segment.get(SEGMENT_NAME, '')
+        segment_comparator = segment_condition.get(SEGMENT_COMPARATOR)
+        segment_conditions = segment.get(SEGMENT_CONDITIONS, [])
+
+        if user is None:
+            if not context.is_missing_user_object_logged:
+                self.log.warning('Cannot evaluate targeting rules and %% options for setting \'%s\' '
+                                 '(User Object is missing). '
+                                 'You should pass a User Object to the evaluation methods like `get_value()` '
+                                 'in order to make targeting work properly. '
+                                 'Read more: https://configcat.com/docs/advanced/user-object/',
+                                 key, event_id=3001)
+                context.is_missing_user_object_logged = True
+            if log_builder:
+                log_builder.append("User %s '%s' " % (SEGMENT_COMPARATOR_TEXTS[segment_comparator], segment_name))
+            return False, 'cannot evaluate, User Object is missing'
+
+        # IS IN SEGMENT, IS NOT IN SEGMENT
+        if segment_comparator in [SegmentComparator.IS_IN, SegmentComparator.IS_NOT_IN]:
+            if log_builder:
+                log_builder.append("User %s '%s'" %
+                                   (SEGMENT_COMPARATOR_TEXTS[segment_comparator], segment_name))
+                log_builder.new_line('(').increase_indent()
+                log_builder.new_line("Evaluating segment '%s':" % segment_name)
+
+            # Set initial condition result based on comparator
+            segment_condition_result = segment_comparator == SegmentComparator.IS_IN
+
+            # Evaluate segment conditions (logically connected by AND)
+            first_segment_rule = True
+            error = None
+            for segment_condition in segment_conditions:
+                if first_segment_rule:
+                    if log_builder:
+                        log_builder.new_line('- IF ')
+                        log_builder.increase_indent()
+                    first_segment_rule = False
+                else:
+                    if log_builder:
+                        log_builder.new_line('AND ')
+
+                result, error = self._evaluate_user_condition(segment_condition, context, segment_name, salt, log_builder)
+                if log_builder:
+                    log_builder.append('=> {}'.format('true' if result else 'false'))
+                    if not result:
+                        log_builder.append(', skipping the remaining AND conditions')
+
+                if not result:
+                    segment_condition_result = False if segment_comparator == SegmentComparator.IS_IN else True
+                    break
+
+            if log_builder:
+                log_builder.decrease_indent()
+                segment_evaluation_result = segment_condition_result if segment_comparator == SegmentComparator.IS_IN \
+                    else not segment_condition_result
+                log_builder.new_line('Segment evaluation result: ')
+                if not error:
+                    log_builder.append('User IS%sIN SEGMENT.' % (' ' if segment_evaluation_result else ' NOT '))
+                else:
+                    log_builder.append('%s.' % error)
+
+                log_builder.new_line("Condition (User %s '%s') " % (SEGMENT_COMPARATOR_TEXTS[segment_comparator],
+                                                                    segment_name))
+                if not error:
+                    log_builder.append("evaluates to %s." % ('true' if segment_condition_result else 'false'))
+                else:
+                    log_builder.append('failed to evaluate.')
+
+                log_builder.decrease_indent().new_line(')')
+                if error:
+                    log_builder.new_line()
+
+            return segment_condition_result, error
+
+        return False, None
+
+    def _evaluate_user_condition(self, user_condition, context, context_salt, salt, log_builder):  # noqa: C901, E501
+        """
+        returns result of user condition, error
+        """
+
+        user = context.user
+        key = context.key
+
+        comparison_attribute = user_condition.get(COMPARISON_ATTRIBUTE)
+        comparator = user_condition.get(COMPARATOR)
+        comparison_value = user_condition.get(COMPARISON_VALUES[comparator])
+        condition = self._format_rule(comparison_attribute, comparator, comparison_value)
+        error = None
+
+        if comparison_attribute is None:
+            raise ValueError('Comparison attribute name is missing.')
+
+        if log_builder:
+            log_builder.append(condition + ' ')
+
+        if user is None:
+            if not context.is_missing_user_object_logged:
+                self.log.warning('Cannot evaluate targeting rules and %% options for setting \'%s\' '
+                                 '(User Object is missing). '
+                                 'You should pass a User Object to the evaluation methods like `get_value()` '
+                                 'in order to make targeting work properly. '
+                                 'Read more: https://configcat.com/docs/advanced/user-object/',
+                                 key, event_id=3001)
+                context.is_missing_user_object_logged = True
+            error = 'cannot evaluate, User Object is missing'
+            return False, error
+
+        user_value = user.get_attribute(comparison_attribute)
+        if user_value is None or not user_value:
+            self.log.warning('Cannot evaluate condition (%s) for setting \'%s\' '
+                             '(the User.%s attribute is missing). You should set the User.%s attribute in order to make '
+                             'targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/',
+                             condition, key, comparison_attribute, comparison_attribute, event_id=3003)
+            error = 'cannot evaluate, the User.{} attribute is missing'.format(comparison_attribute)
+            return False, error
+
+        # IS ONE OF
+        if comparator == Comparator.IS_ONE_OF:
+            user_value = self._get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
+            if user_value in comparison_value:
+                return True, error
+        # IS NOT ONE OF
+        elif comparator == Comparator.IS_NOT_ONE_OF:
+            user_value = self._get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
+            if user_value not in comparison_value:
+                return True, error
+        # CONTAINS ANY OF
+        elif comparator == Comparator.CONTAINS_ANY_OF:
+            user_value = self._get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
+            for comparison in comparison_value:
+                if comparison in user_value:
+                    return True, error
+        # NOT CONTAINS ANY OF
+        elif comparator == Comparator.NOT_CONTAINS_ANY_OF:
+            user_value = self._get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
+            if not any(comparison in user_value for comparison in comparison_value):
+                return True, error
+        # IS ONE OF, IS NOT ONE OF (Semantic version)
+        elif Comparator.IS_ONE_OF_SEMVER <= comparator <= Comparator.IS_NOT_ONE_OF_SEMVER:
+            try:
+                match = False
+                for x in filter(None, [x.strip() for x in comparison_value]):
+                    match = semver.VersionInfo.parse(str(user_value).strip()).match('==' + str(x)) or match
+                if match == (comparator == Comparator.IS_ONE_OF_SEMVER):
+                    return True, error
+            except ValueError:
+                validation_error = "'%s' is not a valid semantic version" % str(user_value).strip()
+                error = self._handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key,
+                                                            validation_error)
+                return False, error
+        # LESS THAN, LESS THAN OR EQUALS TO, GREATER THAN, GREATER THAN OR EQUALS TO (Semantic version)
+        elif Comparator.LESS_THAN_SEMVER <= comparator <= Comparator.GREATER_THAN_OR_EQUAL_SEMVER:
+            try:
+                if semver.VersionInfo.parse(str(user_value).strip()).match(
+                        COMPARATOR_TEXTS[comparator] + str(comparison_value).strip()
+                ):
+                    return True, error
+            except ValueError:
+                validation_error = "'%s' is not a valid semantic version" % str(user_value).strip()
+                error = self._handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key,
+                                                            validation_error)
+                return False, error
+        # =, <>, <, <=, >, >= (number)
+        elif Comparator.EQUALS_NUMBER <= comparator <= Comparator.GREATER_THAN_OR_EQUAL_NUMBER:
+            try:
+                user_value_float = self._convert_numeric_to_float(user_value)
+            except ValueError:
+                validation_error = "'%s' is not a valid decimal number" % str(user_value)
+                error = self._handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key,
+                                                            validation_error)
+                return False, error
+
+            comparison_value_float = float(comparison_value)
+
+            if (comparator == Comparator.EQUALS_NUMBER and user_value_float == comparison_value_float) \
+                    or (comparator == Comparator.NOT_EQUALS_NUMBER and user_value_float != comparison_value_float) \
+                    or (comparator == Comparator.LESS_THAN_NUMBER and user_value_float < comparison_value_float) \
+                    or (comparator == Comparator.LESS_THAN_OR_EQUAL_NUMBER and user_value_float <= comparison_value_float) \
+                    or (comparator == Comparator.GREATER_THAN_NUMBER and user_value_float > comparison_value_float) \
+                    or (comparator == Comparator.GREATER_THAN_OR_EQUAL_NUMBER and user_value_float >= comparison_value_float):
+                return True, error
+        # IS ONE OF (hashed)
+        elif comparator == Comparator.IS_ONE_OF_HASHED:
+            user_value = self._get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
+            if sha256(encode_utf8(user_value), salt, context_salt) in comparison_value:
+                return True, error
+        # IS NOT ONE OF (hashed)
+        elif comparator == Comparator.IS_NOT_ONE_OF_HASHED:
+            user_value = self._get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
+            if sha256(encode_utf8(user_value), salt, context_salt) not in comparison_value:
+                return True, error
+        # BEFORE, AFTER (UTC datetime)
+        elif Comparator.BEFORE_DATETIME <= comparator <= Comparator.AFTER_DATETIME:
+            try:
+                user_value_float = self._get_user_attribute_value_as_seconds_since_epoch(user_value)
+            except ValueError:
+                validation_error = "'%s' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)" % \
+                                   str(user_value)
+                error = self._handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key,
+                                                            validation_error)
+                return False, error
+
+            comparison_value_float = float(comparison_value)
+
+            if (comparator == Comparator.BEFORE_DATETIME and user_value_float < comparison_value_float) \
+                    or (comparator == Comparator.AFTER_DATETIME and user_value_float > comparison_value_float):
+                return True, error
+        # EQUALS (hashed)
+        elif comparator == Comparator.EQUALS_HASHED:
+            user_value = self._get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
+            if sha256(encode_utf8(user_value), salt, context_salt) == comparison_value:
+                return True, error
+        # NOT EQUALS (hashed)
+        elif comparator == Comparator.NOT_EQUALS_HASHED:
+            user_value = self._get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
+            if sha256(encode_utf8(user_value), salt, context_salt) != comparison_value:
+                return True, error
+        # STARTS WITH ANY OF, NOT STARTS WITH ANY OF, ENDS WITH ANY OF, NOT ENDS WITH ANY OF (hashed)
+        elif Comparator.STARTS_WITH_ANY_OF_HASHED <= comparator <= Comparator.NOT_ENDS_WITH_ANY_OF_HASHED:
+            for comparison in comparison_value:
+                underscore_index = comparison.index('_')
+                length = int(comparison[:underscore_index])
+                user_value = self._get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
+                user_value_utf8 = encode_utf8(user_value)
+
+                if len(user_value_utf8) >= length:
+                    comparison_string = comparison[underscore_index + 1:]
+                    if (comparator == Comparator.STARTS_WITH_ANY_OF_HASHED
+                        and sha256(user_value_utf8[:length], salt, context_salt) == comparison_string) or \
+                            (comparator == Comparator.ENDS_WITH_ANY_OF_HASHED
+                             and sha256(user_value_utf8[-length:], salt, context_salt) == comparison_string):
+                        return True, error
+                    elif (comparator == Comparator.NOT_STARTS_WITH_ANY_OF_HASHED
+                          and sha256(user_value_utf8[:length], salt, context_salt) == comparison_string) or \
+                            (comparator == Comparator.NOT_ENDS_WITH_ANY_OF_HASHED
+                             and sha256(user_value_utf8[-length:], salt, context_salt) == comparison_string):
+                        return False, None
+
+            # If no matches were found for the NOT_* conditions, then return True
+            if comparator in [Comparator.NOT_STARTS_WITH_ANY_OF_HASHED, Comparator.NOT_ENDS_WITH_ANY_OF_HASHED]:
+                return True, error
+        # ARRAY CONTAINS ANY OF, ARRAY NOT CONTAINS ANY OF (hashed)
+        elif Comparator.ARRAY_CONTAINS_ANY_OF_HASHED <= comparator <= Comparator.ARRAY_NOT_CONTAINS_ANY_OF_HASHED:
+            try:
+                user_value_list = self._get_user_attribute_value_as_string_list(user_value)
+
+                if sys.version_info[0] == 2:
+                    user_value_list = unicode_to_utf8(user_value_list)  # On Python 2.7, convert unicode to utf-8
+            except ValueError:
+                validation_error = "'%s' is not a valid string array" % str(user_value)
+                error = self._handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key,
+                                                            validation_error)
+                return False, error
+
+            hashed_user_values = [sha256(encode_utf8(x), salt, context_salt) for x in user_value_list]
+            if comparator == Comparator.ARRAY_CONTAINS_ANY_OF_HASHED:
+                for comparison in comparison_value:
+                    if comparison in hashed_user_values:
+                        return True, error
+            else:
+                for comparison in comparison_value:
+                    if comparison in hashed_user_values:
+                        return False, None
+                return True, error
+        # EQUALS
+        elif comparator == Comparator.EQUALS:
+            user_value = self._get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
+            if user_value == comparison_value:
+                return True, error
+        # NOT EQUALS
+        elif comparator == Comparator.NOT_EQUALS:
+            user_value = self._get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
+            if user_value != comparison_value:
+                return True, error
+        # STARTS WITH ANY OF, NOT STARTS WITH ANY OF, ENDS WITH ANY OF, NOT ENDS WITH ANY OF
+        elif Comparator.STARTS_WITH_ANY_OF <= comparator <= Comparator.NOT_ENDS_WITH_ANY_OF:
+            user_value = self._get_user_attribute_value_as_text(comparison_attribute, user_value, condition, key)
+            for comparison in comparison_value:
+                if (comparator == Comparator.STARTS_WITH_ANY_OF and user_value.startswith(comparison)) or \
+                        (comparator == Comparator.ENDS_WITH_ANY_OF and user_value.endswith(comparison)):
+                    return True, error
+                elif (comparator == Comparator.NOT_STARTS_WITH_ANY_OF and user_value.startswith(comparison)) \
+                        or (comparator == Comparator.NOT_ENDS_WITH_ANY_OF and user_value.endswith(comparison)):
+                    return False, None
+
+            # If no matches were found for the NOT_* conditions, then return True
+            if comparator in [Comparator.NOT_STARTS_WITH_ANY_OF, Comparator.NOT_ENDS_WITH_ANY_OF]:
+                return True, error
+        # ARRAY CONTAINS ANY OF, ARRAY NOT CONTAINS ANY OF
+        elif Comparator.ARRAY_CONTAINS_ANY_OF <= comparator <= Comparator.ARRAY_NOT_CONTAINS_ANY_OF:
+            try:
+                user_value_list = self._get_user_attribute_value_as_string_list(user_value)
 
-    def _format_match_rule(self, comparison_attribute, user_value, comparator, comparison_value, value):
-        return 'Evaluating rule: [%s:%s] [%s] [%s] => match, returning: %s' \
-               % (comparison_attribute, user_value, self.COMPARATOR_TEXTS[comparator], comparison_value, value)
+                if sys.version_info[0] == 2:
+                    user_value_list = unicode_to_utf8(user_value_list)  # On Python 2.7, convert unicode to utf-8
+            except ValueError:
+                validation_error = "'%s' is not a valid string array" % str(user_value)
+                error = self._handle_invalid_user_attribute(comparison_attribute, comparator, comparison_value, key,
+                                                            validation_error)
+                return False, error
 
-    def _format_no_match_rule(self, comparison_attribute, user_value, comparator, comparison_value):
-        return 'Evaluating rule: [%s:%s] [%s] [%s] => no match' \
-               % (comparison_attribute, user_value, self.COMPARATOR_TEXTS[comparator], comparison_value)
+            if comparator == Comparator.ARRAY_CONTAINS_ANY_OF:
+                for comparison in comparison_value:
+                    if comparison in user_value_list:
+                        return True, error
+            else:
+                for comparison in comparison_value:
+                    if comparison in user_value_list:
+                        return False, None
+                return True, error
 
-    def _format_validation_error_rule(self, comparison_attribute, user_value, comparator, comparison_value, error):
-        return 'Evaluating rule: [%s:%s] [%s] [%s] => SKIP rule. Validation error: %s' \
-               % (comparison_attribute, user_value, self.COMPARATOR_TEXTS[comparator], comparison_value, error)
+        return False, error
diff --git a/configcatclient/user.py b/configcatclient/user.py
index bb377d7..6eed4f2 100644
--- a/configcatclient/user.py
+++ b/configcatclient/user.py
@@ -2,13 +2,60 @@
 
 __PREDEFINED__ = ['Identifier', 'Email', 'Country']
 
+from collections import OrderedDict
+from datetime import datetime
+
 
 class User(object):
     """
-    The user object for variation evaluation
+    User Object. Contains user attributes which are used for evaluating targeting rules and percentage options.
     """
 
     def __init__(self, identifier, email=None, country=None, custom=None):
+        """
+        Initialize a User object.
+
+        Args:
+            identifier: The unique identifier of the user or session (e.g. email address, primary key, session ID, etc.)
+            email: Email address of the user.
+            country: Country of the user.
+            custom: Custom attributes of the user for advanced targeting rule definitions (e.g. role, subscription type, etc.)
+
+            All comparators support string values as User Object attribute (in some cases they need to be provided in a
+            specific format though, see below), but some of them also support other types of values. It depends on the
+            comparator how the values will be handled. The following rules apply:
+
+            Text-based comparators (EQUALS, IS_ONE_OF, etc.)
+            * accept string values,
+            * all other values are automatically converted to string
+              (a warning will be logged but evaluation will continue as normal).
+
+            SemVer-based comparators (IS_ONE_OF_SEMVER, LESS_THAN_SEMVER, GREATER_THAN_SEMVER, etc.)
+            * accept string values containing a properly formatted, valid semver value,
+            * all other values are considered invalid
+              (a warning will be logged and the currently evaluated targeting rule will be skipped).
+
+            Number-based comparators (EQUALS_NUMBER, LESS_THAN_NUMBER, GREATER_THAN_OR_EQUAL_NUMBER, etc.)
+            * accept float values and all other numeric values which can safely be converted to float,
+            * accept string values containing a properly formatted, valid float value,
+            * all other values are considered invalid
+              (a warning will be logged and the currently evaluated targeting rule will be skipped).
+
+            Date time-based comparators (BEFORE_DATETIME / AFTER_DATETIME)
+            * accept datetime values, which are automatically converted to a second-based Unix timestamp
+              (datetime values with naive timezone are considered to be in UTC),
+            * accept float values representing a second-based Unix timestamp
+              and all other numeric values which can safely be converted to float,
+            * accept string values containing a properly formatted, valid float value,
+            * all other values are considered invalid
+              (a warning will be logged and the currently evaluated targeting rule will be skipped).
+
+            String array-based comparators (ARRAY_CONTAINS_ANY_OF / ARRAY_NOT_CONTAINS_ANY_OF)
+            * accept arrays of strings,
+            * accept string values containing a valid JSON string which can be deserialized to an array of strings,
+            * all other values are considered invalid
+              (a warning will be logged and the currently evaluated targeting rule will be skipped).
+        """
         self.__identifier = identifier if identifier is not None else ''
         self.__data = {'Identifier': identifier, 'Email': email, 'Country': country}
         self.__custom = custom
@@ -24,10 +71,19 @@ def get_attribute(self, attribute):
         return self.__custom.get(attribute) if self.__custom else None
 
     def __str__(self):
-        dump = {
-            'Identifier': self.__identifier,
-            'Email': self.__data.get('Email'),
-            'Country': self.__data.get('Country'),
-            'Custom': self.__custom,
-        }
-        return json.dumps(dump, indent=4)
+        def serializer(obj):
+            if isinstance(obj, datetime):
+                return obj.isoformat()
+
+            raise TypeError("Type not serializable")
+
+        dump = OrderedDict([
+            ('Identifier', self.__identifier),
+            ('Email', self.__data.get('Email')),
+            ('Country', self.__data.get('Country'))
+        ])
+        if self.__custom:
+            dump.update(self.__custom)
+
+        filtered_dump = OrderedDict([(k, v) for k, v in dump.items() if v is not None])
+        return json.dumps(filtered_dump, separators=(',', ':'), default=serializer)
diff --git a/configcatclient/utils.py b/configcatclient/utils.py
index dd53e45..5b12ba8 100644
--- a/configcatclient/utils.py
+++ b/configcatclient/utils.py
@@ -3,7 +3,12 @@
 from qualname import qualname
 from datetime import datetime
 
-epoch_time = datetime(1970, 1, 1)
+try:
+    from datetime import timezone
+except ImportError:
+    import pytz as timezone  # On Python 2.7, datetime.timezone is not available. We use pytz instead.
+
+epoch_time = datetime(1970, 1, 1, tzinfo=timezone.utc)
 distant_future = sys.float_info.max
 distant_past = 0
 
@@ -57,16 +62,48 @@ def method_is_called_from(method, level=1):
 
 
 def get_utc_now():
-    return datetime.utcnow()
+    return datetime.now(timezone.utc)
 
 
 def get_seconds_since_epoch(date_time):
+    # if there is no timezone info, assume UTC
+    if date_time.tzinfo is None:
+        date_time = date_time.replace(tzinfo=timezone.utc)
+
     return (date_time - epoch_time).total_seconds()
 
 
 def get_date_time(seconds_since_epoch):
-    return datetime.utcfromtimestamp(seconds_since_epoch)
+    return datetime.fromtimestamp(seconds_since_epoch, timezone.utc)
 
 
 def get_utc_now_seconds_since_epoch():
     return get_seconds_since_epoch(get_utc_now())
+
+
+def unicode_to_utf8(data):
+    """
+    Convert unicode data in a collection to UTF-8 data. Used for supporting unicode config json strings on Python 2.7.
+    Once Python 2.7 is no longer supported, this function can be removed.
+    """
+    if isinstance(data, dict):
+        return {unicode_to_utf8(key): unicode_to_utf8(value) for key, value in data.iteritems()}
+    elif isinstance(data, list):
+        return [unicode_to_utf8(element) for element in data]
+    elif isinstance(data, unicode):  # noqa: F821 (ignore warning: unicode is undefined in Python 3)
+        return data.encode('utf-8')
+    else:
+        return data
+
+
+def encode_utf8(value):
+    """
+    Get the UTF-8 encoded value of a string. Used for supporting unicode config json strings on Python 2.7.
+    If the value is already UTF-8 encoded, it is returned as is.
+    Once Python 2.7 is no longer supported, this function can be removed.
+    The use of this function can be replaced with encode() method of the string: value.encode('utf-8')
+    """
+    try:
+        return value.encode('utf-8')
+    except UnicodeDecodeError:
+        return value
diff --git a/configcatclient/version.py b/configcatclient/version.py
index 56f6e76..6e70da1 100644
--- a/configcatclient/version.py
+++ b/configcatclient/version.py
@@ -1 +1 @@
-CONFIGCATCLIENT_VERSION = "8.0.1"
+CONFIGCATCLIENT_VERSION = "9.0.0"
diff --git a/configcatclienttests/data/evaluation/1_targeting_rule.json b/configcatclienttests/data/evaluation/1_targeting_rule.json
new file mode 100644
index 0000000..596bd2b
--- /dev/null
+++ b/configcatclienttests/data/evaluation/1_targeting_rule.json
@@ -0,0 +1,41 @@
+{
+  "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d",
+  "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A",
+  "tests": [
+    {
+      "key": "stringContainsDogDefaultCat",
+      "defaultValue": "default",
+      "returnValue": "Cat",
+      "expectedLog": "1_rule_no_user.txt"
+    },
+    {
+      "key": "stringContainsDogDefaultCat",
+      "defaultValue": "default",
+      "user": {
+        "Identifier": "12345"
+      },
+      "returnValue": "Cat",
+      "expectedLog": "1_rule_no_targeted_attribute.txt"
+    },
+    {
+      "key": "stringContainsDogDefaultCat",
+      "defaultValue": "default",
+      "user": {
+        "Identifier": "12345",
+        "Email": "joe@example.com"
+      },
+      "returnValue": "Cat",
+      "expectedLog": "1_rule_not_matching_targeted_attribute.txt"
+    },
+    {
+      "key": "stringContainsDogDefaultCat",
+      "defaultValue": "default",
+      "user": {
+        "Identifier": "12345",
+        "Email": "joe@configcat.com"
+      },
+      "returnValue": "Dog",
+      "expectedLog": "1_rule_matching_targeted_attribute.txt"
+    }
+  ]
+}
diff --git a/configcatclienttests/data/evaluation/1_targeting_rule/1_rule_matching_targeted_attribute.txt b/configcatclienttests/data/evaluation/1_targeting_rule/1_rule_matching_targeted_attribute.txt
new file mode 100644
index 0000000..f05c6f6
--- /dev/null
+++ b/configcatclienttests/data/evaluation/1_targeting_rule/1_rule_matching_targeted_attribute.txt
@@ -0,0 +1,4 @@
+INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => MATCH, applying rule
+  Returning 'Dog'.
diff --git a/configcatclienttests/data/evaluation/1_targeting_rule/1_rule_no_targeted_attribute.txt b/configcatclienttests/data/evaluation/1_targeting_rule/1_rule_no_targeted_attribute.txt
new file mode 100644
index 0000000..80702e9
--- /dev/null
+++ b/configcatclienttests/data/evaluation/1_targeting_rule/1_rule_no_targeted_attribute.txt
@@ -0,0 +1,6 @@
+WARNING [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'stringContainsDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  Returning 'Cat'.
diff --git a/configcatclienttests/data/evaluation/1_targeting_rule/1_rule_no_user.txt b/configcatclienttests/data/evaluation/1_targeting_rule/1_rule_no_user.txt
new file mode 100644
index 0000000..3b01ec8
--- /dev/null
+++ b/configcatclienttests/data/evaluation/1_targeting_rule/1_rule_no_user.txt
@@ -0,0 +1,6 @@
+WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'stringContainsDogDefaultCat'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => cannot evaluate, User Object is missing
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  Returning 'Cat'.
diff --git a/configcatclienttests/data/evaluation/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt b/configcatclienttests/data/evaluation/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt
new file mode 100644
index 0000000..49d1252
--- /dev/null
+++ b/configcatclienttests/data/evaluation/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt
@@ -0,0 +1,4 @@
+INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345","Email":"joe@example.com"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => no match
+  Returning 'Cat'.
diff --git a/configcatclienttests/data/evaluation/2_targeting_rules.json b/configcatclienttests/data/evaluation/2_targeting_rules.json
new file mode 100644
index 0000000..5cf8a3c
--- /dev/null
+++ b/configcatclienttests/data/evaluation/2_targeting_rules.json
@@ -0,0 +1,41 @@
+{
+  "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d",
+  "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A",
+  "tests": [
+    {
+      "key": "stringIsInDogDefaultCat",
+      "defaultValue": "default",
+      "returnValue": "Cat",
+      "expectedLog": "2_rules_no_user.txt"
+    },
+    {
+      "key": "stringIsInDogDefaultCat",
+      "defaultValue": "default",
+      "user": {
+        "Identifier": "12345"
+      },
+      "returnValue": "Cat",
+      "expectedLog": "2_rules_no_targeted_attribute.txt"
+    },
+    {
+      "key": "stringIsInDogDefaultCat",
+      "defaultValue": "default",
+      "user": {
+        "Identifier": "12345",
+        "Custom1": "user"
+      },
+      "returnValue": "Cat",
+      "expectedLog": "2_rules_not_matching_targeted_attribute.txt"
+    },
+    {
+      "key": "stringIsInDogDefaultCat",
+      "defaultValue": "default",
+      "user": {
+        "Identifier": "12345",
+        "Custom1": "admin"
+      },
+      "returnValue": "Dog",
+      "expectedLog": "2_rules_matching_targeted_attribute.txt"
+    }
+  ]
+}
diff --git a/configcatclienttests/data/evaluation/2_targeting_rules/2_rules_matching_targeted_attribute.txt b/configcatclienttests/data/evaluation/2_targeting_rules/2_rules_matching_targeted_attribute.txt
new file mode 100644
index 0000000..d124a4f
--- /dev/null
+++ b/configcatclienttests/data/evaluation/2_targeting_rules/2_rules_matching_targeted_attribute.txt
@@ -0,0 +1,7 @@
+WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"admin"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => MATCH, applying rule
+  Returning 'Dog'.
diff --git a/configcatclienttests/data/evaluation/2_targeting_rules/2_rules_no_targeted_attribute.txt b/configcatclienttests/data/evaluation/2_targeting_rules/2_rules_no_targeted_attribute.txt
new file mode 100644
index 0000000..0e02076
--- /dev/null
+++ b/configcatclienttests/data/evaluation/2_targeting_rules/2_rules_no_targeted_attribute.txt
@@ -0,0 +1,9 @@
+WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+WARNING [3003] Cannot evaluate condition (User.Custom1 IS ONE OF ['admin']) for setting 'stringIsInDogDefaultCat' (the User.Custom1 attribute is missing). You should set the User.Custom1 attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => cannot evaluate, the User.Custom1 attribute is missing
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  Returning 'Cat'.
diff --git a/configcatclienttests/data/evaluation/2_targeting_rules/2_rules_no_user.txt b/configcatclienttests/data/evaluation/2_targeting_rules/2_rules_no_user.txt
new file mode 100644
index 0000000..187d989
--- /dev/null
+++ b/configcatclienttests/data/evaluation/2_targeting_rules/2_rules_no_user.txt
@@ -0,0 +1,8 @@
+WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringIsInDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'stringIsInDogDefaultCat'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, User Object is missing
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => cannot evaluate, User Object is missing
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  Returning 'Cat'.
diff --git a/configcatclienttests/data/evaluation/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt b/configcatclienttests/data/evaluation/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt
new file mode 100644
index 0000000..72217b2
--- /dev/null
+++ b/configcatclienttests/data/evaluation/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt
@@ -0,0 +1,7 @@
+WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"user"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => no match
+  Returning 'Cat'.
diff --git a/configcatclienttests/data/evaluation/and_rules.json b/configcatclienttests/data/evaluation/and_rules.json
new file mode 100644
index 0000000..c6ed879
--- /dev/null
+++ b/configcatclienttests/data/evaluation/and_rules.json
@@ -0,0 +1,22 @@
+{
+  "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb",
+  "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A",
+  "tests": [
+    {
+      "key": "emailAnd",
+      "defaultValue": "default",
+      "returnValue": "Cat",
+      "expectedLog": "and_rules_no_user.txt"
+    },
+    {
+      "key": "emailAnd",
+      "defaultValue": "default",
+      "user": {
+        "Identifier": "12345",
+        "Email": "jane@configcat.com"
+      },
+      "returnValue": "Cat",
+      "expectedLog": "and_rules_user.txt"
+    }
+  ]
+}
diff --git a/configcatclienttests/data/evaluation/and_rules/and_rules_no_user.txt b/configcatclienttests/data/evaluation/and_rules/and_rules_no_user.txt
new file mode 100644
index 0000000..6a05557
--- /dev/null
+++ b/configcatclienttests/data/evaluation/and_rules/and_rules_no_user.txt
@@ -0,0 +1,7 @@
+WARNING [3001] Cannot evaluate targeting rules and % options for setting 'emailAnd' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'emailAnd'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions
+    THEN 'Dog' => cannot evaluate, User Object is missing
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  Returning 'Cat'.
diff --git a/configcatclienttests/data/evaluation/and_rules/and_rules_user.txt b/configcatclienttests/data/evaluation/and_rules/and_rules_user.txt
new file mode 100644
index 0000000..92c59ce
--- /dev/null
+++ b/configcatclienttests/data/evaluation/and_rules/and_rules_user.txt
@@ -0,0 +1,7 @@
+INFO [5000] Evaluating 'emailAnd' for User '{"Identifier":"12345","Email":"jane@configcat.com"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true
+    AND User.Email CONTAINS ANY OF ['@'] => true
+    AND User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions
+    THEN 'Dog' => no match
+  Returning 'Cat'.
diff --git a/configcatclienttests/data/evaluation/circular_dependency.json b/configcatclienttests/data/evaluation/circular_dependency.json
new file mode 100644
index 0000000..cb50d22
--- /dev/null
+++ b/configcatclienttests/data/evaluation/circular_dependency.json
@@ -0,0 +1,14 @@
+{
+  "jsonOverride": "circular_dependency_override.json",
+  "tests": [
+    {
+      "key": "key1",
+      "defaultValue": "default",
+      "user": {
+        "Identifier": "1234"
+      },
+      "returnValue": "first",
+      "expectedLog": "circular_dependency.txt"
+    }
+  ]
+}
diff --git a/configcatclienttests/data/evaluation/comparators.json b/configcatclienttests/data/evaluation/comparators.json
new file mode 100644
index 0000000..5d5631e
--- /dev/null
+++ b/configcatclienttests/data/evaluation/comparators.json
@@ -0,0 +1,20 @@
+{
+  "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb",
+  "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ",
+  "tests": [
+    {
+      "key": "allinone",
+      "defaultValue": "",
+      "user": {
+        "Identifier": "12345",
+        "Email": "joe@example.com",
+        "Country": "[\"USA\"]",
+        "Version": "1.0.0",
+        "Number": "1.0",
+        "Date": "1693497500"
+      },
+      "returnValue": "default",
+      "expectedLog": "allinone.txt"
+    }
+  ]
+}
diff --git a/configcatclienttests/data/evaluation/comparators/allinone.txt b/configcatclienttests/data/evaluation/comparators/allinone.txt
new file mode 100644
index 0000000..84e9b32
--- /dev/null
+++ b/configcatclienttests/data/evaluation/comparators/allinone.txt
@@ -0,0 +1,57 @@
+INFO [5000] Evaluating 'allinone' for User '{"Identifier":"12345","Email":"joe@example.com","Country":"[\"USA\"]","Version":"1.0.0","Number":"1.0","Date":"1693497500"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email EQUALS '<hashed value>' => true
+    AND User.Email NOT EQUALS '<hashed value>' => false, skipping the remaining AND conditions
+    THEN '1h' => no match
+  - IF User.Email EQUALS 'joe@example.com' => true
+    AND User.Email NOT EQUALS 'joe@example.com' => false, skipping the remaining AND conditions
+    THEN '1c' => no match
+  - IF User.Email IS ONE OF [<1 hashed value>] => true
+    AND User.Email IS NOT ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions
+    THEN '2h' => no match
+  - IF User.Email IS ONE OF ['joe@example.com'] => true
+    AND User.Email IS NOT ONE OF ['joe@example.com'] => false, skipping the remaining AND conditions
+    THEN '2c' => no match
+  - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true
+    AND User.Email NOT STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions
+    THEN '3h' => no match
+  - IF User.Email STARTS WITH ANY OF ['joe@'] => true
+    AND User.Email NOT STARTS WITH ANY OF ['joe@'] => false, skipping the remaining AND conditions
+    THEN '3c' => no match
+  - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => true
+    AND User.Email NOT ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions
+    THEN '4h' => no match
+  - IF User.Email ENDS WITH ANY OF ['@example.com'] => true
+    AND User.Email NOT ENDS WITH ANY OF ['@example.com'] => false, skipping the remaining AND conditions
+    THEN '4c' => no match
+  - IF User.Email CONTAINS ANY OF ['e@e'] => true
+    AND User.Email NOT CONTAINS ANY OF ['e@e'] => false, skipping the remaining AND conditions
+    THEN '5' => no match
+  - IF User.Version IS ONE OF ['1.0.0'] => true
+    AND User.Version IS NOT ONE OF ['1.0.0'] => false, skipping the remaining AND conditions
+    THEN '6' => no match
+  - IF User.Version < '1.0.1' => true
+    AND User.Version >= '1.0.1' => false, skipping the remaining AND conditions
+    THEN '7' => no match
+  - IF User.Version > '0.9.9' => true
+    AND User.Version <= '0.9.9' => false, skipping the remaining AND conditions
+    THEN '8' => no match
+  - IF User.Number = '1' => true
+    AND User.Number != '1' => false, skipping the remaining AND conditions
+    THEN '9' => no match
+  - IF User.Number < '1.1' => true
+    AND User.Number >= '1.1' => false, skipping the remaining AND conditions
+    THEN '10' => no match
+  - IF User.Number > '0.9' => true
+    AND User.Number <= '0.9' => false, skipping the remaining AND conditions
+    THEN '11' => no match
+  - IF User.Date BEFORE '1693497600' (2023-08-31T16:00:00.000Z UTC) => true
+    AND User.Date AFTER '1693497600' (2023-08-31T16:00:00.000Z UTC) => false, skipping the remaining AND conditions
+    THEN '12' => no match
+  - IF User.Country ARRAY CONTAINS ANY OF [<1 hashed value>] => true
+    AND User.Country ARRAY NOT CONTAINS ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions
+    THEN '13h' => no match
+  - IF User.Country ARRAY CONTAINS ANY OF ['USA'] => true
+    AND User.Country ARRAY NOT CONTAINS ANY OF ['USA'] => false, skipping the remaining AND conditions
+    THEN '13c' => no match
+  Returning 'default'.
diff --git a/configcatclienttests/data/evaluation/epoch_date_validation.json b/configcatclienttests/data/evaluation/epoch_date_validation.json
new file mode 100644
index 0000000..e916d21
--- /dev/null
+++ b/configcatclienttests/data/evaluation/epoch_date_validation.json
@@ -0,0 +1,16 @@
+{
+  "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb",
+  "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ",
+  "tests": [
+    {
+      "key": "boolTrueIn202304",
+      "defaultValue": true,
+      "returnValue": false,
+      "expectedLog": "date_error.txt",
+      "user": {
+        "Identifier": "12345",
+        "Custom1": "2023.04.10"
+      }
+    }
+  ]
+}
diff --git a/configcatclienttests/data/evaluation/epoch_date_validation/date_error.txt b/configcatclienttests/data/evaluation/epoch_date_validation/date_error.txt
new file mode 100644
index 0000000..fbde23f
--- /dev/null
+++ b/configcatclienttests/data/evaluation/epoch_date_validation/date_error.txt
@@ -0,0 +1,7 @@
+WARNING [3004] Cannot evaluate condition (User.Custom1 AFTER '1680307200' (2023-04-01T00:00:00.000Z UTC)) for setting 'boolTrueIn202304' ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator.
+INFO [5000] Evaluating 'boolTrueIn202304' for User '{"Identifier":"12345","Custom1":"2023.04.10"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Custom1 AFTER '1680307200' (2023-04-01T00:00:00.000Z UTC) => false, skipping the remaining AND conditions
+    THEN 'True' => cannot evaluate, the User.Custom1 attribute is invalid ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch))
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  Returning 'False'.
diff --git a/configcatclienttests/data/evaluation/list_truncation.json b/configcatclienttests/data/evaluation/list_truncation.json
new file mode 100644
index 0000000..64e9426
--- /dev/null
+++ b/configcatclienttests/data/evaluation/list_truncation.json
@@ -0,0 +1,14 @@
+{
+  "jsonOverride": "test_list_truncation.json",
+  "tests": [
+    {
+      "key": "booleanKey1",
+      "defaultValue": false,
+      "user": {
+        "Identifier": "12"
+      },
+      "returnValue": true,
+      "expectedLog": "list_truncation.txt"
+    }
+  ]
+}
diff --git a/configcatclienttests/data/evaluation/list_truncation/list_truncation.txt b/configcatclienttests/data/evaluation/list_truncation/list_truncation.txt
new file mode 100644
index 0000000..a07e52c
--- /dev/null
+++ b/configcatclienttests/data/evaluation/list_truncation/list_truncation.txt
@@ -0,0 +1,7 @@
+INFO [5000] Evaluating 'booleanKey1' for User '{"Identifier":"12"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] => true
+    AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', ... <1 more value>] => true
+    AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', ... <2 more values>] => true
+    THEN 'True' => MATCH, applying rule
+  Returning 'True'.
diff --git a/configcatclienttests/data/evaluation/list_truncation/test_list_truncation.json b/configcatclienttests/data/evaluation/list_truncation/test_list_truncation.json
new file mode 100644
index 0000000..6fdde45
--- /dev/null
+++ b/configcatclienttests/data/evaluation/list_truncation/test_list_truncation.json
@@ -0,0 +1,83 @@
+{
+  "p": {
+    "u": "https://cdn-global.configcat.com",
+    "r": 0,
+    "s": "test-salt"
+  },
+  "f": {
+    "booleanKey1": {
+      "t": 0,
+      "v": {
+        "b": false
+      },
+      "r": [
+        {
+          "c": [
+            {
+              "u": {
+                "a": "Identifier",
+                "c": 2,
+                "l": [
+                  "1",
+                  "2",
+                  "3",
+                  "4",
+                  "5",
+                  "6",
+                  "7",
+                  "8",
+                  "9",
+                  "10"
+                ]
+              }
+            },
+            {
+              "u": {
+                "a": "Identifier",
+                "c": 2,
+                "l": [
+                  "1",
+                  "2",
+                  "3",
+                  "4",
+                  "5",
+                  "6",
+                  "7",
+                  "8",
+                  "9",
+                  "10",
+                  "11"
+                ]
+              }
+            },
+            {
+              "u": {
+                "a": "Identifier",
+                "c": 2,
+                "l": [
+                  "1",
+                  "2",
+                  "3",
+                  "4",
+                  "5",
+                  "6",
+                  "7",
+                  "8",
+                  "9",
+                  "10",
+                  "11",
+                  "12"
+                ]
+              }
+            }
+          ],
+          "s": {
+            "v": {
+              "b": true
+            }
+          }
+        }
+      ]
+    }
+  }
+}
diff --git a/configcatclienttests/data/evaluation/number_validation.json b/configcatclienttests/data/evaluation/number_validation.json
new file mode 100644
index 0000000..640cf3d
--- /dev/null
+++ b/configcatclienttests/data/evaluation/number_validation.json
@@ -0,0 +1,16 @@
+{
+  "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d747f0-5986-c2ef-eef3-ec778e32e10a/244cf8b0-f604-11e8-b543-f23c917f9d8d",
+  "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw",
+  "tests": [
+    {
+      "key": "number",
+      "defaultValue": "default",
+      "returnValue": "Default",
+      "expectedLog": "number_error.txt",
+      "user": {
+        "Identifier": "12345",
+        "Custom1": "not_a_number"
+      }
+    }
+  ]
+}
diff --git a/configcatclienttests/data/evaluation/number_validation/number_error.txt b/configcatclienttests/data/evaluation/number_validation/number_error.txt
new file mode 100644
index 0000000..f936809
--- /dev/null
+++ b/configcatclienttests/data/evaluation/number_validation/number_error.txt
@@ -0,0 +1,6 @@
+WARNING [3004] Cannot evaluate condition (User.Custom1 != '5') for setting 'number' ('not_a_number' is not a valid decimal number). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator.
+INFO [5000] Evaluating 'number' for User '{"Identifier":"12345","Custom1":"not_a_number"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Custom1 != '5' THEN '<>5' => cannot evaluate, the User.Custom1 attribute is invalid ('not_a_number' is not a valid decimal number)
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  Returning 'Default'.
diff --git a/configcatclienttests/data/evaluation/options_after_targeting_rule.json b/configcatclienttests/data/evaluation/options_after_targeting_rule.json
new file mode 100644
index 0000000..803840e
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_after_targeting_rule.json
@@ -0,0 +1,41 @@
+{
+  "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d",
+  "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A",
+  "tests": [
+    {
+      "key": "integer25One25Two25Three25FourAdvancedRules",
+      "defaultValue": 42,
+      "returnValue": -1,
+      "expectedLog": "options_after_targeting_rule_no_user.txt"
+    },
+    {
+      "key": "integer25One25Two25Three25FourAdvancedRules",
+      "defaultValue": 42,
+      "user": {
+        "Identifier": "12345"
+      },
+      "returnValue": 2,
+      "expectedLog": "options_after_targeting_rule_no_targeted_attribute.txt"
+    },
+    {
+      "key": "integer25One25Two25Three25FourAdvancedRules",
+      "defaultValue": 42,
+      "user": {
+        "Identifier": "12345",
+        "Email": "joe@example.com"
+      },
+      "returnValue": 2,
+      "expectedLog": "options_after_targeting_rule_not_matching_targeted_attribute.txt"
+    },
+    {
+      "key": "integer25One25Two25Three25FourAdvancedRules",
+      "defaultValue": 42,
+      "user": {
+        "Identifier": "12345",
+        "Email": "joe@configcat.com"
+      },
+      "returnValue": 5,
+      "expectedLog": "options_after_targeting_rule_matching_targeted_attribute.txt"
+    }
+  ]
+}
diff --git a/configcatclienttests/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt b/configcatclienttests/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt
new file mode 100644
index 0000000..6815fa3
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt
@@ -0,0 +1,4 @@
+INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345","Email":"joe@configcat.com"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => MATCH, applying rule
+  Returning '5'.
diff --git a/configcatclienttests/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt b/configcatclienttests/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt
new file mode 100644
index 0000000..8e6facb
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt
@@ -0,0 +1,9 @@
+WARNING [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'integer25One25Two25Three25FourAdvancedRules' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => cannot evaluate, the User.Email attribute is missing
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  Evaluating % options based on the User.Identifier attribute:
+  - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs)
+  - Hash value 25 selects % option 2 (25%), '2'.
+  Returning '2'.
diff --git a/configcatclienttests/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_user.txt b/configcatclienttests/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_user.txt
new file mode 100644
index 0000000..6262730
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_user.txt
@@ -0,0 +1,7 @@
+WARNING [3001] Cannot evaluate targeting rules and % options for setting 'integer25One25Two25Three25FourAdvancedRules' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => cannot evaluate, User Object is missing
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  Skipping % options because the User Object is missing.
+  Returning '-1'.
diff --git a/configcatclienttests/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt b/configcatclienttests/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt
new file mode 100644
index 0000000..c412e5a
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt
@@ -0,0 +1,7 @@
+INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345","Email":"joe@example.com"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => no match
+  Evaluating % options based on the User.Identifier attribute:
+  - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs)
+  - Hash value 25 selects % option 2 (25%), '2'.
+  Returning '2'.
diff --git a/configcatclienttests/data/evaluation/options_based_on_custom_attr.json b/configcatclienttests/data/evaluation/options_based_on_custom_attr.json
new file mode 100644
index 0000000..5f8d1c6
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_based_on_custom_attr.json
@@ -0,0 +1,31 @@
+{
+  "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb",
+  "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw",
+  "tests": [
+    {
+      "key": "string75Cat0Dog25Falcon0HorseCustomAttr",
+      "defaultValue": "default",
+      "returnValue": "Chicken",
+      "expectedLog": "options_custom_attribute_no_user.txt"
+    },
+    {
+      "key": "string75Cat0Dog25Falcon0HorseCustomAttr",
+      "defaultValue": "default",
+      "user": {
+        "Identifier": "12345"
+      },
+      "returnValue": "Chicken",
+      "expectedLog": "no_options_custom_attribute.txt"
+    },
+    {
+      "key": "string75Cat0Dog25Falcon0HorseCustomAttr",
+      "defaultValue": "default",
+      "user": {
+        "Identifier": "12345",
+        "Country": "US"
+      },
+      "returnValue": "Cat",
+      "expectedLog": "matching_options_custom_attribute.txt"
+    }
+  ]
+}
diff --git a/configcatclienttests/data/evaluation/options_based_on_custom_attr/matching_options_custom_attribute.txt b/configcatclienttests/data/evaluation/options_based_on_custom_attr/matching_options_custom_attribute.txt
new file mode 100644
index 0000000..2621086
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_based_on_custom_attr/matching_options_custom_attribute.txt
@@ -0,0 +1,5 @@
+INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier":"12345","Country":"US"}'
+  Evaluating % options based on the User.Country attribute:
+  - Computing hash in the [0..99] range from User.Country => 70 (this value is sticky and consistent across all SDKs)
+  - Hash value 70 selects % option 1 (75%), 'Cat'.
+  Returning 'Cat'.
diff --git a/configcatclienttests/data/evaluation/options_based_on_custom_attr/no_options_custom_attribute.txt b/configcatclienttests/data/evaluation/options_based_on_custom_attr/no_options_custom_attribute.txt
new file mode 100644
index 0000000..c92c5bc
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_based_on_custom_attr/no_options_custom_attribute.txt
@@ -0,0 +1,4 @@
+WARNING [3003] Cannot evaluate % options for setting 'string75Cat0Dog25Falcon0HorseCustomAttr' (the User.Country attribute is missing). You should set the User.Country attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier":"12345"}'
+  Skipping % options because the User.Country attribute is missing.
+  Returning 'Chicken'.
diff --git a/configcatclienttests/data/evaluation/options_based_on_custom_attr/options_custom_attribute_no_user.txt b/configcatclienttests/data/evaluation/options_based_on_custom_attr/options_custom_attribute_no_user.txt
new file mode 100644
index 0000000..97daf53
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_based_on_custom_attr/options_custom_attribute_no_user.txt
@@ -0,0 +1,4 @@
+WARNING [3001] Cannot evaluate targeting rules and % options for setting 'string75Cat0Dog25Falcon0HorseCustomAttr' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr'
+  Skipping % options because the User Object is missing.
+  Returning 'Chicken'.
diff --git a/configcatclienttests/data/evaluation/options_based_on_user_id.json b/configcatclienttests/data/evaluation/options_based_on_user_id.json
new file mode 100644
index 0000000..442f575
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_based_on_user_id.json
@@ -0,0 +1,21 @@
+{
+  "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d",
+  "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A",
+  "tests": [
+    {
+      "key": "string75Cat0Dog25Falcon0Horse",
+      "defaultValue": "default",
+      "returnValue": "Chicken",
+      "expectedLog": "options_user_attribute_no_user.txt"
+    },
+    {
+      "key": "string75Cat0Dog25Falcon0Horse",
+      "defaultValue": "default",
+      "user": {
+        "Identifier": "12345"
+      },
+      "returnValue": "Cat",
+      "expectedLog": "options_user_attribute_user.txt"
+    }
+  ]
+}
diff --git a/configcatclienttests/data/evaluation/options_based_on_user_id/options_user_attribute_no_user.txt b/configcatclienttests/data/evaluation/options_based_on_user_id/options_user_attribute_no_user.txt
new file mode 100644
index 0000000..484e6b6
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_based_on_user_id/options_user_attribute_no_user.txt
@@ -0,0 +1,4 @@
+WARNING [3001] Cannot evaluate targeting rules and % options for setting 'string75Cat0Dog25Falcon0Horse' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse'
+  Skipping % options because the User Object is missing.
+  Returning 'Chicken'.
diff --git a/configcatclienttests/data/evaluation/options_based_on_user_id/options_user_attribute_user.txt b/configcatclienttests/data/evaluation/options_based_on_user_id/options_user_attribute_user.txt
new file mode 100644
index 0000000..dac8dd6
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_based_on_user_id/options_user_attribute_user.txt
@@ -0,0 +1,5 @@
+INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse' for User '{"Identifier":"12345"}'
+  Evaluating % options based on the User.Identifier attribute:
+  - Computing hash in the [0..99] range from User.Identifier => 21 (this value is sticky and consistent across all SDKs)
+  - Hash value 21 selects % option 1 (75%), 'Cat'.
+  Returning 'Cat'.
diff --git a/configcatclienttests/data/evaluation/options_within_targeting_rule.json b/configcatclienttests/data/evaluation/options_within_targeting_rule.json
new file mode 100644
index 0000000..4c6c533
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_within_targeting_rule.json
@@ -0,0 +1,52 @@
+{
+  "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb",
+  "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw",
+  "tests": [
+    {
+      "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat",
+      "defaultValue": "default",
+      "returnValue": "Cat",
+      "expectedLog": "options_within_targeting_rule_no_user.txt"
+    },
+    {
+      "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat",
+      "defaultValue": "default",
+      "user": {
+        "Identifier": "12345"
+      },
+      "returnValue": "Cat",
+      "expectedLog": "options_within_targeting_rule_no_targeted_attribute.txt"
+    },
+    {
+      "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat",
+      "defaultValue": "default",
+      "user": {
+        "Identifier": "12345",
+        "Email": "joe@example.com"
+      },
+      "returnValue": "Cat",
+      "expectedLog": "options_within_targeting_rule_not_matching_targeted_attribute.txt"
+    },
+    {
+      "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat",
+      "defaultValue": "default",
+      "user": {
+        "Identifier": "12345",
+        "Email": "joe@configcat.com"
+      },
+      "returnValue": "Cat",
+      "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt"
+    },
+    {
+      "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat",
+      "defaultValue": "default",
+      "user": {
+        "Identifier": "12345",
+        "Email": "joe@configcat.com",
+        "Country": "US"
+      },
+      "returnValue": "Cat",
+      "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt"
+    }
+  ]
+}
diff --git a/configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt b/configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt
new file mode 100644
index 0000000..db721f5
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt
@@ -0,0 +1,7 @@
+WARNING [3003] Cannot evaluate % options for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (the User.Country attribute is missing). You should set the User.Country attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule
+    Skipping % options because the User.Country attribute is missing.
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  Returning 'Cat'.
diff --git a/configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt b/configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt
new file mode 100644
index 0000000..8129521
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt
@@ -0,0 +1,7 @@
+INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com","Country":"US"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule
+    Evaluating % options based on the User.Country attribute:
+    - Computing hash in the [0..99] range from User.Country => 63 (this value is sticky and consistent across all SDKs)
+    - Hash value 63 selects % option 1 (75%), 'Cat'.
+  Returning 'Cat'.
diff --git a/configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt b/configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt
new file mode 100644
index 0000000..74f812f
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt
@@ -0,0 +1,6 @@
+WARNING [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => cannot evaluate, the User.Email attribute is missing
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  Returning 'Cat'.
diff --git a/configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_user.txt b/configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_user.txt
new file mode 100644
index 0000000..6224e5a
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_user.txt
@@ -0,0 +1,6 @@
+WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => cannot evaluate, User Object is missing
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  Returning 'Cat'.
diff --git a/configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt b/configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt
new file mode 100644
index 0000000..dd6032e
--- /dev/null
+++ b/configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt
@@ -0,0 +1,4 @@
+INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@example.com"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => no match
+  Returning 'Cat'.
diff --git a/configcatclienttests/data/evaluation/prerequisite_flag.json b/configcatclienttests/data/evaluation/prerequisite_flag.json
new file mode 100644
index 0000000..674e2d3
--- /dev/null
+++ b/configcatclienttests/data/evaluation/prerequisite_flag.json
@@ -0,0 +1,35 @@
+{
+  "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb",
+  "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A",
+  "tests": [
+    {
+      "key": "dependentFeatureWithUserCondition",
+      "defaultValue": "default",
+      "returnValue": "Chicken",
+      "expectedLog": "prerequisite_flag_no_user_needed_by_dep.txt"
+    },
+    {
+      "key": "dependentFeature",
+      "defaultValue": "default",
+      "returnValue": "Chicken",
+      "expectedLog": "prerequisite_flag_no_user_needed_by_prereq.txt"
+    },
+    {
+      "key": "dependentFeatureWithUserCondition2",
+      "defaultValue": "default",
+      "returnValue": "Frog",
+      "expectedLog": "prerequisite_flag_no_user_needed_by_both.txt"
+    },
+    {
+      "key": "dependentFeature",
+      "defaultValue": "default",
+      "user": {
+        "Identifier": "12345",
+        "Email": "kate@configcat.com",
+        "Country": "USA"
+      },
+      "returnValue": "Horse",
+      "expectedLog": "prerequisite_flag.txt"
+    }
+  ]
+}
diff --git a/configcatclienttests/data/evaluation/prerequisite_flag/prerequisite_flag.txt b/configcatclienttests/data/evaluation/prerequisite_flag/prerequisite_flag.txt
new file mode 100644
index 0000000..1d9022b
--- /dev/null
+++ b/configcatclienttests/data/evaluation/prerequisite_flag/prerequisite_flag.txt
@@ -0,0 +1,32 @@
+INFO [5000] Evaluating 'dependentFeature' for User '{"Identifier":"12345","Email":"kate@configcat.com","Country":"USA"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF Flag 'mainFeature' EQUALS 'target'
+    (
+      Evaluating prerequisite flag 'mainFeature':
+      Evaluating targeting rules and applying the first match if any:
+      - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions
+        THEN 'private' => no match
+      - IF User.Country IS ONE OF [<1 hashed value>] => true
+        AND User IS NOT IN SEGMENT 'Beta Users'
+        (
+          Evaluating segment 'Beta Users':
+          - IF User.Email IS ONE OF [<2 hashed values>] => false, skipping the remaining AND conditions
+          Segment evaluation result: User IS NOT IN SEGMENT.
+          Condition (User IS NOT IN SEGMENT 'Beta Users') evaluates to true.
+        ) => true
+        AND User IS NOT IN SEGMENT 'Developers'
+        (
+          Evaluating segment 'Developers':
+          - IF User.Email IS ONE OF [<2 hashed values>] => false, skipping the remaining AND conditions
+          Segment evaluation result: User IS NOT IN SEGMENT.
+          Condition (User IS NOT IN SEGMENT 'Developers') evaluates to true.
+        ) => true
+        THEN 'target' => MATCH, applying rule
+      Prerequisite flag evaluation result: 'target'.
+      Condition (Flag 'mainFeature' EQUALS 'target') evaluates to true.
+    )
+    THEN % options => MATCH, applying rule
+    Evaluating % options based on the User.Identifier attribute:
+    - Computing hash in the [0..99] range from User.Identifier => 78 (this value is sticky and consistent across all SDKs)
+    - Hash value 78 selects % option 4 (25%), 'Horse'.
+  Returning 'Horse'.
diff --git a/configcatclienttests/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt b/configcatclienttests/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt
new file mode 100644
index 0000000..aaf8b57
--- /dev/null
+++ b/configcatclienttests/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt
@@ -0,0 +1,38 @@
+WARNING [3001] Cannot evaluate targeting rules and % options for setting 'dependentFeatureWithUserCondition2' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+WARNING [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+WARNING [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'dependentFeatureWithUserCondition2'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  - IF Flag 'mainFeature' EQUALS 'public'
+    (
+      Evaluating prerequisite flag 'mainFeature':
+      Evaluating targeting rules and applying the first match if any:
+      - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions
+        THEN 'private' => cannot evaluate, User Object is missing
+        The current targeting rule is ignored and the evaluation continues with the next rule.
+      - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions
+        THEN 'target' => cannot evaluate, User Object is missing
+        The current targeting rule is ignored and the evaluation continues with the next rule.
+      Prerequisite flag evaluation result: 'public'.
+      Condition (Flag 'mainFeature' EQUALS 'public') evaluates to true.
+    )
+    THEN % options => MATCH, applying rule
+    Skipping % options because the User Object is missing.
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  - IF Flag 'mainFeature' EQUALS 'public'
+    (
+      Evaluating prerequisite flag 'mainFeature':
+      Evaluating targeting rules and applying the first match if any:
+      - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions
+        THEN 'private' => cannot evaluate, User Object is missing
+        The current targeting rule is ignored and the evaluation continues with the next rule.
+      - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions
+        THEN 'target' => cannot evaluate, User Object is missing
+        The current targeting rule is ignored and the evaluation continues with the next rule.
+      Prerequisite flag evaluation result: 'public'.
+      Condition (Flag 'mainFeature' EQUALS 'public') evaluates to true.
+    )
+    THEN 'Frog' => MATCH, applying rule
+  Returning 'Frog'.
diff --git a/configcatclienttests/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt b/configcatclienttests/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt
new file mode 100644
index 0000000..d69f2df
--- /dev/null
+++ b/configcatclienttests/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt
@@ -0,0 +1,15 @@
+WARNING [3001] Cannot evaluate targeting rules and % options for setting 'dependentFeatureWithUserCondition' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'dependentFeatureWithUserCondition'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  - IF Flag 'mainFeatureWithoutUserCondition' EQUALS 'True'
+    (
+      Evaluating prerequisite flag 'mainFeatureWithoutUserCondition':
+      Prerequisite flag evaluation result: 'True'.
+      Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'True') evaluates to true.
+    )
+    THEN % options => MATCH, applying rule
+    Skipping % options because the User Object is missing.
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  Returning 'Chicken'.
diff --git a/configcatclienttests/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt b/configcatclienttests/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt
new file mode 100644
index 0000000..9dfef8b
--- /dev/null
+++ b/configcatclienttests/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt
@@ -0,0 +1,18 @@
+WARNING [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'dependentFeature'
+  Evaluating targeting rules and applying the first match if any:
+  - IF Flag 'mainFeature' EQUALS 'target'
+    (
+      Evaluating prerequisite flag 'mainFeature':
+      Evaluating targeting rules and applying the first match if any:
+      - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions
+        THEN 'private' => cannot evaluate, User Object is missing
+        The current targeting rule is ignored and the evaluation continues with the next rule.
+      - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions
+        THEN 'target' => cannot evaluate, User Object is missing
+        The current targeting rule is ignored and the evaluation continues with the next rule.
+      Prerequisite flag evaluation result: 'public'.
+      Condition (Flag 'mainFeature' EQUALS 'target') evaluates to false.
+    )
+    THEN % options => no match
+  Returning 'Chicken'.
diff --git a/configcatclienttests/data/evaluation/segment.json b/configcatclienttests/data/evaluation/segment.json
new file mode 100644
index 0000000..41744c2
--- /dev/null
+++ b/configcatclienttests/data/evaluation/segment.json
@@ -0,0 +1,41 @@
+{
+  "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d9f207-6883-43e5-868c-cbf677af3fe6/244cf8b0-f604-11e8-b543-f23c917f9d8d",
+  "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/LcYz135LE0qbcacz2mgXnA",
+  "tests": [
+    {
+      "key": "featureWithSegmentTargeting",
+      "defaultValue": false,
+      "returnValue": false,
+      "expectedLog": "segment_no_user.txt"
+    },
+    {
+      "key": "featureWithNegatedSegmentTargetingCleartext",
+      "defaultValue": false,
+      "user": {
+        "Identifier": "12345"
+      },
+      "returnValue": false,
+      "expectedLog": "segment_no_targeted_attribute.txt"
+    },
+    {
+      "key": "featureWithSegmentTargeting",
+      "defaultValue": false,
+      "user": {
+        "Identifier": "12345",
+        "Email": "jane@example.com"
+      },
+      "returnValue": true,
+      "expectedLog": "segment_matching.txt"
+    },
+    {
+      "key": "featureWithNegatedSegmentTargeting",
+      "defaultValue": false,
+      "user": {
+        "Identifier": "12345",
+        "Email": "jane@example.com"
+      },
+      "returnValue": false,
+      "expectedLog": "segment_no_matching.txt"
+    }
+  ]
+}
diff --git a/configcatclienttests/data/evaluation/segment/segment_matching.txt b/configcatclienttests/data/evaluation/segment/segment_matching.txt
new file mode 100644
index 0000000..ff46528
--- /dev/null
+++ b/configcatclienttests/data/evaluation/segment/segment_matching.txt
@@ -0,0 +1,11 @@
+INFO [5000] Evaluating 'featureWithSegmentTargeting' for User '{"Identifier":"12345","Email":"jane@example.com"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User IS IN SEGMENT 'Beta users'
+    (
+      Evaluating segment 'Beta users':
+      - IF User.Email IS ONE OF [<2 hashed values>] => true
+      Segment evaluation result: User IS IN SEGMENT.
+      Condition (User IS IN SEGMENT 'Beta users') evaluates to true.
+    )
+    THEN 'True' => MATCH, applying rule
+  Returning 'True'.
diff --git a/configcatclienttests/data/evaluation/segment/segment_no_matching.txt b/configcatclienttests/data/evaluation/segment/segment_no_matching.txt
new file mode 100644
index 0000000..3723521
--- /dev/null
+++ b/configcatclienttests/data/evaluation/segment/segment_no_matching.txt
@@ -0,0 +1,11 @@
+INFO [5000] Evaluating 'featureWithNegatedSegmentTargeting' for User '{"Identifier":"12345","Email":"jane@example.com"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User IS NOT IN SEGMENT 'Beta users'
+    (
+      Evaluating segment 'Beta users':
+      - IF User.Email IS ONE OF [<2 hashed values>] => true
+      Segment evaluation result: User IS IN SEGMENT.
+      Condition (User IS NOT IN SEGMENT 'Beta users') evaluates to false.
+    )
+    THEN 'True' => no match
+  Returning 'False'.
diff --git a/configcatclienttests/data/evaluation/segment/segment_no_targeted_attribute.txt b/configcatclienttests/data/evaluation/segment/segment_no_targeted_attribute.txt
new file mode 100644
index 0000000..9f39d8c
--- /dev/null
+++ b/configcatclienttests/data/evaluation/segment/segment_no_targeted_attribute.txt
@@ -0,0 +1,13 @@
+WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['jane@example.com', 'john@example.com']) for setting 'featureWithNegatedSegmentTargetingCleartext' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'featureWithNegatedSegmentTargetingCleartext' for User '{"Identifier":"12345"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User IS NOT IN SEGMENT 'Beta users (cleartext)'
+    (
+      Evaluating segment 'Beta users (cleartext)':
+      - IF User.Email IS ONE OF ['jane@example.com', 'john@example.com'] => false, skipping the remaining AND conditions
+      Segment evaluation result: cannot evaluate, the User.Email attribute is missing.
+      Condition (User IS NOT IN SEGMENT 'Beta users (cleartext)') failed to evaluate.
+    )
+    THEN 'True' => cannot evaluate, the User.Email attribute is missing
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  Returning 'False'.
diff --git a/configcatclienttests/data/evaluation/segment/segment_no_user.txt b/configcatclienttests/data/evaluation/segment/segment_no_user.txt
new file mode 100644
index 0000000..087f85e
--- /dev/null
+++ b/configcatclienttests/data/evaluation/segment/segment_no_user.txt
@@ -0,0 +1,6 @@
+WARNING [3001] Cannot evaluate targeting rules and % options for setting 'featureWithSegmentTargeting' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/
+INFO [5000] Evaluating 'featureWithSegmentTargeting'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User IS IN SEGMENT 'Beta users' THEN 'True' => cannot evaluate, User Object is missing
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  Returning 'False'.
diff --git a/configcatclienttests/data/evaluation/semver_validation.json b/configcatclienttests/data/evaluation/semver_validation.json
new file mode 100644
index 0000000..3a14fc6
--- /dev/null
+++ b/configcatclienttests/data/evaluation/semver_validation.json
@@ -0,0 +1,26 @@
+{
+  "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d745f1-f315-7daf-d163-5541d3786e6f/244cf8b0-f604-11e8-b543-f23c917f9d8d",
+  "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA",
+  "tests": [
+    {
+      "key": "isNotOneOf",
+      "defaultValue": "default",
+      "returnValue": "Default",
+      "expectedLog": "semver_error.txt",
+      "user": {
+        "Identifier": "12345",
+        "Custom1": "wrong_semver"
+      }
+    },
+    {
+      "key": "relations",
+      "defaultValue": "default",
+      "returnValue": "Default",
+      "expectedLog": "semver_relations_error.txt",
+      "user": {
+        "Identifier": "12345",
+        "Custom1": "wrong_semver"
+      }
+    }
+  ]
+}
diff --git a/configcatclienttests/data/evaluation/semver_validation/semver_error.txt b/configcatclienttests/data/evaluation/semver_validation/semver_error.txt
new file mode 100644
index 0000000..e14cc95
--- /dev/null
+++ b/configcatclienttests/data/evaluation/semver_validation/semver_error.txt
@@ -0,0 +1,9 @@
+WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', '']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator.
+WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '3.0.1']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator.
+INFO [5000] Evaluating 'isNotOneOf' for User '{"Identifier":"12345","Custom1":"wrong_semver"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Custom1 IS NOT ONE OF ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', ''] THEN 'Is not one of (1.0.0, 1.0.1, 2.0.0   , 2.0.1, 2.0.2,    )' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version)
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  - IF User.Custom1 IS NOT ONE OF ['1.0.0', '3.0.1'] THEN 'Is not one of (1.0.0, 3.0.1)' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version)
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  Returning 'Default'.
diff --git a/configcatclienttests/data/evaluation/semver_validation/semver_relations_error.txt b/configcatclienttests/data/evaluation/semver_validation/semver_relations_error.txt
new file mode 100644
index 0000000..8198c85
--- /dev/null
+++ b/configcatclienttests/data/evaluation/semver_validation/semver_relations_error.txt
@@ -0,0 +1,18 @@
+WARNING [3004] Cannot evaluate condition (User.Custom1 < '1.0.0,') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator.
+WARNING [3004] Cannot evaluate condition (User.Custom1 < '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator.
+WARNING [3004] Cannot evaluate condition (User.Custom1 <= '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator.
+WARNING [3004] Cannot evaluate condition (User.Custom1 > '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator.
+WARNING [3004] Cannot evaluate condition (User.Custom1 >= '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator.
+INFO [5000] Evaluating 'relations' for User '{"Identifier":"12345","Custom1":"wrong_semver"}'
+  Evaluating targeting rules and applying the first match if any:
+  - IF User.Custom1 < '1.0.0,' THEN '<1.0.0,' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version)
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  - IF User.Custom1 < '1.0.0' THEN '< 1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version)
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  - IF User.Custom1 <= '1.0.0' THEN '<=1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version)
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  - IF User.Custom1 > '2.0.0' THEN '>2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version)
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  - IF User.Custom1 >= '2.0.0' THEN '>=2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version)
+    The current targeting rule is ignored and the evaluation continues with the next rule.
+  Returning 'Default'.
diff --git a/configcatclienttests/data/evaluation/simple_value.json b/configcatclienttests/data/evaluation/simple_value.json
new file mode 100644
index 0000000..070d6f5
--- /dev/null
+++ b/configcatclienttests/data/evaluation/simple_value.json
@@ -0,0 +1,37 @@
+{
+  "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d",
+  "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A",
+  "tests": [
+    {
+      "key": "boolDefaultFalse",
+      "defaultValue": true,
+      "returnValue": false,
+      "expectedLog": "off_flag.txt"
+    },
+    {
+      "key": "boolDefaultTrue",
+      "defaultValue": false,
+      "returnValue": true,
+      "expectedLog": "on_flag.txt"
+    },
+    {
+      "key": "stringDefaultCat",
+      "defaultValue": "Default",
+      "returnValue": "Cat",
+      "expectedLog": "text_setting.txt"
+    },
+    {
+      "key": "integerDefaultOne",
+      "defaultValue": 0,
+      "returnValue": 1,
+      "expectedLog": "int_setting.txt"
+    },
+    {
+      "testName": "double_setting",
+      "key": "doubleDefaultPi",
+      "defaultValue": 0.0,
+      "returnValue": 3.1415,
+      "expectedLog": "double_setting.txt"
+    }
+  ]
+}
diff --git a/configcatclienttests/data/evaluation/simple_value/double_setting.txt b/configcatclienttests/data/evaluation/simple_value/double_setting.txt
new file mode 100644
index 0000000..4a632f7
--- /dev/null
+++ b/configcatclienttests/data/evaluation/simple_value/double_setting.txt
@@ -0,0 +1,2 @@
+INFO [5000] Evaluating 'doubleDefaultPi'
+  Returning '3.1415'.
diff --git a/configcatclienttests/data/evaluation/simple_value/int_setting.txt b/configcatclienttests/data/evaluation/simple_value/int_setting.txt
new file mode 100644
index 0000000..1361843
--- /dev/null
+++ b/configcatclienttests/data/evaluation/simple_value/int_setting.txt
@@ -0,0 +1,2 @@
+INFO [5000] Evaluating 'integerDefaultOne'
+  Returning '1'.
diff --git a/configcatclienttests/data/evaluation/simple_value/off_flag.txt b/configcatclienttests/data/evaluation/simple_value/off_flag.txt
new file mode 100644
index 0000000..17c4a69
--- /dev/null
+++ b/configcatclienttests/data/evaluation/simple_value/off_flag.txt
@@ -0,0 +1,2 @@
+INFO [5000] Evaluating 'boolDefaultFalse'
+  Returning 'False'.
diff --git a/configcatclienttests/data/evaluation/simple_value/on_flag.txt b/configcatclienttests/data/evaluation/simple_value/on_flag.txt
new file mode 100644
index 0000000..a392fe1
--- /dev/null
+++ b/configcatclienttests/data/evaluation/simple_value/on_flag.txt
@@ -0,0 +1,2 @@
+INFO [5000] Evaluating 'boolDefaultTrue'
+  Returning 'True'.
diff --git a/configcatclienttests/data/evaluation/simple_value/text_setting.txt b/configcatclienttests/data/evaluation/simple_value/text_setting.txt
new file mode 100644
index 0000000..831d7c6
--- /dev/null
+++ b/configcatclienttests/data/evaluation/simple_value/text_setting.txt
@@ -0,0 +1,2 @@
+INFO [5000] Evaluating 'stringDefaultCat'
+  Returning 'Cat'.
diff --git a/configcatclienttests/test-simple.json b/configcatclienttests/data/test-simple.json
similarity index 100%
rename from configcatclienttests/test-simple.json
rename to configcatclienttests/data/test-simple.json
diff --git a/configcatclienttests/data/test.json b/configcatclienttests/data/test.json
new file mode 100644
index 0000000..63c93c7
--- /dev/null
+++ b/configcatclienttests/data/test.json
@@ -0,0 +1,24 @@
+{
+  "f": {
+    "disabledFeature": {
+      "t": 0,
+      "v": { "b": false }
+    },
+    "enabledFeature": {
+      "t": 0,
+      "v": { "b": true }
+    },
+    "intSetting": {
+      "t": 2,
+      "v": { "i": 5 }
+    },
+    "doubleSetting": {
+      "t": 3,
+      "v": { "d": 3.14 }
+    },
+    "stringSetting": {
+      "t": 1,
+      "v": { "s": "test" }
+    }
+  }
+}
\ No newline at end of file
diff --git a/configcatclienttests/data/test_circulardependency_v6.json b/configcatclienttests/data/test_circulardependency_v6.json
new file mode 100644
index 0000000..a8a9e17
--- /dev/null
+++ b/configcatclienttests/data/test_circulardependency_v6.json
@@ -0,0 +1,80 @@
+{
+  "p": {
+    "u": "https://cdn-global.configcat.com",
+    "r": 0
+  },
+  "f": {
+    "key1": {
+      "t": 1,
+      "v": { "s": "key1-value" },
+      "r": [
+        {
+          "c": [
+            {
+              "p": {
+                "f": "key1",
+                "c": 0,
+                "v": { "s": "key1-prereq" }
+              }
+            }
+          ],
+          "s": { "v": { "s": "key1-prereq" } }
+        }
+      ]
+    },
+    "key2": {
+      "t": 1,
+      "v": { "s": "key2-value" },
+      "r": [
+        {
+          "c": [
+            {
+              "p": {
+                "f": "key3",
+                "c": 0,
+                "v": { "s": "key3-prereq" }
+              }
+            }
+          ],
+          "s": { "v": { "s": "key2-prereq" } }
+        }
+      ]
+    },
+    "key3": {
+      "t": 1,
+      "v": { "s": "key3-value" },
+      "r": [
+        {
+          "c": [
+            {
+              "p": {
+                "f": "key2",
+                "c": 0,
+                "v": { "s": "key2-prereq" }
+              }
+            }
+          ],
+          "s": { "v": { "s": "key3-prereq" } }
+        }
+      ]
+    },
+    "key4": {
+      "t": 1,
+      "v": { "s": "key4-value" },
+      "r": [
+        {
+          "c": [
+            {
+              "p": {
+                "f": "key3",
+                "c": 0,
+                "v": { "s": "key3-prereq" }
+              }
+            }
+          ],
+          "s": { "v": { "s": "key4-prereq" } }
+        }
+      ]
+    }
+  }
+}
diff --git a/configcatclienttests/data/test_override_flagdependency_v6.json b/configcatclienttests/data/test_override_flagdependency_v6.json
new file mode 100644
index 0000000..62e159e
--- /dev/null
+++ b/configcatclienttests/data/test_override_flagdependency_v6.json
@@ -0,0 +1,44 @@
+{
+  "p": {
+    "u": "https://test-cdn-eu.configcat.com",
+    "r": 0,
+    "s": "TsTuRHo\u002BMHs8h8j16HQY83sooJsLg34Ir5KIVOletFU="
+  },
+  "f": {
+    "mainStringFlag": {
+      "t": 1,
+      "v": {
+        "s": "private"
+      },
+      "i": "24c96275"
+    },
+    "stringDependsOnInt": {
+      "t": 1,
+      "r": [
+        {
+          "c": [
+            {
+              "p": {
+                "f": "mainIntFlag",
+                "c": 0,
+                "v": {
+                  "i": 42
+                }
+              }
+            }
+          ],
+          "s": {
+            "v": {
+              "s": "Dog"
+            },
+            "i": "12531eec"
+          }
+        }
+      ],
+      "v": {
+        "s": "Cat"
+      },
+      "i": "e227d926"
+    }
+  }
+}
diff --git a/configcatclienttests/data/test_override_segments_v6.json b/configcatclienttests/data/test_override_segments_v6.json
new file mode 100644
index 0000000..47bf15c
--- /dev/null
+++ b/configcatclienttests/data/test_override_segments_v6.json
@@ -0,0 +1,66 @@
+{
+  "p": {
+    "u": "https://test-cdn-eu.configcat.com",
+    "r": 0,
+    "s": "80xCU/SlDz1lCiWFaxIBjyJeJecWjq46T4eu6GtozkM="
+  },
+  "s": [
+    {
+      "n": "Beta Users",
+      "r": [
+        {
+          "a": "Email",
+          "c": 16,
+          "l": [
+            "9189c42f6035bd1d2df5eda347a4f62926d27c80540a7aa6cc72cc75bc6757ff"
+          ]
+        }
+      ]
+    },
+    {
+      "n": "Developers",
+      "r": [
+        {
+          "a": "Email",
+          "c": 16,
+          "l": [
+            "a7cdf54e74b5527bd2617889ec47f6d29b825ccfc97ff00832886bcb735abded"
+          ]
+        }
+      ]
+    }
+  ],
+  "f": {
+    "developerAndBetaUserSegment": {
+      "t": 0,
+      "r": [
+        {
+          "c": [
+            {
+              "s": {
+                "s": 1,
+                "c": 0
+              }
+            },
+            {
+              "s": {
+                "s": 0,
+                "c": 1
+              }
+            }
+          ],
+          "s": {
+            "v": {
+              "b": true
+            },
+            "i": "ddc50638"
+          }
+        }
+      ],
+      "v": {
+        "b": false
+      },
+      "i": "6427f4b8"
+    }
+  }
+}
diff --git a/configcatclienttests/testmatrix.csv b/configcatclienttests/data/testmatrix.csv
similarity index 100%
rename from configcatclienttests/testmatrix.csv
rename to configcatclienttests/data/testmatrix.csv
diff --git a/configcatclienttests/data/testmatrix_and_or.csv b/configcatclienttests/data/testmatrix_and_or.csv
new file mode 100644
index 0000000..5a149f4
--- /dev/null
+++ b/configcatclienttests/data/testmatrix_and_or.csv
@@ -0,0 +1,15 @@
+Identifier;Email;Country;Custom1;mainFeature;dependentFeature;emailAnd;emailOr
+##null##;;;;public;Chicken;Cat;Cat
+;;;;public;Chicken;Cat;Cat
+jane@example.com;jane@example.com;##null##;##null##;public;Chicken;Cat;Jane
+john@example.com;john@example.com;##null##;##null##;public;Chicken;Cat;John
+a@example.com;a@example.com;USA;##null##;target;Cat;Cat;Cat
+mark@example.com;mark@example.com;USA;##null##;target;Dog;Cat;Mark
+nora@example.com;nora@example.com;USA;##null##;target;Falcon;Cat;Cat
+stern@msn.com;stern@msn.com;USA;##null##;target;Horse;Cat;Cat
+jane@sensitivecompany.com;jane@sensitivecompany.com;England;##null##;private;Chicken;Dog;Jane
+anna@sensitivecompany.com;anna@sensitivecompany.com;France;##null##;private;Chicken;Cat;Cat
+jane@sensitivecompany.com;jane@sensitivecompany.com;england;##null##;public;Chicken;Dog;Jane
+jane;jane;##null##;##null##;public;Chicken;Cat;Cat
+@sensitivecompany.com;@sensitivecompany.com;##null##;##null##;public;Chicken;Cat;Cat
+jane.sensitivecompany.com;jane.sensitivecompany.com;##null##;##null##;public;Chicken;Cat;Cat
diff --git a/configcatclienttests/data/testmatrix_comparators_v6.csv b/configcatclienttests/data/testmatrix_comparators_v6.csv
new file mode 100644
index 0000000..d53efb5
--- /dev/null
+++ b/configcatclienttests/data/testmatrix_comparators_v6.csv
@@ -0,0 +1,24 @@
+Identifier;Email;Country;Custom1;boolTrueIn202304;stringEqualsDogDefaultCat;stringEqualsCleartextDogDefaultCat;stringDoseNotEqualDogDefaultCat;stringNotEqualsCleartextDogDefaultCat;stringStartsWithDogDefaultCat;stringNotStartsWithDogDefaultCat;stringEndsWithDogDefaultCat;stringNotEndsWithDogDefaultCat;arrayContainsDogDefaultCat;arrayDoesNotContainDogDefaultCat;arrayContainsCaseCheckDogDefaultCat;arrayDoesNotContainCaseCheckDogDefaultCat;customPercentageAttribute;missingPercentageAttribute;countryPercentageAttribute;stringContainsAnyOfDogDefaultCat;stringNotContainsAnyOfDogDefaultCat;stringStartsWithAnyOfDogDefaultCat;stringStartsWithAnyOfCleartextDogDefaultCat;stringNotStartsWithAnyOfDogDefaultCat;stringNotStartsWithAnyOfCleartextDogDefaultCat;stringEndsWithAnyOfDogDefaultCat;stringEndsWithAnyOfCleartextDogDefaultCat;stringNotEndsWithAnyOfDogDefaultCat;stringNotEndsWithAnyOfCleartextDogDefaultCat;stringArrayContainsAnyOfDogDefaultCat;stringArrayContainsAnyOfCleartextDogDefaultCat;stringArrayNotContainsAnyOfDogDefaultCat;stringArrayNotContainsAnyOfCleartextDogDefaultCat
+##null##;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat
+;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat
+a@configcat.com;a@configcat.com;##null##;##null##;False;Dog;Dog;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Chicken;NotFound;Chicken;Cat;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat
+b@configcat.com;b@configcat.com;Hungary;0;False;Cat;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Falcon;Cat;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat
+c@configcat.com;c@configcat.com;United Kingdom;1680307199.9;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat
+anna@configcat.com;anna@configcat.com;Hungary;1681118000.56;True;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat
+bogjobber@verizon.net;bogjobber@verizon.net;##null##;1682899200.1;False;Cat;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Horse;Chicken;Chicken;Dog;Cat;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat
+cliffordj@aol.com;cliffordj@aol.com;Austria;1682999200;False;Cat;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Falcon;Chicken;Falcon;Dog;Cat;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat
+reader@configcat.com;reader@configcat.com;Bahamas;read,execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat
+writer@configcat.com;writer@configcat.com;Belgium;write, execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat
+reader@configcat.com;reader@configcat.com;Canada;execute, Read;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat
+writer@configcat.com;writer@configcat.com;China;Write;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat
+admin@configcat.com;admin@configcat.com;France;read, write,execute;False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat
+user@configcat.com;user@configcat.com;Greece;,execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat
+reader@configcat.com;reader@configcat.com;Bahamas;["read","execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat
+writer@configcat.com;writer@configcat.com;Belgium;["write", "execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat
+reader@configcat.com;reader@configcat.com;Canada;["execute", "Read"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat
+writer@configcat.com;writer@configcat.com;China;["Write"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog
+admin@configcat.com;admin@configcat.com;France;["read", "write","execute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat
+admin@configcat.com;admin@configcat.com;France;["Read", "Write",  "execute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat
+admin@configcat.com;admin@configcat.com;France;["Read",  "Write", "eXecute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog
+user@configcat.com;user@configcat.com;Greece;["","execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat
+user@configcat.com;user@configcat.com;Monaco;,null, ,,nil, None;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat
diff --git a/configcatclienttests/testmatrix_number.csv b/configcatclienttests/data/testmatrix_number.csv
similarity index 100%
rename from configcatclienttests/testmatrix_number.csv
rename to configcatclienttests/data/testmatrix_number.csv
diff --git a/configcatclienttests/data/testmatrix_prerequisite_flag.csv b/configcatclienttests/data/testmatrix_prerequisite_flag.csv
new file mode 100644
index 0000000..dcf68f4
--- /dev/null
+++ b/configcatclienttests/data/testmatrix_prerequisite_flag.csv
@@ -0,0 +1,5 @@
+Identifier;Email;Country;Custom1;mainBoolFlag;mainStringFlag;mainIntFlag;mainDoubleFlag;stringDependsOnBool;stringDependsOnString;stringDependsOnStringCaseCheck;stringDependsOnInt;stringDependsOnDouble;stringDependsOnDoubleIntValue;boolDependsOnBool;intDependsOnBool;doubleDependsOnBool;boolDependsOnBoolDependsOnBool;mainBoolFlagEmpty;stringDependsOnEmptyBool;stringInverseDependsOnEmptyBool;mainBoolFlagInverse;boolDependsOnBoolInverse
+##null##;;;;True;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;True;1;1.1;False;True;EmptyOn;EmptyOn;False;True
+;;;;True;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;True;1;1.1;False;True;EmptyOn;EmptyOn;False;True
+john@sensitivecompany.com;john@sensitivecompany.com;##null##;##null##;False;private;2;0.1;Cat;Dog;Cat;Dog;Dog;Cat;False;42;3.14;True;True;EmptyOn;EmptyOn;True;False
+jane@example.com;jane@example.com;##null##;##null##;True;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;True;1;1.1;False;True;EmptyOn;EmptyOn;False;True
diff --git a/configcatclienttests/data/testmatrix_segments.csv b/configcatclienttests/data/testmatrix_segments.csv
new file mode 100644
index 0000000..b59ba3a
--- /dev/null
+++ b/configcatclienttests/data/testmatrix_segments.csv
@@ -0,0 +1,6 @@
+Identifier;Email;Country;Custom1;developerAndBetaUserSegment;developerAndBetaUserCleartextSegment;notDeveloperAndNotBetaUserSegment;notDeveloperAndNotBetaUserCleartextSegment
+##null##;;;;False;False;False;False
+;;;;False;False;False;False
+john@example.com;john@example.com;##null##;##null##;False;False;False;False
+jane@example.com;jane@example.com;##null##;##null##;False;False;False;False
+kate@example.com;kate@example.com;##null##;##null##;True;True;True;True
diff --git a/configcatclienttests/data/testmatrix_segments_old.csv b/configcatclienttests/data/testmatrix_segments_old.csv
new file mode 100644
index 0000000..9fc605e
--- /dev/null
+++ b/configcatclienttests/data/testmatrix_segments_old.csv
@@ -0,0 +1,6 @@
+Identifier;Email;Country;Custom1;featureWithSegmentTargeting;featureWithSegmentTargetingCleartext;featureWithNegatedSegmentTargeting;featureWithNegatedSegmentTargetingCleartext;featureWithSegmentTargetingInverse;featureWithSegmentTargetingInverseCleartext;featureWithNegatedSegmentTargetingInverse;featureWithNegatedSegmentTargetingInverseCleartext
+##null##;;;;False;False;False;False;False;False;False;False
+;;;;False;False;False;False;False;False;False;False
+john@example.com;john@example.com;##null##;##null##;True;True;False;False;False;False;True;True
+jane@example.com;jane@example.com;##null##;##null##;True;True;False;False;False;False;True;True
+kate@example.com;kate@example.com;##null##;##null##;False;False;True;True;True;True;False;False
diff --git a/configcatclienttests/testmatrix_semantic.csv b/configcatclienttests/data/testmatrix_semantic.csv
similarity index 100%
rename from configcatclienttests/testmatrix_semantic.csv
rename to configcatclienttests/data/testmatrix_semantic.csv
diff --git a/configcatclienttests/testmatrix_semantic_2.csv b/configcatclienttests/data/testmatrix_semantic_2.csv
similarity index 100%
rename from configcatclienttests/testmatrix_semantic_2.csv
rename to configcatclienttests/data/testmatrix_semantic_2.csv
diff --git a/configcatclienttests/testmatrix_sensitive.csv b/configcatclienttests/data/testmatrix_sensitive.csv
similarity index 100%
rename from configcatclienttests/testmatrix_sensitive.csv
rename to configcatclienttests/data/testmatrix_sensitive.csv
diff --git a/configcatclienttests/data/testmatrix_unicode.csv b/configcatclienttests/data/testmatrix_unicode.csv
new file mode 100644
index 0000000..e5b01de
--- /dev/null
+++ b/configcatclienttests/data/testmatrix_unicode.csv
@@ -0,0 +1,14 @@
+Identifier;Email;Country;🆃🅴🆇🆃;boolTextEqualsHashed;boolTextEqualsCleartext;boolTextNotEqualsHashed;boolTextNotEqualsCleartext;boolIsOneOfHashed;boolIsOneOfCleartext;boolIsNotOneOfHashed;boolIsNotOneOfCleartext;boolStartsWithHashed;boolStartsWithCleartext;boolNotStartsWithHashed;boolNotStartsWithCleartext;boolEndsWithHashed;boolEndsWithCleartext;boolNotEndsWithHashed;boolNotEndsWithCleartext;boolContainsCleartext;boolNotContainsCleartext;boolArrayContainsHashed;boolArrayContainsCleartext;boolArrayNotContainsHashed;boolArrayNotContainsCleartext
+1;;;ʄǟռƈʏ ȶɛӼȶ;True;True;False;False;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False
+1;;;ʄaռƈʏ ȶɛӼȶ;False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False
+1;;;ÁRVÍZTŰRŐ tükörfúrógép;False;False;True;True;True;True;False;False;True;True;False;False;True;True;False;False;True;False;False;False;False;False
+1;;;árvíztűrő tükörfúrógép;False;False;True;True;False;False;True;True;False;False;True;True;True;True;False;False;True;False;False;False;False;False
+1;;;ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP;False;False;True;True;False;False;True;True;True;True;False;False;False;False;True;True;True;False;False;False;False;False
+1;;;árvíztűrő TÜKÖRFÚRÓGÉP;False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False
+1;;;u𝖓𝖎𝖈𝖔𝖉e;False;False;True;True;True;True;False;False;True;True;False;False;True;True;False;False;True;False;False;False;False;False
+;;;𝖚𝖓𝖎𝖈𝖔𝖉e;False;False;True;True;False;False;True;True;False;False;True;True;True;True;False;False;True;False;False;False;False;False
+;;;u𝖓𝖎𝖈𝖔𝖉𝖊;False;False;True;True;False;False;True;True;True;True;False;False;False;False;True;True;True;False;False;False;False;False
+;;;𝖚𝖓𝖎𝖈𝖔𝖉𝖊;False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False
+1;;;["ÁRVÍZTŰRŐ tükörfúrógép", "unicode"];False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;True;False;True;True;False;False
+1;;;["ÁRVÍZTŰRŐ", "tükörfúrógép", "u𝖓𝖎𝖈𝖔𝖉e"];False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;True;False;True;True;False;False
+1;;;["ÁRVÍZTŰRŐ", "tükörfúrógép", "unicode"];False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;True;False;False;False;True;True
diff --git a/configcatclienttests/testmatrix_variationId.csv b/configcatclienttests/data/testmatrix_variationId.csv
similarity index 100%
rename from configcatclienttests/testmatrix_variationId.csv
rename to configcatclienttests/data/testmatrix_variationId.csv
diff --git a/configcatclienttests/mocks.py b/configcatclienttests/mocks.py
index 78ad6c5..d3879a8 100644
--- a/configcatclienttests/mocks.py
+++ b/configcatclienttests/mocks.py
@@ -1,6 +1,8 @@
 import json
 import time
+import logging
 
+from configcatclient.config import SettingType
 from configcatclient.configentry import ConfigEntry
 from configcatclient.utils import get_utc_now_seconds_since_epoch, distant_past
 
@@ -13,53 +15,54 @@
 from configcatclient.configfetcher import FetchResponse, ConfigFetcher
 from configcatclient.interfaces import ConfigCache
 
-TEST_JSON = '{' \
-            '   "p": {' \
-            '       "u": "https://cdn-global.configcat.com",' \
-            '       "r": 0' \
-            '   },' \
-            '   "f": {' \
-            '       "testKey": { "v": "testValue", "t": 1, "p": [], "r": [] }' \
-            '   }' \
-            '}'
-
-TEST_JSON_FORMAT = '{{ "f": {{ "testKey": {{ "v": {value}, "p": [], "r": [] }} }} }}'
-
-TEST_JSON2 = '{' \
-             '  "p": {' \
-             '       "u": "https://cdn-global.configcat.com",' \
-             '       "r": 0' \
-             '  },' \
-             '  "f": {' \
-             '      "testKey": { "v": "testValue", "t": 1, "p": [], "r": [] }, ' \
-             '      "testKey2": { "v": "testValue2", "t": 1, "p": [], "r": [] }' \
-             '  }' \
-             '}'
-
-TEST_OBJECT = json.loads(
-    '{'
-    '"p": {'
-    '"u": "https://cdn-global.configcat.com",'
-    '"r": 0'
-    "},"
-    '"f": {'
-    '"testBoolKey": '
-    '{"v": true, "t": 0, "p": [], "r": []},'
-    '"testStringKey": '
-    '{"v": "testValue", "i": "id", "t": 1, "p": [], "r": ['
-    '   {"i":"id1","v":"fake1","a":"Identifier","t":2,"c":"@test1.com"},'
-    '   {"i":"id2","v":"fake2","a":"Identifier","t":2,"c":"@test2.com"}'
-    ']},'
-    '"testIntKey": '
-    '{"v": 1, "t": 2, "p": [], "r": []},'
-    '"testDoubleKey": '
-    '{"v": 1.1, "t": 3,"p": [], "r": []},'
-    '"key1": '
-    '{"v": true, "i": "fakeId1","p": [], "r": []},'
-    '"key2": '
-    '{"v": false, "i": "fakeId2","p": [], "r": []}'
-    '}'
-    '}')
+TEST_SDK_KEY = 'configcat-sdk-test-key/0000000000000000000000'
+TEST_SDK_KEY1 = 'configcat-sdk-test-key/0000000000000000000001'
+TEST_SDK_KEY2 = 'configcat-sdk-test-key/0000000000000000000002'
+
+TEST_JSON = r'''{
+   "p": {
+       "u": "https://cdn-global.configcat.com",
+       "r": 0
+   },
+   "f": {
+       "testKey": { "v": { "s": "testValue" }, "t": 1 }
+   }
+}'''
+
+TEST_JSON_FORMAT = '{{ "f": {{ "testKey": {{ "t": {value_type}, "v": {value}, "p": [], "r": [] }} }} }}'
+
+TEST_JSON2 = r'''{
+  "p": {
+       "u": "https://cdn-global.configcat.com",
+       "r": 0
+  },
+  "f": {
+      "testKey": { "v": { "s": "testValue" }, "t": 1 },
+      "testKey2": { "v": { "s": "testValue2" }, "t": 1 }
+  }
+}'''
+
+TEST_OBJECT = json.loads(r'''{
+  "p": {
+    "u": "https://cdn-global.configcat.com",
+    "r": 0
+  },
+  "s": [
+    {"n": "id1", "r": [{"a": "Identifier", "c": 2, "l": ["@test1.com"]}]},
+    {"n": "id2", "r": [{"a": "Identifier", "c": 2, "l": ["@test2.com"]}]}
+  ],
+  "f": {
+    "testBoolKey": {"v": {"b": true}, "t": 0},
+    "testStringKey": {"v": {"s": "testValue"}, "i": "id", "t": 1, "r": [
+      {"c": [{"s": {"s": 0, "c": 0}}], "s": {"v": {"s": "fake1"}, "i": "id1"}},
+      {"c": [{"s": {"s": 1, "c": 0}}], "s": {"v": {"s": "fake2"}, "i": "id2"}}
+    ]},
+    "testIntKey": {"v": {"i": 1}, "t": 2},
+    "testDoubleKey": {"v": {"d": 1.1}, "t": 3},
+    "key1": {"v": {"b": true}, "t": 0, "i": "fakeId1"},
+    "key2": {"v": {"b": false}, "t": 0, "i": "fakeId2"}
+  }
+}''')
 
 
 class ConfigFetcherMock(ConfigFetcher):
@@ -115,7 +118,8 @@ def __init__(self):
 
     def get_configuration(self, etag=''):
         self._value += 1
-        config_json_string = TEST_JSON_FORMAT.format(value=self._value)
+        value_string = '{ "i": %s }' % self._value
+        config_json_string = TEST_JSON_FORMAT.format(value_type=SettingType.INT, value=value_string)
         config = json.loads(config_json_string)
         return FetchResponse.success(ConfigEntry(config, etag, config_json_string))
 
@@ -145,7 +149,7 @@ def __init__(self, etag):
         self.etag = etag
 
     def get(self, name):
-        if name == 'Etag':
+        if name == 'ETag':
             return self.etag
         return None
 
@@ -197,3 +201,24 @@ def on_error(self, error):
     def callback_exception(self, *args, **kwargs):
         self.callback_exception_call_count += 1
         raise Exception("error")
+
+
+class MockLogHandler(logging.Handler):
+    def __init__(self, *args, **kwargs):
+        super(MockLogHandler, self).__init__(*args, **kwargs)
+        self.error_logs = []
+        self.warning_logs = []
+        self.info_logs = []
+
+    def clear(self):
+        self.error_logs = []
+        self.warning_logs = []
+        self.info_logs = []
+
+    def emit(self, record):
+        if record.levelno == logging.ERROR:
+            self.error_logs.append(record.getMessage())
+        elif record.levelno == logging.WARNING:
+            self.warning_logs.append(record.getMessage())
+        elif record.levelno == logging.INFO:
+            self.info_logs.append(record.getMessage())
diff --git a/configcatclienttests/test.json b/configcatclienttests/test.json
deleted file mode 100644
index d547507..0000000
--- a/configcatclienttests/test.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
-  "f": {
-    "disabledFeature": {
-      "v": false
-    },
-    "enabledFeature": {
-      "v": true
-    },
-    "intSetting": {
-      "v": 5
-    },
-    "doubleSetting": {
-      "v": 3.14
-    },
-    "stringSetting": {
-      "v": "test"
-    }
-  }
-}
\ No newline at end of file
diff --git a/configcatclienttests/test_autopollingcachepolicy.py b/configcatclienttests/test_autopollingcachepolicy.py
index 439d73b..9c8df19 100644
--- a/configcatclienttests/test_autopollingcachepolicy.py
+++ b/configcatclienttests/test_autopollingcachepolicy.py
@@ -10,7 +10,7 @@
 from configcatclient.configentry import ConfigEntry
 from configcatclient.configfetcher import ConfigFetcher
 from configcatclient.configservice import ConfigService
-from configcatclient.constants import VALUE
+from configcatclient.config import VALUE, FEATURE_FLAGS, STRING_VALUE, INT_VALUE
 from configcatclient.logger import Logger
 from configcatclient.utils import get_utc_now_seconds_since_epoch
 from configcatclienttests.mocks import ConfigFetcherMock, ConfigFetcherWithErrorMock, ConfigFetcherWaitMock, \
@@ -39,8 +39,9 @@ def test_wrong_params(self):
                                                                max_init_wait_time_seconds=-1),
                                      Hooks(), config_fetcher, log, config_cache, False)
         time.sleep(2)
-        settings, _ = cache_policy.get_settings()
-        self.assertEqual('testValue', settings.get('testKey').get(VALUE))
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
+        self.assertEqual('testValue', settings.get('testKey').get(VALUE).get(STRING_VALUE))
         cache_policy.close()
 
     def test_init_wait_time_ok(self):
@@ -49,8 +50,9 @@ def test_init_wait_time_ok(self):
         cache_policy = ConfigService('', PollingMode.auto_poll(poll_interval_seconds=60,
                                                                max_init_wait_time_seconds=5),
                                      Hooks(), config_fetcher, log, config_cache, False)
-        settings, _ = cache_policy.get_settings()
-        self.assertEqual('testValue', settings.get('testKey').get(VALUE))
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
+        self.assertEqual('testValue', settings.get('testKey').get(VALUE).get(STRING_VALUE))
         cache_policy.close()
 
     def test_init_wait_time_timeout(self):
@@ -60,10 +62,10 @@ def test_init_wait_time_timeout(self):
         cache_policy = ConfigService('', PollingMode.auto_poll(poll_interval_seconds=60,
                                                                max_init_wait_time_seconds=1),
                                      Hooks(), config_fetcher, log, config_cache, False)
-        settings, _ = cache_policy.get_settings()
+        config, _ = cache_policy.get_config()
         end_time = time.time()
         elapsed_time = end_time - start_time
-        self.assertEqual(settings, None)
+        self.assertEqual(config, None)
         self.assertTrue(elapsed_time > 1)
         self.assertTrue(elapsed_time < 2)
         cache_policy.close()
@@ -76,8 +78,9 @@ def test_fetch_call_count(self):
                                      Hooks(), config_fetcher, log, config_cache, False)
         time.sleep(3)
         self.assertEqual(config_fetcher.get_call_count, 2)
-        settings, _ = cache_policy.get_settings()
-        self.assertEqual('testValue', settings.get('testKey').get(VALUE))
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
+        self.assertEqual('testValue', settings.get('testKey').get(VALUE).get(STRING_VALUE))
         cache_policy.close()
 
     def test_updated_values(self):
@@ -86,11 +89,13 @@ def test_updated_values(self):
         cache_policy = ConfigService('', PollingMode.auto_poll(poll_interval_seconds=2,
                                                                max_init_wait_time_seconds=5),
                                      Hooks(), config_fetcher, log, config_cache, False)
-        settings, _ = cache_policy.get_settings()
-        self.assertEqual(1, settings.get('testKey').get(VALUE))
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
+        self.assertEqual(1, settings.get('testKey').get(VALUE).get(INT_VALUE))
         time.sleep(2.200)
-        settings, _ = cache_policy.get_settings()
-        self.assertEqual(2, settings.get('testKey').get(VALUE))
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
+        self.assertEqual(2, settings.get('testKey').get(VALUE).get(INT_VALUE))
         cache_policy.close()
 
     def test_error(self):
@@ -101,8 +106,8 @@ def test_error(self):
                                      Hooks(), config_fetcher, log, config_cache, False)
 
         # Get value from Config Store, which indicates a config_fetcher call
-        settings, _ = cache_policy.get_settings()
-        self.assertEqual(settings, None)
+        config, _ = cache_policy.get_config()
+        self.assertEqual(config, None)
         cache_policy.close()
 
     def test_close(self):
@@ -112,11 +117,13 @@ def test_close(self):
                                                                max_init_wait_time_seconds=5),
                                      Hooks(), config_fetcher, log, config_cache, False)
         cache_policy.close()
-        settings, _ = cache_policy.get_settings()
-        self.assertEqual(1, settings.get('testKey').get(VALUE))
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
+        self.assertEqual(1, settings.get('testKey').get(VALUE).get(INT_VALUE))
         time.sleep(2.200)
-        settings, _ = cache_policy.get_settings()
-        self.assertEqual(1, settings.get('testKey').get(VALUE))
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
+        self.assertEqual(1, settings.get('testKey').get(VALUE).get(INT_VALUE))
         cache_policy.close()
 
     def test_rerun(self):
@@ -186,8 +193,9 @@ def test_with_failed_refresh(self):
             cache_policy = ConfigService('', polling_mode, Hooks(), config_fetcher, log, NullConfigCache(), False)
 
             # first call
-            settings, _ = cache_policy.get_settings()
-            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE))
+            config, _ = cache_policy.get_config()
+            settings = config.get(FEATURE_FLAGS)
+            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE).get(STRING_VALUE))
 
             response_mock.json.return_value = {}
             response_mock.status_code = 500
@@ -197,8 +205,9 @@ def test_with_failed_refresh(self):
             time.sleep(1.5)
 
             # previous value returned because of the refresh failure
-            settings, _ = cache_policy.get_settings()
-            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE))
+            config, _ = cache_policy.get_config()
+            settings = config.get(FEATURE_FLAGS)
+            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE).get(STRING_VALUE))
 
             cache_policy.close()
 
@@ -218,21 +227,23 @@ def test_return_cached_config_when_cache_is_not_expired(self):
                                                                max_init_wait_time_seconds),
                                      Hooks(), config_fetcher, log, config_cache, False)
 
-        settings, _ = cache_policy.get_settings()
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
         elapsed_time = time.time() - start_time
 
         # max init wait time should be ignored when cache is not expired
         self.assertLessEqual(elapsed_time, max_init_wait_time_seconds)
 
-        self.assertEqual('testValue', settings.get('testKey').get(VALUE))
+        self.assertEqual('testValue', settings.get('testKey').get(VALUE).get(STRING_VALUE))
         self.assertEqual(config_fetcher.get_call_count, 0)
         self.assertEqual(config_fetcher.get_fetch_count, 0)
 
         time.sleep(3)
 
-        settings, _ = cache_policy.get_settings()
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
 
-        self.assertEqual('testValue', settings.get('testKey').get(VALUE))
+        self.assertEqual('testValue', settings.get('testKey').get(VALUE).get(STRING_VALUE))
         self.assertEqual(config_fetcher.get_call_count, 1)
         self.assertEqual(config_fetcher.get_fetch_count, 1)
         cache_policy.close()
@@ -251,9 +262,10 @@ def test_fetch_config_when_cache_is_expired(self):
                                                                max_init_wait_time_seconds),
                                      Hooks(), config_fetcher, log, config_cache, False)
 
-        settings, _ = cache_policy.get_settings()
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
 
-        self.assertEqual('testValue', settings.get('testKey').get(VALUE))
+        self.assertEqual('testValue', settings.get('testKey').get(VALUE).get(STRING_VALUE))
         self.assertEqual(config_fetcher.get_call_count, 1)
         self.assertEqual(config_fetcher.get_fetch_count, 1)
         cache_policy.close()
@@ -274,13 +286,14 @@ def test_init_wait_time_return_cached(self):
                                                                max_init_wait_time_seconds),
                                      Hooks(), config_fetcher, log, config_cache, False)
 
-        settings, _ = cache_policy.get_settings()
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
         elapsed_time = time.time() - start_time
 
         self.assertGreater(elapsed_time, max_init_wait_time_seconds)
         self.assertLess(elapsed_time, max_init_wait_time_seconds + 1)
-        self.assertEqual('testValue', settings.get('testKey').get(VALUE))
-        self.assertEqual('testValue2', settings.get('testKey2').get(VALUE))
+        self.assertEqual('testValue', settings.get('testKey').get(VALUE).get(STRING_VALUE))
+        self.assertEqual('testValue2', settings.get('testKey2').get(VALUE).get(STRING_VALUE))
         cache_policy.close()
 
     def test_online_offline(self):
@@ -289,7 +302,7 @@ def test_online_offline(self):
             request_get.return_value = response_mock
             response_mock.json.return_value = TEST_OBJECT
             response_mock.status_code = 200
-            response_mock.headers = {'Etag': 'test-etag'}
+            response_mock.headers = {'ETag': 'test-etag'}
 
             polling_mode = PollingMode.auto_poll(poll_interval_seconds=1)
             config_fetcher = ConfigFetcher('', log, polling_mode.identifier())
@@ -302,8 +315,9 @@ def test_online_offline(self):
 
             cache_policy.set_offline()
             self.assertTrue(cache_policy.is_offline())
-            settings, _ = cache_policy.get_settings()
-            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE))
+            config, _ = cache_policy.get_config()
+            settings = config.get(FEATURE_FLAGS)
+            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE).get(STRING_VALUE))
             self.assertEqual(2, request_get.call_count)
 
             time.sleep(2)
@@ -323,7 +337,7 @@ def test_init_offline(self):
             request_get.return_value = response_mock
             response_mock.json.return_value = TEST_OBJECT
             response_mock.status_code = 200
-            response_mock.headers = {'Etag': 'test-etag'}
+            response_mock.headers = {'ETag': 'test-etag'}
 
             polling_mode = PollingMode.auto_poll(poll_interval_seconds=1)
             config_fetcher = ConfigFetcher('', log, polling_mode.identifier())
@@ -332,14 +346,14 @@ def test_init_offline(self):
                                          Hooks(), config_fetcher, log, NullConfigCache(), True)
 
             self.assertTrue(cache_policy.is_offline())
-            settings, _ = cache_policy.get_settings()
-            self.assertIsNone(settings)
+            config, _ = cache_policy.get_config()
+            self.assertIsNone(config)
             self.assertEqual(0, request_get.call_count)
 
             time.sleep(2)
 
-            settings, _ = cache_policy.get_settings()
-            self.assertIsNone(settings)
+            config, _ = cache_policy.get_config()
+            self.assertIsNone(config)
             self.assertEqual(0, request_get.call_count)
 
             cache_policy.set_online()
@@ -347,8 +361,9 @@ def test_init_offline(self):
 
             time.sleep(2.5)
 
-            settings, _ = cache_policy.get_settings()
-            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE))
+            config, _ = cache_policy.get_config()
+            settings = config.get(FEATURE_FLAGS)
+            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE).get(STRING_VALUE))
             self.assertGreaterEqual(request_get.call_count, 2)
             cache_policy.close()
 
diff --git a/configcatclienttests/test_concurrency.py b/configcatclienttests/test_concurrency.py
index 19fe9f3..cdfc3d9 100644
--- a/configcatclienttests/test_concurrency.py
+++ b/configcatclienttests/test_concurrency.py
@@ -1,8 +1,11 @@
 import logging
+import sys
 import unittest
 import multiprocessing
 from time import sleep
 
+import pytest
+
 import configcatclient
 from configcatclient.user import User
 
@@ -16,6 +19,7 @@ def _manual_force_refresh(client, repeat=10, delay=0.1):
 
 
 class ConcurrencyTests(unittest.TestCase):
+    @pytest.mark.skipif(sys.platform == 'win32' or sys.platform == 'darwin', reason="TypeError: can't pickle _thread.lock objects")
     def test_concurrency_process(self):
         client = configcatclient.get('PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A')
         value = client.get_value('keySampleText', False, User('key'))
diff --git a/configcatclienttests/test_configcache.py b/configcatclienttests/test_configcache.py
index 716c1af..822fd52 100644
--- a/configcatclienttests/test_configcache.py
+++ b/configcatclienttests/test_configcache.py
@@ -3,12 +3,13 @@
 import unittest
 
 from configcatclient import ConfigCatClient, ConfigCatOptions, PollingMode
+from configcatclient.config import SettingType
 from configcatclient.configcache import InMemoryConfigCache
 from configcatclient.configcatoptions import Hooks
 from configcatclient.configentry import ConfigEntry
 from configcatclient.configservice import ConfigService
 from configcatclient.utils import get_utc_now_seconds_since_epoch
-from configcatclienttests.mocks import TEST_JSON, SingleValueConfigCache, HookCallbacks, TEST_JSON_FORMAT
+from configcatclienttests.mocks import TEST_JSON, SingleValueConfigCache, HookCallbacks, TEST_JSON_FORMAT, TEST_SDK_KEY
 
 logging.basicConfig()
 
@@ -29,8 +30,8 @@ def test_cache(self):
         self.assertEqual(value2, None)
 
     def test_cache_key(self):
-        self.assertEqual("147c5b4c2b2d7c77e1605b1a4309f0ea6684a0c6", ConfigService._get_cache_key('test1'))
-        self.assertEqual("c09513b1756de9e4bc48815ec7a142b2441ed4d5", ConfigService._get_cache_key('test2'))
+        self.assertEqual("7f845c43ecc95e202b91e271435935e6d1391e5d", ConfigService._get_cache_key('test1'))
+        self.assertEqual("a78b7e323ef543a272c74540387566a22415148a", ConfigService._get_cache_key('test2'))
 
     def test_cache_payload(self):
         now_seconds = 1686756435.8449
@@ -41,7 +42,7 @@ def test_cache_payload(self):
     def test_invalid_cache_content(self):
         hook_callbacks = HookCallbacks()
         hooks = Hooks(on_error=hook_callbacks.on_error)
-        config_json_string = TEST_JSON_FORMAT.format(value='"test"')
+        config_json_string = TEST_JSON_FORMAT.format(value_type=SettingType.STRING, value='{"s": "test"}')
         config_cache = SingleValueConfigCache(ConfigEntry(
             config=json.loads(config_json_string),
             etag='test-etag',
@@ -49,9 +50,9 @@ def test_invalid_cache_content(self):
             fetch_time=get_utc_now_seconds_since_epoch()).serialize()
         )
 
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=config_cache,
-                                                              hooks=hooks))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=config_cache,
+                                                                    hooks=hooks))
 
         self.assertEqual('test', client.get_value('testKey', 'default'))
         self.assertEqual(0, hook_callbacks.error_call_count)
@@ -59,14 +60,14 @@ def test_invalid_cache_content(self):
         # Invalid fetch time in cache
         config_cache._value = '\n'.join(['text',
                                          'test-etag',
-                                         TEST_JSON_FORMAT.format(value='"test2"')])
+                                         TEST_JSON_FORMAT.format(value_type=SettingType.STRING, value='{"s": "test2"}')])
 
         self.assertEqual('test', client.get_value('testKey', 'default'))
         self.assertTrue('Error occurred while reading the cache.\nInvalid fetch time: text' in hook_callbacks.error)
 
         # Number of values is fewer than expected
         config_cache._value = '\n'.join([str(get_utc_now_seconds_since_epoch()),
-                                         TEST_JSON_FORMAT.format(value='"test2"')])
+                                         TEST_JSON_FORMAT.format(value_type=SettingType.STRING, value='{"s": "test2"}')])
 
         self.assertEqual('test', client.get_value('testKey', 'default'))
         self.assertTrue('Error occurred while reading the cache.\nNumber of values is fewer than expected.'
diff --git a/configcatclienttests/test_configcatclient.py b/configcatclienttests/test_configcatclient.py
index 11f4a56..75fe747 100644
--- a/configcatclienttests/test_configcatclient.py
+++ b/configcatclienttests/test_configcatclient.py
@@ -1,16 +1,22 @@
 import datetime
+import json
 import logging
 import unittest
+
+import pytest
 import requests
+from parameterized import parameterized
 
 from configcatclient import ConfigCatClientException
 from configcatclient.configcatclient import ConfigCatClient
-from configcatclient.constants import VALUE, COMPARATOR, COMPARISON_ATTRIBUTE, COMPARISON_VALUE
+from configcatclient.configentry import ConfigEntry
+from configcatclient.config import VALUE, SERVED_VALUE, STRING_VALUE
 from configcatclient.user import User
-from configcatclient.configcatoptions import ConfigCatOptions
+from configcatclient.configcatoptions import ConfigCatOptions, Hooks
 from configcatclient.pollingmode import PollingMode
-from configcatclient.utils import get_utc_now
-from configcatclienttests.mocks import ConfigCacheMock, TEST_OBJECT
+from configcatclient.utils import get_utc_now, get_utc_now_seconds_since_epoch
+from configcatclienttests.mocks import ConfigCacheMock, TEST_OBJECT, TEST_SDK_KEY, HookCallbacks, SingleValueConfigCache, \
+    MockLogHandler
 
 # Python2/Python3 support
 try:
@@ -23,18 +29,19 @@
     from mock import Mock, ANY
 
 logging.basicConfig(level=logging.INFO)
+logging.getLogger('configcat').setLevel(logging.INFO)
 
 
 class ConfigCatClientTests(unittest.TestCase):
     def test_ensure_singleton_per_sdk_key(self):
-        client1 = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
-        client2 = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
+        client1 = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
+        client2 = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
 
         self.assertEqual(client1, client2)
 
         ConfigCatClient.close_all()
 
-        client1 = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
+        client1 = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
 
         self.assertNotEqual(client1, client2)
 
@@ -47,53 +54,105 @@ def test_without_sdk_key(self):
         except ConfigCatClientException:
             pass
 
+    def test_invalid_sdk_key(self):
+        with pytest.raises(ConfigCatClientException):
+            ConfigCatClient.get('key')
+
+        with pytest.raises(ConfigCatClientException):
+            ConfigCatClient.get('configcat-proxy/key')
+
+        with pytest.raises(ConfigCatClientException):
+            ConfigCatClient.get('1234567890abcdefghijkl01234567890abcdefghijkl')
+
+        with pytest.raises(ConfigCatClientException):
+            ConfigCatClient.get('configcat-sdk-2/1234567890abcdefghijkl/1234567890abcdefghijkl')
+
+        with pytest.raises(ConfigCatClientException):
+            ConfigCatClient.get('configcat/1234567890abcdefghijkl/1234567890abcdefghijkl')
+
+        ConfigCatClient.get('1234567890abcdefghijkl/1234567890abcdefghijkl')
+        ConfigCatClient.get('configcat-sdk-1/1234567890abcdefghijkl/1234567890abcdefghijkl')
+        ConfigCatClient.get('configcat-proxy/key', options=ConfigCatOptions(base_url='base_url'))
+
     def test_bool(self):
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=ConfigCacheMock()))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=ConfigCacheMock()))
         self.assertEqual(True, client.get_value('testBoolKey', False))
         client.close()
 
     def test_string(self):
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=ConfigCacheMock()))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=ConfigCacheMock()))
         self.assertEqual('testValue', client.get_value('testStringKey', 'default'))
         client.close()
 
     def test_int(self):
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=ConfigCacheMock()))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=ConfigCacheMock()))
         self.assertEqual(1, client.get_value('testIntKey', 0))
         client.close()
 
     def test_double(self):
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=ConfigCacheMock()))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=ConfigCacheMock()))
         self.assertEqual(1.1, client.get_value('testDoubleKey', 0.0))
         client.close()
 
     def test_unknown(self):
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=ConfigCacheMock()))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=ConfigCacheMock()))
         self.assertEqual('default', client.get_value('testUnknownKey', 'default'))
         client.close()
 
     def test_invalidation(self):
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=ConfigCacheMock()))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=ConfigCacheMock()))
         self.assertEqual(True, client.get_value('testBoolKey', False))
         client.close()
 
+    def test_incorrect_json(self):
+        config_json_string = r'''{
+           "f": {
+             "testKey":  {
+               "t": 0,
+               "r": [ {
+                 "c": [ { "u": { "a": "Custom1", "c": 19, "d": "wrong_utc_timestamp" } } ],
+                 "s": { "v": { "b": true } }
+               } ],
+               "v": { "b": false }
+             }
+           }
+        }'''
+        config_cache = SingleValueConfigCache(ConfigEntry(
+            config=json.loads(config_json_string),
+            etag='test-etag',
+            config_json_string=config_json_string,
+            fetch_time=get_utc_now_seconds_since_epoch()).serialize()
+        )
+
+        hook_callbacks = HookCallbacks()
+        hooks = Hooks(
+            on_error=hook_callbacks.on_error
+        )
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=config_cache,
+                                                                    hooks=hooks))
+        self.assertEqual(False, client.get_value('testKey', False, User('1234', custom={'Custom1': 1681118000.56})))
+        self.assertEqual(1, hook_callbacks.error_call_count)
+        self.assertTrue(hook_callbacks.error.startswith("Failed to evaluate setting 'testKey'."))
+        client.close()
+
     def test_get_all_keys(self):
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=ConfigCacheMock()))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=ConfigCacheMock()))
         # Two list should have exactly the same elements, order doesn't matter.
         self.assertEqual({'testBoolKey', 'testStringKey', 'testIntKey', 'testDoubleKey', 'key1', 'key2'},
                          set(client.get_all_keys()))
         client.close()
 
     def test_get_all_values(self):
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=ConfigCacheMock()))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=ConfigCacheMock()))
         all_values = client.get_all_values()
         # Two dictionary should have exactly the same elements, order doesn't matter.
         self.assertEqual(6, len(all_values))
@@ -106,8 +165,8 @@ def test_get_all_values(self):
         client.close()
 
     def test_get_all_value_details(self):
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=ConfigCacheMock()))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=ConfigCacheMock()))
         all_details = client.get_all_value_details()
 
         def details_by_key(all_details, key):
@@ -154,7 +213,7 @@ def test_get_value_details(self):
             response_mock.status_code = 200
             response_mock.headers = {}
 
-            client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
+            client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
             client.force_refresh()
 
             user = User('test@test1.com')
@@ -165,11 +224,8 @@ def test_get_value_details(self):
             self.assertEqual('id1', details.variation_id)
             self.assertFalse(details.is_default_value)
             self.assertIsNone(details.error)
-            self.assertIsNone(details.matched_evaluation_percentage_rule)
-            self.assertEqual('fake1', details.matched_evaluation_rule[VALUE])
-            self.assertEqual(2, details.matched_evaluation_rule[COMPARATOR])
-            self.assertEqual('Identifier', details.matched_evaluation_rule[COMPARISON_ATTRIBUTE])
-            self.assertEqual('@test1.com', details.matched_evaluation_rule[COMPARISON_VALUE])
+            self.assertIsNone(details.matched_percentage_option)
+            self.assertEqual('fake1', details.matched_targeting_rule[SERVED_VALUE][VALUE][STRING_VALUE])
             self.assertEqual(str(user), str(details.user))
             now = get_utc_now()
             self.assertGreaterEqual(now, details.fetch_time)
@@ -178,8 +234,8 @@ def test_get_value_details(self):
             client.close()
 
     def test_default_user_get_value(self):
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=ConfigCacheMock()))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=ConfigCacheMock()))
         user1 = User("test@test1.com")
         user2 = User("test@test2.com")
 
@@ -193,8 +249,8 @@ def test_default_user_get_value(self):
         client.close()
 
     def test_default_user_get_all_values(self):
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=ConfigCacheMock()))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=ConfigCacheMock()))
         user1 = User("test@test1.com")
         user2 = User("test@test2.com")
 
@@ -239,7 +295,7 @@ def test_online_offline(self):
             response_mock.status_code = 200
             response_mock.headers = {}
 
-            client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
+            client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
 
             self.assertFalse(client.is_offline())
 
@@ -271,8 +327,8 @@ def test_init_offline(self):
             response_mock.status_code = 200
             response_mock.headers = {}
 
-            client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                                  offline=True))
+            client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                        offline=True))
 
             self.assertTrue(client.is_offline())
 
@@ -289,6 +345,52 @@ def test_init_offline(self):
 
             client.close()
 
+    @parameterized.expand([
+        # no type mismatch warning
+        ('testStringKey', 'test@example.com', 'default', 'testValue', False),
+        ('testBoolKey', None, False, True, False),
+        ('testBoolKey', None, None, True, False),
+        ('testIntKey', None, 3.14, 1, False),
+        ('testIntKey', None, 42, 1, False),
+        ('testDoubleKey', None, 3.14, 1.1, False),
+        ('testDoubleKey', None, 42, 1.1, False),
+        # type mismatch warning
+        ('testStringKey', 'test@example.com', 0, 'testValue', True),
+        ('testStringKey', 'test@example.com', False, 'testValue', True),
+        ('testBoolKey', None, 0, True, True),
+        ('testBoolKey', None, 0.1, True, True),
+        ('testBoolKey', None, 'default', True, True),
+    ])
+    def test_default_value_and_setting_type_mismatch(self, key, user_id, default_value, expected_value, is_warning):
+        with mock.patch.object(requests, 'get') as request_get:
+            response_mock = Mock()
+            request_get.return_value = response_mock
+            response_mock.json.return_value = TEST_OBJECT
+            response_mock.status_code = 200
+            response_mock.headers = {}
+
+            client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
+            client.force_refresh()
+
+            logger = logging.getLogger('configcat')
+            log_handler = MockLogHandler()
+            logger.addHandler(log_handler)
+
+            user = User(user_id) if user_id else None
+            self.assertEqual(expected_value, client.get_value(key, default_value, user))
+
+            if is_warning:
+                self.assertEqual(1, len(log_handler.warning_logs))
+                warning = log_handler.warning_logs[0]
+                self.assertEqual("[4002] The type of a setting does not match the type of the specified default value (%s). "
+                                 "Setting's type was %s but the default value's type was %s. "
+                                 "Please make sure that using a default value not matching the setting's type was intended." %
+                                 (default_value, type(expected_value), type(default_value)), warning)
+            else:
+                self.assertEqual(0, len(log_handler.warning_logs))
+
+            client.close()
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/configcatclienttests/test_configfetcher.py b/configcatclienttests/test_configfetcher.py
index cb9b3b1..0dffa6c 100644
--- a/configcatclienttests/test_configfetcher.py
+++ b/configcatclienttests/test_configfetcher.py
@@ -43,7 +43,7 @@ def test_fetch_not_modified_etag(self):
             response_mock = Mock()
             response_mock.json.return_value = test_json
             response_mock.status_code = 200
-            response_mock.headers = {'Etag': etag}
+            response_mock.headers = {'ETag': etag}
 
             request_get.return_value = response_mock
             fetch_response = fetcher.get_configuration()
diff --git a/configcatclienttests/test_datagovernance.py b/configcatclienttests/test_datagovernance.py
index 89e621f..ed95a78 100644
--- a/configcatclienttests/test_datagovernance.py
+++ b/configcatclienttests/test_datagovernance.py
@@ -26,7 +26,7 @@
 
 # An organization with Global data_governance config.json representation
 def mocked_requests_get_global(*args, **kwargs):
-    if args[0] == 'https://cdn-global.configcat.com/configuration-files//config_v5.json':
+    if args[0] == 'https://cdn-global.configcat.com/configuration-files//config_v6.json':
         return MockResponse({
             "p": {
                 "u": "https://cdn-global.configcat.com",
@@ -34,7 +34,7 @@ def mocked_requests_get_global(*args, **kwargs):
             },
             "f": FEATURE_TEST_JSON
         }, 200)
-    elif args[0] == 'https://cdn-eu.configcat.com/configuration-files//config_v5.json':
+    elif args[0] == 'https://cdn-eu.configcat.com/configuration-files//config_v6.json':
         return MockResponse({
             "p": {
                 "u": "https://cdn-global.configcat.com",
@@ -47,7 +47,7 @@ def mocked_requests_get_global(*args, **kwargs):
 
 # An organization with EuOnly data_governance config.json representation
 def mocked_requests_get_eu_only(*args, **kwargs):
-    if args[0] == 'https://cdn-global.configcat.com/configuration-files//config_v5.json':
+    if args[0] == 'https://cdn-global.configcat.com/configuration-files//config_v6.json':
         return MockResponse({
             "p": {
                 "u": "https://cdn-eu.configcat.com",
@@ -55,7 +55,7 @@ def mocked_requests_get_eu_only(*args, **kwargs):
             },
             "f": {}
         }, 200)
-    elif args[0] == 'https://cdn-eu.configcat.com/configuration-files//config_v5.json':
+    elif args[0] == 'https://cdn-eu.configcat.com/configuration-files//config_v6.json':
         return MockResponse({
             "p": {
                 "u": "https://cdn-eu.configcat.com",
@@ -68,7 +68,7 @@ def mocked_requests_get_eu_only(*args, **kwargs):
 
 # An organization with Global data_governance config.json representation with custom baseurl
 def mocked_requests_get_custom(*args, **kwargs):
-    if args[0] == 'https://custom.configcat.com/configuration-files//config_v5.json':
+    if args[0] == 'https://custom.configcat.com/configuration-files//config_v6.json':
         return MockResponse({
             "p": {
                 "u": "https://cdn-global.configcat.com",
@@ -81,7 +81,7 @@ def mocked_requests_get_custom(*args, **kwargs):
 
 # Redirect loop in config.json
 def mocked_requests_get_redirect_loop(*args, **kwargs):
-    if args[0] == 'https://cdn-global.configcat.com/configuration-files//config_v5.json':
+    if args[0] == 'https://cdn-global.configcat.com/configuration-files//config_v6.json':
         return MockResponse({
             "p": {
                 "u": "https://cdn-eu.configcat.com",
@@ -89,7 +89,7 @@ def mocked_requests_get_redirect_loop(*args, **kwargs):
             },
             "f": FEATURE_TEST_JSON
         }, 200)
-    elif args[0] == 'https://cdn-eu.configcat.com/configuration-files//config_v5.json':
+    elif args[0] == 'https://cdn-eu.configcat.com/configuration-files//config_v6.json':
         return MockResponse({
             "p": {
                 "u": "https://cdn-global.configcat.com",
@@ -102,10 +102,10 @@ def mocked_requests_get_redirect_loop(*args, **kwargs):
 
 # An organization with forced=2 redirection config.json representation
 def mocked_requests_get_forced_2(*args, **kwargs):
-    if args[0] == 'https://custom.configcat.com/configuration-files//config_v5.json' \
-            or args[0] == 'https://cdn-global.configcat.com/configuration-files//config_v5.json' \
-            or args[0] == 'https://cdn-eu.configcat.com/configuration-files//config_v5.json'\
-            or args[0] == 'https://forced.configcat.com/configuration-files//config_v5.json':
+    if args[0] == 'https://custom.configcat.com/configuration-files//config_v6.json' \
+            or args[0] == 'https://cdn-global.configcat.com/configuration-files//config_v6.json' \
+            or args[0] == 'https://cdn-eu.configcat.com/configuration-files//config_v6.json'\
+            or args[0] == 'https://forced.configcat.com/configuration-files//config_v6.json':
         return MockResponse({
             "p": {
                 "u": "https://forced.configcat.com",
@@ -116,13 +116,13 @@ def mocked_requests_get_forced_2(*args, **kwargs):
     return MockResponse(None, 404)
 
 
-call_to_global = mock.call('https://cdn-global.configcat.com/configuration-files//config_v5.json',
+call_to_global = mock.call('https://cdn-global.configcat.com/configuration-files//config_v6.json',
                            auth=ANY, headers=ANY, proxies=ANY, timeout=ANY)
-call_to_eu = mock.call('https://cdn-eu.configcat.com/configuration-files//config_v5.json',
+call_to_eu = mock.call('https://cdn-eu.configcat.com/configuration-files//config_v6.json',
                        auth=ANY, headers=ANY, proxies=ANY, timeout=ANY)
-call_to_custom = mock.call('https://custom.configcat.com/configuration-files//config_v5.json',
+call_to_custom = mock.call('https://custom.configcat.com/configuration-files//config_v6.json',
                            auth=ANY, headers=ANY, proxies=ANY, timeout=ANY)
-call_to_forced = mock.call('https://forced.configcat.com/configuration-files//config_v5.json',
+call_to_forced = mock.call('https://forced.configcat.com/configuration-files//config_v6.json',
                            auth=ANY, headers=ANY, proxies=ANY, timeout=ANY)
 
 
diff --git a/configcatclienttests/test_evaluationlog.py b/configcatclienttests/test_evaluationlog.py
new file mode 100644
index 0000000..2a9c652
--- /dev/null
+++ b/configcatclienttests/test_evaluationlog.py
@@ -0,0 +1,192 @@
+import json
+import logging
+import os
+import unittest
+import re
+import sys
+
+try:
+    from cStringIO import StringIO  # Python 2.7
+except ImportError:
+    from io import StringIO
+
+from configcatclient import ConfigCatClient, ConfigCatOptions, PollingMode
+from configcatclient.localfiledatasource import LocalFileFlagOverrides
+from configcatclient.overridedatasource import OverrideBehaviour
+from configcatclient.user import User
+from configcatclienttests.mocks import TEST_SDK_KEY
+
+logging.basicConfig(level=logging.INFO)
+
+
+# Remove the u prefix from unicode strings on python 2.7. When we only support python 3 this can be removed.
+def remove_unicode_prefix(string):
+    return re.sub(r"u'(.*?)'", r"'\1'", string)
+
+
+class EvaluationLogTests(unittest.TestCase):
+    def test_simple_value(self):
+        self.assertTrue(self._evaluation_log('data/evaluation/simple_value.json'))
+
+    def test_1_targeting_rule(self):
+        self.assertTrue(self._evaluation_log('data/evaluation/1_targeting_rule.json'))
+
+    def test_2_targeting_rules(self):
+        self.assertTrue(self._evaluation_log('data/evaluation/2_targeting_rules.json'))
+
+    def test_options_based_on_user_id(self):
+        self.assertTrue(self._evaluation_log('data/evaluation/options_based_on_user_id.json'))
+
+    def test_options_based_on_custom_attr(self):
+        self.assertTrue(self._evaluation_log('data/evaluation/options_based_on_custom_attr.json'))
+
+    def test_options_after_targeting_rule(self):
+        self.assertTrue(self._evaluation_log('data/evaluation/options_after_targeting_rule.json'))
+
+    def test_options_within_targeting_rule(self):
+        self.assertTrue(self._evaluation_log('data/evaluation/options_within_targeting_rule.json'))
+
+    def test_and_rules(self):
+        self.assertTrue(self._evaluation_log('data/evaluation/and_rules.json'))
+
+    def test_segment(self):
+        self.assertTrue(self._evaluation_log('data/evaluation/segment.json'))
+
+    def test_prerequisite_flag(self):
+        self.assertTrue(self._evaluation_log('data/evaluation/prerequisite_flag.json'))
+
+    def test_semver_validation(self):
+        self.assertTrue(self._evaluation_log('data/evaluation/semver_validation.json'))
+
+    def test_epoch_date_validation(self):
+        self.assertTrue(self._evaluation_log('data/evaluation/epoch_date_validation.json'))
+
+    def test_number_validation(self):
+        self.assertTrue(self._evaluation_log('data/evaluation/number_validation.json'))
+
+    def test_comparators_validation(self):
+        self.maxDiff = None
+        self.assertTrue(self._evaluation_log('data/evaluation/comparators.json'))
+
+    def test_list_truncation_validation(self):
+        self.assertTrue(self._evaluation_log('data/evaluation/list_truncation.json'))
+
+    def _evaluation_log(self, file_path, test_filter=None, generate_expected_log=False):
+        script_dir = os.path.dirname(__file__)
+        file_path = os.path.join(script_dir, file_path)
+        self.assertTrue(os.path.isfile(file_path))
+        name = os.path.basename(file_path)[:-5]
+        file_dir = os.path.join(os.path.dirname(file_path), name)
+
+        with open(file_path, 'r') as f:
+            data = json.load(f)
+            sdk_key = data.get('sdkKey')
+            base_url = data.get('baseUrl')
+            json_override = data.get('jsonOverride')
+            flag_overrides = None
+            if json_override:
+                flag_overrides = LocalFileFlagOverrides(
+                    file_path=os.path.join(file_dir, json_override),
+                    override_behaviour=OverrideBehaviour.LocalOnly
+                )
+                if not sdk_key:
+                    sdk_key = TEST_SDK_KEY
+
+            client = ConfigCatClient.get(sdk_key, ConfigCatOptions(
+                polling_mode=PollingMode.manual_poll(),
+                flag_overrides=flag_overrides,
+                base_url=base_url
+            ))
+            client.force_refresh()
+
+            # setup logging
+            log_stream = StringIO()
+            log_handler = logging.StreamHandler(log_stream)
+            log_handler.setFormatter(logging.Formatter('%(levelname)s %(message)s'))
+            logger = logging.getLogger('configcat')
+            logger.setLevel(logging.INFO)
+            logger.addHandler(log_handler)
+
+            for test in data['tests']:
+                key = test.get('key')
+                default_value = test.get('defaultValue')
+                return_value = test.get('returnValue')
+                user = test.get('user')
+                expected_log_file = test.get('expectedLog')
+                test_name = expected_log_file[:-4]
+
+                # apply test filter
+                if test_filter and test_name not in test_filter:
+                    continue
+
+                expected_log_file_path = os.path.join(file_dir, expected_log_file)
+                user_object = None
+                if user:
+                    custom = {k: v for k, v in user.items() if k not in {'Identifier', 'Email', 'Country'}}
+                    if len(custom) == 0:
+                        custom = None
+                    user_object = User(user.get('Identifier'), user.get('Email'), user.get('Country'), custom)
+
+                # clear log
+                log_stream.seek(0)
+                log_stream.truncate()
+
+                value = client.get_value(key, default_value, user_object)
+                log = remove_unicode_prefix(log_stream.getvalue())
+
+                if generate_expected_log:
+                    # create directory if needed
+                    if not os.path.exists(file_dir):
+                        os.makedirs(file_dir)
+
+                    with open(expected_log_file_path, 'w') as file:
+                        file.write(log)
+                else:
+                    self.assertTrue(os.path.isfile(expected_log_file_path))
+                    with open(expected_log_file_path, 'r') as file:
+                        expected_log = file.read()
+
+                    # On <= Python 3.5 the order of the keys in the serialized user object is random.
+                    # We need to cut out the JSON part and compare the JSON objects separately.
+                    if sys.version_info[:2] <= (3, 5):
+                        if expected_log.startswith('INFO [5000]') and log.startswith('INFO [5000]'):
+                            # Extract the JSON part from expected_log
+                            match = re.search(r'(\{.*?\})', expected_log)
+                            expected_log_json = None
+                            if match:
+                                expected_log_json = json.loads(match.group(1))
+                                # Remove the JSON-like part from the original string
+                                expected_log = re.sub(r'\{.*?\}', '', expected_log)
+
+                            # Extract the JSON part from log
+                            log_json = None
+                            match = re.search(r'(\{.*?\})', log)
+                            if match:
+                                log_json = json.loads(match.group(1))
+                                # Remove the JSON-like part from the original string
+                                log = re.sub(r'\{.*?\}', '', log)
+
+                            self.assertEqual(expected_log_json, log_json, 'User object mismatch for test: ' + test_name)
+
+                    self.assertEqual(expected_log, log, 'Log mismatch for test: ' + test_name)
+                    self.assertEqual(return_value, value, 'Return value mismatch for test: ' + test_name)
+
+            client.close()
+            return True
+
+        return False
+
+
+'''
+    def test_generate_all_evaluation_logs(self):
+        script_dir = os.path.dirname(__file__)
+        file_path = os.path.join(script_dir, 'data/evaluation')
+        self.assertTrue(os.path.isdir(file_path))
+        for file in os.listdir(file_path):
+            if file.endswith('.json'):
+                self._evaluation_log(os.path.join('data/evaluation', file), generate_expected_log=True)
+'''
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/configcatclienttests/test_hooks.py b/configcatclienttests/test_hooks.py
index 7a3cd8d..d2afb84 100644
--- a/configcatclienttests/test_hooks.py
+++ b/configcatclienttests/test_hooks.py
@@ -4,12 +4,13 @@
 import requests
 
 from configcatclient.configcatclient import ConfigCatClient
-from configcatclient.constants import FEATURE_FLAGS, VALUE, COMPARATOR, COMPARISON_ATTRIBUTE, COMPARISON_VALUE
+from configcatclient.config import FEATURE_FLAGS, VALUE, SERVED_VALUE, STRING_VALUE, \
+    extend_config_with_inline_salt_and_segment
 from configcatclient.user import User
 from configcatclient.configcatoptions import ConfigCatOptions, Hooks
 from configcatclient.pollingmode import PollingMode
 from configcatclient.utils import get_utc_now
-from configcatclienttests.mocks import ConfigCacheMock, HookCallbacks, TEST_OBJECT
+from configcatclienttests.mocks import ConfigCacheMock, HookCallbacks, TEST_OBJECT, TEST_SDK_KEY
 
 # Python2/Python3 support
 try:
@@ -36,16 +37,18 @@ def test_init(self):
         )
 
         config_cache = ConfigCacheMock()
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=config_cache,
-                                                              hooks=hooks))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=config_cache,
+                                                                    hooks=hooks))
 
         value = client.get_value('testStringKey', '')
 
         self.assertEqual('testValue', value)
         self.assertTrue(hook_callbacks.is_ready)
         self.assertEqual(1, hook_callbacks.is_ready_call_count)
-        self.assertEqual(TEST_OBJECT.get(FEATURE_FLAGS), hook_callbacks.changed_config)
+        extended_config = TEST_OBJECT
+        extend_config_with_inline_salt_and_segment(extended_config)
+        self.assertEqual(extended_config.get(FEATURE_FLAGS), hook_callbacks.changed_config)
         self.assertEqual(1, hook_callbacks.changed_config_call_count)
         self.assertTrue(hook_callbacks.evaluation_details)
         self.assertEqual(1, hook_callbacks.evaluation_details_call_count)
@@ -63,9 +66,9 @@ def test_subscribe(self):
         hooks.add_on_error(hook_callbacks.on_error)
 
         config_cache = ConfigCacheMock()
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=config_cache,
-                                                              hooks=hooks))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=config_cache,
+                                                                    hooks=hooks))
 
         value = client.get_value('testStringKey', '')
 
@@ -90,7 +93,7 @@ def test_evaluation(self):
             response_mock.headers = {}
 
             hook_callbacks = HookCallbacks()
-            client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
+            client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
 
             client.get_hooks().add_on_flag_evaluated(hook_callbacks.on_flag_evaluated)
 
@@ -106,11 +109,8 @@ def test_evaluation(self):
             self.assertEqual('id1', details.variation_id)
             self.assertFalse(details.is_default_value)
             self.assertIsNone(details.error)
-            self.assertIsNone(details.matched_evaluation_percentage_rule)
-            self.assertEqual('fake1', details.matched_evaluation_rule[VALUE])
-            self.assertEqual(2, details.matched_evaluation_rule[COMPARATOR])
-            self.assertEqual('Identifier', details.matched_evaluation_rule[COMPARISON_ATTRIBUTE])
-            self.assertEqual('@test1.com', details.matched_evaluation_rule[COMPARISON_VALUE])
+            self.assertIsNone(details.matched_percentage_option)
+            self.assertEqual('fake1', details.matched_targeting_rule[SERVED_VALUE][VALUE][STRING_VALUE])
             self.assertEqual(str(user), str(details.user))
             now = get_utc_now()
             self.assertGreaterEqual(now, details.fetch_time)
@@ -133,8 +133,8 @@ def test_callback_exception(self):
                 on_flag_evaluated=hook_callbacks.callback_exception,
                 on_error=hook_callbacks.callback_exception
             )
-            client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                                  hooks=hooks))
+            client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                        hooks=hooks))
 
             client.force_refresh()
 
diff --git a/configcatclienttests/test_integration_configcatclient.py b/configcatclienttests/test_integration_configcatclient.py
index 4d24501..5bfcb75 100644
--- a/configcatclienttests/test_integration_configcatclient.py
+++ b/configcatclienttests/test_integration_configcatclient.py
@@ -42,12 +42,14 @@ def test_get_all_keys(self):
         keys = client.get_all_keys()
         self.assertEqual(5, len(keys))
         self.assertTrue('keySampleText' in keys)
+        client.close()
 
     def test_get_all_values(self):
         client = configcatclient.get(_SDK_KEY)
         all_values = client.get_all_values()
         self.assertEqual(5, len(all_values))
         self.assertEqual('This text came from ConfigCat', all_values['keySampleText'])
+        client.close()
 
     def test_force_refresh(self):
         client = configcatclient.get(_SDK_KEY)
diff --git a/configcatclienttests/test_lazyloadingcachepolicy.py b/configcatclienttests/test_lazyloadingcachepolicy.py
index 76dc732..e09f88c 100644
--- a/configcatclienttests/test_lazyloadingcachepolicy.py
+++ b/configcatclienttests/test_lazyloadingcachepolicy.py
@@ -10,7 +10,7 @@
 from configcatclient.configentry import ConfigEntry
 from configcatclient.configfetcher import FetchResponse, ConfigFetcher
 from configcatclient.configservice import ConfigService
-from configcatclient.constants import VALUE
+from configcatclient.config import VALUE, FEATURE_FLAGS, STRING_VALUE, SettingType
 from configcatclient.logger import Logger
 from configcatclient.utils import get_seconds_since_epoch, get_utc_now_seconds_since_epoch
 
@@ -26,7 +26,7 @@
 
 from configcatclient.configcache import NullConfigCache
 from configcatclienttests.mocks import ConfigFetcherMock, ConfigFetcherWithErrorMock, TEST_JSON, SingleValueConfigCache, \
-    TEST_OBJECT
+    TEST_OBJECT, TEST_JSON_FORMAT
 
 logging.basicConfig()
 log = Logger('configcat', Hooks())
@@ -37,8 +37,9 @@ def test_wrong_params(self):
         config_fetcher = ConfigFetcherMock()
         config_cache = NullConfigCache()
         cache_policy = ConfigService('', PollingMode.lazy_load(0), Hooks(), config_fetcher, log, config_cache, False)
-        settings, _ = cache_policy.get_settings()
-        self.assertEqual('testValue', settings.get('testKey').get(VALUE))
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
+        self.assertEqual('testValue', settings.get('testKey').get(VALUE).get(STRING_VALUE))
         cache_policy.close()
 
     def test_get(self):
@@ -47,19 +48,22 @@ def test_get(self):
         cache_policy = ConfigService('', PollingMode.lazy_load(1), Hooks(), config_fetcher, log, config_cache, False)
 
         # Get value from Config Store, which indicates a config_fetcher call
-        settings, _ = cache_policy.get_settings()
-        self.assertEqual('testValue', settings.get('testKey').get(VALUE))
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
+        self.assertEqual('testValue', settings.get('testKey').get(VALUE).get(STRING_VALUE))
         self.assertEqual(config_fetcher.get_call_count, 1)
 
         # Get value from Config Store, which doesn't indicate a config_fetcher call (cache)
-        settings, _ = cache_policy.get_settings()
-        self.assertEqual('testValue', settings.get('testKey').get(VALUE))
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
+        self.assertEqual('testValue', settings.get('testKey').get(VALUE).get(STRING_VALUE))
         self.assertEqual(config_fetcher.get_call_count, 1)
 
         # Get value from Config Store, which indicates a config_fetcher call - 1 sec cache TTL
         time.sleep(1)
-        settings, _ = cache_policy.get_settings()
-        self.assertEqual('testValue', settings.get('testKey').get(VALUE))
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
+        self.assertEqual('testValue', settings.get('testKey').get(VALUE).get(STRING_VALUE))
         self.assertEqual(config_fetcher.get_call_count, 2)
         cache_policy.close()
 
@@ -69,8 +73,9 @@ def test_refresh(self):
         cache_policy = ConfigService('', PollingMode.lazy_load(160), Hooks(), config_fetcher, log, config_cache, False)
 
         # Get value from Config Store, which indicates a config_fetcher call
-        settings, fetch_time = cache_policy.get_settings()
-        self.assertEqual('testValue', settings.get('testKey').get(VALUE))
+        config, fetch_time = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
+        self.assertEqual('testValue', settings.get('testKey').get(VALUE).get(STRING_VALUE))
         self.assertEqual(config_fetcher.get_call_count, 1)
 
         with mock.patch('configcatclient.utils.get_utc_now_seconds_since_epoch') as mock_get_utc_now_since_epoch:
@@ -79,8 +84,9 @@ def test_refresh(self):
             mock_get_utc_now_since_epoch.return_value = fetch_time + 161
             # Get value from Config Store, which indicates a config_fetcher call after cache invalidation
             cache_policy.refresh()
-            settings, _ = cache_policy.get_settings()
-            self.assertEqual('testValue', settings.get('testKey').get(VALUE))
+            config, _ = cache_policy.get_config()
+            settings = config.get(FEATURE_FLAGS)
+            self.assertEqual('testValue', settings.get('testKey').get(VALUE).get(STRING_VALUE))
             self.assertEqual(config_fetcher.get_call_count, 2)
             cache_policy.close()
 
@@ -96,16 +102,16 @@ def test_get_skips_hitting_api_after_update_from_different_thread(self):
             now = datetime.datetime(2020, 5, 20, 0, 0, 0)
             mock_get_utc_now.return_value = now
             successful_fetch_response.entry.fetch_time = get_seconds_since_epoch(now)
-            cache_policy.get_settings()
+            cache_policy.get_config()
             self.assertEqual(config_fetcher.get_configuration.call_count, 1)
             # when the cache timeout is still within the limit skip any network
             # requests, as this could be that multiple threads have attempted
             # to acquire the lock at the same time, but only really one needs to update
             successful_fetch_response.entry.fetch_time = get_seconds_since_epoch(now - datetime.timedelta(seconds=159))
-            cache_policy.get_settings()
+            cache_policy.get_config()
             self.assertEqual(config_fetcher.get_configuration.call_count, 1)
             successful_fetch_response.entry.fetch_time = get_seconds_since_epoch(now - datetime.timedelta(seconds=161))
-            cache_policy.get_settings()
+            cache_policy.get_config()
             self.assertEqual(config_fetcher.get_configuration.call_count, 2)
 
     def test_error(self):
@@ -114,8 +120,8 @@ def test_error(self):
         cache_policy = ConfigService('', PollingMode.lazy_load(160), Hooks(), config_fetcher, log, config_cache, False)
 
         # Get value from Config Store, which indicates a config_fetcher call
-        settings, _ = cache_policy.get_settings()
-        self.assertEqual(settings, None)
+        config, _ = cache_policy.get_config()
+        self.assertEqual(config, None)
         cache_policy.close()
 
     def test_return_cached_config_when_cache_is_not_expired(self):
@@ -129,17 +135,19 @@ def test_return_cached_config_when_cache_is_not_expired(self):
 
         cache_policy = ConfigService('', PollingMode.lazy_load(1), Hooks(), config_fetcher, log, config_cache, False)
 
-        settings, _ = cache_policy.get_settings()
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
 
-        self.assertEqual('testValue', settings.get('testKey').get(VALUE))
+        self.assertEqual('testValue', settings.get('testKey').get(VALUE).get(STRING_VALUE))
         self.assertEqual(config_fetcher.get_call_count, 0)
         self.assertEqual(config_fetcher.get_fetch_count, 0)
 
         time.sleep(1)
 
-        settings, _ = cache_policy.get_settings()
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
 
-        self.assertEqual('testValue', settings.get('testKey').get(VALUE))
+        self.assertEqual('testValue', settings.get('testKey').get(VALUE).get(STRING_VALUE))
         self.assertEqual(config_fetcher.get_call_count, 1)
         self.assertEqual(config_fetcher.get_fetch_count, 1)
 
@@ -163,12 +171,55 @@ def test_fetch_config_when_cache_is_expired(self):
             False
         )
 
-        settings, _ = cache_policy.get_settings()
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
 
-        self.assertEqual('testValue', settings.get('testKey').get(VALUE))
+        self.assertEqual('testValue', settings.get('testKey').get(VALUE).get(STRING_VALUE))
         self.assertEqual(config_fetcher.get_call_count, 1)
         self.assertEqual(config_fetcher.get_fetch_count, 1)
 
+    def test_cache_TTL_respects_external_cache(self):
+        with mock.patch.object(requests, 'get') as request_get:
+            response_mock = Mock()
+            request_get.return_value = response_mock
+            config_json_string_remote = TEST_JSON_FORMAT.format(value_type=SettingType.STRING, value='{"s": "test-remote"}')
+            response_mock.json.return_value = json.loads(config_json_string_remote)
+            response_mock.text = config_json_string_remote
+            response_mock.status_code = 200
+            # response_mock.headers = {'ETag': 'etag'}
+
+            config_json_string_local = TEST_JSON_FORMAT.format(value_type=SettingType.STRING, value='{"s": "test-local"}')
+            config_cache = SingleValueConfigCache(ConfigEntry(
+                config=json.loads(config_json_string_local),
+                etag='etag',
+                config_json_string=config_json_string_local,
+                fetch_time=get_utc_now_seconds_since_epoch()).serialize())
+
+            polling_mode = PollingMode.lazy_load(cache_refresh_interval_seconds=1)
+            config_fetcher = ConfigFetcherMock()
+            cache_policy = ConfigService('', polling_mode, Hooks(), config_fetcher, log, config_cache, False)
+
+            config, _ = cache_policy.get_config()
+            settings = config.get(FEATURE_FLAGS)
+
+            self.assertEqual('test-local', settings.get('testKey').get(VALUE).get(STRING_VALUE))
+            self.assertEqual(config_fetcher.get_fetch_count, 0)
+
+            time.sleep(1)
+
+            config_json_string_local = TEST_JSON_FORMAT.format(value_type=SettingType.STRING, value='{"s": "test-local2"}')
+            config_cache._value = ConfigEntry(
+                config=json.loads(config_json_string_local),
+                etag='etag2',
+                config_json_string=config_json_string_local,
+                fetch_time=get_utc_now_seconds_since_epoch()).serialize()
+
+            config, _ = cache_policy.get_config()
+            settings = config.get(FEATURE_FLAGS)
+
+            self.assertEqual('test-local2', settings.get('testKey').get(VALUE).get(STRING_VALUE))
+            self.assertEqual(config_fetcher.get_fetch_count, 0)
+
     def test_online_offline(self):
         with mock.patch.object(requests, 'get') as request_get:
             response_mock = Mock()
@@ -183,8 +234,9 @@ def test_online_offline(self):
                                          Hooks(), config_fetcher, log, NullConfigCache(), False)
 
             self.assertFalse(cache_policy.is_offline())
-            settings, _ = cache_policy.get_settings()
-            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE))
+            config, _ = cache_policy.get_config()
+            settings = config.get(FEATURE_FLAGS)
+            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE).get(STRING_VALUE))
             self.assertEqual(1, request_get.call_count)
 
             cache_policy.set_offline()
@@ -192,15 +244,17 @@ def test_online_offline(self):
 
             time.sleep(1.5)
 
-            settings, _ = cache_policy.get_settings()
-            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE))
+            config, _ = cache_policy.get_config()
+            settings = config.get(FEATURE_FLAGS)
+            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE).get(STRING_VALUE))
             self.assertEqual(1, request_get.call_count)
 
             cache_policy.set_online()
             self.assertFalse(cache_policy.is_offline())
 
-            settings, _ = cache_policy.get_settings()
-            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE))
+            config, _ = cache_policy.get_config()
+            settings = config.get(FEATURE_FLAGS)
+            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE).get(STRING_VALUE))
             self.assertEqual(2, request_get.call_count)
 
             cache_policy.close()
@@ -219,21 +273,22 @@ def test_init_offline(self):
                                          Hooks(), config_fetcher, log, NullConfigCache(), True)
 
             self.assertTrue(cache_policy.is_offline())
-            settings, _ = cache_policy.get_settings()
-            self.assertIsNone(settings)
+            config, _ = cache_policy.get_config()
+            self.assertIsNone(config)
             self.assertEqual(0, request_get.call_count)
 
             time.sleep(1.5)
 
-            settings, _ = cache_policy.get_settings()
-            self.assertIsNone(settings)
+            config, _ = cache_policy.get_config()
+            self.assertIsNone(config)
             self.assertEqual(0, request_get.call_count)
 
             cache_policy.set_online()
             self.assertFalse(cache_policy.is_offline())
 
-            settings, _ = cache_policy.get_settings()
-            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE))
+            config, _ = cache_policy.get_config()
+            settings = config.get(FEATURE_FLAGS)
+            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE).get(STRING_VALUE))
             self.assertEqual(1, request_get.call_count)
 
             cache_policy.close()
diff --git a/configcatclienttests/test_manualpollingcachepolicy.py b/configcatclienttests/test_manualpollingcachepolicy.py
index d55de9e..659ace7 100644
--- a/configcatclienttests/test_manualpollingcachepolicy.py
+++ b/configcatclienttests/test_manualpollingcachepolicy.py
@@ -9,7 +9,7 @@
 from configcatclient.configcatoptions import Hooks
 from configcatclient.configfetcher import ConfigFetcher
 from configcatclient.configservice import ConfigService
-from configcatclient.constants import VALUE
+from configcatclient.config import VALUE, FEATURE_FLAGS, STRING_VALUE, SettingType
 from configcatclient.logger import Logger
 from configcatclient.utils import get_utc_now_seconds_since_epoch
 from configcatclienttests.mocks import ConfigFetcherMock, ConfigFetcherWithErrorMock, TEST_OBJECT, TEST_JSON_FORMAT
@@ -33,8 +33,8 @@ def test_without_refresh(self):
         config_fetcher = ConfigFetcherMock()
         config_cache = NullConfigCache()
         cache_policy = ConfigService('', PollingMode.manual_poll(), Hooks(), config_fetcher, log, config_cache, False)
-        settings, _ = cache_policy.get_settings()
-        self.assertEqual(settings, None)
+        config, _ = cache_policy.get_config()
+        self.assertEqual(config, None)
         self.assertEqual(config_fetcher.get_call_count, 0)
         cache_policy.close()
 
@@ -43,8 +43,9 @@ def test_with_refresh(self):
         config_cache = NullConfigCache()
         cache_policy = ConfigService('', PollingMode.manual_poll(), Hooks(), config_fetcher, log, config_cache, False)
         cache_policy.refresh()
-        settings, _ = cache_policy.get_settings()
-        self.assertEqual('testValue', settings.get('testKey').get(VALUE))
+        config, _ = cache_policy.get_config()
+        settings = config.get(FEATURE_FLAGS)
+        self.assertEqual('testValue', settings.get('testKey').get(VALUE).get(STRING_VALUE))
         self.assertEqual(config_fetcher.get_call_count, 1)
         cache_policy.close()
 
@@ -53,8 +54,8 @@ def test_with_refresh_error(self):
         config_cache = NullConfigCache()
         cache_policy = ConfigService('', PollingMode.manual_poll(), Hooks(), config_fetcher, log, config_cache, False)
         cache_policy.refresh()
-        settings, _ = cache_policy.get_settings()
-        self.assertEqual(settings, None)
+        config, _ = cache_policy.get_config()
+        self.assertEqual(config, None)
         cache_policy.close()
 
     def test_with_failed_refresh(self):
@@ -70,16 +71,18 @@ def test_with_failed_refresh(self):
             cache_policy = ConfigService('', polling_mode, Hooks(), config_fetcher, log, NullConfigCache(), False)
 
             cache_policy.refresh()
-            settings, _ = cache_policy.get_settings()
-            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE))
+            config, _ = cache_policy.get_config()
+            settings = config.get(FEATURE_FLAGS)
+            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE).get(STRING_VALUE))
 
             response_mock.json.return_value = {}
             response_mock.status_code = 500
             response_mock.headers = {}
 
             cache_policy.refresh()
-            settings, _ = cache_policy.get_settings()
-            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE))
+            config, _ = cache_policy.get_config()
+            settings = config.get(FEATURE_FLAGS)
+            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE).get(STRING_VALUE))
 
             cache_policy.close()
 
@@ -87,11 +90,11 @@ def test_cache(self):
         with mock.patch.object(requests, 'get') as request_get:
             response_mock = Mock()
             request_get.return_value = response_mock
-            config_json_string = TEST_JSON_FORMAT.format(value='"test"')
+            config_json_string = TEST_JSON_FORMAT.format(value_type=SettingType.STRING, value='{"s": "test"}')
             response_mock.json.return_value = json.loads(config_json_string)
             response_mock.text = config_json_string
             response_mock.status_code = 200
-            response_mock.headers = {'Etag': 'test-etag'}
+            response_mock.headers = {'ETag': 'test-etag'}
 
             polling_mode = PollingMode.manual_poll()
             config_cache = InMemoryConfigCache()
@@ -100,8 +103,9 @@ def test_cache(self):
 
             start_time_milliseconds = int(get_utc_now_seconds_since_epoch() * 1000)
             cache_policy.refresh()
-            settings, _ = cache_policy.get_settings()
-            self.assertEqual('test', settings.get('testKey').get(VALUE))
+            config, _ = cache_policy.get_config()
+            settings = config.get(FEATURE_FLAGS)
+            self.assertEqual('test', settings.get('testKey').get(VALUE).get(STRING_VALUE))
             self.assertEqual(1, request_get.call_count)
             self.assertEqual(1, len(config_cache._value))
 
@@ -113,14 +117,15 @@ def test_cache(self):
             self.assertEqual('test-etag', cache_tokens[1])
             self.assertEqual(config_json_string, cache_tokens[2])
 
-            config_json_string = TEST_JSON_FORMAT.format(value='"test2"')
+            config_json_string = TEST_JSON_FORMAT.format(value_type=SettingType.STRING, value='{"s": "test2"}')
             response_mock.json.return_value = json.loads(config_json_string)
             response_mock.text = config_json_string
 
-            start_time_milliseconds = get_utc_now_seconds_since_epoch()
+            start_time_milliseconds = int(get_utc_now_seconds_since_epoch() * 1000)
             cache_policy.refresh()
-            settings, _ = cache_policy.get_settings()
-            self.assertEqual('test2', settings.get('testKey').get(VALUE))
+            config, _ = cache_policy.get_config()
+            settings = config.get(FEATURE_FLAGS)
+            self.assertEqual('test2', settings.get('testKey').get(VALUE).get(STRING_VALUE))
             self.assertEqual(2, request_get.call_count)
             self.assertEqual(1, len(config_cache._value))
 
@@ -148,8 +153,9 @@ def test_online_offline(self):
 
             self.assertFalse(cache_policy.is_offline())
             self.assertTrue(cache_policy.refresh().is_success)
-            settings, _ = cache_policy.get_settings()
-            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE))
+            config, _ = cache_policy.get_config()
+            settings = config.get(FEATURE_FLAGS)
+            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE).get(STRING_VALUE))
             self.assertEqual(1, request_get.call_count)
 
             cache_policy.set_offline()
@@ -186,8 +192,9 @@ def test_init_offline(self):
 
             self.assertFalse(cache_policy.is_offline())
             self.assertTrue(cache_policy.refresh().is_success)
-            settings, _ = cache_policy.get_settings()
-            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE))
+            config, _ = cache_policy.get_config()
+            settings = config.get(FEATURE_FLAGS)
+            self.assertEqual('testValue', settings.get('testStringKey').get(VALUE).get(STRING_VALUE))
             self.assertEqual(1, request_get.call_count)
 
             cache_policy.close()
diff --git a/configcatclienttests/test_override.py b/configcatclienttests/test_override.py
index fd1b445..923f661 100644
--- a/configcatclienttests/test_override.py
+++ b/configcatclienttests/test_override.py
@@ -4,12 +4,14 @@
 import tempfile
 import json
 import time
+from parameterized import parameterized
 
 from configcatclient import ConfigCatClient
 from configcatclient.localdictionarydatasource import LocalDictionaryFlagOverrides
 from configcatclient.localfiledatasource import LocalFileFlagOverrides
 from configcatclient.overridedatasource import OverrideBehaviour
-from configcatclienttests.mocks import MockResponse
+from configcatclient.user import User
+from configcatclienttests.mocks import MockResponse, TEST_SDK_KEY
 from configcatclient.configcatoptions import ConfigCatOptions
 from configcatclient.pollingmode import PollingMode
 
@@ -27,7 +29,7 @@
 
 
 def mocked_requests_get(*args, **kwargs):
-    return MockResponse({"f": {"fakeKey": {"v": False}}}, 200)
+    return MockResponse({"f": {"fakeKey": {"v": {"b": False}, "t": 0}, "fakeKey2": {"v": {"s": "test"}, "t": 1}}}, 200)
 
 
 class OverrideTests(unittest.TestCase):
@@ -36,9 +38,9 @@ class OverrideTests(unittest.TestCase):
     def test_file(self):
         options = ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
                                    flag_overrides=LocalFileFlagOverrides(
-                                       file_path=path.join(OverrideTests.script_dir, 'test.json'),
+                                       file_path=path.join(OverrideTests.script_dir, 'data/test.json'),
                                        override_behaviour=OverrideBehaviour.LocalOnly))
-        client = ConfigCatClient.get(sdk_key='test', options=options)
+        client = ConfigCatClient.get(sdk_key='', options=options)
 
         self.assertTrue(client.get_value('enabledFeature', False))
         self.assertFalse(client.get_value('disabledFeature', True))
@@ -50,7 +52,7 @@ def test_file(self):
     def test_simple_file(self):
         options = ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
                                    flag_overrides=LocalFileFlagOverrides(
-                                       file_path=path.join(OverrideTests.script_dir, 'test-simple.json'),
+                                       file_path=path.join(OverrideTests.script_dir, 'data/test-simple.json'),
                                        override_behaviour=OverrideBehaviour.LocalOnly))
         client = ConfigCatClient.get(sdk_key='test', options=options)
 
@@ -66,12 +68,12 @@ def test_non_existent_file(self):
                                    flag_overrides=LocalFileFlagOverrides(
                                        file_path='non_existent.json',
                                        override_behaviour=OverrideBehaviour.LocalOnly))
-        client = ConfigCatClient.get(sdk_key='test', options=options)
+        client = ConfigCatClient.get(sdk_key=TEST_SDK_KEY, options=options)
         self.assertFalse(client.get_value('enabledFeature', False))
         client.close()
 
     def test_reload_file(self):
-        temp = tempfile.NamedTemporaryFile(mode="w")
+        temp = tempfile.NamedTemporaryFile(mode="w", delete=False)
         dictionary = {'flags': {
             'enabledFeature': False
         }}
@@ -82,7 +84,7 @@ def test_reload_file(self):
                                    flag_overrides=LocalFileFlagOverrides(
                                        file_path=temp.name,
                                        override_behaviour=OverrideBehaviour.LocalOnly))
-        client = ConfigCatClient.get(sdk_key='test', options=options)
+        client = ConfigCatClient.get(sdk_key=TEST_SDK_KEY, options=options)
 
         self.assertFalse(client.get_value('enabledFeature', True))
 
@@ -100,9 +102,10 @@ def test_reload_file(self):
         self.assertTrue(client.get_value('enabledFeature', False))
 
         client.close()
+        temp.close()
 
     def test_invalid_file(self):
-        temp = tempfile.NamedTemporaryFile(mode="w")
+        temp = tempfile.NamedTemporaryFile(mode="w", delete=False)
         temp.write('{"flags": {"enabledFeature": true}')
         temp.flush()
 
@@ -110,11 +113,12 @@ def test_invalid_file(self):
                                    flag_overrides=LocalFileFlagOverrides(
                                        file_path=temp.name,
                                        override_behaviour=OverrideBehaviour.LocalOnly))
-        client = ConfigCatClient.get(sdk_key='test', options=options)
+        client = ConfigCatClient.get(sdk_key=TEST_SDK_KEY, options=options)
 
         self.assertFalse(client.get_value('enabledFeature', False))
 
         client.close()
+        temp.close()
 
     def test_dictionary(self):
         dictionary = {
@@ -129,7 +133,7 @@ def test_dictionary(self):
                                    flag_overrides=LocalDictionaryFlagOverrides(
                                        source=dictionary,
                                        override_behaviour=OverrideBehaviour.LocalOnly))
-        client = ConfigCatClient.get(sdk_key='test', options=options)
+        client = ConfigCatClient.get(sdk_key=TEST_SDK_KEY, options=options)
 
         self.assertTrue(client.get_value('enabledFeature', False))
         self.assertFalse(client.get_value('disabledFeature', True))
@@ -149,10 +153,11 @@ def test_local_over_remote(self, mock_get):
                                    flag_overrides=LocalDictionaryFlagOverrides(
                                        source=dictionary,
                                        override_behaviour=OverrideBehaviour.LocalOverRemote))
-        client = ConfigCatClient.get(sdk_key='test', options=options)
+        client = ConfigCatClient.get(sdk_key=TEST_SDK_KEY, options=options)
         client.force_refresh()
 
         self.assertTrue(client.get_value('fakeKey', False))
+        self.assertEqual('test', client.get_value('fakeKey2', 'default'))
         self.assertTrue(client.get_value('nonexisting', False))
         client.close()
 
@@ -167,13 +172,73 @@ def test_remote_over_local(self, mock_get):
                                    flag_overrides=LocalDictionaryFlagOverrides(
                                        source=dictionary,
                                        override_behaviour=OverrideBehaviour.RemoteOverLocal))
-        client = ConfigCatClient.get(sdk_key='test', options=options)
+        client = ConfigCatClient.get(sdk_key=TEST_SDK_KEY, options=options)
         client.force_refresh()
 
         self.assertFalse(client.get_value('fakeKey', True))
+        self.assertEqual('test', client.get_value('fakeKey2', 'default'))
         self.assertTrue(client.get_value('nonexisting', False))
         client.close()
 
+    @parameterized.expand([
+        ('stringDependsOnString', '1', 'john@sensitivecompany.com', None, 'Dog'),
+        ('stringDependsOnString', '1', 'john@sensitivecompany.com', OverrideBehaviour.RemoteOverLocal, 'Dog'),
+        ('stringDependsOnString', '1', 'john@sensitivecompany.com', OverrideBehaviour.LocalOverRemote, 'Dog'),
+        ('stringDependsOnString', '1', 'john@sensitivecompany.com', OverrideBehaviour.LocalOnly, None),
+        ('stringDependsOnString', '2', 'john@notsensitivecompany.com', None, 'Cat'),
+        ('stringDependsOnString', '2', 'john@notsensitivecompany.com', OverrideBehaviour.RemoteOverLocal, 'Cat'),
+        ('stringDependsOnString', '2', 'john@notsensitivecompany.com', OverrideBehaviour.LocalOverRemote, 'Dog'),
+        ('stringDependsOnString', '2', 'john@notsensitivecompany.com', OverrideBehaviour.LocalOnly, None),
+        ('stringDependsOnInt', '1', 'john@sensitivecompany.com', None, 'Dog'),
+        ('stringDependsOnInt', '1', 'john@sensitivecompany.com', OverrideBehaviour.RemoteOverLocal, 'Dog'),
+        ('stringDependsOnInt', '1', 'john@sensitivecompany.com', OverrideBehaviour.LocalOverRemote, 'Cat'),
+        ('stringDependsOnInt', '1', 'john@sensitivecompany.com', OverrideBehaviour.LocalOnly, None),
+        ('stringDependsOnInt', '2', 'john@notsensitivecompany.com', None, 'Cat'),
+        ('stringDependsOnInt', '2', 'john@notsensitivecompany.com', OverrideBehaviour.RemoteOverLocal, 'Cat'),
+        ('stringDependsOnInt', '2', 'john@notsensitivecompany.com', OverrideBehaviour.LocalOverRemote, 'Dog'),
+        ('stringDependsOnInt', '2', 'john@notsensitivecompany.com', OverrideBehaviour.LocalOnly, None)
+    ])
+    def test_prerequisite_flag_override(self, key, user_id, email, override_behaviour, expected_value):
+        # The flag override alters the definition of the following flags:
+        # * 'mainStringFlag': to check the case where a prerequisite flag is overridden (dependent flag: 'stringDependsOnString')
+        # * 'stringDependsOnInt': to check the case where a dependent flag is overridden (prerequisite flag: 'mainIntFlag')
+        options = ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                   flag_overrides=None if override_behaviour is None else LocalFileFlagOverrides(
+                                       file_path=path.join(OverrideTests.script_dir, 'data/test_override_flagdependency_v6.json'),
+                                       override_behaviour=override_behaviour))
+        client = ConfigCatClient.get(sdk_key='configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg', options=options)
+        client.force_refresh()
+        value = client.get_value(key, None, User(user_id, email))
+
+        self.assertEqual(expected_value, value)
+        client.close()
+
+    @parameterized.expand([
+        ('developerAndBetaUserSegment', '1', 'john@example.com', None, False),
+        ('developerAndBetaUserSegment', '1', 'john@example.com', OverrideBehaviour.RemoteOverLocal, False),
+        ('developerAndBetaUserSegment', '1', 'john@example.com', OverrideBehaviour.LocalOverRemote, True),
+        ('developerAndBetaUserSegment', '1', 'john@example.com', OverrideBehaviour.LocalOnly, True),
+        ('notDeveloperAndNotBetaUserSegment', '2', 'kate@example.com', None, True),
+        ('notDeveloperAndNotBetaUserSegment', '2', 'kate@example.com', OverrideBehaviour.RemoteOverLocal, True),
+        ('notDeveloperAndNotBetaUserSegment', '2', 'kate@example.com', OverrideBehaviour.LocalOverRemote, True),
+        ('notDeveloperAndNotBetaUserSegment', '2', 'kate@example.com', OverrideBehaviour.LocalOnly, None)
+    ])
+    def test_config_salt_segment_override(self, key, user_id, email, override_behaviour, expected_value):
+        # The flag override uses a different config json salt than the downloaded one and
+        # overrides the following segments:
+        # * 'Beta Users': User.Email IS ONE OF ['jane@example.com']
+        # * 'Developers': User.Email IS ONE OF ['john@example.com']
+        options = ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                   flag_overrides=None if override_behaviour is None else LocalFileFlagOverrides(
+                                       file_path=path.join(OverrideTests.script_dir, 'data/test_override_segments_v6.json'),
+                                       override_behaviour=override_behaviour))
+        client = ConfigCatClient.get(sdk_key='configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/h99HYXWWNE2bH8eWyLAVMA', options=options)
+        client.force_refresh()
+        value = client.get_value(key, None, User(user_id, email))
+
+        self.assertEqual(expected_value, value)
+        client.close()
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/configcatclienttests/test_rollout.py b/configcatclienttests/test_rollout.py
index dcf092d..c96f012 100644
--- a/configcatclienttests/test_rollout.py
+++ b/configcatclienttests/test_rollout.py
@@ -1,42 +1,112 @@
 import logging
+import sys
 import unittest
+from datetime import datetime, timedelta
 from os import path
+from parameterized import parameterized
+
+try:
+    from datetime import timezone
+    utc_plus_2 = timezone(timedelta(hours=2))
+except ImportError:
+    import pytz as timezone  # On Python 2.7, datetime.timezone is not available. We use pytz instead.
+    utc_plus_2 = timezone.FixedOffset(120)  # 120 minutes (2 hours)
 
 import configcatclient
+from configcatclient import PollingMode, ConfigCatOptions, ConfigCatClient
+from configcatclient.config import SettingType
+from configcatclient.configcatoptions import Hooks
+from configcatclient.localdictionarydatasource import LocalDictionaryFlagOverrides
+from configcatclient.localfiledatasource import LocalFileDataSource
+from configcatclient.logger import Logger
+from configcatclient.overridedatasource import OverrideBehaviour
+from configcatclient.rolloutevaluator import RolloutEvaluator
 from configcatclient.user import User
+import codecs
+
+from configcatclient.utils import unicode_to_utf8
+from configcatclienttests.mocks import MockLogHandler
 
 logging.basicConfig(level=logging.WARNING)
 
 
 class RolloutTests(unittest.TestCase):
-
+    script_dir = path.dirname(__file__)
     value_test_type = "value_test"
     variation_test_type = "variation_test"
 
     def test_matrix_text(self):
-        self._test_matrix('./testmatrix.csv', 'PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A', self.value_test_type)
+        # https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d
+        self._test_matrix('data/testmatrix.csv',
+                          'configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/AG6C1ngVb0CvM07un6JisQ', self.value_test_type)
 
     def test_matrix_semantic(self):
-        self._test_matrix('./testmatrix_semantic.csv', 'PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA', self.value_test_type)
+        # https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d745f1-f315-7daf-d163-5541d3786e6f/244cf8b0-f604-11e8-b543-f23c917f9d8d
+        self._test_matrix('data/testmatrix_semantic.csv',
+                          'configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg', self.value_test_type)
 
     def test_matrix_semantic_2(self):
-        self._test_matrix('./testmatrix_semantic_2.csv', 'PKDVCLf-Hq-h-kCzMp-L7Q/q6jMCFIp-EmuAfnmZhPY7w', self.value_test_type)
+        # https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d77fa1-a796-85f9-df0c-57c448eb9934/244cf8b0-f604-11e8-b543-f23c917f9d8d
+        self._test_matrix('data/testmatrix_semantic_2.csv',
+                          'configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/U8nt3zEhDEO5S2ulubCopA', self.value_test_type)
 
     def test_matrix_number(self):
-        self._test_matrix('./testmatrix_number.csv', 'PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw', self.value_test_type)
+        # https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d747f0-5986-c2ef-eef3-ec778e32e10a/244cf8b0-f604-11e8-b543-f23c917f9d8d
+        self._test_matrix('data/testmatrix_number.csv',
+                          'configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw', self.value_test_type)
 
     def test_matrix_sensitive(self):
-        self._test_matrix('./testmatrix_sensitive.csv', 'PKDVCLf-Hq-h-kCzMp-L7Q/qX3TP2dTj06ZpCCT1h_SPA', self.value_test_type)
+        # https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d7b724-9285-f4a7-9fcd-00f64f1e83d5/244cf8b0-f604-11e8-b543-f23c917f9d8d
+        self._test_matrix('data/testmatrix_sensitive.csv',
+                          'configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/-0YmVOUNgEGKkgRF-rU65g', self.value_test_type)
+
+    def test_matrix_comparators_v6(self):
+        # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb
+        self._test_matrix('data/testmatrix_comparators_v6.csv',
+                          'configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ', self.value_test_type)
+
+    def test_matrix_segments(self):
+        # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9cfb-486f-8906-72a57c693615/08dbc325-9ebd-4587-8171-88f76a3004cb
+        self._test_matrix('data/testmatrix_segments.csv',
+                          'configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/h99HYXWWNE2bH8eWyLAVMA', self.value_test_type)
+
+    def test_matrix_segments_old(self):
+        # https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d9f207-6883-43e5-868c-cbf677af3fe6/244cf8b0-f604-11e8-b543-f23c917f9d8d
+        self._test_matrix('data/testmatrix_segments_old.csv',
+                          'PKDVCLf-Hq-h-kCzMp-L7Q/LcYz135LE0qbcacz2mgXnA', self.value_test_type)
+
+    def test_matrix_prerequisite_flag(self):
+        # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9b74-45cb-86d0-4d61c25af1aa/08dbc325-9ebd-4587-8171-88f76a3004cb
+        self._test_matrix('data/testmatrix_prerequisite_flag.csv',
+                          'configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg', self.value_test_type)
+
+    def test_matrix_and_or(self):
+        # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb
+        self._test_matrix('data/testmatrix_and_or.csv',
+                          'configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A', self.value_test_type)
 
     def test_matrix_variation_id(self):
-        self._test_matrix('./testmatrix_variationId.csv', 'PKDVCLf-Hq-h-kCzMp-L7Q/nQ5qkhRAUEa6beEyyrVLBA', self.variation_test_type)
+        # https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d774b9-3d05-0027-d5f4-3e76c3dba752/244cf8b0-f604-11e8-b543-f23c917f9d8d
+        self._test_matrix('data/testmatrix_variationId.csv',
+                          'PKDVCLf-Hq-h-kCzMp-L7Q/nQ5qkhRAUEa6beEyyrVLBA', self.variation_test_type)
 
-    def _test_matrix(self, file_path, sdk_key, type):
+    def test_matrix_unicode(self):
+        # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbd63c-9774-49d6-8187-5f2aab7bd606/08dbc325-9ebd-4587-8171-88f76a3004cb
+        self._test_matrix('data/testmatrix_unicode.csv',
+                          'configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/Da6w8dBbmUeMUBhh0iEeQQ', self.value_test_type)
+
+    def _test_matrix(self, file_path, sdk_key, type, base_url=None):
         script_dir = path.dirname(__file__)
         file_path = path.join(script_dir, file_path)
 
-        with open(file_path, 'r') as f:
-            content = f.readlines()
+        # On Python 2.7, convert unicode to utf-8
+        if sys.version_info[0] == 2:
+            with codecs.open(file_path, 'r', encoding='utf-8') as f:
+                content = f.readlines()
+                content = unicode_to_utf8(content)  # On Python 2.7, convert unicode to utf-8
+        else:
+            with open(file_path, 'r', encoding='utf-8') as f:
+                content = f.readlines()
 
         # CSV header
         header = content[0].rstrip()
@@ -44,7 +114,8 @@ def _test_matrix(self, file_path, sdk_key, type):
         custom_key = header.split(';')[3]
         content.pop(0)
 
-        client = configcatclient.get(sdk_key)
+        options = configcatclient.ConfigCatOptions(base_url=base_url)
+        client = configcatclient.get(sdk_key, options)
         errors = ''
         for line in content:
             user_descriptor = line.rstrip().split(';')
@@ -86,22 +157,253 @@ def test_wrong_user_object(self):
         self.assertEqual('Cat', setting_value)
         configcatclient.close_all()
 
+    # https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb
+    @parameterized.expand([
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", None, None, None, "Cat", False, False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", None, None, "Cat", False, False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@example.com", None, "Dog", True, False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@configcat.com", None, "Cat", False, False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@configcat.com", "", "Frog", True, True),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@configcat.com", "US", "Fish", True, True),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "b@configcat.com", None, "Cat", False, False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "b@configcat.com", "", "Falcon", False, True),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "b@configcat.com", "US", "Spider", False, True)
+    ])
+    def test_evaluation_details_matched_evaluation_rule_and_percentage_option(self, sdk_key, key, user_id, email, percentage_base, expected_return_value, expected_matched_targeting_rule, expected_matched_percentage_option):
+        client = ConfigCatClient.get(sdk_key=sdk_key, options=ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
+        client.force_refresh()
+
+        user = User(user_id, email=email, custom={"PercentageBase": percentage_base}) if user_id is not None else None
+
+        evaluation_details = client.get_value_details(key, None, user)
+
+        self.assertEqual(expected_return_value, evaluation_details.value)
+        self.assertEqual(expected_matched_targeting_rule, evaluation_details.matched_targeting_rule is not None)
+        self.assertEqual(expected_matched_percentage_option, evaluation_details.matched_percentage_option is not None)
+
+    def test_user_object_attribute_value_conversion_text_comparison(self):
+        client = ConfigCatClient.get(sdk_key='configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ',
+                                     options=ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
+        client.force_refresh()
+
+        logger = logging.getLogger('configcat')
+        log_handler = MockLogHandler()
+        logger.addHandler(log_handler)
+
+        custom_attribute_name = 'Custom1'
+        custom_attribute_value = 42
+        user = User('12345', custom={custom_attribute_name: custom_attribute_value})
+
+        key = 'boolTextEqualsNumber'
+        value = client.get_value(key, None, user)
+
+        self.assertEqual(True, value)
+
+        self.assertEqual(1, len(log_handler.warning_logs))
+        warning_log = log_handler.warning_logs[0]
+        self.assertEqual("[3005] Evaluation of condition (User.%s EQUALS '%s') for setting '%s' may not produce the expected"
+                         " result (the User.%s attribute is not a string value, thus it was automatically converted to the "
+                         "string value '%s'). Please make sure that using a non-string value was intended." %
+                         (custom_attribute_name, custom_attribute_value, key, custom_attribute_name, custom_attribute_value),
+                         warning_log)
+
+    def test_wrong_config_json_type_mismatch(self):
+        config = {
+            'f': {
+                'test': {
+                    't': 1,  # SettingType.STRING
+                    'v': {'b': True},  # bool value instead of string (type mismatch)
+                    'p': [],
+                    'r': []
+                }
+            }
+        }
+
+        log = Logger('configcat', Hooks())
+        logger = logging.getLogger('configcat')
+        log_handler = MockLogHandler()
+        logger.addHandler(log_handler)
+        evaluator = RolloutEvaluator(log)
+        value, _, _, _, _ = evaluator.evaluate('test', None, False, 'default_variation_id', config, None)
+
+        self.assertFalse(value)
+        self.assertEqual(1, len(log_handler.error_logs))
+        error = log_handler.error_logs[0]
+        if sys.version_info[0] == 2:
+            # On Python 2.7 the serializer returns <type 'str'> instead of <class 'str'>
+            self.assertTrue(error.startswith("[2001] Failed to evaluate setting 'test'. "
+                                             "(Setting value is not of the expected type <type 'str'>)"))
+        else:
+            self.assertTrue(error.startswith("[2001] Failed to evaluate setting 'test'. "
+                                             "(Setting value is not of the expected type <class 'str'>)"))
+
+    @parameterized.expand([
+        # SemVer-based comparisons
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "0.0", "20%"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "0.9.9", "< 1.0.0"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "1.0.0", "20%"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "1.1", "20%"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", 0, "20%"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", 0.9, "20%"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", 2, "20%"),
+        # Number-based comparisons
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", float('-inf'), "<2.1"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", -1, "<2.1"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 2, "<2.1"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 2.1, "<=2,1"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 3, "<>4.2"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 5, ">=5"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", float('inf'), ">5"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", float('nan'), "<>4.2"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "-inf", "<2.1"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "-1", "<2.1"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2", "<2.1"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2.1", "<=2,1"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2,1", "<=2,1"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "3", "<>4.2"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "5", ">=5"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "inf", ">5"),
+        ("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "nan", "<>4.2"),
+        # Date time-based comparisons
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", datetime(2023, 3, 31, 23, 59, 59, 999000), False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", datetime(2023, 3, 31, 23, 59, 59, 999000, timezone.utc), False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", datetime(2023, 4, 1, 1, 59, 59, 999000, utc_plus_2), False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", datetime(2023, 4, 1, 0, 0, 0, 1000, timezone.utc), True),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", datetime(2023, 4, 1, 2, 0, 0, 1000, utc_plus_2), True),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", datetime(2023, 4, 30, 23, 59, 59, 999000, timezone.utc), True),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", datetime(2023, 5, 1, 1, 59, 59, 999000, utc_plus_2), True),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", datetime(2023, 5, 1, 0, 0, 0, 1000, timezone.utc), False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", datetime(2023, 5, 1, 2, 0, 0, 1000, utc_plus_2), False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", float('-inf'), False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307199.999, False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307200.001, True),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899199.999, True),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899200.001, False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", float('inf'), False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", float("nan"), False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307199, False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307201, True),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899199, True),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899201, False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "-inf", False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1680307199.999", False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1680307200.001", True),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1682899199.999", True),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1682899200.001", False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "+inf", False),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "NaN", False),
+        # String array-based comparisons
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", ["x", "read"], "Dog"),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", ["x", "Read"], "Cat"),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", "[\"x\", \"read\"]", "Dog"),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", "[\"x\", \"Read\"]", "Cat"),
+        ("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", "x, read", "Cat")
+    ])
+    def test_user_object_attribute_value_conversion_non_text_comparisons(self, sdk_key, key, user_id, custom_attribute_name,
+                                                                         custom_attribute_value, expected_return_value):
+        client = ConfigCatClient.get(sdk_key=sdk_key,
+                                     options=ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
+        client.force_refresh()
+        user = User(user_id, custom={custom_attribute_name: custom_attribute_value})
+        value = client.get_value(key, None, user)
+
+        self.assertEqual(expected_return_value, value)
+
+    @parameterized.expand([
+        ("key1", "'key1' -> 'key1'"),
+        ("key2", "'key2' -> 'key3' -> 'key2'"),
+        ("key4", "'key4' -> 'key3' -> 'key2' -> 'key3'")
+    ])
+    def test_prerequisite_flag_circular_dependency(self, key, dependency_cycle):
+        config = LocalFileDataSource(path.join(self.script_dir, 'data/test_circulardependency_v6.json'),
+                                     OverrideBehaviour.LocalOnly, None).get_overrides()
+
+        log = Logger('configcat', Hooks())
+        logger = logging.getLogger('configcat')
+        log_handler = MockLogHandler()
+        logger.addHandler(log_handler)
+        evaluator = RolloutEvaluator(log)
+
+        value, _, _, _, _ = evaluator.evaluate(key, None, 'default_value', 'default_variation_id', config, None)
+
+        self.assertEqual('default_value', value)
+        error_log = log_handler.error_logs[0]
+        self.assertTrue('Circular dependency detected' in error_log)
+        self.assertTrue(dependency_cycle in error_log)
+
+    @parameterized.expand([
+        ("stringDependsOnBool", bool, "mainBoolFlag", True, "Dog"),
+        ("stringDependsOnBool", bool, "mainBoolFlag", False, "Cat"),
+        ("stringDependsOnBool", bool, "mainBoolFlag", "1", None),
+        ("stringDependsOnBool", bool, "mainBoolFlag", 1, None),
+        ("stringDependsOnBool", bool, "mainBoolFlag", 1.0, None),
+        ("stringDependsOnBool", bool, "mainBoolFlag", [True], None),
+        ("stringDependsOnBool", bool, "mainBoolFlag", None, None),
+        ("stringDependsOnString", str, "mainStringFlag", "private", "Dog"),
+        ("stringDependsOnString", str, "mainStringFlag", "Private", "Cat"),
+        ("stringDependsOnString", str, "mainStringFlag", True, None),
+        ("stringDependsOnString", str, "mainStringFlag", 1, None),
+        ("stringDependsOnString", str, "mainStringFlag", 1.0, None),
+        ("stringDependsOnString", str, "mainStringFlag", ["private"], None),
+        ("stringDependsOnString", str, "mainStringFlag", None, None),
+        ("stringDependsOnInt", int, "mainIntFlag", 2, "Dog"),
+        ("stringDependsOnInt", int, "mainIntFlag", 1, "Cat"),
+        ("stringDependsOnInt", int, "mainIntFlag", "2", None),
+        ("stringDependsOnInt", int, "mainIntFlag", True, None),
+        ("stringDependsOnInt", int, "mainIntFlag", 2.0, None),
+        ("stringDependsOnInt", int, "mainIntFlag", [2], None),
+        ("stringDependsOnInt", int, "mainIntFlag", None, None),
+        ("stringDependsOnDouble", float, "mainDoubleFlag", 0.1, "Dog"),
+        ("stringDependsOnDouble", float, "mainDoubleFlag", 0.11, "Cat"),
+        ("stringDependsOnDouble", float, "mainDoubleFlag", "0.1", None),
+        ("stringDependsOnDouble", float, "mainDoubleFlag", True, None),
+        ("stringDependsOnDouble", float, "mainDoubleFlag", 1, None),
+        ("stringDependsOnDouble", float, "mainDoubleFlag", [0.1], None),
+        ("stringDependsOnDouble", float, "mainDoubleFlag", None, None)
+    ])
+    def test_prerequisite_flag_comparison_value_type_mismatch(self, key, comparison_value_type, prerequisite_flag_key, prerequisite_flag_value, expected_value):
+        override_dictionary = {prerequisite_flag_key: prerequisite_flag_value}
+        options = ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                   flag_overrides=LocalDictionaryFlagOverrides(
+                                       source=override_dictionary,
+                                       override_behaviour=OverrideBehaviour.LocalOverRemote))
+        client = ConfigCatClient.get(sdk_key='configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg', options=options)
+        client.force_refresh()
+
+        logger = logging.getLogger('configcat')
+        log_handler = MockLogHandler()
+        logger.addHandler(log_handler)
+
+        value = client.get_value(key, None)
+
+        self.assertEqual(expected_value, value)
+
+        if expected_value is None:
+            self.assertEqual(1, len(log_handler.error_logs))
+            error_log = log_handler.error_logs[0]
+            prerequisite_flag_value_type = SettingType.to_type(SettingType.from_type(type(prerequisite_flag_value)))
+
+            self.assertTrue(("Type mismatch between comparison value type %s and type %s of prerequisite flag '%s'" %
+                             (comparison_value_type, prerequisite_flag_value_type, prerequisite_flag_key)) in error_log)
+
+        client.close()
+
 
 '''
     def test_create_matrix_text(self):
-        self._test_create_matrix('./testmatrix.csv', './testmatrix_out.csv',
+        self._test_create_matrix('data/testmatrix.csv', 'data/testmatrix_out.csv',
                                  'PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A')
     
     def test_create_matrix_semantic(self):
-        self._test_create_matrix('./testmatrix_semantic.csv', './testmatrix_semantic_out.csv',
+        self._test_create_matrix('data/testmatrix_semantic.csv', 'data/testmatrix_semantic_out.csv',
                                  'PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA')
 
     def test_create_matrix_semnatic_2(self):
-        self._test_create_matrix('./testmatrix_input_semantic_2.csv', './testmatrix_semantic_2.csv',
+        self._test_create_matrix('data/testmatrix_input_semantic_2.csv', 'data/testmatrix_semantic_2.csv',
                                  'PKDVCLf-Hq-h-kCzMp-L7Q/q6jMCFIp-EmuAfnmZhPY7w')
 
     def test_create_matrix_number(self):
-        self._test_create_matrix('./testmatrix_number.csv', './testmatrix_number_out.csv',
+        self._test_create_matrix('data/testmatrix_number.csv', 'data/testmatrix_number_out.csv',
                                  'PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw')
 
     def _test_create_matrix(self, file_path, out_file_path, sdk_key):
diff --git a/configcatclienttests/test_user.py b/configcatclienttests/test_user.py
index 0491527..224c1b6 100644
--- a/configcatclienttests/test_user.py
+++ b/configcatclienttests/test_user.py
@@ -1,7 +1,16 @@
+import logging
 import unittest
 import json
+from datetime import datetime
 from configcatclient.user import User
 
+try:
+    from datetime import timezone
+except ImportError:
+    import pytz as timezone  # On Python 2.7, datetime.timezone is not available. We use pytz instead.
+
+logging.basicConfig()
+
 
 class UserTests(unittest.TestCase):
     def test_empty_or_none_identifier(self):
@@ -34,7 +43,12 @@ def test_to_str(self):
         user_id = 'id'
         email = 'test@test.com'
         country = 'country'
-        custom = {'custom': 'test'}
+        custom = {
+            'string': 'test',
+            'datetime': datetime(2023, 9, 19, 11, 1, 35, 999000, tzinfo=timezone.utc),
+            'int': 42,
+            'float': 3.14
+        }
         user = User(identifier=user_id, email=email, country=country, custom=custom)
 
         user_json = json.loads(str(user))
@@ -42,4 +56,7 @@ def test_to_str(self):
         self.assertEqual(user_id, user_json['Identifier'])
         self.assertEqual(email, user_json['Email'])
         self.assertEqual(country, user_json['Country'])
-        self.assertEqual(custom, user_json['Custom'])
+        self.assertEqual('test', user_json['string'])
+        self.assertEqual(42, user_json['int'])
+        self.assertEqual(3.14, user_json['float'])
+        self.assertEqual("2023-09-19T11:01:35.999000+00:00", user_json['datetime'])
diff --git a/configcatclienttests/test_variation_id.py b/configcatclienttests/test_variation_id.py
index f562683..77ecb6c 100644
--- a/configcatclienttests/test_variation_id.py
+++ b/configcatclienttests/test_variation_id.py
@@ -2,7 +2,7 @@
 import unittest
 
 from configcatclient.configcatclient import ConfigCatClient
-from configcatclienttests.mocks import ConfigCacheMock
+from configcatclienttests.mocks import ConfigCacheMock, TEST_SDK_KEY
 from configcatclient.configcatoptions import ConfigCatOptions
 from configcatclient.pollingmode import PollingMode
 
@@ -11,27 +11,27 @@
 
 class VariationIdTests(unittest.TestCase):
     def test_get_variation_id(self):
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=ConfigCacheMock()))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=ConfigCacheMock()))
         self.assertEqual('fakeId1', client.get_value_details('key1', None).variation_id)
         self.assertEqual('fakeId2', client.get_value_details('key2', None).variation_id)
         client.close()
 
     def test_get_variation_id_not_found(self):
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=ConfigCacheMock()))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=ConfigCacheMock()))
         self.assertEqual(None, client.get_value_details('nonexisting', 'default_value').variation_id)
         client.close()
 
     def test_get_variation_id_empty_config(self):
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=ConfigCacheMock()))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=ConfigCacheMock()))
         self.assertEqual(None, client.get_value_details('nonexisting', 'default_value').variation_id)
         client.close()
 
     def test_get_key_and_value(self):
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=ConfigCacheMock()))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=ConfigCacheMock()))
         result = client.get_key_and_value('fakeId1')
         self.assertEqual('key1', result.key)
         self.assertTrue(result.value)
@@ -41,14 +41,14 @@ def test_get_key_and_value(self):
         client.close()
 
     def test_get_key_and_value_not_found(self):
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
-                                                              config_cache=ConfigCacheMock()))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
+                                                                    config_cache=ConfigCacheMock()))
         result = client.get_key_and_value('nonexisting')
         self.assertIsNone(result)
         client.close()
 
     def test_get_key_and_value_empty_config(self):
-        client = ConfigCatClient.get('test', ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
+        client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
         result = client.get_key_and_value('nonexisting')
         self.assertIsNone(result)
         client.close()
diff --git a/configcatclienttests/testmatrix_input_semantic_2.csv b/configcatclienttests/testmatrix_input_semantic_2.csv
deleted file mode 100644
index eea1bbb..0000000
--- a/configcatclienttests/testmatrix_input_semantic_2.csv
+++ /dev/null
@@ -1,95 +0,0 @@
-Identifier;Email;Country;AppVersion;precedenceTests
-dontcare;;;1.9.1-1
-dontcare;;;1.9.1-2
-dontcare;;;1.9.1-10
-dontcare;;;1.9.1-10a
-dontcare;;;1.9.1-1a
-dontcare;;;1.9.1-alpha
-dontcare;;;1.9.99-alpha
-dontcare;;;1.9.99-alpha+build1
-dontcare;;;1.9.99-alpha+build2
-dontcare;;;1.9.99-alpha2
-dontcare;;;1.9.99-beta
-dontcare;;;1.9.99-rc
-dontcare;;;1.9.99-rc.1
-dontcare;;;1.9.99-rc.2
-dontcare;;;1.9.99-rc.9
-dontcare;;;1.9.99-rc.20
-dontcare;;;1.9.99-rc.20a
-dontcare;;;1.9.99-rc.2a
-dontcare;;;1.9.99
-dontcare;;;1.9.100
-dontcare;;;1.10.0-alpha
-dontcare;;;1.10.0
-dontcare;;;1.10.1
-dontcare;;;1.10.2
-dontcare;;;2.0.0
-dontcare;;;2.0.0+build3
-dontcare;;;2.0.0+001
-dontcare;;;2.0.0+20130313144700
-dontcare;;;2.0.0+exp.sha.5114f85
-dontcare;;;3.0.0
-dontcare;;;4.0.0
-dontcare;;;5.0.0
-dontcare;;;6.0.0
-dontcare;;;7.0.0-patch+metadata
-dontcare;;;8.0.0-patch+metadata
-dontcare;;;9.0.0-patch
-dontcare;;;10.0.0
-dontcare;;;104.0.0
-dontcare;;;103.0.0
-dontcare;;;102.0.0
-dontcare;;;101.0.0
-dontcare;;;90.104.0
-dontcare;;;90.103.0
-dontcare;;;90.102.0
-dontcare;;;90.101.0
-dontcare;;;80.0.104
-dontcare;;;80.0.103
-dontcare;;;80.0.102
-dontcare;;;80.0.101
-dontcare;;;73.0.0
-dontcare;;;72.0.0
-dontcare;;;72.0.0-beta.2
-dontcare;;;72.0.0-beta.1
-dontcare;;;72.0.0-beta
-dontcare;;;72.0.0-alpha
-dontcare;;;72.0.0-1a
-dontcare;;;72.0.0-10aa
-dontcare;;;72.0.0-10a
-dontcare;;;72.0.0-2
-dontcare;;;71.0.0+metadata
-dontcare;;;71.0.0-patch3+metadata
-dontcare;;;71.0.0-patch2+metadata
-dontcare;;;71.0.0-patch1
-dontcare;;;60.73.0
-dontcare;;;60.72.0
-dontcare;;;60.72.0-beta.2
-dontcare;;;60.72.0-beta.1
-dontcare;;;60.72.0-beta
-dontcare;;;60.72.0-alpha
-dontcare;;;60.72.0-1a
-dontcare;;;60.72.0-10aa
-dontcare;;;60.72.0-10a
-dontcare;;;60.72.0-2
-dontcare;;;60.71.0+metadata
-dontcare;;;60.71.0-patch3+metadata
-dontcare;;;60.71.0-patch2+metadata
-dontcare;;;60.71.0-patch1
-dontcare;;;50.60.73
-dontcare;;;50.60.72
-dontcare;;;50.60.72-beta.2
-dontcare;;;50.60.72-beta.1
-dontcare;;;50.60.72-beta
-dontcare;;;50.60.72-alpha
-dontcare;;;50.60.72-1a
-dontcare;;;50.60.72-10aa
-dontcare;;;50.60.72-10a
-dontcare;;;50.60.72-2
-dontcare;;;50.60.71+metadata
-dontcare;;;50.60.71-patch3+metadata
-dontcare;;;50.60.71-patch2+metadata
-dontcare;;;50.60.71-patch1
-dontcare;;;50.60.71-patch1+anothermetadata
-dontcare;;;40.0.0-patch
-dontcare;;;30.0.0-beta
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 8d799d6..596c298 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,3 +3,4 @@ requests>=2.31.0; python_version >= "3.7"
 semver>=2.10.2
 enum-compat>=0.0.3
 qualname>=0.1.0
+pytz; python_version == "2.7"
diff --git a/setup.py b/setup.py
index b30d48e..f2635ba 100644
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ def parse_requirements(filename):
     return [line for line in lines if line]
 
 
-configcatclient_version = '8.0.1'
+configcatclient_version = '9.0.0'
 
 requirements = parse_requirements('requirements.txt')
 
@@ -39,6 +39,7 @@ def parse_requirements(filename):
         'Programming Language :: Python :: 3.9',
         'Programming Language :: Python :: 3.10',
         'Programming Language :: Python :: 3.11',
+        'Programming Language :: Python :: 3.12',
         'Topic :: Software Development',
         'Topic :: Software Development :: Libraries',
     ],
diff --git a/tox.ini b/tox.ini
index 1c354a9..501841f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,6 +6,7 @@ passenv = LD_PRELOAD
 deps =
     pytest
     pytest-cov
+    parameterized
 commands =
     pytest --cov=configcatclient configcatclienttests
 
@@ -20,4 +21,5 @@ commands =
 deps =
     pytest
     pytest-cov
+    parameterized
     mock