Skip to content

Commit

Permalink
Merge ab40d9e into a1e31eb
Browse files Browse the repository at this point in the history
  • Loading branch information
Mat001 committed Nov 24, 2021
2 parents a1e31eb + ab40d9e commit ce002e6
Show file tree
Hide file tree
Showing 11 changed files with 1,975 additions and 885 deletions.
407 changes: 242 additions & 165 deletions optimizely/decision_service.py

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions optimizely/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,23 @@ def get_audience_conditions_or_ids(self):
def __str__(self):
return self.key

@staticmethod
def get_default():
""" returns an empty experiment object. """
experiment = Experiment(
id='',
key='',
layerId='',
status='',
variations=[],
trafficAllocation=[],
audienceIds=[],
audienceConditions=[],
forcedVariations={}
)

return experiment


class FeatureFlag(BaseEntity):
def __init__(self, id, key, experimentIds, rolloutId, variables, groupId=None, **kwargs):
Expand All @@ -94,6 +111,7 @@ def __init__(self, id, policy, experiments, trafficAllocation, **kwargs):


class Layer(BaseEntity):
"""Layer acts as rollout."""
def __init__(self, id, experiments, **kwargs):
self.id = id
self.experiments = experiments
Expand Down
3 changes: 3 additions & 0 deletions optimizely/event/user_event_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ def create_impression_event(

if variation_id and experiment_id:
variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
# need this condition when we send events involving forced decisions
elif variation_id and flag_key:
variation = project_config.get_flag_variation(flag_key, 'id', variation_id)
event_context = user_event.EventContext(
project_config.account_id, project_config.project_id, project_config.revision, project_config.anonymize_ip,
)
Expand Down
11 changes: 11 additions & 0 deletions optimizely/helpers/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,17 @@ class Errors(object):
UNSUPPORTED_DATAFILE_VERSION = 'This version of the Python SDK does not support the given datafile version: "{}".'


class ForcedDecisionLogs(object):
USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED = 'Variation ({}) is mapped to flag ({}), rule ({}) and user ({}) ' \
'in the forced decision map.'
USER_HAS_FORCED_DECISION_WITHOUT_RULE_SPECIFIED = 'Variation ({}) is mapped to flag ({}) and user ({}) ' \
'in the forced decision map.'
USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED_BUT_INVALID = 'Invalid variation is mapped to flag ({}), rule ({}) ' \
'and user ({}) in the forced decision map.'
USER_HAS_FORCED_DECISION_WITHOUT_RULE_SPECIFIED_BUT_INVALID = 'Invalid variation is mapped to flag ({}) ' \
'and user ({}) in the forced decision map.'


class HTTPHeaders(object):
AUTHORIZATION = 'Authorization'
IF_MODIFIED_SINCE = 'If-Modified-Since'
Expand Down
424 changes: 224 additions & 200 deletions optimizely/optimizely.py

Large diffs are not rendered by default.

187 changes: 186 additions & 1 deletion optimizely/optimizely_user_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@
# limitations under the License.
#

import copy
import threading

from . import logger
from .decision.optimizely_decision_message import OptimizelyDecisionMessage
from .helpers import enums


class OptimizelyUserContext(object):
"""
Expand All @@ -41,9 +46,41 @@ def __init__(self, optimizely_client, user_id, user_attributes=None):

self._user_attributes = user_attributes.copy() if user_attributes else {}
self.lock = threading.Lock()
self.forced_decisions_map = {}
self.log = logger.SimpleLogger(min_level=enums.LogLevels.INFO)

# decision context
class OptimizelyDecisionContext(object):
""" Using class with attributes here instead of namedtuple because
class is extensible, it's easy to add another attribute if we wanted
to extend decision context.
"""
def __init__(self, flag_key, rule_key=None):
self.flag_key = flag_key
self.rule_key = rule_key

def __hash__(self):
return hash((self.flag_key, self.rule_key))

def __eq__(self, other):
return (self.flag_key, self.rule_key) == (other.flag_key, other.rule_key)

# forced decision
class OptimizelyForcedDecision(object):
def __init__(self, variation_key):
self.variation_key = variation_key

def _clone(self):
return OptimizelyUserContext(self.client, self.user_id, self.get_user_attributes())
if not self.client:
return None

user_context = OptimizelyUserContext(self.client, self.user_id, self.get_user_attributes())

with self.lock:
if self.forced_decisions_map:
user_context.forced_decisions_map = copy.deepcopy(self.forced_decisions_map)

return user_context

def get_user_attributes(self):
with self.lock:
Expand Down Expand Up @@ -114,3 +151,151 @@ def as_json(self):
'user_id': self.user_id,
'attributes': self.get_user_attributes(),
}

def set_forced_decision(self, decision_context, decision):
"""
Sets the forced decision for a given decision context.
Args:
decision_context: a decision context.
decision: a forced decision.
Returns:
True if the forced decision has been set successfully.
"""
if not self.client.is_valid or not self.client.config_manager.get_config():
self.log.logger.error(OptimizelyDecisionMessage.SDK_NOT_READY)
return False

with self.lock:
self.forced_decisions_map[decision_context] = decision

return True

def get_forced_decision(self, decision_context):
"""
Gets the forced decision (variation key) for a given decision context.
Args:
decision_context: a decision context.
Returns:
A forced_decision or None if forced decisions are not set for the parameters.
"""
if not self.client.is_valid or not self.client.config_manager.get_config():
self.log.logger.error(OptimizelyDecisionMessage.SDK_NOT_READY)
return None

forced_decision = self.find_forced_decision(decision_context)

return forced_decision

def remove_forced_decision(self, decision_context):
"""
Removes the forced decision for a given decision context.
Args:
decision_context: a decision context.
Returns:
Returns: true if the forced decision has been removed successfully.
"""
if not self.client.is_valid or not self.client.config_manager.get_config():
self.log.logger.error(OptimizelyDecisionMessage.SDK_NOT_READY)
return False

with self.lock:
if decision_context in self.forced_decisions_map:
del self.forced_decisions_map[decision_context]
return True

return False

def remove_all_forced_decisions(self):
"""
Removes all forced decisions bound to this user context.
Returns:
True if forced decisions have been removed successfully.
"""
if not self.client.is_valid or not self.client.config_manager.get_config():
self.log.logger.error(OptimizelyDecisionMessage.SDK_NOT_READY)
return False

with self.lock:
self.forced_decisions_map.clear()

return True

def find_forced_decision(self, decision_context):
"""
Gets forced decision from forced decision map.
Args:
decision_context: a decision context.
Returns:
Forced decision.
"""
with self.lock:
if not self.forced_decisions_map:
return None

# must allow None to be returned for the Flags only case
return self.forced_decisions_map.get(decision_context)

def find_validated_forced_decision(self, decision_context):
"""
Gets forced decisions based on flag key, rule key and variation.
Args:
decision context: a decision context
Returns:
Variation of the forced decision.
"""
reasons = []

forced_decision = self.find_forced_decision(decision_context)

flag_key = decision_context.flag_key
rule_key = decision_context.rule_key

if forced_decision:
# we use config here so we can use get_flag_variation() function which is defined in project_config
# otherwise we would us self.client instead of config
config = self.client.config_manager.get_config() if self.client else None
variation = config.get_flag_variation(flag_key, 'key', forced_decision.variation_key)
if variation:
if rule_key:
user_has_forced_decision = enums.ForcedDecisionLogs \
.USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED.format(forced_decision.variation_key,
flag_key,
rule_key,
self.user_id)

else:
user_has_forced_decision = enums.ForcedDecisionLogs \
.USER_HAS_FORCED_DECISION_WITHOUT_RULE_SPECIFIED.format(forced_decision.variation_key,
flag_key,
self.user_id)

reasons.append(user_has_forced_decision)
self.log.logger.debug(user_has_forced_decision)

return variation, reasons

else:
if rule_key:
user_has_forced_decision_but_invalid = enums.ForcedDecisionLogs \
.USER_HAS_FORCED_DECISION_WITH_RULE_SPECIFIED_BUT_INVALID.format(flag_key,
rule_key,
self.user_id)
else:
user_has_forced_decision_but_invalid = enums.ForcedDecisionLogs \
.USER_HAS_FORCED_DECISION_WITHOUT_RULE_SPECIFIED_BUT_INVALID.format(flag_key, self.user_id)

reasons.append(user_has_forced_decision_but_invalid)
self.log.logger.debug(user_has_forced_decision_but_invalid)

return None, reasons
Loading

0 comments on commit ce002e6

Please sign in to comment.