1313# limitations under the License.
1414#
1515
16+ import copy
1617import threading
1718
19+ from .helpers import enums
20+
1821
1922class OptimizelyUserContext (object ):
2023 """
2124 Representation of an Optimizely User Context using which APIs are to be called.
2225 """
2326
24- def __init__ (self , optimizely_client , user_id , user_attributes = None ):
27+ def __init__ (self , optimizely_client , logger , user_id , user_attributes = None ):
2528 """ Create an instance of the Optimizely User Context.
2629
2730 Args:
2831 optimizely_client: client used when calling decisions for this user context
32+ logger: logger for logging
2933 user_id: user id of this user context
3034 user_attributes: user attributes to use for this user context
3135
@@ -34,16 +38,48 @@ def __init__(self, optimizely_client, user_id, user_attributes=None):
3438 """
3539
3640 self .client = optimizely_client
41+ self .logger = logger
3742 self .user_id = user_id
3843
3944 if not isinstance (user_attributes , dict ):
4045 user_attributes = {}
4146
4247 self ._user_attributes = user_attributes .copy () if user_attributes else {}
4348 self .lock = threading .Lock ()
49+ self .forced_decisions_map = {}
50+
51+ # decision context
52+ class OptimizelyDecisionContext (object ):
53+ """ Using class with attributes here instead of namedtuple because
54+ class is extensible, it's easy to add another attribute if we wanted
55+ to extend decision context.
56+ """
57+ def __init__ (self , flag_key , rule_key = None ):
58+ self .flag_key = flag_key
59+ self .rule_key = rule_key
60+
61+ def __hash__ (self ):
62+ return hash ((self .flag_key , self .rule_key ))
63+
64+ def __eq__ (self , other ):
65+ return (self .flag_key , self .rule_key ) == (other .flag_key , other .rule_key )
66+
67+ # forced decision
68+ class OptimizelyForcedDecision (object ):
69+ def __init__ (self , variation_key ):
70+ self .variation_key = variation_key
4471
4572 def _clone (self ):
46- return OptimizelyUserContext (self .client , self .user_id , self .get_user_attributes ())
73+ if not self .client :
74+ return None
75+
76+ user_context = OptimizelyUserContext (self .client , self .logger , self .user_id , self .get_user_attributes ())
77+
78+ with self .lock :
79+ if self .forced_decisions_map :
80+ user_context .forced_decisions_map = copy .deepcopy (self .forced_decisions_map )
81+
82+ return user_context
4783
4884 def get_user_attributes (self ):
4985 with self .lock :
@@ -114,3 +150,136 @@ def as_json(self):
114150 'user_id' : self .user_id ,
115151 'attributes' : self .get_user_attributes (),
116152 }
153+
154+ def set_forced_decision (self , decision_context , decision ):
155+ """
156+ Sets the forced decision for a given decision context.
157+
158+ Args:
159+ decision_context: a decision context.
160+ decision: a forced decision.
161+
162+ Returns:
163+ True if the forced decision has been set successfully.
164+ """
165+ with self .lock :
166+ self .forced_decisions_map [decision_context ] = decision
167+
168+ return True
169+
170+ def get_forced_decision (self , decision_context ):
171+ """
172+ Gets the forced decision (variation key) for a given decision context.
173+
174+ Args:
175+ decision_context: a decision context.
176+
177+ Returns:
178+ A forced_decision or None if forced decisions are not set for the parameters.
179+ """
180+ forced_decision = self .find_forced_decision (decision_context )
181+ return forced_decision
182+
183+ def remove_forced_decision (self , decision_context ):
184+ """
185+ Removes the forced decision for a given decision context.
186+
187+ Args:
188+ decision_context: a decision context.
189+
190+ Returns:
191+ Returns: true if the forced decision has been removed successfully.
192+ """
193+ with self .lock :
194+ if decision_context in self .forced_decisions_map :
195+ del self .forced_decisions_map [decision_context ]
196+ return True
197+
198+ return False
199+
200+ def remove_all_forced_decisions (self ):
201+ """
202+ Removes all forced decisions bound to this user context.
203+
204+ Returns:
205+ True if forced decisions have been removed successfully.
206+ """
207+ with self .lock :
208+ self .forced_decisions_map .clear ()
209+
210+ return True
211+
212+ def find_forced_decision (self , decision_context ):
213+ """
214+ Gets forced decision from forced decision map.
215+
216+ Args:
217+ decision_context: a decision context.
218+
219+ Returns:
220+ Forced decision.
221+ """
222+ with self .lock :
223+ if not self .forced_decisions_map :
224+ return None
225+
226+ # must allow None to be returned for the Flags only case
227+ return self .forced_decisions_map .get (decision_context )
228+
229+ def find_validated_forced_decision (self , decision_context ):
230+ """
231+ Gets forced decisions based on flag key, rule key and variation.
232+
233+ Args:
234+ decision context: a decision context
235+
236+ Returns:
237+ Variation of the forced decision.
238+ """
239+ reasons = []
240+
241+ forced_decision = self .find_forced_decision (decision_context )
242+
243+ flag_key = decision_context .flag_key
244+ rule_key = decision_context .rule_key
245+
246+ if forced_decision :
247+ # we use config here so we can use get_flag_variation() function which is defined in project_config
248+ # otherwise we would us self.client instead of config
249+ config = self .client .config_manager .get_config () if self .client else None
250+ if not config :
251+ return None , reasons
252+ variation = config .get_flag_variation (flag_key , 'key' , forced_decision .variation_key )
253+ if variation :
254+ if rule_key :
255+ user_has_forced_decision = enums .ForcedDecisionLogs \
256+ .USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED .format (forced_decision .variation_key ,
257+ flag_key ,
258+ rule_key ,
259+ self .user_id )
260+
261+ else :
262+ user_has_forced_decision = enums .ForcedDecisionLogs \
263+ .USER_HAS_FORCED_DECISION_WITHOUT_RULE_SPECIFIED .format (forced_decision .variation_key ,
264+ flag_key ,
265+ self .user_id )
266+
267+ reasons .append (user_has_forced_decision )
268+ self .logger .debug (user_has_forced_decision )
269+
270+ return variation , reasons
271+
272+ else :
273+ if rule_key :
274+ user_has_forced_decision_but_invalid = enums .ForcedDecisionLogs \
275+ .USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED_BUT_INVALID .format (flag_key ,
276+ rule_key ,
277+ self .user_id )
278+ else :
279+ user_has_forced_decision_but_invalid = enums .ForcedDecisionLogs \
280+ .USER_HAS_FORCED_DECISION_WITHOUT_RULE_SPECIFIED_BUT_INVALID .format (flag_key , self .user_id )
281+
282+ reasons .append (user_has_forced_decision_but_invalid )
283+ self .logger .debug (user_has_forced_decision_but_invalid )
284+
285+ return None , reasons
0 commit comments