Skip to content

Commit 1785902

Browse files
oakbanijaeopt
authored andcommitted
feat: get_optimizely_config API (#226)
* Structure Optimizely Config
1 parent 0a6086c commit 1785902

File tree

5 files changed

+756
-7
lines changed

5 files changed

+756
-7
lines changed

Diff for: optimizely/optimizely.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2019, Optimizely
1+
# Copyright 2016-2020, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -25,6 +25,7 @@
2525
from .event_dispatcher import EventDispatcher as default_event_dispatcher
2626
from .helpers import enums, validator
2727
from .notification_center import NotificationCenter
28+
from .optimizely_config import OptimizelyConfigService
2829

2930

3031
class Optimizely(object):
@@ -733,3 +734,21 @@ def get_forced_variation(self, experiment_key, user_id):
733734

734735
forced_variation = self.decision_service.get_forced_variation(project_config, experiment_key, user_id)
735736
return forced_variation.key if forced_variation else None
737+
738+
def get_optimizely_config(self):
739+
""" Gets OptimizelyConfig instance for the current project config.
740+
741+
Returns:
742+
OptimizelyConfig instance. None if the optimizely instance is invalid or
743+
project config isn't available.
744+
"""
745+
if not self.is_valid:
746+
self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('get_optimizely_config'))
747+
return None
748+
749+
project_config = self.config_manager.get_config()
750+
if not project_config:
751+
self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_optimizely_config'))
752+
return None
753+
754+
return OptimizelyConfigService(project_config).get_config()

