Skip to content

Commit

Permalink
Merge 19bb7fd into d57e068
Browse files Browse the repository at this point in the history
  • Loading branch information
oakbani committed Dec 31, 2019
2 parents d57e068 + 19bb7fd commit a6546a0
Show file tree
Hide file tree
Showing 5 changed files with 756 additions and 7 deletions.
21 changes: 20 additions & 1 deletion optimizely/optimizely.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2016-2019, Optimizely
# Copyright 2016-2020, 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 @@ -25,6 +25,7 @@
from .event_dispatcher import EventDispatcher as default_event_dispatcher
from .helpers import enums, validator
from .notification_center import NotificationCenter
from .optimizely_config import OptimizelyConfigService


class Optimizely(object):
Expand Down Expand Up @@ -733,3 +734,21 @@ def get_forced_variation(self, experiment_key, user_id):

forced_variation = self.decision_service.get_forced_variation(project_config, experiment_key, user_id)
return forced_variation.key if forced_variation else None

def get_optimizely_config(self):
""" Gets OptimizelyConfig instance for the current project config.
Returns:
OptimizelyConfig instance. None if the optimizely instance is invalid or
project config isn't available.
"""
if not self.is_valid:
self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('get_optimizely_config'))
return None

project_config = self.config_manager.get_config()
if not project_config:
self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_optimizely_config'))
return None

return OptimizelyConfigService(project_config).get_config()
225 changes: 225 additions & 0 deletions optimizely/optimizely_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# Copyright 2020, 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
#
# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import copy

from .project_config import ProjectConfig


class OptimizelyConfig(object):
def __init__(self, revision, experiments_map, features_map):
self.revision = revision
self.experiments_map = experiments_map
self.features_map = features_map


class OptimizelyExperiment(object):
def __init__(self, id, key, variations_map):
self.id = id
self.key = key
self.variations_map = variations_map


class OptimizelyFeature(object):
def __init__(self, id, key, experiments_map, variables_map):
self.id = id
self.key = key
self.experiments_map = experiments_map
self.variables_map = variables_map


class OptimizelyVariation(object):
def __init__(self, id, key, feature_enabled, variables_map):
self.id = id
self.key = key
self.feature_enabled = feature_enabled
self.variables_map = variables_map


class OptimizelyVariable(object):
def __init__(self, id, key, variable_type, value):
self.id = id
self.key = key
self.type = variable_type
self.value = value


class OptimizelyConfigService(object):
""" Class encapsulating methods to be used in creating instance of OptimizelyConfig. """

def __init__(self, project_config):
"""
Args:
project_config ProjectConfig
"""
self.is_valid = True

if not isinstance(project_config, ProjectConfig):
self.is_valid = False
return

self.experiments = project_config.experiments
self.feature_flags = project_config.feature_flags
self.groups = project_config.groups
self.revision = project_config.revision

self._create_lookup_maps()

def get_config(self):
""" Gets instance of OptimizelyConfig
Returns:
Optimizely Config instance or None if OptimizelyConfigService is invalid.
"""

if not self.is_valid:
return None

experiments_key_map, experiments_id_map = self._get_experiments_maps()
features_map = self._get_features_map(experiments_id_map)

return OptimizelyConfig(self.revision, experiments_key_map, features_map)

def _create_lookup_maps(self):
""" Creates lookup maps to avoid redundant iteration of config objects. """

self.exp_id_to_feature_map = {}
self.feature_key_variable_key_to_variable_map = {}
self.feature_key_variable_id_to_variable_map = {}

for feature in self.feature_flags:
for experiment_id in feature['experimentIds']:
self.exp_id_to_feature_map[experiment_id] = feature

variables_key_map = {}
variables_id_map = {}
for variable in feature.get('variables', []):
opt_variable = OptimizelyVariable(
variable['id'], variable['key'], variable['type'], variable['defaultValue']
)
variables_key_map[variable['key']] = opt_variable
variables_id_map[variable['id']] = opt_variable

self.feature_key_variable_key_to_variable_map[feature['key']] = variables_key_map
self.feature_key_variable_id_to_variable_map[feature['key']] = variables_id_map

def _get_variables_map(self, variation, experiment):
""" Gets variables map for given variation and experiment.
Args:
variation dict
experiment dict
Returns:
dict - Map of variable key to OptimizelyVariable for the given variation.
"""
feature_flag = self.exp_id_to_feature_map.get(experiment['id'], None)
if feature_flag is None:
return {}

# set default variables for each variation
variables_map = {}
variables_map = copy.deepcopy(self.feature_key_variable_key_to_variable_map[feature_flag['key']])

# set variation specific variable value if any
if variation.get('featureEnabled'):
for variable in variation.get('variables', []):
feature_variable = self.feature_key_variable_id_to_variable_map[feature_flag['key']][variable['id']]
variables_map[feature_variable.key].value = variable['value']

