Skip to content

Commit

Permalink
🖊️ Further work
Browse files Browse the repository at this point in the history
  • Loading branch information
oakbani committed Oct 22, 2018
1 parent d4f7ea4 commit f972e14
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 86 deletions.
99 changes: 52 additions & 47 deletions optimizely/helpers/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

import json

from six import string_types


CUSTOM_ATTRIBUTE_CONDITION_TYPE = 'custom_attribute'

Expand Down Expand Up @@ -44,14 +46,6 @@ class ConditionalMatchTypes(object):
ConditionalMatchTypes.SUBSTRING
]

EVALUATORS_BY_MATCH_TYPE = {
ConditionalMatchTypes.EXACT: 'exact_evaluator',
ConditionalMatchTypes.EXISTS: 'exists_evaluator',
ConditionalMatchTypes.GREATER_THAN: 'greater_than_evaluator',
ConditionalMatchTypes.LESS_THAN: 'less_than_evaluator',
ConditionalMatchTypes.SUBSTRING: 'substring_evaluator'
}


class ConditionEvaluator(object):
""" Class encapsulating methods to be used in audience condition evaluation. """
Expand All @@ -60,18 +54,6 @@ def __init__(self, condition_data, attributes):
self.condition_data = condition_data
self.attributes = attributes

def evaluator(self, condition):
""" Method to compare single audience condition against provided user data i.e. attributes.
Args:
condition: Integer representing the index of condition_data that needs to be used for comparison.
Returns:
Boolean indicating the result of comparing the condition value against the user attributes.
"""

return self.attributes.get(self.condition_data[condition][0]) == self.condition_data[condition][1]

