Skip to content

Commit

Permalink
Merge 15fa71b into ce0827d
Browse files Browse the repository at this point in the history
  • Loading branch information
msohailhussain committed Aug 27, 2020
2 parents ce0827d + 15fa71b commit 861646d
Show file tree
Hide file tree
Showing 5 changed files with 1,083 additions and 7 deletions.
334 changes: 333 additions & 1 deletion optimizely/helpers/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from . import validator
from .enums import CommonAudienceEvaluationLogs as audience_logs
from .enums import Errors, SemverType


class ConditionOperatorTypes(object):
Expand All @@ -30,7 +31,14 @@ class ConditionMatchTypes(object):
EXACT = 'exact'
EXISTS = 'exists'
GREATER_THAN = 'gt'
GREATER_THAN_OR_EQUAL = 'ge'
LESS_THAN = 'lt'
LESS_THAN_OR_EQUAL = 'le'
SEMVER_EQ = 'semver_eq'
SEMVER_GE = 'semver_ge'
SEMVER_GT = 'semver_gt'
SEMVER_LE = 'semver_le'
SEMVER_LT = 'semver_lt'
SUBSTRING = 'substring'


Expand Down Expand Up @@ -84,6 +92,105 @@ def is_value_a_number(self, value):

return False

def is_pre_release(self, target):
""" Method to check if the given version contains "-"
Args:
target: Given version in string.
Returns:
Boolean:
- True if the given version does contain "-"
- False if it doesn't
"""
return SemverType.IS_PRE_RELEASE in target

def is_build(self, target):
""" Method to check if the given version contains "+"
Args:
target: Given version in string.
Returns:
Boolean:
- True if the given version does contain "+"
- False if it doesn't
"""
return SemverType.IS_BUILD in target

def has_white_space(self, target):
""" Method to check if the given version contains " " (white space)
Args:
target: Given version in string.
Returns:
Boolean:
- True if the given version does contain " "
- False if it doesn't
"""
return SemverType.HAS_WHITE_SPACE in target

def compare_user_version_with_target_version(self, index):
""" Method to compare user version with target version.
Args:
index: Index of the condition to be evaluated.
Returns:
Int:
- 0 if user version is equal to target version.
- 1 if user version is greater than target version.
- -1 if user version is less than target version or, in case of exact string match, doesn't match the target
version.
None:
- if the user version value is not string type or is null.
"""
condition_name = self.condition_data[index][0]
target_version = self.condition_data[index][1]
user_version = self.attributes.get(condition_name)

if not isinstance(target_version, string_types):
self.logger.warning(audience_logs.UNKNOWN_CONDITION_VALUE.format(self._get_condition_json(index), ))
return None

if not isinstance(user_version, string_types):
self.logger.warning(
audience_logs.UNEXPECTED_TYPE.format(
self._get_condition_json(index), type(user_version), condition_name
)
)
return None

target_version_parts = self.split_semantic_version(target_version)
if target_version_parts is None:
return None

user_version_parts = self.split_semantic_version(user_version)
if user_version_parts is None:
return None

user_version_parts_len = len(user_version_parts)

for (idx, _) in enumerate(target_version_parts):
if user_version_parts_len <= idx:
return 1 if self.is_pre_release(target_version) else -1
elif not user_version_parts[idx].isdigit():
if user_version_parts[idx] < target_version_parts[idx]:
return -1
elif user_version_parts[idx] > target_version_parts[idx]:
return 1
else:
user_version_part = int(user_version_parts[idx])
target_version_part = int(target_version_parts[idx])
if user_version_part > target_version_part:
return 1
elif user_version_part < target_version_part:
return -1
if self.is_pre_release(user_version) and not self.is_pre_release(target_version):
return -1
return 0