Diff for: optimizely/optimizely_config.py

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# Copyright 2020, Optimizely
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
import copy
15+
16+
from .project_config import ProjectConfig
17+
18+
19+
class OptimizelyConfig(object):
20+
def __init__(self, revision, experiments_map, features_map):
21+
self.revision = revision
22+
self.experiments_map = experiments_map
23+
self.features_map = features_map
24+
25+
26+
class OptimizelyExperiment(object):
27+
def __init__(self, id, key, variations_map):
28+
self.id = id
29+
self.key = key
30+
self.variations_map = variations_map
31+
32+
33+
class OptimizelyFeature(object):
34+
def __init__(self, id, key, experiments_map, variables_map):
35+
self.id = id
36+
self.key = key
37+
self.experiments_map = experiments_map
38+
self.variables_map = variables_map
39+
40+
41+
class OptimizelyVariation(object):
42+
def __init__(self, id, key, feature_enabled, variables_map):
43+
self.id = id
44+
self.key = key
45+
self.feature_enabled = feature_enabled
46+
self.variables_map = variables_map
47+
48+
49+
class OptimizelyVariable(object):
50+
def __init__(self, id, key, variable_type, value):
51+
self.id = id
52+
self.key = key
53+
self.type = variable_type
54+
self.value = value
55+
56+
57+
class OptimizelyConfigService(object):
58+
""" Class encapsulating methods to be used in creating instance of OptimizelyConfig. """
59+
60+
def __init__(self, project_config):
61+
"""
62+
Args:
63+
project_config ProjectConfig
64+
"""
65+
self.is_valid = True
66+
67+
if not isinstance(project_config, ProjectConfig):
68+
self.is_valid = False
69+
return
70+
71+
self.experiments = project_config.experiments
72+
self.feature_flags = project_config.feature_flags
73+
self.groups = project_config.groups
74+
self.revision = project_config.revision
75+
76+
self._create_lookup_maps()
77+
78+
def get_config(self):
79+
""" Gets instance of OptimizelyConfig
80+
81+
Returns:
82+
Optimizely Config instance or None if OptimizelyConfigService is invalid.
83+
"""
84+
85+
if not self.is_valid:
86+
return None
87+
88+
experiments_key_map, experiments_id_map = self._get_experiments_maps()
89+
features_map = self._get_features_map(experiments_id_map)
90+
91+
return OptimizelyConfig(self.revision, experiments_key_map, features_map)
92+
93+
def _create_lookup_maps(self):
94+
""" Creates lookup maps to avoid redundant iteration of config objects. """
95+
96+
self.exp_id_to_feature_map = {}
97+
self.feature_key_variable_key_to_variable_map = {}
98+
self.feature_key_variable_id_to_variable_map = {}
99+
100+
for feature in self.feature_flags:
101+
for experiment_id in feature['experimentIds']:
102+
self.exp_id_to_feature_map[experiment_id] = feature
103+
104+
variables_key_map = {}
105+
variables_id_map = {}
106+
for variable in feature.get('variables', []):
107+
opt_variable = OptimizelyVariable(
108+
variable['id'], variable['key'], variable['type'], variable['defaultValue']
109+
)
110+
variables_key_map[variable['key']] = opt_variable
111+
variables_id_map[variable['id']] = opt_variable
112+
113+
self.feature_key_variable_key_to_variable_map[feature['key']] = variables_key_map
114+
self.feature_key_variable_id_to_variable_map[feature['key']] = variables_id_map
115+
116+
def _get_variables_map(self, experiment, variation):
117+
""" Gets variables map for given experiment and variation.
118+
119+
Args:
120+
experiment dict -- Experiment parsed from the datafile.
121+
variation dict -- Variation of the given experiment.
122+
123+
Returns:
124+
dict - Map of variable key to OptimizelyVariable for the given variation.
125+
"""
126+
feature_flag = self.exp_id_to_feature_map.get(experiment['id'], None)
127+
if feature_flag is None:
128+
return {}
129+
130+
# set default variables for each variation
131+
variables_map = {}
132+
variables_map = copy.deepcopy(self.feature_key_variable_key_to_variable_map[feature_flag['key']])
133+
134+
# set variation specific variable value if any
135+
if variation.get('featureEnabled'):
136+
for variable in variation.get('variables', []):
137+
feature_variable = self.feature_key_variable_id_to_variable_map[feature_flag['key']][variable['id']]
138+
variables_map[feature_variable.key].value = variable['value']
139+
140+
return variables_map
141+
142+
def _get_variations_map(self, experiment):
143+
""" Gets variation map for the given experiment.
144+
145+
Args:
146+
experiment dict -- Experiment parsed from the datafile.
147+
148+
Returns:
149+
dict -- Map of variation key to OptimizelyVariation.
150+
"""
151+
variations_map = {}
152+
153+
for variation in experiment.get('variations', []):
154+
variables_map = self._get_variables_map(experiment, variation)
155+
feature_enabled = variation.get('featureEnabled', None)
156+
157+
optly_variation = OptimizelyVariation(
158+
variation['id'], variation['key'], feature_enabled, variables_map
159+
)
160+
161+
variations_map[variation['key']] = optly_variation
162+
163+
return variations_map
164+
165+
def _get_all_experiments(self):
166+
""" Gets all experiments in the project config.
167+
168+
Returns:
169+
list -- List of dicts of experiments.
170+
"""
171+
experiments = self.experiments
172+
173+
for group in self.groups:
174+
experiments = experiments + group['experiments']
175+
176+
return experiments
177+
178+
def _get_experiments_maps(self):
179+
""" Gets maps for all the experiments in the project config.
180+
181+
Returns:
182+
dict, dict -- experiment key/id to OptimizelyExperiment maps.
183+
"""
184+
# Key map is required for the OptimizelyConfig response.
185+
experiments_key_map = {}
186+
# Id map comes in handy to figure out feature experiment.
187+
experiments_id_map = {}
188+
189+
all_experiments = self._get_all_experiments()
190+
for exp in all_experiments:
191+
optly_exp = OptimizelyExperiment(
192+
exp['id'], exp['key'], self._get_variations_map(exp)
193+
)
194+
195+
experiments_key_map[exp['key']] = optly_exp
196+
experiments_id_map[exp['id']] = optly_exp
197+
198+
return experiments_key_map, experiments_id_map
199+
200+
def _get_features_map(self, experiments_id_map):
201+
""" Gets features map for the project config.
202+
203+
Args:
204+
experiments_id_map dict -- experiment id to OptimizelyExperiment map
205+
206+
Returns:
207+
dict -- feaure key to OptimizelyFeature map
208+
"""
209+
features_map = {}
210+
211+
for feature in self.feature_flags:
212+
exp_map = {}
213+
for experiment_id in feature.get('experimentIds', []):
214+
optly_exp = experiments_id_map[experiment_id]
215+
exp_map[optly_exp.key] = optly_exp
216+
217+
variables_map = self.feature_key_variable_key_to_variable_map[feature['key']]
218+
219+
optly_feature = OptimizelyFeature(
220+
feature['id'], feature['key'], exp_map, variables_map
221+
)
222+
223+
features_map[feature['key']] = optly_feature
224+
225+
return features_map