return variables_map

def _get_variations_map(self, experiment):
""" Gets variation map for the given experiment.
Args:
experiment dict
Returns:
dict -- Map of variation key to OptimizelyVariation.
"""
variations_map = {}

for variation in experiment.get('variations', []):
variables_map = self._get_variables_map(variation, experiment)
feature_enabled = variation.get('featureEnabled', None)

optly_variation = OptimizelyVariation(
variation['id'], variation['key'], feature_enabled, variables_map
)

variations_map[variation['key']] = optly_variation

return variations_map

def _get_all_experiments(self):
""" Gets all experiments in the project config.
Returns:
list -- List of dicts of experiments.
"""
experiments = self.experiments

for group in self.groups:
experiments = experiments + group['experiments']

return experiments

def _get_experiments_maps(self):
""" Gets maps for all the experiments in the project config.
Returns:
dict, dict -- experiment key/id to OptimizelyExperiment maps.
"""
# Key map is required for the OptimizelyConfig response.
experiments_key_map = {}
# Id map comes in handy to figure out feature experiment.
experiments_id_map = {}

all_experiments = self._get_all_experiments()
for exp in all_experiments:
optly_exp = OptimizelyExperiment(
exp['id'], exp['key'], self._get_variations_map(exp)
)

experiments_key_map[exp['key']] = optly_exp
experiments_id_map[exp['id']] = optly_exp

return experiments_key_map, experiments_id_map

def _get_features_map(self, experiments_id_map):
""" Gets features map for the project config.
Args:
experiments_id_map dict -- experiment id to OptimizelyExperiment map
Returns:
dict -- feaure key to OptimizelyFeature map
"""
features_map = {}

for feature in self.feature_flags:
exp_map = {}
for experiment_id in feature.get('experimentIds', []):
optly_exp = experiments_id_map[experiment_id]
exp_map[optly_exp.key] = optly_exp

variables_map = self.feature_key_variable_key_to_variable_map[feature['key']]

optly_feature = OptimizelyFeature(
feature['id'], feature['key'], exp_map, variables_map
)

features_map[feature['key']] = optly_feature

return features_map
8 changes: 3 additions & 5 deletions tests/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2016-2019, Optimizely
# Copyright 2016-2020, 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 @@ -182,14 +182,12 @@ def setUp(self, config_dict='config_dict'):
{
'id': '122239',
'key': 'control',
'featureEnabled': True,
'variables': [{'id': '155551', 'value': '42.42'}],
'variables': [],
},
{
'id': '122240',
'key': 'variation',
'featureEnabled': True,
'variables': [{'id': '155551', 'value': '13.37'}],
'variables': [],
},
],
},
Expand Down
36 changes: 35 additions & 1 deletion tests/test_optimizely.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2016-2019, Optimizely
# Copyright 2016-2020, 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 @@ -23,6 +23,7 @@
from optimizely import exceptions
from optimizely import logger
from optimizely import optimizely
from optimizely import optimizely_config
from optimizely import project_config
from optimizely import version
from optimizely.event.event_factory import EventFactory
Expand Down Expand Up @@ -3911,6 +3912,39 @@ def test_get_feature_variable_returns__default_value__complex_audience_match(sel
self.assertEqual(10, opt_obj.get_feature_variable_integer('feat2_with_var', 'z', 'user1', {}))
self.assertEqual(10, opt_obj.get_feature_variable('feat2_with_var', 'z', 'user1', {}))

def test_get_optimizely_config__invalid_object(self):
""" Test that get_optimizely_config logs error if Optimizely instance is invalid. """

class InvalidConfigManager(object):
pass

opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager())

with mock.patch.object(opt_obj, 'logger') as mock_client_logging:
self.assertIsNone(opt_obj.get_optimizely_config())

mock_client_logging.error.assert_called_once_with(
'Optimizely instance is not valid. Failing "get_optimizely_config".')

def test_get_optimizely_config__invalid_config(self):
""" Test that get_optimizely_config logs error if config is invalid. """

opt_obj = optimizely.Optimizely('invalid_datafile')

with mock.patch.object(opt_obj, 'logger') as mock_client_logging:
self.assertIsNone(opt_obj.get_optimizely_config())

mock_client_logging.error.assert_called_once_with(
'Invalid config. Optimizely instance is not valid. ' 'Failing "get_optimizely_config".'
)

def test_get_optimizely_config_returns_instance_of_optimizely_config(self):
""" Test that get_optimizely_config returns an instance of OptimizelyConfig. """

opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
opt_config = opt_obj.get_optimizely_config()
self.assertIsInstance(opt_config, optimizely_config.OptimizelyConfig)


class OptimizelyWithExceptionTest(base.BaseTest):
def setUp(self):
Expand Down

0 comments on commit a6546a0

Please sign in to comment.