Skip to content

Commit ed01b76

Browse files
committed
Initial commit
0 parents  commit ed01b76

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+4091
-0
lines changed

.coveragerc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[run]
2+
omit =
3+
optimizely/lib/pymmh3*

.coveralls.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
service_name: travis-pro

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
*.pyc
2+
MANIFEST
3+
.idea/*
4+
.virtualenv/*
5+
.py3virtualenv/*
6+
7+
# Output of building package
8+
*.egg-info
9+
dist
10+
11+
# Output of running coverage locally
12+
.coverage

.travis.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
language: python
2+
python:
3+
- "2.7"
4+
- "3.4"
5+
install: "pip install -r requirements/core.txt;pip install -r requirements/test.txt"
6+
script: "nosetests --with-coverage --cover-package=optimizely"
7+
after_success:
8+
- coveralls

CHANGELOG

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-------------------------------------------------------------------------------
2+
0.1.1
3+
-------------------------------------------------------------------------------
4+
* Introduce option to skip JSON schema validation.
5+
-------------------------------------------------------------------------------
6+
7+
-------------------------------------------------------------------------------
8+
0.1.0
9+
-------------------------------------------------------------------------------
10+
* Beta release of the Python SDK for server-side testing.
11+
-------------------------------------------------------------------------------
12+
13+
------------------------------------------------------------------------------

LICENSE

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Copyright 2016 Optimizely
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.

MANIFEST.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include LICENSE
2+
include CHANGELOG
3+
include requirements/*
4+
recursive-exclude tests *

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#Optimizely Python SDK
2+
[![Build Status](https://travis-ci.com/optimizely/optimizely-testing-sdk-python.svg?token=xoLe5GgfDMgLPXDntAq3&branch=master)](https://travis-ci.com/optimizely/optimizely-testing-sdk-python)
3+
[![Coverage Status](https://coveralls.io/repos/github/optimizely/optimizely-testing-sdk-python/badge.svg?branch=master&t=YTzJg8)](https://coveralls.io/github/optimizely/optimizely-testing-sdk-python?branch=master)
4+
[![Apache 2.0](https://img.shields.io/github/license/nebula-plugins/gradle-extra-configurations-plugin.svg)](http://www.apache.org/licenses/LICENSE-2.0)
5+
6+
This Python SDK is an interface to the Optimizely testing framework allowing you to setup and manage your Custom experiments.
7+
8+
###Installing the SDK
9+
10+
Build the SDK using the following command:
11+
```
12+
python setup.py sdist
13+
```
14+
15+
This will create a tarball under `dist/`
16+
17+
Install the SDK by typing the following command:
18+
```
19+
pip install optimizely-testing-sdk-python-{VERSION}.tar.gz
20+
```
21+
22+
The install command will set up all requisite packages.
23+
24+
###Using the SDK
25+
26+
Instructions on using the SDK can be found [here](http://developers.optimizely.com/server/reference/index).
27+
28+
###Unit tests
29+
30+
#####Run all tests
31+
You can trigger all unit tests by typing the following command:
32+
```
33+
nosetests
34+
```
35+
36+
#####Run all tests in file
37+
In order to run all tests under a particular test file you can run the following command:
38+
```
39+
nosetests tests.<file_name_without_extension>
40+
```
41+
42+
For example to run all tests under `test_event`, the command would be:
43+
```
44+
nosetests tests.test_event
45+
```
46+
47+
#####Run all tests under class
48+
In order to run all tests under a particular class of tests you can run the following command:
49+
```
50+
nosetests tests.<file_name_without_extension>:ClassName
51+
```
52+
53+
For example to run all tests under `test_event.EventTest`, the command would be:
54+
```
55+
nosetests tests.test_event:EventTest
56+
```
57+
58+
#####Run single test
59+
In order to run one single test the command would be:
60+
```
61+
nosetests tests.<file_name_without_extension>:ClassName:test_name
62+
```
63+
64+
For example in order to run `test_event.EventTest.test_dispatch`, the command would be:
65+
```
66+
nosetests tests.test_event:EventTest.test_dispatch
67+
```

optimizely/__init__.py

Whitespace-only changes.

optimizely/bucketer.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import math
2+
try:
3+
import mmh3
4+
except ImportError:
5+
from .lib import pymmh3 as mmh3
6+
7+
from .helpers import enums
8+
from . import exceptions
9+
10+
MAX_TRAFFIC_VALUE = 10000
11+
UNSIGNED_MAX_32_BIT_VALUE = 0xFFFFFFFF
12+
MAX_HASH_VALUE = math.pow(2, 32)
13+
HASH_SEED = 1
14+
BUCKETING_ID_TEMPLATE = '{user_id}{parent_id}'
15+
GROUP_POLICIES = ['random']
16+
17+
18+
class Bucketer(object):
19+
""" Optimizely bucketing algorithm that evenly distributes visitors. """
20+
21+
def __init__(self, project_config):
22+
""" Bucketer init method to set bucketing seed and project config data.
23+
24+
Args:
25+
project_config: Project config data to be used in making bucketing decisions.
26+
"""
27+
28+
self.bucket_seed = HASH_SEED
29+
self.config = project_config
30+
31+
def _generate_unsigned_hash_code_32_bit(self, bucketing_id):
32+
""" Helper method to retrieve hash code.
33+
34+
Args:
35+
bucketing_id: ID for bucketing.
36+
37+
Returns:
38+
Hash code which is a 32 bit unsigned integer.
39+
"""
40+
41+
# Adjusting MurmurHash code to be unsigned
42+
return (mmh3.hash(bucketing_id, self.bucket_seed) & UNSIGNED_MAX_32_BIT_VALUE)
43+
44+
def _generate_bucket_value(self, bucketing_id):
45+
""" Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE).
46+
47+
Args:
48+
bucketing_id: ID for bucketing.
49+
50+
Returns:
51+
Bucket value corresponding to the provided bucketing ID.
52+
"""
53+
54+
ratio = float(self._generate_unsigned_hash_code_32_bit(bucketing_id)) / MAX_HASH_VALUE
55+
return math.floor(ratio * MAX_TRAFFIC_VALUE)
56+
57+
def _find_bucket(self, user_id, parent_id, traffic_allocations):
58+
""" Determine entity based on bucket value and traffic allocations.
59+
60+
Args:
61+
user_id: ID for user.
62+
parent_id: ID representing group or experiment.
63+
traffic_allocations: Traffic allocations representing traffic allotted to experiments or variations.
64+
65+
Returns:
66+
Entity ID which may represent experiment or group.
67+
"""
68+
69+
bucketing_id = BUCKETING_ID_TEMPLATE.format(user_id=user_id, parent_id=parent_id)
70+
bucketing_number = self._generate_bucket_value(bucketing_id)
71+
self.config.logger.log(enums.LogLevels.DEBUG, 'Assigned bucket %s to user "%s".' % (bucketing_number, user_id))
72+
73+
for traffic_allocation in traffic_allocations:
74+
current_end_of_range = traffic_allocation.get('endOfRange')
75+
if bucketing_number < current_end_of_range:
76+
return traffic_allocation.get('entityId')
77+
78+
return None
79+
80+
def bucket(self, experiment_key, user_id):
81+
""" For a given experiment key and bucketing ID determines ID of variation to be shown to visitor.
82+
83+
Args:
84+
experiment_key: Key representing experiment for which visitor is to be bucketed.
85+
user_id: ID for user.
86+
87+
Returns:
88+
Variation ID for variation in which the visitor with ID user_id will be put in. None if no variation.
89+
"""
90+
91+
# Check if user is white-listed for a variation
92+
forced_variations = self.config.get_experiment_forced_variations(experiment_key)
93+
if forced_variations and user_id in forced_variations:
94+
variation_key = forced_variations.get(user_id)
95+
variation_id = self.config.get_variation_id(experiment_key, variation_key)
96+
if variation_id:
97+
self.config.logger.log(enums.LogLevels.INFO,
98+
'User "%s" is forced in variation "%s".' % (user_id, variation_key))
99+
return variation_id
100+
101+
# Determine experiment ID
102+
experiment_id = self.config.get_experiment_id(experiment_key)
103+
if not experiment_id:
104+
return None
105+
106+
# Determine if experiment is in a mutually exclusive group
107+
group_policy = self.config.get_experiment_group_policy(experiment_key)
108+
if group_policy in GROUP_POLICIES:
109+
group_id = self.config.get_experiment_group_id(experiment_key)
110+
111+
if not group_id:
112+
return None
113+
114+
group_traffic_allocations = self.config.get_traffic_allocation(self.config.group_id_map, group_id)
115+
116+
if not group_traffic_allocations:
117+
self.config.logger.log(enums.LogLevels.ERROR, 'Group ID "%s" is not in datafile.' % group_id)
118+
self.config.error_handler.handle_error(
119+
exceptions.InvalidExperimentException(enums.Errors.INVALID_GROUP_ID_ERROR)
120+
)
121+
return None
122+
123+
user_experiment_id = self._find_bucket(user_id, group_id, group_traffic_allocations)
124+
if not user_experiment_id:
125+
self.config.logger.log(enums.LogLevels.INFO, 'User "%s" is in no experiment.' % user_id)
126+
return None
127+
128+
if user_experiment_id != experiment_id:
129+
self.config.logger.log(enums.LogLevels.INFO, 'User "%s" is not in experiment "%s" of group %s.' %
130+
(user_id, experiment_key, group_id))
131+
return None
132+
133+
self.config.logger.log(enums.LogLevels.INFO, 'User "%s" is in experiment %s of group %s.' %
134+
(user_id, experiment_key, group_id))
135+
136+
# Bucket user if not in white-list and in group (if any)
137+
experiment_traffic_allocations = self.config.get_traffic_allocation(self.config.experiment_key_map, experiment_key)
138+
if not experiment_traffic_allocations:
139+
self.config.logger.log(enums.LogLevels.ERROR, 'Experiment key "%s" is not in datafile.' % experiment_key)
140+
self.config.error_handler.handle_error(
141+
exceptions.InvalidExperimentException(enums.Errors.INVALID_EXPERIMENT_KEY_ERROR)
142+
)
143+
return None
144+
145+
variation_id = self._find_bucket(user_id, experiment_id, experiment_traffic_allocations)
146+
if variation_id:
147+
variation_key = self.config.get_variation_key_from_id(experiment_key, variation_id)
148+
self.config.logger.log(enums.LogLevels.INFO, 'User "%s" is in variation "%s" of experiment %s.' %
149+
(user_id, variation_key, experiment_key))
150+
return variation_id
151+
152+
self.config.logger.log(enums.LogLevels.INFO, 'User "%s" is in no variation.' % user_id)
153+
return None

0 commit comments

Comments
 (0)