Diff for: tests/base.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2019, Optimizely
1+
# Copyright 2016-2020, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -182,14 +182,12 @@ def setUp(self, config_dict='config_dict'):
182182
{
183183
'id': '122239',
184184
'key': 'control',
185-
'featureEnabled': True,
186-
'variables': [{'id': '155551', 'value': '42.42'}],
185+
'variables': [],
187186
},
188187
{
189188
'id': '122240',
190189
'key': 'variation',
191-
'featureEnabled': True,
192-
'variables': [{'id': '155551', 'value': '13.37'}],
190+
'variables': [],
193191
},
194192
],
195193
},

Diff for: tests/test_optimizely.py

+35-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2019, Optimizely
1+
# Copyright 2016-2020, Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -23,6 +23,7 @@
2323
from optimizely import exceptions
2424
from optimizely import logger
2525
from optimizely import optimizely
26+
from optimizely import optimizely_config
2627
from optimizely import project_config
2728
from optimizely import version
2829
from optimizely.event.event_factory import EventFactory
@@ -3911,6 +3912,39 @@ def test_get_feature_variable_returns__default_value__complex_audience_match(sel
39113912
self.assertEqual(10, opt_obj.get_feature_variable_integer('feat2_with_var', 'z', 'user1', {}))
39123913
self.assertEqual(10, opt_obj.get_feature_variable('feat2_with_var', 'z', 'user1', {}))
39133914

3915+
def test_get_optimizely_config__invalid_object(self):
3916+
""" Test that get_optimizely_config logs error if Optimizely instance is invalid. """
3917+
3918+
class InvalidConfigManager(object):
3919+
pass
3920+
3921+
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager())
3922+
3923+
with mock.patch.object(opt_obj, 'logger') as mock_client_logging:
3924+
self.assertIsNone(opt_obj.get_optimizely_config())
3925+
3926+
mock_client_logging.error.assert_called_once_with(
3927+
'Optimizely instance is not valid. Failing "get_optimizely_config".')
3928+
3929+
def test_get_optimizely_config__invalid_config(self):
3930+
""" Test that get_optimizely_config logs error if config is invalid. """
3931+
3932+
opt_obj = optimizely.Optimizely('invalid_datafile')
3933+
3934+
with mock.patch.object(opt_obj, 'logger') as mock_client_logging:
3935+
self.assertIsNone(opt_obj.get_optimizely_config())
3936+
3937+
mock_client_logging.error.assert_called_once_with(
3938+
'Invalid config. Optimizely instance is not valid. ' 'Failing "get_optimizely_config".'
3939+
)
3940+
3941+
def test_get_optimizely_config_returns_instance_of_optimizely_config(self):
3942+
""" Test that get_optimizely_config returns an instance of OptimizelyConfig. """
3943+
3944+
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
3945+
opt_config = opt_obj.get_optimizely_config()
3946+
self.assertIsInstance(opt_config, optimizely_config.OptimizelyConfig)
3947+
39143948

39153949
class OptimizelyWithExceptionTest(base.BaseTest):
39163950
def setUp(self):

0 commit comments

Comments
 (0)