Skip to content

Commit dc226c1

Browse files
add decide apis
1 parent c8cabbd commit dc226c1

File tree

1 file changed

+154
-14
lines changed

1 file changed

+154
-14
lines changed

optimizely/optimizely.py

Lines changed: 154 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,15 @@
2020
from .config_manager import AuthDatafilePollingConfigManager
2121
from .config_manager import PollingConfigManager
2222
from .config_manager import StaticConfigManager
23+
from .decision.decide_option import DecideOption
24+
from .decision.decision import Decision
25+
from .decision.decision_message import DecisionMessage
2326
from .error_handler import NoOpErrorHandler as noop_error_handler
2427
from .event import event_factory, user_event_factory
2528
from .event.event_processor import ForwardingEventProcessor
2629
from .event_dispatcher import EventDispatcher as default_event_dispatcher
2730
from .helpers import enums, validator
31+
from .helpers.enums import DecisionSources
2832
from .notification_center import NotificationCenter
2933
from .optimizely_config import OptimizelyConfigService
3034
from .user_context import UserContext
@@ -34,18 +38,19 @@ class Optimizely(object):
3438
""" Class encapsulating all SDK functionality. """
3539

3640
def __init__(
37-
self,
38-
datafile=None,
39-
event_dispatcher=None,
40-
logger=None,
41-
error_handler=None,
42-
skip_json_validation=False,
43-
user_profile_service=None,
44-
sdk_key=None,
45-
config_manager=None,
46-
notification_center=None,
47-
event_processor=None,
48-
datafile_access_token=None,
41+
self,
42+
datafile=None,
43+
event_dispatcher=None,
44+
logger=None,
45+
error_handler=None,
46+
skip_json_validation=False,
47+
user_profile_service=None,
48+
sdk_key=None,
49+
config_manager=None,
50+
notification_center=None,
51+
event_processor=None,
52+
datafile_access_token=None,
53+
default_decisions=None
4954
):
5055
""" Optimizely init method for managing Custom projects.
5156
@@ -69,6 +74,7 @@ def __init__(
6974
which simply forwards events to the event dispatcher.
7075
To enable event batching configure and use optimizely.event.event_processor.BatchEventProcessor.
7176
datafile_access_token: Optional string used to fetch authenticated datafile for a secure project environment.
77+
default_decisions: Optional list of decide options used with the decide APIs.
7278
"""
7379
self.logger_name = '.'.join([__name__, self.__class__.__name__])
7480
self.is_valid = True
@@ -80,6 +86,7 @@ def __init__(
8086
self.event_processor = event_processor or ForwardingEventProcessor(
8187
self.event_dispatcher, logger=self.logger, notification_center=self.notification_center,
8288
)
89+
self.default_decisions = default_decisions or []
8390

8491
try:
8592
self._validate_instantiation_options()
@@ -192,7 +199,7 @@ def _send_impression_event(self, project_config, experiment, variation, flag_key
192199
)
193200

194201
def _get_feature_variable_for_type(
195-
self, project_config, feature_key, variable_key, variable_type, user_id, attributes,
202+
self, project_config, feature_key, variable_key, variable_type, user_id, attributes,
196203
):
197204
""" Helper method to determine value for a certain variable attached to a feature flag based on type of variable.
198205
@@ -296,7 +303,7 @@ def _get_feature_variable_for_type(
296303
return actual_value
297304

298305
def _get_all_feature_variables_for_type(
299-
self, project_config, feature_key, user_id, attributes,
306+
self, project_config, feature_key, user_id, attributes,
300307
):
301308
""" Helper method to determine value for all variables attached to a feature flag.
302309
@@ -935,3 +942,136 @@ def create_user_context(self, user_id, attributes=None):
935942

936943
user_context = UserContext(self, user_id, attributes)
937944
return user_context
945+
946+
def decide(self, user_context, key, decide_options=None):
947+
# raising on user context as it is internal and not provided directly by the user.
948+
if not isinstance(user_context, UserContext):
949+
raise
950+
951+
reasons = []
952+
953+
# check if SDK is ready
954+
if not self.is_valid:
955+
self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('decide'))
956+
reasons.append(DecisionMessage.SDK_NOT_READY)
957+
return Decision(flag_key=key, user_context=user_context, reasons=reasons)
958+
959+
# validate that key is a string
960+
if not isinstance(key, string_types):
961+
self.logger.error('Key parameter is invalid')
962+
reasons.append(DecisionMessage.FLAG_KEY_INVALID.format(key))
963+
return Decision.new(flag_key=key, user_context=user_context, reasons=reasons)
964+
965+
# validate that key maps to a feature flag
966+
config = self.project_config
967+
feature_flag = config.get_feature_flag_from_key(key)
968+
if feature_flag is None:
969+
self.logger.error("No feature flag was found for key '#{key}'.")
970+
reasons.push(DecisionMessage.FLAG_KEY_INVALID.format(key))
971+
return Decision(flag_key=key, user_context=user_context, reasons=reasons)
972+
973+
# merge decide_options and default_decide_options
974+
if isinstance(decide_options, list):
975+
decide_options += self.default_decisions
976+
else:
977+
self.logger.debug('Provided decide options is not an array. Using default decide options.')
978+
decide_options = self.default_decisions
979+
980+
# Create Optimizely Decision Result.
981+
user_id = user_context.user_id
982+
attributes = user_context.user_attributes
983+
variation_key = None
984+
feature_enabled = False
985+
rule_key = None
986+
flag_key = key
987+
all_variables = {}
988+
decision_event_dispatched = False
989+
experiment = None
990+
decision_source = DecisionSources.ROLLOUT
991+
source_info = {}
992+
993+
decision = self.decision_service.get_variation_for_feature(config, feature_flag, user_id, attributes,
994+
decide_options, reasons)
995+
996+
# Send impression event if Decision came from a feature test and decide options doesn't include disableDecisionEvent
997+
if decision.source == enums.DecisionSources.FEATURE_TEST:
998+
experiment = decision.experiment
999+
rule_key = experiment['key']
1000+
variation = decision['variation']
1001+
variation_key = variation['key']
1002+
feature_enabled = variation['featureEnabled']
1003+
decision_source = decision.source
1004+
source_info["variation"] = variation
1005+
source_info["experiment"] = experiment
1006+
1007+
if DecideOption.DISABLE_DECISION_EVENT not in decide_options:
1008+
if decision_source == DecisionSources.FEATURE_TEST or config.send_flag_decisions:
1009+
self._send_impression_event(config, experiment, variation_key or '', flag_key, rule_key or '',
1010+
feature_enabled, decision_source,
1011+
user_id, attributes)
1012+
decision_event_dispatched = True
1013+
1014+
# Generate all variables map if decide options doesn't include excludeVariables
1015+
if DecideOption.EXCLUDE_VARIABLES not in decide_options:
1016+
for v in feature_flag['variables']:
1017+
project_config = self.config_manager.get_config()
1018+
all_variables[v['key']] = self._get_feature_variable_for_type(project_config, feature_flag['key'],
1019+
v['key'], v['type'], user_id, attributes)
1020+
1021+
# Send notification
1022+
self.notification_center.send_notifications(
1023+
enums.NotificationTypes.DECISION,
1024+
enums.DecisionNotificationTypes.FEATURE,
1025+
user_id,
1026+
attributes or {},
1027+
{
1028+
'feature_key': key,
1029+
'feature_enabled': feature_enabled,
1030+
'source': decision.source,
1031+
'source_info': source_info,
1032+
},
1033+
)
1034+
1035+
include_reasons = []
1036+
if DecideOption.INCLUDE_REASONS in decide_options:
1037+
include_reasons = reasons
1038+
1039+
return Decision(variation_key=variation_key, enabled=feature_enabled, variables=all_variables,
1040+
rule_key=rule_key,
1041+
flag_key=flag_key, user_context=user_context, reasons=include_reasons)
1042+
1043+
def decide_all(self, user_context, decide_options=None):
1044+
# raising on user context as it is internal and not provided directly by the user.
1045+
if not isinstance(user_context, UserContext):
1046+
raise
1047+
1048+
# check if SDK is ready
1049+
if not self.is_valid:
1050+
self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('decide_all'))
1051+
return {}
1052+
1053+
keys = []
1054+
for f in self.project_config:
1055+
keys.append(f['key'])
1056+
1057+
return self.decide_for_keys(user_context, keys, decide_options)
1058+
1059+
def decide_for_keys(self, user_context, keys, decide_options=[]):
1060+
# raising on user context as it is internal and not provided directly by the user.
1061+
if not isinstance(user_context, UserContext):
1062+
raise
1063+
1064+
# check if SDK is ready
1065+
if not self.is_valid:
1066+
self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('decide_for_keys'))
1067+
return {}
1068+
1069+
enabled_flags_only = DecideOption.ENABLED_FLAGS_ONLY in decide_options
1070+
decisions = {}
1071+
for key in keys:
1072+
decision = self.decide(user_context, key, decide_options)
1073+
if enabled_flags_only and not decision.enabled:
1074+
continue
1075+
decisions[key] = decision
1076+
1077+
return decisions

0 commit comments

Comments
 (0)