def and_evaluator(self, conditions):
""" Evaluates a list of conditions as if the evaluator had been applied
to each entry and the results AND-ed together
Expand Down Expand Up @@ -123,38 +105,15 @@ def not_evaluator(self, single_condition):

return not self.evaluate(single_condition[0])

OPERATORS = {
ConditionalOperatorTypes.AND: and_evaluator,
ConditionalOperatorTypes.OR: or_evaluator,
ConditionalOperatorTypes.NOT: not_evaluator
}

def evaluate(self, conditions):
""" Top level method to evaluate audience conditions.
Args:
conditions: Nested list of and/or conditions.
Ex: ['and', operand_1, ['or', operand_2, operand_3]]
Returns:
Boolean result of evaluating the conditions evaluate
"""

if isinstance(conditions, list):
if conditions[0] in DEFAULT_OPERATOR_TYPES:
return self.OPERATORS[conditions[0]](self, conditions[1:])
else:
return False

return self.evaluator(conditions)

def is_value_valid_for_exact_conditions(self, value):
return isinstance(value, string_types) or isinstance(value, bool)
return math.isfinite(value)

def exact_evaluator(self, leaf_condition):
condition_value = leaf_condition['value']
def exact_evaluator(self, condition):
condition_value = self.condition_data[condition][1]
condition_value_type = type(condition_value.encode('utf8'))

user_provided_value = self.attributes.get(leaf_condition['name'])
user_provided_value = self.attributes.get(self.condition_data[condition][0])
user_provided_value_type = type(user_provided_value)

if not self.is_value_valid_for_exact_conditions(condition_value) or \
Expand Down Expand Up @@ -196,6 +155,47 @@ def substring_evaluator(self, leaf_condition):

return condition_value in user_provided_value

EVALUATORS_BY_OPERATOR_TYPE = {
ConditionalOperatorTypes.AND: and_evaluator,
ConditionalOperatorTypes.OR: or_evaluator,
ConditionalOperatorTypes.NOT: not_evaluator
}

EVALUATORS_BY_MATCH_TYPE = {
ConditionalMatchTypes.EXACT: exact_evaluator,
ConditionalMatchTypes.EXISTS: exists_evaluator,
ConditionalMatchTypes.GREATER_THAN: greater_than_evaluator,
ConditionalMatchTypes.LESS_THAN: less_than_evaluator,
ConditionalMatchTypes.SUBSTRING: substring_evaluator
}

def evaluate(self, conditions):
""" Top level method to evaluate audience conditions.
Args:
conditions: Nested list of and/or conditions.
Ex: ['and', operand_1, ['or', operand_2, operand_3]]
Returns:
Boolean result of evaluating the conditions evaluate
"""

if isinstance(conditions, list):
if conditions[0] in DEFAULT_OPERATOR_TYPES:
return self.EVALUATORS_BY_OPERATOR_TYPE[conditions[0]](self, conditions[1:])
else:
return self.EVALUATORS_BY_OPERATOR_TYPE[ConditionalOperatorTypes.OR](self, conditions[1:])

leaf_condition = conditions

if self.condition_data[leaf_condition][2] != CUSTOM_ATTRIBUTE_CONDITION_TYPE:
return null

condition_match = self.condition_data[leaf_condition][3]

if condition_match not in MATCH_TYPES:
return null

return self.EVALUATORS_BY_MATCH_TYPE[condition_match](self, leaf_condition)


class ConditionDecoder(object):
""" Class which provides an object_hook method for decoding dict
Expand Down Expand Up @@ -233,7 +233,12 @@ def _audience_condition_deserializer(obj_dict):
Returns:
List consisting of condition key and corresponding value.
"""
return [obj_dict.get('name'), obj_dict.get('value')]
return [
obj_dict.get('name'),
obj_dict.get('value'),
obj_dict.get('type'),
obj_dict.get('match', ConditionalMatchTypes.EXACT)
]


def loads(conditions_string):
Expand Down
37 changes: 1 addition & 36 deletions tests/helpers_tests/test_condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,41 +32,6 @@ def setUp(self):
}
self.condition_evaluator = condition_helper.ConditionEvaluator(self.condition_list, attributes)

def test_evaluator__returns_true(self):
""" Test that evaluator correctly returns True when there is an exact match.
Also test that evaluator works for falsy values. """

# string attribute value
condition_list = [['test_attribute', '']]
condition_evaluator = condition_helper.ConditionEvaluator(condition_list, {'test_attribute': ''})
self.assertTrue(self.condition_evaluator.evaluator(0))

# boolean attribute value
condition_list = [['boolean_key', False]]
condition_evaluator = condition_helper.ConditionEvaluator(condition_list, {'boolean_key': False})
self.assertTrue(condition_evaluator.evaluator(0))

# integer attribute value
condition_list = [['integer_key', 0]]
condition_evaluator = condition_helper.ConditionEvaluator(condition_list, {'integer_key': 0})
self.assertTrue(condition_evaluator.evaluator(0))

# double attribute value
condition_list = [['double_key', 0.0]]
condition_evaluator = condition_helper.ConditionEvaluator(condition_list, {'double_key': 0.0})
self.assertTrue(condition_evaluator.evaluator(0))

def test_evaluator__returns_false(self):
""" Test that evaluator correctly returns False when there is no match. """

attributes = {
'browser_type': 'chrome',
'location': 'San Francisco'
}
self.condition_evaluator = condition_helper.ConditionEvaluator(self.condition_list, attributes)

self.assertFalse(self.condition_evaluator.evaluator(0))

def test_and_evaluator__returns_true(self):
""" Test that and_evaluator returns True when all conditions evaluate to True. """

Expand Down Expand Up @@ -140,4 +105,4 @@ def test_loads(self):
)

self.assertEqual(['and', ['or', ['or', 0]]], condition_structure)
self.assertEqual([['test_attribute', 'test_value_1']], condition_list)
self.assertEqual([['test_attribute', 'test_value_1', 'custom_attribute', 'exact']], condition_list)
6 changes: 3 additions & 3 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,13 @@ def test_init(self):
'11154', 'Test attribute users 1',
'["and", ["or", ["or", {"name": "test_attribute", "type": "custom_attribute", "value": "test_value_1"}]]]',
conditionStructure=['and', ['or', ['or', 0]]],
conditionList=[['test_attribute', 'test_value_1']]
conditionList=[['test_attribute', 'test_value_1', 'custom_attribute', 'exact']]
),
'11159': entities.Audience(
'11159', 'Test attribute users 2',
'["and", ["or", ["or", {"name": "test_attribute", "type": "custom_attribute", "value": "test_value_2"}]]]',
conditionStructure=['and', ['or', ['or', 0]]],
conditionList=[['test_attribute', 'test_value_2']]
conditionList=[['test_attribute', 'test_value_2', 'custom_attribute', 'exact']]
)
}
expected_variation_key_map = {
Expand Down Expand Up @@ -521,7 +521,7 @@ def test_init__with_v4_datafile(self):
'11154', 'Test attribute users',
'["and", ["or", ["or", {"name": "test_attribute", "type": "custom_attribute", "value": "test_value"}]]]',
conditionStructure=['and', ['or', ['or', 0]]],
conditionList=[['test_attribute', 'test_value']]
conditionList=[['test_attribute', 'test_value', 'custom_attribute', 'exact']]
)
}
expected_variation_key_map = {
Expand Down

0 comments on commit f972e14

Please sign in to comment.