Skip to content
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

refact: Decorate input validation in APIs #163

Closed
wants to merge 1 commit into from
Closed
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
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