Skip to content

Commit

Permalink
Merge 16a02d6 into a6709f2
Browse files Browse the repository at this point in the history
  • Loading branch information
oakbani committed Jan 18, 2019
2 parents a6709f2 + 16a02d6 commit 02c8216
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 192 deletions.
4 changes: 1 addition & 3 deletions optimizely/helpers/enums.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2016-2018, Optimizely
# Copyright 2016-2019, 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 Down Expand Up @@ -28,10 +28,8 @@ class DatafileVersions(object):

class Errors(object):
INVALID_ATTRIBUTE_ERROR = 'Provided attribute is not in datafile.'
INVALID_ATTRIBUTE_FORMAT = 'Attributes provided are in an invalid format.'
INVALID_AUDIENCE_ERROR = 'Provided audience is not in datafile.'
INVALID_DATAFILE = 'Datafile has invalid format. Failing "{}".'
INVALID_EVENT_TAG_FORMAT = 'Event tags provided are in an invalid format.'
INVALID_EXPERIMENT_KEY_ERROR = 'Provided experiment is not in datafile.'
INVALID_EVENT_KEY_ERROR = 'Provided event is not in datafile.'
INVALID_FEATURE_KEY_ERROR = 'Provided feature key is not in the datafile.'
Expand Down
73 changes: 68 additions & 5 deletions optimizely/helpers/validator.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2016-2018, Optimizely
# Copyright 2016-2019, 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 @@ -11,14 +11,18 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import functools
import inspect
import json
import jsonschema
import math
import numbers
from six import string_types

from optimizely import exceptions
from optimizely.user_profile import UserProfile
from . import constants
from . import enums


def is_datafile_valid(datafile):
Expand Down Expand Up @@ -98,7 +102,7 @@ def is_error_handler_valid(error_handler):


def are_attributes_valid(attributes):
""" Determine if attributes provided are dict or not.
""" Determine if attributes provided are dict if not None.
Args:
attributes: User attributes which need to be validated.
Expand All @@ -107,11 +111,11 @@ def are_attributes_valid(attributes):
Boolean depending upon whether attributes are in valid format or not.
"""

return type(attributes) is dict
return attributes is None or type(attributes) is dict


def are_event_tags_valid(event_tags):
""" Determine if event tags provided are dict or not.
""" Determine if event tags provided are dict if not None.
Args:
event_tags: Event tags which need to be validated.
Expand All @@ -120,7 +124,7 @@ def are_event_tags_valid(event_tags):
Boolean depending upon whether event_tags are in valid format or not.
"""

return type(event_tags) is dict
return event_tags is None or type(event_tags) is dict


def is_user_profile_valid(user_profile):
Expand Down Expand Up @@ -253,3 +257,62 @@ def are_values_same_type(first_val, second_val):
return True

return False


def validate_inputs(return_value):
""" Method to decorate a function by performing argument validation
before executing the function.
Args:
return_value: Value to return if argument validation fails.
Returns:
Function: The decorated function.
"""
validators = {}
validators['attributes'] = are_attributes_valid
validators['event_key'] = is_non_empty_string
validators['event_tags'] = are_event_tags_valid
validators['experiment_key'] = is_non_empty_string
validators['feature_key'] = is_non_empty_string
validators['user_id'] = lambda user_id: isinstance(user_id, string_types)
validators['variable_key'] = is_non_empty_string

exceptions_dict = {}
exceptions_dict['attributes'] = exceptions.InvalidAttributeException
exceptions_dict['event_tags'] = exceptions.InvalidEventTagException

def call_error_handler(self, key):
exception_to_handle = exceptions_dict.get(key)
if exception_to_handle is not None:
self.error_handler.handle_error(
exception_to_handle(enums.Errors.INVALID_INPUT_ERROR.format(key))
)

def wrapper_decorator(func):
args_keys = inspect.getargspec(func).args
# ignore self
args_keys = args_keys[1:]

@functools.wraps(func)
def wrapper_inner(self, *args, **kwargs):
if not self.is_valid:
self.logger.error(enums.Errors.INVALID_DATAFILE.format(func.__name__))
return return_value

# Add positional args in kwargs
for i in range(len(args)):
kwargs[args_keys[i]] = args[i]

for arg_key in kwargs:
if arg_key in validators:
if not validators[arg_key](kwargs[arg_key]):
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format(arg_key))
call_error_handler(self, arg_key)
return return_value

