diff options
-rw-r--r-- | CHANGES | 66 | ||||
-rw-r--r-- | redis/client.py | 81 | ||||
-rw-r--r-- | tests/server_commands.py | 51 |
3 files changed, 134 insertions, 64 deletions
@@ -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) |