Skip to content
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
117 changes: 75 additions & 42 deletions optimizely/config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from . import project_config
from .error_handler import NoOpErrorHandler as noop_error_handler
from .helpers import enums

from .helpers import validator

ABC = abc.ABCMeta('ABC', (object,), {'__slots__': ()})

Expand Down Expand Up @@ -56,29 +56,75 @@ class StaticConfigManager(BaseConfigManager):
def __init__(self,
datafile=None,
logger=None,
error_handler=None):
error_handler=None,
skip_json_validation=False):
""" Initialize config manager. Datafile has to be provided to use.

Args:
datafile: JSON string representing the Optimizely project.
logger: Provides a logger instance.
error_handler: Provides a handle_error method to handle exceptions.
skip_json_validation: Optional boolean param which allows skipping JSON schema
validation upon object invocation. By default
JSON schema validation will be performed.
"""
super(StaticConfigManager, self).__init__(logger=logger, error_handler=error_handler)
self._config = None
if datafile:
self._config = project_config.ProjectConfig(datafile, self.logger, self.error_handler)
self.validate_schema = not skip_json_validation
self._set_config(datafile)

def _set_config(self, datafile):
""" Looks up and sets datafile and config based on response body.

Args:
datafile: JSON string representing the Optimizely project.
"""

if self.validate_schema:
if not validator.is_datafile_valid(datafile):
self.logger.error(enums.Errors.INVALID_INPUT.format('datafile'))
return

error_msg = None
error_to_handle = None
config = None

try:
config = project_config.ProjectConfig(datafile, self.logger, self.error_handler)
except optimizely_exceptions.UnsupportedDatafileVersionException as error:
error_msg = error.args[0]
error_to_handle = error
except:
error_msg = enums.Errors.INVALID_INPUT.format('datafile')
error_to_handle = optimizely_exceptions.InvalidInputException(error_msg)
finally:
if error_msg:
self.logger.error(error_msg)
self.error_handler.handle_error(error_to_handle)
return

previous_revision = self._config.get_revision() if self._config else None

if previous_revision == config.get_revision():
return

# TODO(ali): Add notification listener.
self._config = config
self.logger.debug(
'Received new datafile and updated config. '
'Old revision number: {}. New revision number: {}.'.format(previous_revision, config.get_revision())
)

def get_config(self):
""" Returns instance of ProjectConfig.

Returns:
ProjectConfig.
ProjectConfig. None if not set.
"""
return self._config


class PollingConfigManager(BaseConfigManager):
class PollingConfigManager(StaticConfigManager):
""" Config manager that polls for the datafile and updated ProjectConfig based on an update interval. """

def __init__(self,
Expand All @@ -88,31 +134,38 @@ def __init__(self,
url=None,
url_template=None,
logger=None,
error_handler=None):
error_handler=None,
skip_json_validation=False):
""" Initialize config manager. One of sdk_key or url has to be set to be able to use.

Args:
sdk_key: Optional string uniquely identifying the datafile.
datafile: Optional JSON string representing the project.
update_interval: Optional floating point number representing time interval in seconds
at which to request datafile and set ProjectConfig.
url: Optional string representing URL from where to fetch the datafile. If set it supersedes the sdk_key.
url_template: Optional string template which in conjunction with sdk_key
determines URL from where to fetch the datafile.
logger: Provides a logger instance.
error_handler: Provides a handle_error method to handle exceptions.
sdk_key: Optional string uniquely identifying the datafile.
datafile: Optional JSON string representing the project.
update_interval: Optional floating point number representing time interval in seconds
at which to request datafile and set ProjectConfig.
url: Optional string representing URL from where to fetch the datafile. If set it supersedes the sdk_key.
url_template: Optional string template which in conjunction with sdk_key
determines URL from where to fetch the datafile.
logger: Provides a logger instance.
error_handler: Provides a handle_error method to handle exceptions.
skip_json_validation: Optional boolean param which allows skipping JSON schema
validation upon object invocation. By default
JSON schema validation will be performed.

"""
super(PollingConfigManager, self).__init__(logger=logger, error_handler=error_handler)
super(PollingConfigManager, self).__init__(logger=logger,
error_handler=error_handler,
skip_json_validation=skip_json_validation)
self.datafile_url = self.get_datafile_url(sdk_key, url,
url_template or enums.ConfigManager.DATAFILE_URL_TEMPLATE)
self.set_update_interval(update_interval)
self.last_modified = None
self._datafile = datafile
self._config = None
self._polling_thread = threading.Thread(target=self._run)
self._polling_thread.setDaemon(True)
if self._datafile:
self.set_config(self._datafile)
if datafile:
self._set_config(datafile)
self._polling_thread.start()

