Permalink
Browse files

Added ``REDIS_URL`` configuration variable for configuring the Redis …

…connection.
  • Loading branch information...
jpvanhal committed Jun 3, 2012
1 parent 765f03f commit 9494c66407d4e1850b22e5a98aaeb88c40a14909
Showing with 230 additions and 192 deletions.
  1. +7 −5 CHANGES.rst
  2. +4 −0 docs/index.rst
  3. +6 −3 flask_split/core.py
  4. +38 −41 flask_split/models.py
  5. +26 −0 flask_split/utils.py
  6. +10 −5 flask_split/views.py
  7. +1 −1 setup.py
  8. +4 −3 tests/__init__.py
  9. +10 −10 tests/test_dashboard.py
  10. +54 −54 tests/test_extension.py
  11. +70 −70 tests/test_models.py
View
@@ -3,13 +3,15 @@ Changelog
Here you can see the full list of changes between each Flask-Split release.
-0.1.3 (unreleased)
+0.2.0 (unreleased)
^^^^^^^^^^^^^^^^^^
-- Fixed :func:`finished` incrementing alternative's completion count multiple
- times, when the test is not reset after it has been finished. The fix for
- this issue in the previous release did not work, when the version of the test
- was greater than 0.
+- Added ``REDIS_URL`` configuration variable for configuring the Redis
+ connection.
+- Fixed properly :func:`finished` incrementing alternative's completion count
+ multiple times, when the test is not reset after it has been finished. The
+ fix for this issue in the previous release did not work, when the version of
+ the test was greater than 0.
0.1.3 (2012-05-30)
^^^^^^^^^^^^^^^^^^
View
@@ -82,6 +82,10 @@ ways.
A list of configuration keys currently understood by the extension:
+``REDIS_URL``
+ The database URL that should be used for the Redis connection. Defaults to
+ ``'redis://localhost:6379'``.
+
``SPLIT_ALLOW_MULTIPLE_EXPERIMENTS``
If set to `True` Flask-Split will allow users to participate in multiple
experiments.
View
@@ -15,6 +15,7 @@
from redis import ConnectionError
from .models import Alternative, Experiment
+from .utils import _get_redis_connection
from .views import split
@@ -74,9 +75,10 @@ def ab_test(experiment_name, *alternatives):
default each alternative has the weight of 1. The first alternative
is the control. Every experiment must have at least two alternatives.
"""
+ redis = _get_redis_connection()
try:
experiment = Experiment.find_or_create(
- experiment_name, *alternatives)
+ redis, experiment_name, *alternatives)
if experiment.winner:
return experiment.winner.name
else:
@@ -114,16 +116,17 @@ def finished(experiment_name, reset=True):
"""
if _exclude_visitor():
return
+ redis = _get_redis_connection()
try:
- experiment = Experiment.find(experiment_name)
+ experiment = Experiment.find(redis, experiment_name)
if not experiment:
return
alternative_name = _get_session().get(experiment.key)
if alternative_name:
if 'split_finished' not in session:
session['split_finished'] = set()
if experiment.key not in session['split_finished']:
- alternative = Alternative(alternative_name, experiment_name)
+ alternative = Alternative(redis, alternative_name, experiment_name)
alternative.increment_completion()
if reset:
_get_session().pop(experiment.key, None)
View
@@ -13,14 +13,10 @@
from math import sqrt
from random import random
-from redis import Redis
-
-
-redis = Redis()
-
class Alternative(object):
- def __init__(self, name, experiment_name):
+ def __init__(self, redis, name, experiment_name):
+ self.redis = redis
self.experiment_name = experiment_name
if isinstance(name, tuple):
self.name, self.weight = name
@@ -29,32 +25,32 @@ def __init__(self, name, experiment_name):
self.weight = 1
def _get_participant_count(self):
- return int(redis.hget(self.key, 'participant_count') or 0)
+ return int(self.redis.hget(self.key, 'participant_count') or 0)
def _set_participant_count(self, count):
- redis.hset(self.key, 'participant_count', int(count))
+ self.redis.hset(self.key, 'participant_count', int(count))
participant_count = property(
_get_participant_count,
_set_participant_count
)
def _get_completed_count(self):
- return int(redis.hget(self.key, 'completed_count') or 0)
+ return int(self.redis.hget(self.key, 'completed_count') or 0)
def _set_completed_count(self, count):
- redis.hset(self.key, 'completed_count', int(count))
+ self.redis.hset(self.key, 'completed_count', int(count))
completed_count = property(
_get_completed_count,
_set_completed_count
)
def increment_participation(self):
- redis.hincrby(self.key, 'participant_count', 1)
+ self.redis.hincrby(self.key, 'participant_count', 1)
def increment_completion(self):
- redis.hincrby(self.key, 'completed_count', 1)
+ self.redis.hincrby(self.key, 'completed_count', 1)
@property
def is_control(self):
@@ -68,20 +64,20 @@ def conversion_rate(self):
@property
def experiment(self):
- return Experiment.find(self.experiment_name)
+ return Experiment.find(self.redis, self.experiment_name)
def save(self):
- redis.hsetnx(self.key, 'participant_count', 0)
- redis.hsetnx(self.key, 'completed_count', 0)
+ self.redis.hsetnx(self.key, 'participant_count', 0)
+ self.redis.hsetnx(self.key, 'completed_count', 0)
def reset(self):
- redis.hmset(self.key, {
+ self.redis.hmset(self.key, {
'participant_count': 0,
'completed_count': 0
})
def delete(self):
- redis.delete(self.key)
+ self.redis.delete(self.key)
@property
def key(self):
@@ -134,10 +130,11 @@ def confidence_level(self):
class Experiment(object):
- def __init__(self, name, *alternative_names):
+ def __init__(self, redis, name, *alternative_names):
+ self.redis = redis
self.name = name
self.alternatives = [
- Alternative(alternative, name)
+ Alternative(redis, alternative, name)
for alternative in alternative_names
]
@@ -146,12 +143,12 @@ def control(self):
return self.alternatives[0]
def _get_winner(self):
- winner = redis.hget('experiment_winner', self.name)
+ winner = self.redis.hget('experiment_winner', self.name)
if winner:
- return Alternative(winner, self.name)
+ return Alternative(self.redis, winner, self.name)
def _set_winner(self, winner_name):
- redis.hset('experiment_winner', self.name, winner_name)
+ self.redis.hset('experiment_winner', self.name, winner_name)
winner = property(
_get_winner,
@@ -160,12 +157,12 @@ def _set_winner(self, winner_name):
def reset_winner(self):
"""Reset the winner of this experiment."""
- redis.hdel('experiment_winner', self.name)
+ self.redis.hdel('experiment_winner', self.name)
@property
def start_time(self):
"""The start time of this experiment."""
- t = redis.hget('experiment_start_times', self.name)
+ t = self.redis.hget('experiment_start_times', self.name)
if t:
return datetime.strptime(t, '%Y-%m-%dT%H:%M:%S')
@@ -199,10 +196,10 @@ def random_alternative(self):
@property
def version(self):
- return int(redis.get('%s:version' % self.name) or 0)
+ return int(self.redis.get('%s:version' % self.name) or 0)
def increment_version(self):
- redis.incr('%s:version' % self.name)
+ self.redis.incr('%s:version' % self.name)
@property
def key(self):
@@ -223,53 +220,53 @@ def delete(self):
for alternative in self.alternatives:
alternative.delete()
self.reset_winner()
- redis.srem('experiments', self.name)
- redis.delete(self.name)
+ self.redis.srem('experiments', self.name)
+ self.redis.delete(self.name)
self.increment_version()
@property
def is_new_record(self):
- return self.name not in redis
+ return self.name not in self.redis
def save(self):
if self.is_new_record:
start_time = self._get_time().isoformat()[:19]
- redis.sadd('experiments', self.name)
- redis.hset('experiment_start_times', self.name, start_time)
+ self.redis.sadd('experiments', self.name)
+ self.redis.hset('experiment_start_times', self.name, start_time)
for alternative in reversed(self.alternatives):
- redis.lpush(self.name, alternative.name)
+ self.redis.lpush(self.name, alternative.name)
@classmethod
- def load_alternatives_for(cls, name):
+ def load_alternatives_for(cls, redis, name):
return redis.lrange(name, 0, -1)
@classmethod
- def all(cls):
- return [cls.find(e) for e in redis.smembers('experiments')]
+ def all(cls, redis):
+ return [cls.find(redis, e) for e in redis.smembers('experiments')]
@classmethod
- def find(cls, name):
+ def find(cls, redis, name):
if name in redis:
- return cls(name, *cls.load_alternatives_for(name))
+ return cls(redis, name, *cls.load_alternatives_for(redis, name))
@classmethod
- def find_or_create(cls, key, *alternatives):
+ def find_or_create(cls, redis, key, *alternatives):
name = key.split(':')[0]
if len(alternatives) < 2:
raise TypeError('You must declare at least 2 alternatives.')
- experiment = cls.find(name)
+ experiment = cls.find(redis, name)
if experiment:
alts = [a[0] if isinstance(a, tuple) else a for a in alternatives]
if [a.name for a in experiment.alternatives] != alts:
experiment.reset()
for alternative in experiment.alternatives:
alternative.delete()
- experiment = cls(name, *alternatives)
+ experiment = cls(redis, name, *alternatives)
experiment.save()
else:
- experiment = cls(name, *alternatives)
+ experiment = cls(redis, name, *alternatives)
experiment.save()
return experiment
View
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.ext.split.utils
+ ~~~~~~~~~~~~~~~~~~~~~
+
+ Generic utility functions.
+
+ :copyright: (c) 2012 by Janne Vanhala.
+ :license: MIT, see LICENSE for more details.
+"""
+
+from flask import current_app
+import redis
+
+
+def _get_redis_connection():
+ """
+ Return a Redis connection based on the Flask application's configuration.
+
+ The connection parameters are retrieved from `REDIS_URL` configuration
+ variable.
+
+ :return: an instance of :class:`redis.Connection`
+ """
+ url = current_app.config.get('REDIS_URL', 'redis://localhost:6379')
+ return redis.from_url(url)
View
@@ -14,6 +14,7 @@
from flask import Blueprint, redirect, render_template, request, url_for
from .models import Alternative, Experiment
+from .utils import _get_redis_connection
root = os.path.abspath(os.path.dirname(__file__))
@@ -33,18 +34,20 @@ def inject_version():
@split.route('/')
def index():
"""Render a dashboard that lists all active experiments."""
+ redis = _get_redis_connection()
return render_template('split/index.html',
- experiments=Experiment.all()
+ experiments=Experiment.all(redis)
)
@split.route('/<experiment>', methods=['POST'])
def set_experiment_winner(experiment):
"""Mark an alternative as the winner of the experiment."""
- experiment = Experiment.find(experiment)
+ redis = _get_redis_connection()
+ experiment = Experiment.find(redis, experiment)
if experiment:
alternative_name = request.form.get('alternative')
- alternative = Alternative(alternative_name, experiment.name)
+ alternative = Alternative(redis, alternative_name, experiment.name)
if alternative.name in experiment.alternative_names:
experiment.winner = alternative.name
return redirect(url_for('.index'))
@@ -53,7 +56,8 @@ def set_experiment_winner(experiment):
@split.route('/<experiment>/reset', methods=['POST'])
def reset_experiment(experiment):
"""Delete all data for an experiment."""
- experiment = Experiment.find(experiment)
+ redis = _get_redis_connection()
+ experiment = Experiment.find(redis, experiment)
if experiment:
experiment.reset()
return redirect(url_for('.index'))
@@ -62,7 +66,8 @@ def reset_experiment(experiment):
@split.route('/<experiment>/delete', methods=['POST'])
def delete_experiment(experiment):
"""Delete an experiment and all its data."""
- experiment = Experiment.find(experiment)
+ redis = _get_redis_connection()
+ experiment = Experiment.find(redis, experiment)
if experiment:
experiment.delete()
return redirect(url_for('.index'))
View
@@ -32,7 +32,7 @@ def run(self):
platforms='any',
install_requires=[
'Flask>=0.7',
- 'Redis>=2.0',
+ 'Redis>=2.4.13',
],
cmdclass={'test': PyTest},
classifiers=[
View
@@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
from flask import Flask
-from flask_split import split
-from flask_split.models import redis
+from flask.ext.split import split
+from redis import Redis
class TestCase(object):
def setup_method(self, method):
- redis.flushall()
+ self.redis = Redis()
+ self.redis.flushall()
self.app = Flask(__name__)
self.app.debug = True
self.app.secret_key = 'very secret'
Oops, something went wrong.

0 comments on commit 9494c66

Please sign in to comment.