Skip to content

refact: Decide API #314

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Feb 1, 2021
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ python:
# - "3.8" is handled in 'Test' job using xenial as Python 3.8 is not available for trusty.
- "pypy"
- "pypy3"
before_install:
- sudo apt-get --auto-remove --yes remove python-openssl
- sudo pip install pyOpenSSL
install: "pip install -r requirements/core.txt;pip install -r requirements/test.txt"
script: "pytest --cov=optimizely"
after_success:
Expand Down
48 changes: 32 additions & 16 deletions optimizely/bucketer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2016-2017, 2019-2020 Optimizely
# Copyright 2016-2017, 2019-2021 Optimizely
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
Expand Down Expand Up @@ -71,13 +71,13 @@ def find_bucket(self, project_config, bucketing_id, parent_id, traffic_allocatio
traffic_allocations: Traffic allocations representing traffic allotted to experiments or variations.

Returns:
Entity ID which may represent experiment or variation.
Entity ID which may represent experiment or variation and
"""

bucketing_key = BUCKETING_ID_TEMPLATE.format(bucketing_id=bucketing_id, parent_id=parent_id)
bucketing_number = self._generate_bucket_value(bucketing_key)
message = 'Assigned bucket %s to user with bucketing ID "%s".' % (bucketing_number, bucketing_id)
project_config.logger.debug(
'Assigned bucket %s to user with bucketing ID "%s".' % (bucketing_number, bucketing_id)
message
)

for traffic_allocation in traffic_allocations:
Expand All @@ -97,41 +97,57 @@ def bucket(self, project_config, experiment, user_id, bucketing_id):
bucketing_id: ID to be used for bucketing the user.

Returns:
Variation in which user with ID user_id will be put in. None if no variation.
Variation in which user with ID user_id will be put in. None if no variation
and array of log messages representing decision making.
*/.
"""

decide_reasons = []
if not experiment:
return None
return None, decide_reasons

# Determine if experiment is in a mutually exclusive group.
# This will not affect evaluation of rollout rules.
if experiment.groupPolicy in GROUP_POLICIES:
group = project_config.get_group(experiment.groupId)

if not group:
return None
return None, decide_reasons

user_experiment_id = self.find_bucket(
project_config, bucketing_id, experiment.groupId, group.trafficAllocation,
)

if not user_experiment_id:
project_config.logger.info('User "%s" is in no experiment.' % user_id)
return None
message = 'User "%s" is in no experiment.' % user_id
project_config.logger.info(message)
decide_reasons.append(message)
return None, decide_reasons

if user_experiment_id != experiment.id:
message = 'User "%s" is not in experiment "%s" of group %s.' \
% (user_id, experiment.key, experiment.groupId)
project_config.logger.info(
'User "%s" is not in experiment "%s" of group %s.' % (user_id, experiment.key, experiment.groupId)
message
)
return None
decide_reasons.append(message)
return None, decide_reasons

message = 'User "%s" is in experiment %s of group %s.' % (user_id, experiment.key, experiment.groupId)
project_config.logger.info(
'User "%s" is in experiment %s of group %s.' % (user_id, experiment.key, experiment.groupId)
message
)
decide_reasons.append(message)

# Bucket user if not in white-list and in group (if any)
variation_id = self.find_bucket(project_config, bucketing_id, experiment.id, experiment.trafficAllocation)
variation_id = self.find_bucket(project_config, bucketing_id,
experiment.id, experiment.trafficAllocation)
if variation_id:
variation = project_config.get_variation_from_id(experiment.key, variation_id)
return variation
return variation, decide_reasons

return None
else:
message = 'Bucketed into an empty traffic range. Returning nil.'
project_config.logger.info(message)
decide_reasons.append(message)

return None, decide_reasons
2 changes: 1 addition & 1 deletion optimizely/decision/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2020, Optimizely
# Copyright 2021, Optimizely
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2020, Optimizely
# Copyright 2021, Optimizely
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
Expand All @@ -12,7 +12,7 @@
# limitations under the License.


class DecideOption(object):
class OptimizelyDecideOption(object):
DISABLE_DECISION_EVENT = 'DISABLE_DECISION_EVENT'
ENABLED_FLAGS_ONLY = 'ENABLED_FLAGS_ONLY'
IGNORE_USER_PROFILE_SERVICE = 'IGNORE_USER_PROFILE_SERVICE'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2020, Optimizely
# Copyright 2021, Optimizely
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
Expand All @@ -12,7 +12,7 @@
# limitations under the License.


class Decision(object):
class OptimizelyDecision(object):
def __init__(self, variation_key=None, enabled=None,
variables=None, rule_key=None, flag_key=None, user_context=None, reasons=None):
self.variation_key = variation_key
Expand All @@ -22,3 +22,14 @@ def __init__(self, variation_key=None, enabled=None,
self.flag_key = flag_key
self.user_context = user_context
self.reasons = reasons or []

def as_json(self):
return {
'variation_key': self.variation_key,
'enabled': self.enabled,
'variables': self.variables,
'rule_key': self.rule_key,
'flag_key': self.flag_key,
'user_context': self.user_context.as_json(),
'reasons': self.reasons
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2020, Optimizely
# Copyright 2021, Optimizely
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
Expand All @@ -12,7 +12,7 @@
# limitations under the License.


class DecisionMessage(object):
class OptimizelyDecisionMessage(object):
SDK_NOT_READY = 'Optimizely SDK not configured properly yet.'
FLAG_KEY_INVALID = 'No flag was found for key "%s".'
VARIABLE_VALUE_INVALID = 'Variable value for key "%s" is invalid or wrong type.'
FLAG_KEY_INVALID = 'No flag was found for key "{}".'
VARIABLE_VALUE_INVALID = 'Variable value for key "{}" is invalid or wrong type.'
Loading