Skip to content

Commit

Permalink
Merge remote-tracking branch 'remotes/origin/master' into oakbani/-do…
Browse files Browse the repository at this point in the history
…nt-target-nan-inf-1e53
  • Loading branch information
oakbani committed Dec 20, 2018
2 parents c532525 + ad0cb1d commit 5b96ff2
Show file tree
Hide file tree
Showing 13 changed files with 2,023 additions and 256 deletions.
14 changes: 14 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,17 @@ addons:
script: "nosetests --with-coverage --cover-package=optimizely"
after_success:
- coveralls

jobs:
include:
- stage: 'Integration tests'
env: SDK=python
language: python
before_install: skip
install:
- "pip install awscli"
before_script:
- "aws s3 cp s3://optimizely-travisci-artifacts/ci/trigger_fullstack-sdk-compat.sh ci/ && chmod u+x ci/trigger_fullstack-sdk-compat.sh"
script:
- "ci/trigger_fullstack-sdk-compat.sh"
after_success: skip
7 changes: 6 additions & 1 deletion optimizely/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,23 @@ def __init__(self, id, key, experimentIds, **kwargs):
class Experiment(BaseEntity):

def __init__(self, id, key, status, audienceIds, variations, forcedVariations,
trafficAllocation, layerId, groupId=None, groupPolicy=None, **kwargs):
trafficAllocation, layerId, audienceConditions=None, groupId=None, groupPolicy=None, **kwargs):
self.id = id
self.key = key
self.status = status
self.audienceIds = audienceIds
self.audienceConditions = audienceConditions
self.variations = variations
self.forcedVariations = forcedVariations
self.trafficAllocation = trafficAllocation
self.layerId = layerId
self.groupId = groupId
self.groupPolicy = groupPolicy

def getAudienceConditionsOrIds(self):
""" Returns audienceConditions if present, otherwise audienceIds. """
return self.audienceConditions if self.audienceConditions is not None else self.audienceIds


class FeatureFlag(BaseEntity):

Expand Down
56 changes: 30 additions & 26 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,20 +12,7 @@
# limitations under the License.

from . import condition as condition_helper


def is_match(audience, attributes):
""" Given audience information and user attributes determine if user meets the conditions.
Args:
audience: Dict representing the audience.
attributes: Dict representing user attributes which will be used in determining if the audience conditions are met.
Return:
Boolean representing if user satisfies audience conditions or not.
"""
condition_evaluator = condition_helper.ConditionEvaluator(audience.conditionList, attributes)
return condition_evaluator.evaluate(audience.conditionStructure)
from . import condition_tree_evaluator


def is_user_in_experiment(config, experiment, attributes):
Expand All @@ -34,25 +21,42 @@ 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.
"""

# Return True in case there are no audiences
if not experiment.audienceIds:
audience_conditions = experiment.getAudienceConditionsOrIds()
if audience_conditions is None or audience_conditions == []:
return True

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

def evaluate_custom_attr(audienceId, index):
audience = config.get_audience(audienceId)
custom_attr_condition_evaluator = condition_helper.CustomAttributeConditionEvaluator(
audience.conditionList, attributes)

return custom_attr_condition_evaluator.evaluate(index)

def evaluate_audience(audienceId):
audience = config.get_audience(audienceId)

if audience is None:
return None

# Return True if conditions for any one audience are met
for audience_id in experiment.audienceIds:
audience = config.get_audience(audience_id)
return condition_tree_evaluator.evaluate(
audience.conditionStructure,
lambda index: evaluate_custom_attr(audienceId, index)
)

if is_match(audience, attributes):
return True
eval_result = condition_tree_evaluator.evaluate(
audience_conditions,
evaluate_audience
)

return False
return eval_result or False
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 5b96ff2

Please sign in to comment.