|
| 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 |
0 commit comments