def exact_evaluator(self, index):
""" Evaluate the given exact match condition for the user attributes.
Expand Down Expand Up @@ -171,6 +278,40 @@ def greater_than_evaluator(self, index):

return user_value > condition_value

def greater_than_or_equal_evaluator(self, index):
""" Evaluate the given greater than or equal to 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 or equal to the condition value.
- False if the user attribute value is less than the condition value.
None: if the condition value isn't finite or the user attribute value isn't finite.
"""
condition_name = self.condition_data[index][0]
condition_value = self.condition_data[index][1]
user_value = self.attributes.get(condition_name)

if not validator.is_finite_number(condition_value):
self.logger.warning(audience_logs.UNKNOWN_CONDITION_VALUE.format(self._get_condition_json(index)))
return None

if not self.is_value_a_number(user_value):
self.logger.warning(
audience_logs.UNEXPECTED_TYPE.format(self._get_condition_json(index), type(user_value), condition_name)
)
return None

if not validator.is_finite_number(user_value):
self.logger.warning(
audience_logs.INFINITE_ATTRIBUTE_VALUE.format(self._get_condition_json(index), condition_name)
)
return None

return user_value >= condition_value

def less_than_evaluator(self, index):
""" Evaluate the given less than match condition for the user attributes.
Expand Down Expand Up @@ -205,6 +346,40 @@ def less_than_evaluator(self, index):

return user_value < condition_value

def less_than_or_equal_evaluator(self, index):
""" Evaluate the given less than or equal to match condition for the user attributes.
Args:
index: Index of the condition to be evaluated.
Returns:
Boolean:
- True if the user attribute value is less than or equal to the condition value.
- False if the user attribute value is greater than the condition value.
None: if the condition value isn't finite or the user attribute value isn't finite.
"""
condition_name = self.condition_data[index][0]
condition_value = self.condition_data[index][1]
user_value = self.attributes.get(condition_name)

if not validator.is_finite_number(condition_value):
self.logger.warning(audience_logs.UNKNOWN_CONDITION_VALUE.format(self._get_condition_json(index)))
return None

if not self.is_value_a_number(user_value):
self.logger.warning(
audience_logs.UNEXPECTED_TYPE.format(self._get_condition_json(index), type(user_value), condition_name)
)
return None

if not validator.is_finite_number(user_value):
self.logger.warning(
audience_logs.INFINITE_ATTRIBUTE_VALUE.format(self._get_condition_json(index), condition_name)
)
return None

return user_value <= condition_value

