-
Notifications
You must be signed in to change notification settings - Fork 36
feat (audience match types): Update condition evaluator for new audience match types #146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3c8f6f0
b519b34
7a038a9
9c8ae5b
2262ddc
96088c1
2a8e931
aa2030c
9fcf5af
88fa25d
e6bddbb
992ce35
e9d3b48
88159da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
# Copyright 2016, Optimizely | ||
# Copyright 2016, 2018, Optimizely | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
|
@@ -12,115 +12,180 @@ | |
# limitations under the License. | ||
|
||
import json | ||
import numbers | ||
|
||
from six import string_types | ||
|
||
class ConditionalOperatorTypes(object): | ||
from . import validator | ||
|
||
|
||
class ConditionOperatorTypes(object): | ||
AND = 'and' | ||
OR = 'or' | ||
NOT = 'not' | ||
|
||
|
||
DEFAULT_OPERATOR_TYPES = [ | ||
ConditionalOperatorTypes.AND, | ||
ConditionalOperatorTypes.OR, | ||
ConditionalOperatorTypes.NOT | ||
] | ||
class ConditionMatchTypes(object): | ||
EXACT = 'exact' | ||
EXISTS = 'exists' | ||
GREATER_THAN = 'gt' | ||
LESS_THAN = 'lt' | ||
SUBSTRING = 'substring' | ||
|
||
|
||
class CustomAttributeConditionEvaluator(object): | ||
""" Class encapsulating methods to be used in audience leaf condition evaluation. """ | ||
|
||
class ConditionEvaluator(object): | ||
""" Class encapsulating methods to be used in audience condition evaluation. """ | ||
CUSTOM_ATTRIBUTE_CONDITION_TYPE = 'custom_attribute' | ||
|
||
def __init__(self, condition_data, attributes): | ||
self.condition_data = condition_data | ||
self.attributes = attributes | ||
self.attributes = attributes or {} | ||
|
||
def evaluator(self, condition): | ||
""" Method to compare single audience condition against provided user data i.e. attributes. | ||
def is_value_valid_for_exact_conditions(self, value): | ||
nchilada marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" Method to validate if the value is valid for exact match type evaluation. | ||
|
||
Args: | ||
condition: Integer representing the index of condition_data that needs to be used for comparison. | ||
value: Value to validate. | ||
|
||
Returns: | ||
Boolean indicating the result of comparing the condition value against the user attributes. | ||
Boolean: True if value is a string type, or a boolean, or is finite. Otherwise False. | ||
""" | ||
if isinstance(value, string_types) or isinstance(value, bool) or validator.is_finite_number(value): | ||
return True | ||
|
||
return self.attributes.get(self.condition_data[condition][0]) == self.condition_data[condition][1] | ||
return False | ||
|
||
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 | ||
def exact_evaluator(self, index): | ||
""" Evaluate the given exact match condition for the user attributes. | ||
|
||
Args: | ||
conditions: List of conditions ex: [operand_1, operand_2] | ||
index: Index of the condition to be evaluated. | ||
|
||
Returns: | ||
Boolean: True if all operands evaluate to True | ||
Boolean: | ||
- True if the user attribute value is equal (===) to the condition value. | ||
- False if the user attribute value is not equal (!==) to the condition value. | ||
None: | ||
- if the condition value or user attribute value has an invalid type. | ||
- if there is a mismatch between the user attribute type and the condition value type. | ||
""" | ||
condition_value = self.condition_data[index][1] | ||
user_value = self.attributes.get(self.condition_data[index][0]) | ||
|
||
if not self.is_value_valid_for_exact_conditions(condition_value) or \ | ||
not self.is_value_valid_for_exact_conditions(user_value) or \ | ||
not validator.are_values_same_type(condition_value, user_value): | ||
return None | ||
|
||
return condition_value == user_value | ||
|
||
def exists_evaluator(self, index): | ||
""" Evaluate the given exists match condition for the user attributes. | ||
|
||
Args: | ||
index: Index of the condition to be evaluated. | ||
|
||
Returns: | ||
Boolean: True if the user attributes have a non-null value for the given condition, | ||
otherwise False. | ||
""" | ||
attr_name = self.condition_data[index][0] | ||
return self.attributes.get(attr_name) is not None | ||
|
||
def greater_than_evaluator(self, index): | ||
""" Evaluate the given greater than match condition for the user attributes. | ||
|
||
Args: | ||
index: Index of the condition to be evaluated. | ||
|
||
Returns: | ||
Boolean: | ||
- True if the user attribute value is greater than the condition value. | ||
- False if the user attribute value is less than or equal to the condition value. | ||
None: if the condition value isn't finite or the user attribute value isn't finite. | ||
""" | ||
condition_value = self.condition_data[index][1] | ||
user_value = self.attributes.get(self.condition_data[index][0]) | ||
|
||
for condition in conditions: | ||
result = self.evaluate(condition) | ||
if result is False: | ||
return False | ||
if not validator.is_finite_number(condition_value) or not validator.is_finite_number(user_value): | ||
return None | ||
|
||
return True | ||
return user_value > condition_value | ||
|
||
def or_evaluator(self, conditions): | ||
""" Evaluates a list of conditions as if the evaluator had been applied | ||
to each entry and the results OR-ed together | ||
def less_than_evaluator(self, index): | ||
""" Evaluate the given less than match condition for the user attributes. | ||
|
||
Args: | ||
conditions: List of conditions ex: [operand_1, operand_2] | ||
index: Index of the condition to be evaluated. | ||
|
||
Returns: | ||
Boolean: True if any operand evaluates to True | ||
Boolean: | ||
- True if the user attribute value is less than the condition value. | ||
- False if the user attribute value is greater than or equal to the condition value. | ||
None: if the condition value isn't finite or the user attribute value isn't finite. | ||
""" | ||
condition_value = self.condition_data[index][1] | ||
user_value = self.attributes.get(self.condition_data[index][0]) | ||
|
||
for condition in conditions: | ||
result = self.evaluate(condition) | ||
if result is True: | ||
return True | ||
if not validator.is_finite_number(condition_value) or not validator.is_finite_number(user_value): | ||
return None | ||
|
||
return False | ||
return user_value < condition_value | ||
|
||
def not_evaluator(self, single_condition): | ||
""" Evaluates a list of conditions as if the evaluator had been applied | ||
to a single entry and NOT was applied to the result. | ||
def substring_evaluator(self, index): | ||
""" Evaluate the given substring match condition for the given user attributes. | ||
|
||
Args: | ||
single_condition: List of of a single condition ex: [operand_1] | ||
index: Index of the condition to be evaluated. | ||
|
||
Returns: | ||
Boolean: True if the operand evaluates to False | ||
Boolean: | ||
- True if the condition value is a substring of the user attribute value. | ||
- False if the condition value is not a substring of the user attribute value. | ||
None: if the condition value isn't a string or the user attribute value isn't a string. | ||
""" | ||
if len(single_condition) != 1: | ||
return False | ||
condition_value = self.condition_data[index][1] | ||
user_value = self.attributes.get(self.condition_data[index][0]) | ||
|
||
if not isinstance(condition_value, string_types) or not isinstance(user_value, string_types): | ||
return None | ||
|
||
return not self.evaluate(single_condition[0]) | ||
return condition_value in user_value | ||
|
||
OPERATORS = { | ||
ConditionalOperatorTypes.AND: and_evaluator, | ||
ConditionalOperatorTypes.OR: or_evaluator, | ||
ConditionalOperatorTypes.NOT: not_evaluator | ||
EVALUATORS_BY_MATCH_TYPE = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm guessing @aliabbasrizvi would want this, too, to be declared above? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🙇 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose can't declare it before defining evaluator methods |
||
ConditionMatchTypes.EXACT: exact_evaluator, | ||
ConditionMatchTypes.EXISTS: exists_evaluator, | ||
ConditionMatchTypes.GREATER_THAN: greater_than_evaluator, | ||
ConditionMatchTypes.LESS_THAN: less_than_evaluator, | ||
ConditionMatchTypes.SUBSTRING: substring_evaluator | ||
} | ||
|
||
def evaluate(self, conditions): | ||
""" Top level method to evaluate audience conditions. | ||
def evaluate(self, index): | ||
""" Given a custom attribute audience condition and user attributes, evaluate the | ||
condition against the attributes. | ||
|
||
Args: | ||
conditions: Nested list of and/or conditions. | ||
Ex: ['and', operand_1, ['or', operand_2, operand_3]] | ||
index: Index of the condition to be evaluated. | ||
|
||
Returns: | ||
Boolean result of evaluating the conditions evaluate | ||
Boolean: | ||
- True if the user attributes match the given condition. | ||
- False if the user attributes don't match the given condition. | ||
None: if the user attributes and condition can't be evaluated. | ||
""" | ||
|
||
if isinstance(conditions, list): | ||
if conditions[0] in DEFAULT_OPERATOR_TYPES: | ||
return self.OPERATORS[conditions[0]](self, conditions[1:]) | ||
else: | ||
return False | ||
if self.condition_data[index][2] != self.CUSTOM_ATTRIBUTE_CONDITION_TYPE: | ||
return None | ||
|
||
condition_match = self.condition_data[index][3] | ||
if condition_match is None: | ||
condition_match = ConditionMatchTypes.EXACT | ||
|
||
if condition_match not in self.EVALUATORS_BY_MATCH_TYPE: | ||
return None | ||
|
||
return self.evaluator(conditions) | ||
return self.EVALUATORS_BY_MATCH_TYPE[condition_match](self, index) | ||
|
||
|
||
class ConditionDecoder(object): | ||
|
@@ -157,9 +222,14 @@ def _audience_condition_deserializer(obj_dict): | |
obj_dict: Dict representing one audience condition. | ||
|
||
Returns: | ||
List consisting of condition key and corresponding value. | ||
List consisting of condition key with corresponding value, type and match. | ||
""" | ||
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') | ||
] | ||
|
||
|
||
def loads(conditions_string): | ||
|
Uh oh!
There was an error while loading. Please reload this page.