Skip to content

Commit d80c555

Browse files
committed
add OptimizelyDecisionContext and OptmizelyForcedDecisions
1 parent 2fe78ab commit d80c555

File tree

4 files changed

+101
-35
lines changed

4 files changed

+101
-35
lines changed

optimizely/decision_service.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .helpers import enums
2121
from .helpers import experiment as experiment_helper
2222
from .helpers import validator
23+
from .optimizely_user_context import OptimizelyUserContext
2324
from .user_profile import UserProfile
2425

2526
Decision = namedtuple('Decision', 'experiment variation source')
@@ -407,7 +408,11 @@ def get_variation_from_experiment_rule(self, config, flag_key, rule, user, optio
407408
decide_reasons = []
408409

409410
# check forced decision first
410-
forced_decision_variation, reasons_received = user.find_validated_forced_decision(flag_key, rule.key, options)
411+
optimizely_decision_context = OptimizelyUserContext.OptimizelyDecisionContext(flag_key, rule.key)
412+
413+
forced_decision_variation, reasons_received = user.find_validated_forced_decision(
414+
optimizely_decision_context,
415+
options)
411416
decide_reasons += reasons_received
412417

413418
if forced_decision_variation:
@@ -442,9 +447,10 @@ def get_variation_from_delivery_rule(self, config, feature, rules, rule_index, u
442447

443448
# check forced decision first
444449
rule = rules[rule_index]
445-
forced_decision_variation, reasons_received = user.find_validated_forced_decision(feature.key,
446-
rule.key,
450+
optimizely_decision_context = OptimizelyUserContext.OptimizelyDecisionContext(feature.key, rule.key)
451+
forced_decision_variation, reasons_received = user.find_validated_forced_decision(optimizely_decision_context,
447452
options)
453+
448454
decide_reasons += reasons_received
449455

450456
if forced_decision_variation:

optimizely/optimizely.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1036,7 +1036,8 @@ def _decide(self, user_context, key, decide_options=None):
10361036
ignore_ups = OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE in decide_options
10371037

10381038
# Check forced decisions first
1039-
forced_decision_response = user_context.find_validated_forced_decision(flag_key=key, rule_key=rule_key,
1039+
optimizely_decision_context = OptimizelyUserContext.OptimizelyDecisionContext(flag_key=key, rule_key=rule_key)
1040+
forced_decision_response = user_context.find_validated_forced_decision(optimizely_decision_context,
10401041
options=decide_options)
10411042

10421043
variation, received_response = forced_decision_response
@@ -1187,6 +1188,9 @@ def _decide_for_keys(self, user_context, keys, decide_options=None):
11871188

11881189
def get_flag_variation_by_key(self, flag_key, variation_key):
11891190
"""
1191+
Gets variation by key.
1192+
variation_key can be a string or in case of forced decisions, it can be an object.
1193+
11901194
Args:
11911195
flag_key: flag key
11921196
variation_key: variation key
@@ -1199,6 +1203,10 @@ def get_flag_variation_by_key(self, flag_key, variation_key):
11991203
if not config:
12001204
return None
12011205

1206+
# this will take care of force decision objects which contain variation_key inside them
1207+
if isinstance(variation_key, OptimizelyUserContext.OptimizelyForcedDecision):
1208+
variation_key = variation_key.variation_key
1209+
12021210
variations = config.flag_variations_map[flag_key]
12031211

12041212
for variation in variations:

optimizely/optimizely_user_context.py

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
import copy
1717
import threading
18-
from collections import namedtuple
1918

2019
from . import logger
2120
from .decision.optimizely_decision_message import OptimizelyDecisionMessage
@@ -50,7 +49,22 @@ def __init__(self, optimizely_client, user_id, user_attributes=None):
5049
self.forced_decisions = {}
5150
self.log = logger.SimpleLogger(min_level=enums.LogLevels.INFO)
5251

53-
ForcedDecisionKeys = namedtuple('ForcedDecisionKeys', 'flag_key rule_key')
52+
# decision context
53+
class OptimizelyDecisionContext(object):
54+
def __init__(self, flag_key, rule_key):
55+
self.flag_key = flag_key
56+
self.rule_key = rule_key
57+
58+
def __hash__(self):
59+
return hash((self.flag_key, self.rule_key))
60+
61+
def __eq__(self, other):
62+
return (self.flag_key, self.rule_key) == (other.flag_key, other.rule_key)
63+
64+
# forced decision
65+
class OptimizelyForcedDecision(object):
66+
def __init__(self, variation_key):
67+
self.variation_key = variation_key
5468

5569
def _clone(self):
5670
if not self.client:
@@ -133,14 +147,13 @@ def as_json(self):
133147
'attributes': self.get_user_attributes(),
134148
}
135149

136-
def set_forced_decision(self, flag_key, rule_key, variation_key):
150+
def set_forced_decision(self, OptimizelyDecisionContext, OptimizelyForcedDecision):
137151
"""
138-
Sets the forced decision (variation key) for a given flag and an optional rule.
152+
Sets the forced decision for a given decision context.
139153
140154
Args:
141-
flag_key: A flag key.
142-
rule_key: An experiment or delivery rule key (optional).
143-
variation_key: A variation key.
155+
OptimizelyDecisionContext: a decision context.
156+
OptimizelyForcedDecision: a forced decision.
144157
145158
Returns:
146159
True if the forced decision has been set successfully.
@@ -151,18 +164,19 @@ def set_forced_decision(self, flag_key, rule_key, variation_key):
151164
self.log.logger.error(OptimizelyDecisionMessage.SDK_NOT_READY)
152165
return False
153166

154-
forced_decision_key = self.ForcedDecisionKeys(flag_key, rule_key)
155-
self.forced_decisions[forced_decision_key] = variation_key
167+
context = OptimizelyDecisionContext
168+
decision = OptimizelyForcedDecision
169+
170+
self.forced_decisions[context] = decision
156171

157172
return True
158173

159-
def get_forced_decision(self, flag_key, rule_key):
174+
def get_forced_decision(self, OptimizelyDecisionContext):
160175
"""
161-
Gets the forced decision (variation key) for a given flag and an optional rule.
176+
Gets the forced decision (variation key) for a given decision context.
162177
163178
Args:
164-
flag_key: A flag key.
165-
rule_key: An experiment or delivery rule key (optional).
179+
OptimizelyDecisionContext: a decision context.
166180
167181
Returns:
168182
A variation key or None if forced decisions are not set for the parameters.
@@ -173,17 +187,16 @@ def get_forced_decision(self, flag_key, rule_key):
173187
self.log.logger.error(OptimizelyDecisionMessage.SDK_NOT_READY)
174188
return None
175189

176-
forced_decision_key = self.find_forced_decision(flag_key, rule_key)
190+
forced_decision_key = self.find_forced_decision(OptimizelyDecisionContext)
177191

178192
return forced_decision_key if forced_decision_key else None
179193

180-
def remove_forced_decision(self, flag_key, rule_key):
194+
def remove_forced_decision(self, OptimizelyDecisionContext):
181195
"""
182196
Removes the forced decision for a given flag and an optional rule.
183197
184198
Args:
185-
flag_key: A flag key.
186-
rule_key: An experiment or delivery rule key (optional).
199+
OptimizelyDecisionContext: a decision context.
187200
188201
Returns:
189202
Returns: true if the forced decision has been removed successfully.
@@ -194,9 +207,9 @@ def remove_forced_decision(self, flag_key, rule_key):
194207
self.log.logger.error(OptimizelyDecisionMessage.SDK_NOT_READY)
195208
return False
196209

197-
forced_decision_key = self.ForcedDecisionKeys(flag_key, rule_key)
198-
if self.forced_decisions[forced_decision_key]:
199-
del self.forced_decisions[forced_decision_key]
210+
if self.forced_decisions[OptimizelyDecisionContext]:
211+
del self.forced_decisions[OptimizelyDecisionContext]
212+
return True
200213

201214
return False
202215

@@ -217,35 +230,36 @@ def remove_all_forced_decisions(self):
217230

218231
return True
219232

220-
def find_forced_decision(self, flag_key, rule_key):
233+
def find_forced_decision(self, OptimizelyDecisionContext):
221234

222235
if not self.forced_decisions:
223236
return None
224237

225-
forced_decision_key = self.ForcedDecisionKeys(flag_key, rule_key)
226-
variation_key = self.forced_decisions.get(forced_decision_key, None)
227-
228-
return variation_key if variation_key else None
238+
# must allow None to be returned for the Flags only case
239+
return self.forced_decisions.get(OptimizelyDecisionContext)
229240

230-
def find_validated_forced_decision(self, flag_key, rule_key, options):
241+
def find_validated_forced_decision(self, OptimizelyDecisionContext, options):
231242

232243
reasons = []
233244

234-
variation_key = self.find_forced_decision(flag_key, rule_key)
245+
forced_decision_response = self.find_forced_decision(OptimizelyDecisionContext)
246+
247+
flag_key = OptimizelyDecisionContext.flag_key
248+
rule_key = OptimizelyDecisionContext.rule_key
235249

236-
if variation_key:
237-
variation = self.client.get_flag_variation_by_key(flag_key, variation_key)
250+
if forced_decision_response:
251+
variation = self.client.get_flag_variation_by_key(flag_key, forced_decision_response)
238252
if variation:
239253
if rule_key:
240254
user_has_forced_decision = enums.ForcedDecisionLogs \
241-
.USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED.format(variation_key,
255+
.USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED.format(forced_decision_response,
242256
flag_key,
243257
rule_key,
244258
self.user_id)
245259

246260
else:
247261
user_has_forced_decision = enums.ForcedDecisionLogs \
248-
.USER_HAS_FORCED_DECISION_WITHOUT_RULE_SPECIFIED.format(variation_key,
262+
.USER_HAS_FORCED_DECISION_WITHOUT_RULE_SPECIFIED.format(forced_decision_response,
249263
flag_key,
250264
self.user_id)
251265

tests/test_user_context.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,3 +1337,41 @@ def test_decide_experiment(self):
13371337
user_context = opt_obj.create_user_context('test_user')
13381338
decision = user_context.decide('test_feature_in_experiment', [DecideOption.DISABLE_DECISION_EVENT])
13391339
self.assertTrue(decision.enabled, "decision should be enabled")
1340+
1341+
def test_forced_decision_return_status__invalid_datafile(self):
1342+
"""
1343+
Should return invalid status for invalid datafile in forced decision calls.
1344+
"""
1345+
opt_obj = optimizely.Optimizely(json.dumps("invalid datafile"))
1346+
user_context = OptimizelyUserContext(opt_obj, "test_user", {})
1347+
1348+
context = OptimizelyUserContext.OptimizelyDecisionContext('test_feature_in_rollout', None)
1349+
decision = OptimizelyUserContext.OptimizelyForcedDecision('211129')
1350+
1351+
status = user_context.set_forced_decision(context, decision)
1352+
self.assertFalse(status)
1353+
status = user_context.get_forced_decision(context)
1354+
self.assertIsNone(status)
1355+
status = user_context.remove_forced_decision(context)
1356+
self.assertFalse(status)
1357+
status = user_context.remove_all_forced_decisions()
1358+
self.assertFalse(status)
1359+
1360+
def test_forced_decision_return_status(self):
1361+
"""
1362+
Should return valid status for a valid datafile in forced decision calls.
1363+
"""
1364+
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
1365+
user_context = OptimizelyUserContext(opt_obj, "test_user", {})
1366+
1367+
context = OptimizelyUserContext.OptimizelyDecisionContext('test_feature_in_rollout', None)
1368+
decision = OptimizelyUserContext.OptimizelyForcedDecision('211129')
1369+
1370+
status = user_context.set_forced_decision(context, decision)
1371+
self.assertTrue(status)
1372+
status = user_context.get_forced_decision(context)
1373+
self.assertEqual(status.variation_key, '211129')
1374+
status = user_context.remove_forced_decision(context)
1375+
self.assertTrue(status)
1376+
status = user_context.remove_all_forced_decisions()
1377+
self.assertTrue(status)

0 commit comments

Comments
 (0)