def substring_evaluator(self, index):
""" Evaluate the given substring match condition for the given user attributes.
Expand Down Expand Up @@ -233,14 +408,171 @@ def substring_evaluator(self, index):

return condition_value in user_value

def semver_equal_evaluator(self, index):
""" Evaluate the given semantic version equal match target version for the user version.
Args:
index: Index of the condition to be evaluated.
Returns:
Boolean:
- True if the user version is equal (==) to the target version.
- False if the user version is not equal (!=) to the target version.
None:
- if the user version value is not string type or is null.
"""
result = self.compare_user_version_with_target_version(index)
if result is None:
return None

return result == 0

def semver_greater_than_evaluator(self, index):
""" Evaluate the given semantic version greater than match target version for the user version.
Args:
index: Index of the condition to be evaluated.
Returns:
Boolean:
- True if the user version is greater than the target version.
- False if the user version is less than or equal to the target version.
None:
- if the user version value is not string type or is null.
"""
result = self.compare_user_version_with_target_version(index)
if result is None:
return None

return result > 0

def semver_less_than_evaluator(self, index):
""" Evaluate the given semantic version less than match target version for the user version.
Args:
index: Index of the condition to be evaluated.
Returns:
Boolean:
- True if the user version is less than the target version.
- False if the user version is greater than or equal to the target version.
None:
- if the user version value is not string type or is null.
"""
result = self.compare_user_version_with_target_version(index)
if result is None:
return None

return result < 0

def semver_less_than_or_equal_evaluator(self, index):
""" Evaluate the given semantic version less than or equal to match target version for the user version.
Args:
index: Index of the condition to be evaluated.
Returns:
Boolean:
- True if the user version is less than or equal to the target version.
- False if the user version is greater than the target version.
None:
- if the user version value is not string type or is null.
"""
result = self.compare_user_version_with_target_version(index)
if result is None:
return None

return result <= 0

def semver_greater_than_or_equal_evaluator(self, index):
""" Evaluate the given semantic version greater than or equal to match target version for the user version.
Args:
index: Index of the condition to be evaluated.
Returns:
Boolean:
- True if the user version is greater than or equal to the target version.
- False if the user version is less than the target version.
None:
- if the user version value is not string type or is null.
"""
result = self.compare_user_version_with_target_version(index)
if result is None:
return None

return result >= 0

EVALUATORS_BY_MATCH_TYPE = {
ConditionMatchTypes.EXACT: exact_evaluator,
ConditionMatchTypes.EXISTS: exists_evaluator,
ConditionMatchTypes.GREATER_THAN: greater_than_evaluator,
ConditionMatchTypes.GREATER_THAN_OR_EQUAL: greater_than_or_equal_evaluator,
ConditionMatchTypes.LESS_THAN: less_than_evaluator,
ConditionMatchTypes.SUBSTRING: substring_evaluator,
ConditionMatchTypes.LESS_THAN_OR_EQUAL: less_than_or_equal_evaluator,
ConditionMatchTypes.SEMVER_EQ: semver_equal_evaluator,
ConditionMatchTypes.SEMVER_GE: semver_greater_than_or_equal_evaluator,
ConditionMatchTypes.SEMVER_GT: semver_greater_than_evaluator,
ConditionMatchTypes.SEMVER_LE: semver_less_than_or_equal_evaluator,
ConditionMatchTypes.SEMVER_LT: semver_less_than_evaluator,
ConditionMatchTypes.SUBSTRING: substring_evaluator
}

def split_semantic_version(self, target):
""" Method to split the given version.
Args:
target: Given version.
Returns:
List:
- The array of version split into smaller parts i.e major, minor, patch etc
None:
- if the given version is invalid in format
"""
target_prefix = target
target_suffix = ""
target_parts = []

# check that target shouldn't have white space
if self.has_white_space(target):
self.logger.warning(Errors.INVALID_ATTRIBUTE_FORMAT)
return None

# check for pre release e.g. 1.0.0-alpha where 'alpha' is a pre release
# otherwise check for build e.g. 1.0.0+001 where 001 is a build metadata
if self.is_pre_release(target):
target_parts = target.split(SemverType.IS_PRE_RELEASE)
elif self.is_build(target):
target_parts = target.split(SemverType.IS_BUILD)

# split target version into prefix and suffix
if target_parts:
if len(target_parts) < 1:
self.logger.warning(Errors.INVALID_ATTRIBUTE_FORMAT)
return None
target_prefix = str(target_parts[0])
target_suffix = target_parts[1:]

# check dot counts in target_prefix
dot_count = target_prefix.count(".")
if dot_count > 2:
self.logger.warning(Errors.INVALID_ATTRIBUTE_FORMAT)
return None

target_version_parts = target_prefix.split(".")
if len(target_version_parts) != dot_count + 1:
self.logger.warning(Errors.INVALID_ATTRIBUTE_FORMAT)
return None
for part in target_version_parts:
if not part.isdigit():
self.logger.warning(Errors.INVALID_ATTRIBUTE_FORMAT)
return None

if target_suffix:
target_version_parts.extend(target_suffix)
return target_version_parts

def evaluate(self, index):
""" Given a custom attribute audience condition and user attributes, evaluate the
condition against the attributes.
Expand Down
6 changes: 6 additions & 0 deletions optimizely/helpers/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,9 @@ class NotificationTypes(object):
OPTIMIZELY_CONFIG_UPDATE = 'OPTIMIZELY_CONFIG_UPDATE'
TRACK = 'TRACK:event_key, user_id, attributes, event_tags, event'
LOG_EVENT = 'LOG_EVENT:log_event'


class SemverType(object):
IS_PRE_RELEASE = '-'
HAS_WHITE_SPACE = ' '
IS_BUILD = '+'
Loading

0 comments on commit 861646d

Please sign in to comment.