summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES66
-rw-r--r--redis/client.py81
-rw-r--r--tests/server_commands.py51
3 files changed, 134 insertions, 64 deletions
diff --git a/CHANGES b/CHANGES
index 0d1955b..ea91ee0 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,58 +1,8 @@
-* 1.3.6
- * Implementation of all Hash commands
- * Pipelines now wrap their execution with MULTI and EXEC commands to
- process all commands atomically.
- * Connections can now set timeout. If command execution exceeds the
- timeout, an exception is raised.
- * Numerous bug fixes and more tests.
-* 1.3.4
- * Skipped version numbers ahead so that the client version matches the
- Redis version it is feature-compatible with. Going forward, the client
- will stay in sync with Redis version numbers when client updates are
- made.
- * Completely refactored the client library. It's now trivial to maintain
- and add new commands. The library is also much more consistent.
- * With the exception of "Response value type inference" (see below), the
- client should be backwards compatible with 0.6.1. Some older, less
- consistent methods will emit DeprecationWarnings, indicating that you
- should use another command or option, but these should continue to
- work as expected for the next few releases.
- * WARNING: BACKWARDS INCOMPATIBLE CHANGE: "Response value type inference"
- Previously, all values returned from Redis went through a decoding
- process. In this process, if the response was numeric, it would be
- automatically converted to an int or float type prior to being returned.
- Otherwise the response would be decoded as a unicode string. This meant
- that storing the string "123" would actually return an integer 123, and
- that the string "foo" would be returned as the unicode object u"foo".
- This fundamentally breaks the retrieval of binary data (byte strings) and
- values that might accidentally look like a number (a hash value). After
- discussing this in detail with a number of users and on the Redis mailing
- list (http://groups.google.com/group/redis-db/browse_thread/thread/9888eb9ff383c90c/ec44fe80b6400f7b#ec44fe80b6400f7b)
- *ALL* values returned from methods such as get() now return raw
- Python strings. It is now your responsibility to convert that data to
- whatever datatype you need. Other methods that *always* return integer
- or float values, such as INCR, DECR, LLEN, ZSCORE, etc., will continue
- returning values of the appropriate type. This resolves issue #2, #8
- and #11:
- http://github.com/andymccurdy/redis-py/issues#issue/2
- http://github.com/andymccurdy/redis-py/issues#issue/8
- http://github.com/andymccurdy/redis-py/issues#issue/11
- * The "select" method now takes a "host" and "port" argument in addition
- to the database. Behind the scenes, select() swaps out the underlying
- socket connection. This resolves issue #4:
- http://github.com/andymccurdy/redis-py/issues#issue/4
- * The client now supports pipelining of Redis commands. Use the pipeline()
- method to create a new Pipeline object. Each command called on the
- pipeline object will be buffered until the pipeline if executed.
- A list of each command's results will be returned by execution. Use
- this for batch processing in order to eliminate multiple request/response
- cycles.
-
-* 0.6.1
- * Added support for ZINCRBY via the `zincr` command
- * Swapped score and member parameters to zadd to make it more similar to other commands.
- * Added support for Python 2.4 (thanks David Moss)
-* 0.6.0 Changed to Andy McCurdy's codebase on github
-* 0.5.5 Patch from David Moss, SHUTDOWN and doctest bugfix
-* 0.5.1-4 Bugfixes, no code changes, just packaging, 10/2/09
-* 0.5 Initial release, redis.py version 1.0.1, 10/2/09
+* 2.2.0
+ * Implemented SLAVEOF
+ * Implemented CONFIG as config_get and config_set
+ * Implemented GETBIT/SETBIT
+ * Implemented BRPOPLPUSH
+ * Implemented STRLEN
+ * Implemented PERSIST
+ * Implemented SETRANGE
diff --git a/redis/client.py b/redis/client.py
index 3f9af0d..967bcee 100644
--- a/redis/client.py
+++ b/redis/client.py
@@ -193,6 +193,11 @@ def float_or_none(response):
return None
return float(response)
+def parse_config(response, **options):
+ # this is stupid, but don't have a better option right now
+ if options['parse'] == 'GET':
+ return response and pairs_to_dict(response) or {}
+ return response == 'OK'
class Redis(threading.local):
"""
@@ -207,13 +212,13 @@ class Redis(threading.local):
RESPONSE_CALLBACKS = dict_merge(
string_keys_to_dict(
'AUTH DEL EXISTS EXPIRE EXPIREAT HDEL HEXISTS HMSET MOVE MSETNX '
- 'RENAMENX SADD SISMEMBER SMOVE SETEX SETNX SREM ZADD ZREM',
+ 'PERSIST RENAMENX SADD SISMEMBER SMOVE SETEX SETNX SREM ZADD ZREM',
bool
),
string_keys_to_dict(
- 'DECRBY HLEN INCRBY LINSERT LLEN LPUSHX RPUSHX SCARD SDIFFSTORE '
- 'SINTERSTORE SUNIONSTORE ZCARD ZREMRANGEBYRANK ZREMRANGEBYSCORE '
- 'ZREVRANK',
+ 'DECRBY GETBIT HLEN INCRBY LINSERT LLEN LPUSHX RPUSHX SCARD '
+ 'SDIFFSTORE SETBIT SETRANGE SINTERSTORE STRLEN SUNIONSTORE ZCARD '
+ 'ZREMRANGEBYRANK ZREMRANGEBYSCORE ZREVRANK',
int
),
string_keys_to_dict(
@@ -224,7 +229,7 @@ class Redis(threading.local):
string_keys_to_dict('ZSCORE ZINCRBY', float_or_none),
string_keys_to_dict(
'FLUSHALL FLUSHDB LSET LTRIM MSET RENAME '
- 'SAVE SELECT SET SHUTDOWN WATCH UNWATCH',
+ 'SAVE SELECT SET SHUTDOWN SLAVEOF WATCH UNWATCH',
lambda r: r == 'OK'
),
string_keys_to_dict('BLPOP BRPOP', lambda r: r and tuple(r) or None),
@@ -236,6 +241,8 @@ class Redis(threading.local):
'BGREWRITEAOF': lambda r: \
r == 'Background rewriting of AOF file started',
'BGSAVE': lambda r: r == 'Background saving started',
+ 'BRPOPLPUSH': lambda r: r and r or None,
+ 'CONFIG': parse_config,
'HGETALL': lambda r: r and pairs_to_dict(r) or {},
'INFO': parse_info,
'LASTSAVE': timestamp_to_datetime,
@@ -474,6 +481,14 @@ class Redis(threading.local):
"""
return self.execute_command('BGSAVE')
+ def config_get(self, pattern="*"):
+ "Return a dictionary of configuration based on the ``pattern``"
+ return self.execute_command('CONFIG', 'GET', pattern, parse='GET')
+
+ def config_set(self, name, value):
+ "Set config item ``name`` with ``value``"
+ return self.execute_command('CONFIG', 'SET', name, value, parse='SET')
+
def dbsize(self):
"Returns the number of keys in the current database"
return self.execute_command('DBSIZE')
@@ -521,6 +536,16 @@ class Redis(threading.local):
"""
return self.execute_command('SAVE')
+ def slaveof(self, host=None, port=None):
+ """
+ Set the server to be a replicated slave of the instance identified
+ by the ``host`` and ``port``. If called without arguements, the
+ instance is promoted to a master instead.
+ """
+ if host is None and port is None:
+ return self.execute_command("SLAVEOF NO ONE")
+ return self.execute_command("SLAVEOF", host, port)
+
#### BASIC KEY COMMANDS ####
def append(self, key, value):
"""
@@ -562,6 +587,10 @@ class Redis(threading.local):
return self.execute_command('GET', name)
__getitem__ = get
+ def getbit(self, name, offset):
+ "Returns a boolean indicating the value of ``offset`` in ``name``"
+ return self.execute_command('GETBIT', name, offset)
+
def getset(self, name, value):
"""
Set the value at key ``name`` to ``value`` if key doesn't exist
@@ -610,6 +639,10 @@ class Redis(threading.local):
"Moves the key ``name`` to a different Redis database ``db``"
return self.execute_command('MOVE', name, db)
+ def persist(self, name):
+ "Removes an expiration on ``name``"
+ return self.execute_command('PERSIST', name)
+
def randomkey(self):
"Returns the name of a random key"
return self.execute_command('RANDOMKEY')
@@ -662,6 +695,14 @@ class Redis(threading.local):
return self.execute_command('SET', name, value)
__setitem__ = set
+ def setbit(self, name, offset, value):
+ """
+ Flag the ``offset`` in ``name`` as ``value``. Returns a boolean
+ indicating the previous value of ``offset``.
+ """
+ value = value and 1 or 0
+ return self.execute_command('SETBIT', name, offset, value)
+
def setex(self, name, value, time):
"""
Set the value of key ``name`` to ``value``
@@ -673,6 +714,23 @@ class Redis(threading.local):
"Set the value of key ``name`` to ``value`` if key doesn't exist"
return self.execute_command('SETNX', name, value)
+ def setrange(self, name, offset, value):
+ """
+ Overwrite bytes in the value of ``name`` starting at ``offset`` with
+ ``value``. If ``offset`` plus the length of ``value`` exceeds the
+ length of the original value, the new value will be larger than before.
+ If ``offset`` exceeds the length of the original value, null bytes
+ will be used to pad between the end of the previous value and the start
+ of what's being injected.
+
+ Returns the length of the new string.
+ """
+ return self.execute_command('SETRANGE', name, offset, value)
+
+ def strlen(self, name):
+ "Return the number of bytes stored in the value of ``name``"
+ return self.execute_command('STRLEN', name)
+
def substr(self, name, start, end=-1):
"""
Return a substring of the string at key ``name``. ``start`` and ``end``
@@ -747,6 +805,19 @@ class Redis(threading.local):
keys.append(timeout)
return self.execute_command('BRPOP', *keys)
+ def brpoplpush(self, src, dst, timeout=0):
+ """
+ Pop a value off the tail of ``src``, push it on the head of ``dst``
+ and then return it.
+
+ This command blocks until a value is in ``src`` or until ``timeout``
+ seconds elapse, whichever is first. A ``timeout`` value of 0 blocks
+ forever.
+ """
+ if timeout is None:
+ timeout = 0
+ return self.execute_command('BRPOPLPUSH', src, dst, timeout)
+
def lindex(self, name, index):
"""
Return the item from list ``name`` at position ``index``
diff --git a/tests/server_commands.py b/tests/server_commands.py
index 5455174..2738e91 100644
--- a/tests/server_commands.py
+++ b/tests/server_commands.py
@@ -52,6 +52,22 @@ class ServerCommandsTestCase(unittest.TestCase):
del self.client['a']
self.assertEquals(self.client['a'], None)
+ def test_config_get(self):
+ data = self.client.config_get()
+ self.assert_('maxmemory' in data)
+ self.assert_(data['maxmemory'].isdigit())
+
+ def test_config_set(self):
+ data = self.client.config_get()
+ rdbname = data['dbfilename']
+ self.assert_(self.client.config_set('dbfilename', 'redis_py_test.rdb'))
+ self.assertEquals(
+ self.client.config_get()['dbfilename'],
+ 'redis_py_test.rdb'
+ )
+ self.assert_(self.client.config_set('dbfilename', rdbname))
+ self.assertEquals(self.client.config_get()['dbfilename'], rdbname)
+
def test_info(self):
self.client['a'] = 'foo'
self.client['b'] = 'bar'
@@ -91,11 +107,13 @@ class ServerCommandsTestCase(unittest.TestCase):
self.client['a'] = 'foo'
self.assertEquals(self.client.exists('a'), True)
- def test_expire_and_ttl(self):
+ def test_expire(self):
self.assertEquals(self.client.expire('a', 10), False)
self.client['a'] = 'foo'
self.assertEquals(self.client.expire('a', 10), True)
self.assertEquals(self.client.ttl('a'), 10)
+ self.assertEquals(self.client.persist('a'), True)
+ self.assertEquals(self.client.ttl('a'), None)
def test_expireat(self):
expire_at = datetime.datetime.now() + datetime.timedelta(minutes=1)
@@ -110,6 +128,17 @@ class ServerCommandsTestCase(unittest.TestCase):
self.assertEquals(self.client.expireat('b', expire_at), True)
self.assertEquals(self.client.ttl('b'), 60)
+ def test_get_set_bit(self):
+ self.assertEquals(self.client.getbit('a', 5), False)
+ self.assertEquals(self.client.setbit('a', 5, True), False)
+ self.assertEquals(self.client.getbit('a', 5), True)
+ self.assertEquals(self.client.setbit('a', 4, False), False)
+ self.assertEquals(self.client.getbit('a', 4), False)
+ self.assertEquals(self.client.setbit('a', 4, True), False)
+ self.assertEquals(self.client.setbit('a', 5, True), True)
+ self.assertEquals(self.client.getbit('a', 4), True)
+ self.assertEquals(self.client.getbit('a', 5), True)
+
def test_getset(self):
self.assertEquals(self.client.getset('a', 'foo'), None)
self.assertEquals(self.client.getset('a', 'bar'), 'foo')
@@ -185,6 +214,17 @@ class ServerCommandsTestCase(unittest.TestCase):
self.assert_(not self.client.setnx('a', '2'))
self.assertEquals(self.client['a'], '1')
+ def test_setrange(self):
+ self.assertEquals(self.client.setrange('a', 5, 'abcdef'), 11)
+ self.assertEquals(self.client['a'], '\0\0\0\0\0abcdef')
+ self.client['a'] = 'Hello World'
+ self.assertEquals(self.client.setrange('a', 6, 'Redis'), 11)
+ self.assertEquals(self.client['a'], 'Hello Redis')
+
+ def test_strlen(self):
+ self.client['a'] = 'abcdef'
+ self.assertEquals(self.client.strlen('a'), 6)
+
def test_substr(self):
# invalid key type
self.client.rpush('a', 'a1')
@@ -259,6 +299,15 @@ class ServerCommandsTestCase(unittest.TestCase):
self.make_list('c', 'a')
self.assertEquals(self.client.brpop('c', timeout=1), ('c', 'a'))
+ def test_brpoplpush(self):
+ self.make_list('a', '12')
+ self.make_list('b', '34')
+ self.assertEquals(self.client.brpoplpush('a', 'b'), '2')
+ self.assertEquals(self.client.brpoplpush('a', 'b'), '1')
+ self.assertEquals(self.client.brpoplpush('a', 'b', timeout=1), None)
+ self.assertEquals(self.client.lrange('a', 0, -1), [])
+ self.assertEquals(self.client.lrange('b', 0, -1), ['1', '2', '3', '4'])
+
def test_lindex(self):
# no key
self.assertEquals(self.client.lindex('a', '0'), None)