return func(self, **kwargs)

return wrapper_inner

return wrapper_decorator
140 changes: 8 additions & 132 deletions optimizely/optimizely.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,30 +109,6 @@ def _validate_instantiation_options(self, datafile, skip_json_validation):
if not validator.is_error_handler_valid(self.error_handler):
raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT_ERROR.format('error_handler'))

def _validate_user_inputs(self, attributes=None, event_tags=None):
""" Helper method to validate user inputs.
Args:
attributes: Dict representing user attributes.
event_tags: Dict representing metadata associated with an event.
Returns:
Boolean True if inputs are valid. False otherwise.
"""

if attributes and not validator.are_attributes_valid(attributes):
self.logger.error('Provided attributes are in an invalid format.')
self.error_handler.handle_error(exceptions.InvalidAttributeException(enums.Errors.INVALID_ATTRIBUTE_FORMAT))
return False

if event_tags and not validator.are_event_tags_valid(event_tags):
self.logger.error('Provided event tags are in an invalid format.')
self.error_handler.handle_error(exceptions.InvalidEventTagException(enums.Errors.INVALID_EVENT_TAG_FORMAT))
return False

return True

def _send_impression_event(self, experiment, variation, user_id, attributes):
""" Helper method to send impression event.
Expand Down Expand Up @@ -161,6 +137,7 @@ def _send_impression_event(self, experiment, variation, user_id, attributes):
self.notification_center.send_notifications(enums.NotificationTypes.ACTIVATE,
experiment, user_id, attributes, variation, impression_event)

@validator.validate_inputs(return_value=None)
def _get_feature_variable_for_type(self, feature_key, variable_key, variable_type, user_id, attributes):
""" Helper method to determine value for a certain variable attached to a feature flag based on type of variable.
Expand All @@ -177,20 +154,6 @@ def _get_feature_variable_for_type(self, feature_key, variable_key, variable_typ
- Variable key is invalid.
- Mismatch with type of variable.
"""
if not validator.is_non_empty_string(feature_key):
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('feature_key'))
return None

if not validator.is_non_empty_string(variable_key):
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('variable_key'))
return None

if not isinstance(user_id, string_types):
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
return None

if not self._validate_user_inputs(attributes):
return None

feature_flag = self.config.get_feature_from_key(feature_key)
if not feature_flag:
Expand Down Expand Up @@ -227,6 +190,7 @@ def _get_feature_variable_for_type(self, feature_key, variable_key, variable_typ

return actual_value

@validator.validate_inputs(return_value=None)
def activate(self, experiment_key, user_id, attributes=None):
""" Buckets visitor and sends impression event to Optimizely.
Expand All @@ -239,19 +203,6 @@ def activate(self, experiment_key, user_id, attributes=None):
Variation key representing the variation the user will be bucketed in.
None if user is not in experiment or if experiment is not Running.
"""

if not self.is_valid:
self.logger.error(enums.Errors.INVALID_DATAFILE.format('activate'))
return None

if not validator.is_non_empty_string(experiment_key):
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('experiment_key'))
return None

if not isinstance(user_id, string_types):
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
return None

variation_key = self.get_variation(experiment_key, user_id, attributes)

if not variation_key:
Expand All @@ -267,6 +218,7 @@ def activate(self, experiment_key, user_id, attributes=None):

return variation.key

@validator.validate_inputs(return_value=None)
def track(self, event_key, user_id, attributes=None, event_tags=None):
""" Send conversion event to Optimizely.
Expand All @@ -276,22 +228,6 @@ def track(self, event_key, user_id, attributes=None, event_tags=None):
attributes: Dict representing visitor attributes and values which need to be recorded.
event_tags: Dict representing metadata associated with the event.
"""

if not self.is_valid:
self.logger.error(enums.Errors.INVALID_DATAFILE.format('track'))
return

if not validator.is_non_empty_string(event_key):
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('event_key'))
return

if not isinstance(user_id, string_types):
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
return

if not self._validate_user_inputs(attributes, event_tags):
return

event = self.config.get_event(event_key)
if not event:
self.logger.info('Not tracking user "%s" for event "%s".' % (user_id, event_key))
Expand All @@ -310,6 +246,7 @@ def track(self, event_key, user_id, attributes=None, event_tags=None):
self.notification_center.send_notifications(enums.NotificationTypes.TRACK, event_key, user_id,
attributes, event_tags, conversion_event)

@validator.validate_inputs(return_value=None)
def get_variation(self, experiment_key, user_id, attributes=None):
""" Gets variation where user will be bucketed.
Expand All @@ -322,19 +259,6 @@ def get_variation(self, experiment_key, user_id, attributes=None):
Variation key representing the variation the user will be bucketed in.
None if user is not in experiment or if experiment is not Running.
"""

if not self.is_valid:
self.logger.error(enums.Errors.INVALID_DATAFILE.format('get_variation'))
return None

if not validator.is_non_empty_string(experiment_key):
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('experiment_key'))
return None

if not isinstance(user_id, string_types):
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
return None

experiment = self.config.get_experiment_from_key(experiment_key)

if not experiment:
Expand All @@ -344,15 +268,13 @@ def get_variation(self, experiment_key, user_id, attributes=None):
))
return None

if not self._validate_user_inputs(attributes):
return None

variation = self.decision_service.get_variation(experiment, user_id, attributes)
if variation:
return variation.key

return None

@validator.validate_inputs(return_value=False)
def is_feature_enabled(self, feature_key, user_id, attributes=None):
""" Returns true if the feature is enabled for the given user.
Expand All @@ -365,21 +287,6 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None):
True if the feature is enabled for the user. False otherwise.
"""

if not self.is_valid:
self.logger.error(enums.Errors.INVALID_DATAFILE.format('is_feature_enabled'))
return False

if not validator.is_non_empty_string(feature_key):
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('feature_key'))
return False

if not isinstance(user_id, string_types):
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
return False

if not self._validate_user_inputs(attributes):
return False

feature = self.config.get_feature_from_key(feature_key)
if not feature:
return False
Expand All @@ -400,6 +307,7 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None):
self.logger.info('Feature "%s" is not enabled for user "%s".' % (feature_key, user_id))
return False

@validator.validate_inputs(return_value=[])
def get_enabled_features(self, user_id, attributes=None):
""" Returns the list of features that are enabled for the user.
Expand All @@ -412,16 +320,6 @@ def get_enabled_features(self, user_id, attributes=None):
"""

enabled_features = []
if not self.is_valid:
self.logger.error(enums.Errors.INVALID_DATAFILE.format('get_enabled_features'))
return enabled_features

if not isinstance(user_id, string_types):
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
return enabled_features

if not self._validate_user_inputs(attributes):
return enabled_features

for feature in self.config.feature_key_map.values():
if self.is_feature_enabled(feature.key, user_id, attributes):
Expand Down Expand Up @@ -505,6 +403,7 @@ def get_feature_variable_string(self, feature_key, variable_key, user_id, attrib
variable_type = entities.Variable.Type.STRING
return self._get_feature_variable_for_type(feature_key, variable_key, variable_type, user_id, attributes)

@validator.validate_inputs(return_value=False)
def set_forced_variation(self, experiment_key, user_id, variation_key):
""" Force a user into a variation for a given experiment.
Expand All @@ -518,20 +417,9 @@ def set_forced_variation(self, experiment_key, user_id, variation_key):
A boolean value that indicates if the set completed successfully.
"""

if not self.is_valid:
self.logger.error(enums.Errors.INVALID_DATAFILE.format('set_forced_variation'))
return False

if not validator.is_non_empty_string(experiment_key):
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('experiment_key'))
return False

if not isinstance(user_id, string_types):
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
return False

return self.config.set_forced_variation(experiment_key, user_id, variation_key)

@validator.validate_inputs(return_value=None)
def get_forced_variation(self, experiment_key, user_id):
""" Gets the forced variation for a given user and experiment.
Expand All @@ -543,17 +431,5 @@ def get_forced_variation(self, experiment_key, user_id):
The forced variation key. None if no forced variation key.
"""

if not self.is_valid:
self.logger.error(enums.Errors.INVALID_DATAFILE.format('get_forced_variation'))
return None

if not validator.is_non_empty_string(experiment_key):
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('experiment_key'))
return None

if not isinstance(user_id, string_types):
self.logger.error(enums.Errors.INVALID_INPUT_ERROR.format('user_id'))
return None

forced_variation = self.config.get_forced_variation(experiment_key, user_id)
return forced_variation.key if forced_variation else None
Loading

0 comments on commit 02c8216

Please sign in to comment.