Skip to content

Commit

Permalink
Merge ebb0ae3 into e078a48
Browse files Browse the repository at this point in the history
  • Loading branch information
aliabbasrizvi committed Jun 12, 2019
2 parents e078a48 + ebb0ae3 commit 777e11f
Show file tree
Hide file tree
Showing 13 changed files with 648 additions and 338 deletions.
117 changes: 75 additions & 42 deletions optimizely/config_manager.py
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
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
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
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

0 comments on commit 777e11f

Please sign in to comment.