Skip to content

feat (audience match types): Condition evaluator and Project Config PRs consolidation. #153

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

Merged
merged 1 commit into from
Dec 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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