Skip to content

Instantly share code, notes, and snippets.

@dpopowich
Last active August 29, 2015 14:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dpopowich/11374809 to your computer and use it in GitHub Desktop.
Save dpopowich/11374809 to your computer and use it in GitHub Desktop.
A python module providing a pythonic interface to redis-py's StrictRedis class.
# Copyright (c) 2014 Daniel Popowich
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
'''
This gist accompanies a blog post at:
http://www.artandlogic.com/blog/2014/05/making-an-inherited-api-pythonic/
This module provides classes which offer pythonic interfaces to
redis-py, the defacto standard python package for redis.
An abstract base class, RedisBase, provides base implementation for
five classes that map to the five data structures in redis:
RedisString
RedisSet
RedisSortedSet
RedisList
RedisHash
Each of the subclasses delegates redis commands to the underlying
redis-py method, giving a more pythonic interface to the redis
types. Typical usage might be::
class UserSession(RedisString):
pass
session = UserSession('phred')
session.set(someValue)
The above will create a key in redis, "usersession:phred", with the
value `str(someValue)`.
The prefix can be specified in one of three ways:
1) Setting a `prefix` attribute on the class.
2) passing it to the constructor, e.g.:
RedisString("mykey", prefix="myprefix")
3) the default is the class name, lower-cased. A colon will be
used to separate the prefix from the key.
See the redis documentation at http://redis.io/commands for details
about each data structure's methods.
NOTE: some of the classes use python's data model to offer python
semantics for certain actions. For example if `rset` is an
instance of RedisSet, then both of these are equivalent:
if rset.sismember(item):
...
if item in rset:
...
See the doc strings for each class for what is available. (Way
more is possible than is implemented in this module. Fork and
extend!)
==========
INSTALLING
==========
The only dependency of this module is redis-py:
pip install redis
=======
TESTING
=======
If pytest is installed, you can run the test suite:
py.test redis-pythonic.py
BUT CAUTION: the test-suite flushes the redis database! Use a test
database!!!
'''
import redis
def get_redis_connection(*args, **kwargs):
'''For purposes of this gist, we need a simple mechanism to manage a
global redis connection. Production code will want to replace this
or implement a more robust version.
With args or kwargs, initializes (or resets) a global connection
object. Without args or kwargs, returns the previously created
redis connection, or creates a default connection if not previously
called with args or kwargs.
Args and kwargs are passed directly to the redis.StrictRedis
constructor:
_the_connection = redis.StrictRedis(*args, **kwargs)
'''
global _the_connection
if args or kwargs:
_the_connection = redis.StrictRedis(*args, **kwargs)
else:
try:
_the_connection
except NameError:
_the_connection = redis.StrictRedis()
return _the_connection
class RedisBase(object):
'''See module doc string.
Essentially, any redis command that accepts a "key" as its first
argument is available as a method on a subclass and that method
delegates the call to the underlying StrictRedis connection
instance.
'''
# http://redis.io/commands#generic
_redis_methods = ['delete', 'exists', 'expire',
'expireat', 'persist', 'pexpire',
'pexpireat', 'pttl', 'ttl',]
def __init__(self, key, prefix=None, sep=':'):
'''Create a redis helper object.
Args:
key: the key name
prefix: (optional) the key's prefix. Uses class `prefix`
attribute or class name (lower-cased) as prefix if not
specified here. The final key will be `prefix` + `sep` +
`key`, unless prefix is an empty string in which case
`key` will be used with no prefix.
sep: The string to separate `prefix` and `key` in generating
the final key.
'''
if self.__class__ == RedisBase:
raise RuntimeError, 'RedisBase is an abstract base class'
# assert some order over the key space
assert len(key)
if prefix != '':
try:
if prefix is None:
# see if prefix is set on the class
prefix = self.prefix
except AttributeError:
prefix = self.__class__.__name__.lower()
key = prefix + sep + str(key) if prefix else str(key)
self.key = key
def __getattribute__(self, name):
'The magic that delegates redis commands to the underlying redis-py methods'
def helper(*args, **kwargs):
method = getattr(get_redis_connection(), name)
return method(self.key, *args, **kwargs)
methods = object.__getattribute__(self, '_redis_methods')
if name in methods:
return helper
return object.__getattribute__(self, name)
class RedisString(RedisBase):
'''Model a redis String.
Also supports len().
'''
# http://redis.io/commands#string
_redis_methods = RedisBase._redis_methods + \
['append', 'bitcount', 'decr', 'decrby', 'get',
'getbit', 'getrange', 'getset', 'incr', 'incrby', 'incrbyfloat',
'psetex', 'set', 'setbit', 'setex', 'setnx', 'setrange',
'strlen']
def __len__(self):
return self.strlen()
class RedisSet(RedisBase):
'''Model a redis Set.
Also supports `in`, len(), and iteration.
'''
# http://redis.io/commands#set
_redis_methods = RedisBase._redis_methods + \
['sadd', 'scard', 'sdiff', 'sinter',
'sismember', 'smembers', 'spop',
'srandmember', 'srem', 'sunion', ]
def __contains__(self, item):
return self.sismember(item)
def __len__(self):
return self.scard()
def __iter__(self):
return iter(self.smembers())
class RedisSortedSet(RedisBase):
'''Model a redis Sorted Set
Also supports `in`, len(), and iteration.
'''
# http://redis.io/commands#sorted_set
_redis_methods = RedisBase._redis_methods + \
['zadd', 'zcard', 'zcount', 'zincrby',
'zrange', 'zrangebyscore',
'zrank', 'zrem', 'zremrangebyrank',
'zremrangebyscore', 'zrevrange', 'zrevrangebyscore',
'zrevrank', 'zscore', ]
def __contains__(self, item):
return self.zscore(item)
def __len__(self):
return self.zcard()
def __iter__(self):
return iter(self.zrange(0, -1))
class RedisList(RedisBase):
'''Model a redis List
Also supports len(), and iteration.
'''
# http://redis.io/commands#list
_redis_methods = RedisBase._redis_methods + \
['blpop', 'brpop', 'lindex', 'linsert', 'llen',
'lpop', 'lpush', 'lpushx', 'lrange', 'lrem',
'lset', 'ltrim', 'rpop', 'rpush', 'rpushx',]
def __len__(self):
return self.llen()
def __iter__(self):
return iter(self.lrange(0, -1))
class RedisHash(RedisBase):
'''Model a redis Hash.
Also supports len() and iteration over keys.
'''
# http://redis.io/commands#hash
_redis_methods = RedisBase._redis_methods + \
['hdel', 'hexists', 'hget', 'hgetall',
'hincrby', 'hincrbyfloat', 'hkeys', 'hlen',
'hmget', 'hmset', 'hset', 'hsetnx', 'hvals',]
def __len__(self):
return self.hlen()
def __iter__(self):
return iter(self.hkeys())
# if pytest is available...
try:
import pytest
import random
import time
@pytest.fixture()
def flushdb():
'''Flush the database.
'''
return get_redis_connection().flushdb()
@pytest.fixture(scope='session')
def proper_names():
'return list of names'
# 100 names
names = '''QWFyb24KQWRhCkFkYW0KQWRsYWkKQWRyaWFuCkFkcmllbm5lCkFnYXRoYQpBZ25ldGhhCkFo
bWVkCkFobWV0CkFpbWVlCkFsCkFsYWluCkFsYW4KQWxhc2RhaXIKQWxhc3RhaXIKQWxiZXJ0
CkFsYmVydG8KQWxlamFuZHJvCkFsZXgKQWxleGEKQWxleGFuZGVyCkFsZXhpYQpBbGV4aXMK
QWxmCkFsZnJlZApBbGljZQpBbGlzb24KQWxsYW4KQWxsZW4KQWx2aW4KQW1hbmRhCkFtYXJ0
aApBbWVkZW8KQW1pCkFtaWdvCkFtaXIKQW1vcwpBbXkKQW5haXMKQW5hc3Rhc2lhCkFuYXRv
bGUKQW5hdG9seQpBbmRlcnNvbgpBbmRyZQpBbmRyZWEKQW5kcmVhcwpBbmRyZXcKQW5kcmll
cwpBbmR5CkFuZ2VsYQpBbmdlbGljYQpBbmd1cwpBbml0YQpBbm4KQW5uYQpBbm5hcmQKQW5u
ZQpBbm5pZQpBbnRoZWEKQW50aG9ueQpBbnRvbgpBbnRvbmVsbGEKQW50b25pbwpBbnRvbnkK
QW55YQpBcHJpbApBcmNoaWJhbGQKQXJjaGllCkFyaWVsCkFybGVuZQpBcm5lCkFybm9sZApB
cnQKQXJ0aHVyCkF0aGVsCkF1YmVyb24KQXVicmV5CkF1ZHJleQpBdWd1c3R1cwpBdmVyeQpB
eGVsCkJhcmJhcmEKQmFyYnJhCkJhcm5leQpCYXJyZXR0CkJhcnJpbwpCYXJyeQpCYXJ0CkJh
cnRvbgpCZWEKQmVja2llCkJlY2t5CkJlaGRhZApCZWxpbmRhCkJlbgpCZW5qYW1pbgpCZW5u
eQpCZW5zb24KQmVybmFyZAo='''.decode('base64').split()
return names
def test_helper():
'test that prefix expectations are met'
class PrefixTest(RedisString):
pass
with pytest.raises(TypeError):
# need a key
PrefixTest()
# assert various ways a prefix can be set
# class name
assert PrefixTest('mykey').key == 'prefixtest:mykey'
# explicit
assert PrefixTest('mykey', prefix='foo').key == 'foo:mykey'
# class attribute
PrefixTest.prefix = 'bar'
assert PrefixTest('mykey').key == 'bar:mykey'
# different separator
assert PrefixTest('mykey', sep='^').key == 'bar^mykey'
# empty prefix
assert PrefixTest('keyonly', prefix='').key == 'keyonly'
# key needs to be non-empty
with pytest.raises(AssertionError):
PrefixTest('', prefix='')
assert PrefixTest('xx', prefix='').key == 'xx'
def test_no_save(flushdb):
'creating keys in python should not create keys in redis'
assert flushdb
for x in range(100):
key = 's%d' % x
RedisString('str' + key)
RedisSet('set' + key)
RedisSortedSet('zset' + key)
RedisHash('hash' + key)
assert not get_redis_connection().keys()
# class used in next two tests
class StringTest(RedisString):
pass
def test_string(flushdb, proper_names):
'test creating strings'
assert flushdb
for name in proper_names:
StringTest(name).set(True)
assert len(get_redis_connection().keys()) == len(proper_names)
for name in proper_names:
assert StringTest(name).exists()
def test_expire(flushdb, proper_names):
'test expiring keys'
EXPIRES = 1
assert flushdb
names = proper_names[:10]
for name in names:
st = StringTest(name)
st.setex(EXPIRES, True)
assert st.exists()
time.sleep(EXPIRES)
for name in names:
assert not StringTest(name).exists()
def test_set(flushdb, proper_names):
assert flushdb
model = RedisSet('settest', prefix='')
for name in proper_names:
model.sadd(name)
assert model.scard() == len(proper_names)
model.srem(*proper_names)
assert model.scard() == 0
def test_sortedset(flushdb, proper_names):
assert flushdb
model = RedisSortedSet('sortedsettest', prefix='')
count = len(proper_names)
# add in reverse order
for i, name in enumerate(proper_names):
score = count - i
model.zadd(score, name)
assert model.zcard() == len(proper_names)
# read them back and reverse the list
names = model.zrange(0,-1)
names.reverse()
assert names == proper_names
def test_list(flushdb, proper_names):
assert flushdb
model = RedisList('listtest', prefix='')
count = len(proper_names)
# add from the tail, thus producing a list in the same order as
# proper_names
for name in proper_names:
model.rpush(name)
assert len(model) == len(proper_names)
# read them back and assert they're in the expected order
for i, name in enumerate(model):
assert name == proper_names[i]
def test_hashset(flushdb, proper_names):
assert flushdb
class HashTest(RedisHash):
pass
people = {}
for name in proper_names:
# create a "person" object, with attributes age and friend
age = random.randrange(20,60)
friends = random.sample(proper_names, random.randrange(5))
attrs = dict(age=str(age), friends=','.join(friends))
people[name] = attrs
HashTest(name).hmset(attrs)
# read back and compare
for name in proper_names:
assert HashTest(name).hgetall() == people[name]
except ImportError:
pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment