Skip to content

Commit abb6723

Browse files
msohailhussainaliabbasrizvi
authored andcommitted
feat (audience match types): Update condition evaluator for new audience match types (#146) (#153)
1 parent e5ec118 commit abb6723

File tree

11 files changed

+1588
-158
lines changed

11 files changed

+1588
-158
lines changed

optimizely/helpers/audience.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016, Optimizely
1+
# Copyright 2016, 2018, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -12,6 +12,7 @@
1212
# limitations under the License.
1313

1414
from . import condition as condition_helper
15+
from . import condition_tree_evaluator
1516

1617

1718
def is_match(audience, attributes):
@@ -24,8 +25,15 @@ def is_match(audience, attributes):
2425
Return:
2526
Boolean representing if user satisfies audience conditions or not.
2627
"""
27-
condition_evaluator = condition_helper.ConditionEvaluator(audience.conditionList, attributes)
28-
return condition_evaluator.evaluate(audience.conditionStructure)
28+
custom_attr_condition_evaluator = condition_helper.CustomAttributeConditionEvaluator(
29+
audience.conditionList, attributes)
30+
31+
is_match = condition_tree_evaluator.evaluate(
32+
audience.conditionStructure,
33+
lambda index: custom_attr_condition_evaluator.evaluate(index)
34+
)
35+
36+
return is_match or False
2937

3038

3139
def is_user_in_experiment(config, experiment, attributes):
@@ -34,7 +42,8 @@ def is_user_in_experiment(config, experiment, attributes):
3442
Args:
3543
config: project_config.ProjectConfig object representing the project.
3644
experiment: Object representing the experiment.
37-
attributes: Dict representing user attributes which will be used in determining if the audience conditions are met.
45+
attributes: Dict representing user attributes which will be used in determining
46+
if the audience conditions are met. If not provided, default to an empty dict.
3847
3948
Returns:
4049
Boolean representing if user satisfies audience conditions for any of the audiences or not.
@@ -44,9 +53,8 @@ def is_user_in_experiment(config, experiment, attributes):
4453
if not experiment.audienceIds:
4554
return True
4655

47-
# Return False if there are audiences, but no attributes
48-
if not attributes:
49-
return False
56+
if attributes is None:
57+
attributes = {}
5058

5159
# Return True if conditions for any one audience are met
5260
for audience_id in experiment.audienceIds:

optimizely/helpers/condition.py

Lines changed: 130 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016, Optimizely
1+
# Copyright 2016, 2018, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -12,115 +12,180 @@
1212
# limitations under the License.
1313

1414
import json
15+
import numbers
1516

17+
from six import string_types
1618

17-
class ConditionalOperatorTypes(object):
19+
from . import validator
20+
21+
22+
class ConditionOperatorTypes(object):
1823
AND = 'and'
1924
OR = 'or'
2025
NOT = 'not'
2126

2227

23-
DEFAULT_OPERATOR_TYPES = [
24-
ConditionalOperatorTypes.AND,
25-
ConditionalOperatorTypes.OR,
26-
ConditionalOperatorTypes.NOT
27-
]
28+
class ConditionMatchTypes(object):
29+
EXACT = 'exact'
30+
EXISTS = 'exists'
31+
GREATER_THAN = 'gt'
32+
LESS_THAN = 'lt'
33+
SUBSTRING = 'substring'
34+
2835

36+
class CustomAttributeConditionEvaluator(object):
37+
""" Class encapsulating methods to be used in audience leaf condition evaluation. """
2938

30-
class ConditionEvaluator(object):
31-
""" Class encapsulating methods to be used in audience condition evaluation. """
39+
CUSTOM_ATTRIBUTE_CONDITION_TYPE = 'custom_attribute'
3240

3341
def __init__(self, condition_data, attributes):
3442
self.condition_data = condition_data
35-
self.attributes = attributes
43+
self.attributes = attributes or {}
3644

37-
def evaluator(self, condition):
38-
""" Method to compare single audience condition against provided user data i.e. attributes.
45+
def is_value_valid_for_exact_conditions(self, value):
46+
""" Method to validate if the value is valid for exact match type evaluation.
3947
4048
Args:
41-
condition: Integer representing the index of condition_data that needs to be used for comparison.
49+
value: Value to validate.
4250
4351
Returns:
44-
Boolean indicating the result of comparing the condition value against the user attributes.
52+
Boolean: True if value is a string type, or a boolean, or is finite. Otherwise False.
4553
"""
54+
if isinstance(value, string_types) or isinstance(value, bool) or validator.is_finite_number(value):
55+
return True
4656

47-
return self.attributes.get(self.condition_data[condition][0]) == self.condition_data[condition][1]
57+
return False
4858

49-
def and_evaluator(self, conditions):
50-
""" Evaluates a list of conditions as if the evaluator had been applied
51-
to each entry and the results AND-ed together
59+
def exact_evaluator(self, index):
60+
""" Evaluate the given exact match condition for the user attributes.
5261
5362
Args:
54-
conditions: List of conditions ex: [operand_1, operand_2]
63+
index: Index of the condition to be evaluated.
5564
5665
Returns:
57-
Boolean: True if all operands evaluate to True
66+
Boolean:
67+
- True if the user attribute value is equal (===) to the condition value.
68+
- False if the user attribute value is not equal (!==) to the condition value.
69+
None:
70+
- if the condition value or user attribute value has an invalid type.
71+
- if there is a mismatch between the user attribute type and the condition value type.
72+
"""
73+
condition_value = self.condition_data[index][1]
74+
user_value = self.attributes.get(self.condition_data[index][0])
75+
76+
if not self.is_value_valid_for_exact_conditions(condition_value) or \
77+
not self.is_value_valid_for_exact_conditions(user_value) or \
78+
not validator.are_values_same_type(condition_value, user_value):
79+
return None
80+
81+
return condition_value == user_value
82+
83+
def exists_evaluator(self, index):
84+
""" Evaluate the given exists match condition for the user attributes.
85+
86+
Args:
87+
index: Index of the condition to be evaluated.
88+
89+
Returns:
90+
Boolean: True if the user attributes have a non-null value for the given condition,
91+
otherwise False.
92+
"""
93+
attr_name = self.condition_data[index][0]
94+
return self.attributes.get(attr_name) is not None
95+
96+
def greater_than_evaluator(self, index):
97+
""" Evaluate the given greater than match condition for the user attributes.
98+
99+
Args:
100+
index: Index of the condition to be evaluated.
101+
102+
Returns:
103+
Boolean:
104+
- True if the user attribute value is greater than the condition value.
105+
- False if the user attribute value is less than or equal to the condition value.
106+
None: if the condition value isn't finite or the user attribute value isn't finite.
58107
"""
108+
condition_value = self.condition_data[index][1]
109+
user_value = self.attributes.get(self.condition_data[index][0])
59110

60-
for condition in conditions:
61-
result = self.evaluate(condition)
62-
if result is False:
63-
return False
111+
if not validator.is_finite_number(condition_value) or not validator.is_finite_number(user_value):
112+
return None
64113

65-
return True
114+
return user_value > condition_value
66115

67-
def or_evaluator(self, conditions):
68-
""" Evaluates a list of conditions as if the evaluator had been applied
69-
to each entry and the results OR-ed together
116+
def less_than_evaluator(self, index):
117+
""" Evaluate the given less than match condition for the user attributes.
70118
71119
Args:
72-
conditions: List of conditions ex: [operand_1, operand_2]
120+
index: Index of the condition to be evaluated.
73121
74122
Returns:
75-
Boolean: True if any operand evaluates to True
123+
Boolean:
124+
- True if the user attribute value is less than the condition value.
125+
- False if the user attribute value is greater than or equal to the condition value.
126+
None: if the condition value isn't finite or the user attribute value isn't finite.
76127
"""
128+
condition_value = self.condition_data[index][1]
129+
user_value = self.attributes.get(self.condition_data[index][0])
77130

78-
for condition in conditions:
79-
result = self.evaluate(condition)
80-
if result is True:
81-
return True
131+
if not validator.is_finite_number(condition_value) or not validator.is_finite_number(user_value):
132+
return None
82133

83-
return False
134+
return user_value < condition_value
84135

85-
def not_evaluator(self, single_condition):
86-
""" Evaluates a list of conditions as if the evaluator had been applied
87-
to a single entry and NOT was applied to the result.
136+
def substring_evaluator(self, index):
137+
""" Evaluate the given substring match condition for the given user attributes.
88138
89139
Args:
90-
single_condition: List of of a single condition ex: [operand_1]
140+
index: Index of the condition to be evaluated.
91141
92142
Returns:
93-
Boolean: True if the operand evaluates to False
143+
Boolean:
144+
- True if the condition value is a substring of the user attribute value.
145+
- False if the condition value is not a substring of the user attribute value.
146+
None: if the condition value isn't a string or the user attribute value isn't a string.
94147
"""
95-
if len(single_condition) != 1:
96-
return False
148+
condition_value = self.condition_data[index][1]
149+
user_value = self.attributes.get(self.condition_data[index][0])
150+
151+
if not isinstance(condition_value, string_types) or not isinstance(user_value, string_types):
152+
return None
97153

98-
return not self.evaluate(single_condition[0])
154+
return condition_value in user_value
99155

100-
OPERATORS = {
101-
ConditionalOperatorTypes.AND: and_evaluator,
102-
ConditionalOperatorTypes.OR: or_evaluator,
103-
ConditionalOperatorTypes.NOT: not_evaluator
156+
EVALUATORS_BY_MATCH_TYPE = {
157+
ConditionMatchTypes.EXACT: exact_evaluator,
158+
ConditionMatchTypes.EXISTS: exists_evaluator,
159+
ConditionMatchTypes.GREATER_THAN: greater_than_evaluator,
160+
ConditionMatchTypes.LESS_THAN: less_than_evaluator,
161+
ConditionMatchTypes.SUBSTRING: substring_evaluator
104162
}
105163

106-
def evaluate(self, conditions):
107-
""" Top level method to evaluate audience conditions.
164+
def evaluate(self, index):
165+
""" Given a custom attribute audience condition and user attributes, evaluate the
166+
condition against the attributes.
108167
109168
Args:
110-
conditions: Nested list of and/or conditions.
111-
Ex: ['and', operand_1, ['or', operand_2, operand_3]]
169+
index: Index of the condition to be evaluated.
112170
113171
Returns:
114-
Boolean result of evaluating the conditions evaluate
172+
Boolean:
173+
- True if the user attributes match the given condition.
174+
- False if the user attributes don't match the given condition.
175+
None: if the user attributes and condition can't be evaluated.
115176
"""
116177

117-
if isinstance(conditions, list):
118-
if conditions[0] in DEFAULT_OPERATOR_TYPES:
119-
return self.OPERATORS[conditions[0]](self, conditions[1:])
120-
else:
121-
return False
178+
if self.condition_data[index][2] != self.CUSTOM_ATTRIBUTE_CONDITION_TYPE:
179+
return None
180+
181+
condition_match = self.condition_data[index][3]
182+
if condition_match is None:
183+
condition_match = ConditionMatchTypes.EXACT
184+
185+
if condition_match not in self.EVALUATORS_BY_MATCH_TYPE:
186+
return None
122187

123-
return self.evaluator(conditions)
188+
return self.EVALUATORS_BY_MATCH_TYPE[condition_match](self, index)
124189

125190

126191
class ConditionDecoder(object):
@@ -157,9 +222,14 @@ def _audience_condition_deserializer(obj_dict):
157222
obj_dict: Dict representing one audience condition.
158223
159224
Returns:
160-
List consisting of condition key and corresponding value.
225+
List consisting of condition key with corresponding value, type and match.
161226
"""
162-
return [obj_dict.get('name'), obj_dict.get('value')]
227+
return [
228+
obj_dict.get('name'),
229+
obj_dict.get('value'),
230+
obj_dict.get('type'),
231+
obj_dict.get('match')
232+
]
163233

164234

165235
def loads(conditions_string):

0 commit comments

Comments
 (0)