Skip to content

Commit

Permalink
Merge 8e596c5 into e5ec118
Browse files Browse the repository at this point in the history
  • Loading branch information
msohailhussain committed Dec 18, 2018
2 parents e5ec118 + 8e596c5 commit de1b203
Show file tree
Hide file tree
Showing 11 changed files with 1,588 additions and 158 deletions.
22 changes: 15 additions & 7 deletions optimizely/helpers/audience.py
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
Expand All @@ -12,6 +12,7 @@
# limitations under the License.

from . import condition as condition_helper
from . import condition_tree_evaluator


def is_match(audience, attributes):
Expand All @@ -24,8 +25,15 @@ def is_match(audience, attributes):
Return:
Boolean representing if user satisfies audience conditions or not.
"""
condition_evaluator = condition_helper.ConditionEvaluator(audience.conditionList, attributes)
return condition_evaluator.evaluate(audience.conditionStructure)
custom_attr_condition_evaluator = condition_helper.CustomAttributeConditionEvaluator(
audience.conditionList, attributes)

is_match = condition_tree_evaluator.evaluate(
audience.conditionStructure,
lambda index: custom_attr_condition_evaluator.evaluate(index)
)

return is_match or False


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

# Return False if there are audiences, but no attributes
if not attributes:
return False
if attributes is None:
attributes = {}

# Return True if conditions for any one audience are met
for audience_id in experiment.audienceIds:
Expand Down
190 changes: 130 additions & 60 deletions optimizely/helpers/condition.py
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
Expand All @@ -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):
""" 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 = {
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):
Expand Down Expand Up @@ -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):
Expand Down
Loading

0 comments on commit de1b203

Please sign in to comment.