├── .gitignore ├── LICENSE ├── README.md ├── proclaim.py ├── setup.py └── test_proclaim.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | redis.egg-info 3 | build/ 4 | dist/ 5 | dump.rdb 6 | *.tmproj 7 | *.*.swp 8 | *.egg-info 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 James Golick 2 | Copyright (c) 2010 Curt Micol 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proclaim 2 | 3 | Conditionally roll out features with Redis. 4 | 5 | Based on James Golick's [rollout](http://github.com/jamesgolick/rollout). 6 | 7 | ## Usage 8 | 9 | Activate 20% of users to view 'feature': 10 | 11 | >>> import redis 12 | >>> from proclaim import Proclaim 13 | >>> redis = redis.Redis() 14 | >>> pc = Proclaim(redis) 15 | >>> pc.activate_percentage("feature", 20) 16 | 17 | Works with groups and users. User should have an 'id', Proclaim uses it to 18 | verify whether user is 'active'. 19 | 20 | ## Copyright 21 | 22 | Copyright © 2010 James Golick 23 | Copyright © 2010 Curt Micol 24 | 25 | See `LICENSE` for details. 26 | -------------------------------------------------------------------------------- /proclaim.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Proclaim(object): 4 | 5 | def __init__(self, redis): 6 | self.redis = redis 7 | self.groups = { "all": [] } 8 | 9 | def activate_group(self, feature, group): 10 | if group in self.groups: 11 | self.redis.sadd(_group_key(feature), group) 12 | 13 | def deactivate_group(self, feature, group): 14 | self.redis.srem(_group_key(feature), group) 15 | 16 | def deactivate_all(self, feature): 17 | self.redis.delete(_group_key(feature)) 18 | self.redis.delete(_user_key(feature)) 19 | self.redis.delete(_percentage_key(feature)) 20 | 21 | def activate_user(self, feature, user): 22 | self.redis.sadd(_user_key(feature), user.id) 23 | 24 | def deactivate_user(self, feature, user): 25 | self.redis.srem(_user_key(feature), user.id) 26 | 27 | def define_group(self, group, *users): 28 | self.groups[group] = [] 29 | for user in users: 30 | self.groups[group].append(user.id) 31 | 32 | def is_active(self, feature, user): 33 | if self._user_in_active_group(feature, user): 34 | return True 35 | if self._user_active(feature, user): 36 | return True 37 | if self._user_within_active_percentage(feature, user): 38 | return True 39 | return False 40 | 41 | def activate_percentage(self, feature, percentage): 42 | self.redis.set(_percentage_key(feature), percentage) 43 | 44 | def deactivate_percentage(self, feature, percentage): 45 | self.redis.delete(_percentage_key(feature), percentage) 46 | 47 | def _user_in_active_group(self, feature, user): 48 | if self.redis.exists(_group_key(feature)): 49 | active_groups = self.redis.smembers(_group_key(feature)) 50 | if active_groups: 51 | for grp in active_groups: 52 | if user.id in self.groups[grp]: 53 | return True 54 | return False 55 | 56 | def _user_active(self, feature, user): 57 | if self.redis.sismember(_user_key(feature), user.id): 58 | return True 59 | return False 60 | 61 | def _user_within_active_percentage(self, feature, user): 62 | if self.redis.exists(_percentage_key(feature)): 63 | percentage = self.redis.get(_percentage_key(feature)) 64 | if int(user.id) % 10 < int(percentage) / 10: 65 | return True 66 | return False 67 | 68 | def _key(name): 69 | return "feature:%s" % name 70 | 71 | def _group_key(name): 72 | return "%s:groups" % (_key(name)) 73 | 74 | def _user_key(name): 75 | return "%s:users" % (_key(name)) 76 | 77 | def _percentage_key(name): 78 | return "%s:percentage" % (_key(name)) 79 | 80 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools.core import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | setup( 7 | name="proclaim", 8 | version="0.5", 9 | description="Conditionally roll out features with Redis", 10 | long_description=""" 11 | Conditionally roll out features with Redis by assigning 12 | percentages, groups or users to features. This is a port of jamesgolick's 13 | rollout: http://github.com/jamesgolick/rollout""", 14 | author="Curt Micol", 15 | license="MIT", 16 | author_email="asenchi@asenchi.com", 17 | url="http://github.com/asenchi/proclaim", 18 | download_url="http://github.com/asenchi/proclaim/downloads", 19 | keywords="redis rollout", 20 | classifiers = [ 21 | "Development Status :: 5 - Alpha", 22 | "Intended Audience :: Developers", 23 | "Intended Audience :: Web Developers", 24 | "License :: OSI Approved :: MIT License", 25 | "Operating System :: OS Independant", 26 | "Topic :: Software Development" 27 | ], 28 | py_modules=["proclaim"], 29 | ) 30 | -------------------------------------------------------------------------------- /test_proclaim.py: -------------------------------------------------------------------------------- 1 | import redis 2 | import unittest 3 | 4 | from proclaim import Proclaim 5 | 6 | class User(object): 7 | def __init__(self, **entries): 8 | self.__dict__.update(entries) 9 | 10 | jim = User(id=1, username='jim@test.com') 11 | bob = User(id=23, username='bob@test.com') 12 | joan = User(id=40, username='joan@test.com') 13 | 14 | class TestProclaim(unittest.TestCase): 15 | 16 | def setUp(self): 17 | self.redis = redis.Redis(host='localhost', port=6379) 18 | self.proclaim = Proclaim(self.redis) 19 | self.proclaim.define_group("a", jim, joan) 20 | self.proclaim.define_group("b", jim, joan, bob) 21 | 22 | def test_groups(self): 23 | assert len(self.proclaim.groups["b"]) == 3 24 | assert jim.id in self.proclaim.groups["a"] 25 | 26 | def test_activate_group(self): 27 | self.proclaim.activate_group("f1", "b") 28 | assert self.proclaim.is_active("f1", jim) 29 | 30 | def test_deactivate_group(self): 31 | self.proclaim.deactivate_group("f1", "b") 32 | assert not self.proclaim.is_active("f1", jim) 33 | 34 | def test_activate_user(self): 35 | self.proclaim.activate_user("f2", joan) 36 | assert self.proclaim.is_active("f2", joan) 37 | 38 | def test_deactivate_user(self): 39 | self.proclaim.deactivate_user("f2", joan) 40 | assert not self.proclaim.is_active("f2", joan) 41 | 42 | def test_activate_percentage(self): 43 | self.proclaim.activate_percentage("f3", 25) 44 | assert self.proclaim.is_active("f3", jim) 45 | assert self.proclaim.is_active("f3", joan) 46 | assert not self.proclaim.is_active("f3", bob) 47 | 48 | def test_deactivate_percentage(self): 49 | self.proclaim.deactivate_percentage("f3", 25) 50 | assert not self.proclaim.is_active("f3", jim) 51 | --------------------------------------------------------------------------------