@staticmethod
def get_datafile_url(sdk_key, url, url_template):
Expand Down Expand Up @@ -166,30 +219,10 @@ def set_last_modified(self, response_headers):
""" Looks up and sets last modified time based on Last-Modified header in the response.

Args:
response_headers: requests.Response.headers
response_headers: requests.Response.headers
"""
self.last_modified = response_headers.get(enums.HTTPHeaders.LAST_MODIFIED)

def set_config(self, datafile):
""" Looks up and sets datafile and config based on response body.

Args:
datafile: JSON string representing the Optimizely project.
"""
# TODO(ali): Add validation here to make sure that we do not update datafile and config if not a valid datafile.
self._datafile = datafile
# TODO(ali): Add notification listener.
self._config = project_config.ProjectConfig(self._datafile, self.logger, self.error_handler)
self.logger.debug('Received new datafile and updated config.')

def get_config(self):
""" Returns instance of ProjectConfig.

Returns:
ProjectConfig.
"""
return self._config

def _handle_response(self, response):
""" Helper method to handle response containing datafile.

Expand All @@ -208,7 +241,7 @@ def _handle_response(self, response):
return

self.set_last_modified(response.headers)
self.set_config(response.content)
self._set_config(response.content)

def fetch_datafile(self):
""" Fetch datafile and set ProjectConfig. """
Expand Down
2 changes: 1 addition & 1 deletion optimizely/decision_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ def get_variation_for_feature(self, project_config, feature, user_id, attributes
))
return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST)
else:
self.logger.error(enums.Errors.INVALID_GROUP_ID_ERROR.format('_get_variation_for_feature'))
self.logger.error(enums.Errors.INVALID_GROUP_ID.format('_get_variation_for_feature'))

# Next check if the feature is being experimented on
elif feature.experimentIds:
Expand Down
21 changes: 11 additions & 10 deletions optimizely/helpers/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,19 @@ class DecisionSources(object):


class Errors(object):
INVALID_ATTRIBUTE_ERROR = 'Provided attribute is not in datafile.'
INVALID_ATTRIBUTE = '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_AUDIENCE = 'Provided audience is not in datafile.'
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.'
INVALID_GROUP_ID_ERROR = 'Provided group is not in datafile.'
INVALID_INPUT_ERROR = 'Provided "{}" is in an invalid format.'
INVALID_VARIATION_ERROR = 'Provided variation is not in datafile.'
INVALID_VARIABLE_KEY_ERROR = 'Provided variable key is not in the feature flag.'
INVALID_EXPERIMENT_KEY = 'Provided experiment is not in datafile.'
INVALID_EVENT_KEY = 'Provided event is not in datafile.'
INVALID_FEATURE_KEY = 'Provided feature key is not in the datafile.'
INVALID_GROUP_ID = 'Provided group is not in datafile.'
INVALID_INPUT = 'Provided "{}" is in an invalid format.'
INVALID_OPTIMIZELY = 'Optimizely instance is not valid. Failing "{}".'
INVALID_PROJECT_CONFIG = 'Invalid config. Optimizely instance is not valid. Failing "{}".'
INVALID_VARIATION = 'Provided variation is not in datafile.'
INVALID_VARIABLE_KEY = 'Provided variable key is not in the feature flag.'
NONE_FEATURE_KEY_PARAMETER = '"None" is an invalid value for feature key.'
NONE_USER_ID_PARAMETER = '"None" is an invalid value for user ID.'
NONE_VARIABLE_KEY_PARAMETER = '"None" is an invalid value for variable key.'
Expand Down
39 changes: 26 additions & 13 deletions optimizely/helpers/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,32 @@ def _has_method(obj, method):
return getattr(obj, method, None) is not None


def is_config_manager_valid(config_manager):
""" Given a config_manager determine if it is valid or not i.e. provides a get_config method.

Args:
config_manager: Provides a get_config method to handle exceptions.

Returns:
Boolean depending upon whether config_manager is valid or not.
"""

return _has_method(config_manager, 'get_config')


def is_error_handler_valid(error_handler):
""" Given a error_handler determine if it is valid or not i.e. provides a handle_error method.

Args:
error_handler: Provides a handle_error method to handle exceptions.

Returns:
Boolean depending upon whether error_handler is valid or not.
"""

return _has_method(error_handler, 'handle_error')


def is_event_dispatcher_valid(event_dispatcher):
""" Given a event_dispatcher determine if it is valid or not i.e. provides a dispatch_event method.

Expand All @@ -84,19 +110,6 @@ def is_logger_valid(logger):
return _has_method(logger, 'log')


def is_error_handler_valid(error_handler):
""" Given a error_handler determine if it is valid or not i.e. provides a handle_error method.

Args:
error_handler: Provides a handle_error method to handle exceptions.

Returns:
Boolean depending upon whether error_handler is valid or not.
"""

return _has_method(error_handler, 'handle_error')


def are_attributes_valid(attributes):
""" Determine if attributes provided are dict or not.

Expand Down
Loading