From 641c297fafffcb90596ac6227da25ac48970b2e5 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 21 Jan 2011 23:52:48 +0800 Subject: [PATCH 1/6] Separated the 1.3 compatible cache class from the rest of the code and reverted a few unneeded changes. This also handles the key better internally to prevent multiple "making" of the key. --- README.rst | 16 +++++--- redis_cache/__init__.py | 1 + redis_cache/cache.py | 87 +++++++++++++++++++++++++---------------- tests/testapp/tests.py | 10 ++--- 4 files changed, 68 insertions(+), 46 deletions(-) diff --git a/README.rst b/README.rst index 3baa93cc..ffe8fb2e 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ Redis Django Cache Backend ========================== -A simple Redis cache backend for Django >= 1.3 +A simple Redis cache backend for Django Notes ----- @@ -18,19 +18,23 @@ Usage 1. Run ``python setup.py install`` to install, or place ``redis_cache`` on your Python path. -2. Modify your Django settings to use ``redis_cache`` :: +2. Modify your Django settings to use ``redis_cache`` : + +On Django < 1.3:: + + CACHE_BACKEND = 'redis_cache.cache://:' + +On Django >= 1.3:: CACHES = { 'default': { - 'BACKEND': 'redis_cache.cache.CacheClass', + 'BACKEND': 'redis_cache.RedisCache', 'LOCATION': ':', 'OPTIONS': { # optional 'DB': 1, + 'PASSWORD': 'yadayada', }, }, } - - .. _redis-py: http://github.com/andymccurdy/redis-py/ - diff --git a/redis_cache/__init__.py b/redis_cache/__init__.py index e69de29b..792cacfa 100644 --- a/redis_cache/__init__.py +++ b/redis_cache/__init__.py @@ -0,0 +1 @@ +from redis_cache.cache import RedisCache diff --git a/redis_cache/cache.py b/redis_cache/cache.py index 2297982d..429ce2b1 100644 --- a/redis_cache/cache.py +++ b/redis_cache/cache.py @@ -14,18 +14,25 @@ "Redis cache backend requires the 'redis-py' library") +class CacheKey(basestring): + """ + A stub string class that we can use to check if a key was created already. + """ + pass + class CacheClass(BaseCache): def __init__(self, server, params): """ Connect to Redis, and set up cache backend. """ - BaseCache.__init__(self, params) - db = params.get('OPTIONS', {}).get('db', 1) + super(CacheClass, self).__init__(params) + options = params.get('OPTIONS', {}) + password = params.get('password', options.get('PASSWORD', None)) + db = params.get('db', options.get('DB', 1)) try: db = int(db) except (ValueError, TypeError): db = 1 - password = params.get('password', None) if ':' in server: host, port = server.split(':') try: @@ -39,9 +46,12 @@ def __init__(self, server, params): def make_key(self, key, version=None): """ - Returns the utf-8 encoded bytestring of the given key. + Returns the utf-8 encoded bytestring of the given key as a CacheKey + instance to be able to check if it was "made" before. """ - return smart_str(super(CacheClass, self).make_key(key, version)) + if isinstance(key, CacheKey): + key = CacheKey(smart_str(key)) + return key def add(self, key, value, timeout=None, version=None): """ @@ -49,37 +59,32 @@ def add(self, key, value, timeout=None, version=None): Returns ``True`` if the object was added, ``False`` if not. """ - _key = key key = self.make_key(key, version=version) if self._cache.exists(key): return False - return self.set(_key, value, timeout) + return self.set(key, value, timeout) def get(self, key, default=None, version=None): """ Retrieve a value from the cache. - Returns unpicked value if key is found, ``None`` if not. + Returns unpickled value if key is found, the default if not. """ - # get the value from the cache - value = self._cache.get(self.make_key(key, version=version)) + key = self.make_key(key, version=version) + value = self._cache.get(key) if value is None: return default - # pickle doesn't want a unicode! - value = smart_str(value) - # hydrate that pickle - return pickle.loads(value) + return self.unpickle(value) def set(self, key, value, timeout=None, version=None): """ Persist a value to the cache, and set an optional expiration time. """ - _key = key key = self.make_key(key, version=version) # store the pickled value result = self._cache.set(key, pickle.dumps(value)) # set expiration if needed - self.expire(_key, timeout, version=version) + self.expire(key, timeout, version=version) # result is a boolean return result @@ -87,7 +92,6 @@ def expire(self, key, timeout=None, version=None): """ Set content expiration, if necessary """ - _key = key key = self.make_key(key, version=version) if timeout == 0: # force the key to be non-volatile @@ -98,22 +102,22 @@ def expire(self, key, timeout=None, version=None): # If the expiration command returns false, we need to reset the key # with the new expiration if not self._cache.expire(key, timeout): - value = self.get(_key, version=version) - self.set(_key, value, timeout, version=version) + value = self.get(key, version=version) + self.set(key, value, timeout, version=version) def delete(self, key, version=None): """ Remove a key from the cache. """ - self._cache.delete(self.make_key(key, version)) + self._cache.delete(self.make_key(key, version=version)) def delete_many(self, keys, version=None): """ Remove multiple keys at once. """ if keys: - l = lambda x: self.make_key(x, version=version) - self._cache.delete(*map(l, keys)) + keys = map(lambda key: self.make_key(key, version=version), keys) + self._cache.delete(*keys) def clear(self): """ @@ -122,21 +126,27 @@ def clear(self): # TODO : potential data loss here, should we only delete keys based on the correct version ? self._cache.flushdb() + def unpickle(self, value): + """ + Unpickles the given value. + """ + # pickle doesn't want a unicode! + value = smart_str(value) + # hydrate that pickle + return pickle.loads(value) + def get_many(self, keys, version=None): """ Retrieve many keys. """ recovered_data = SortedDict() - new_keys = map(lambda x: self.make_key(x, version=version), keys) - results = self._cache.mget(new_keys) + new_keys = map(lambda key: self.make_key(key, version=version), keys) map_keys = dict(zip(new_keys, keys)) + results = self._cache.mget(new_keys) for key, value in zip(new_keys, results): if value is None: continue - # pickle doesn't want a unicode! - value = smart_str(value) - # hydrate that pickle - value = pickle.loads(value) + value = self.unpickle(value) if isinstance(value, basestring): value = smart_unicode(value) recovered_data[map_keys[key]] = value @@ -164,17 +174,26 @@ def close(self, **kwargs): """ self._cache.connection.disconnect() +class RedisCache(CacheClass): + """ + A subclass that is supposed to be used on Django >= 1.3. + """ + def make_key(self, key, version=None): + if not isinstance(key, CacheKey): + key = CacheKey(smart_str(super(CacheClass, self).make_key(key, version))) + return key + def incr_version(self, key, delta=1, version=None): - """Adds delta to the cache version for the supplied key. Returns the + """ + Adds delta to the cache version for the supplied key. Returns the new version. """ if version is None: version = self.version - + key = self.make_key(key, version) value = self.get(key, version=version) if value is None: raise ValueError("Key '%s' not found" % key) - - self._cache.rename(self.make_key(key, version), self.make_key(key, - version=version+delta)) - return version+delta \ No newline at end of file + incr_key = self.make_key(key, version=version+delta) + self._cache.rename(key, incr_key) + return version + delta diff --git a/tests/testapp/tests.py b/tests/testapp/tests.py index 9ed6781a..d0a43bdc 100644 --- a/tests/testapp/tests.py +++ b/tests/testapp/tests.py @@ -168,17 +168,15 @@ def test_expiration(self): def test_set_expiration_timeout_None(self): key, value = 'key', 'value' self.cache.set(key, value); - self.assertEqual(self.cache._cache.ttl(key), None) - + self.assertTrue(self.cache._cache.ttl(key) > 0) def test_set_expiration_timeout_0(self): key, value = 'key', 'value' - _key = self.cache.make_key(key) - self.cache.set(key, value); - self.assertTrue(self.cache._cache.ttl(_key) > 0) + self.cache.set(key, value) + self.assertTrue(self.cache._cache.ttl(key) > 0) self.cache.expire(key, 0) self.assertEqual(self.cache.get(key), value) - self.assertEqual(self.cache._cache.ttl(_key), None) + self.assertTrue(self.cache._cache.ttl(key) < 0) def test_set_expiration_first_expire_call(self): key, value = self.cache.make_key('key'), 'value' From e62840cdc7ab5866bb672da43f1c2be7ea6acf23 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 21 Jan 2011 23:52:53 +0800 Subject: [PATCH 2/6] Bumped version a bit. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d2e55df4..a66d063c 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ url = "http://github.com/blackbrrr/django-redis-cache/", author = "Matt Dennewitz", author_email = "mattdennewitz@gmail.com", - version = "0.2a9", + version = "0.2a10", packages = ["redis_cache"], description = "Redis Cache Backend for Django", classifiers = [ From 99d202c5c49cbbccae680c3437dcb3086e9cd380 Mon Sep 17 00:00:00 2001 From: Sean Bleier Date: Sat, 22 Jan 2011 10:40:42 +0800 Subject: [PATCH 3/6] Added more tests to increase coverage and fixed a couple bugs. We can't inherit a class from a basestring and instantiate it, so I changed the base class to 'object' so we can still to the type checking. I also discovered a bug with the Django 1.3 compatibility work. In the incr_version method, we were using the Redis's rename function to move the key, but in Redis 2.0 you cannot rename a volitile key. Added some moar authors --- AUTHORS.rst | 2 ++ redis_cache/cache.py | 44 +++++++++++++++++++++++++++++------------- runtests.py | 14 ++++++++++++-- tests/settings.py | 11 +++++++++++ tests/testapp/tests.py | 44 ++++++++++++++++++++++++++++++++++++++---- 5 files changed, 96 insertions(+), 19 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 1e3814c6..bb45def0 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -2,5 +2,7 @@ Sean Bleier Matt Dennewitz Jannis Leidel S. Angel / Twidi +Noah Kantrowitz / coderanger +Martin Mahner / bartTC Forked from http://github.com/sebleier/django-redis-cache diff --git a/redis_cache/cache.py b/redis_cache/cache.py index 429ce2b1..a3804d72 100644 --- a/redis_cache/cache.py +++ b/redis_cache/cache.py @@ -14,11 +14,22 @@ "Redis cache backend requires the 'redis-py' library") -class CacheKey(basestring): +class CacheKey(object): """ A stub string class that we can use to check if a key was created already. """ - pass + def __init__(self, key): + self._key = key + + def __eq__(self, other): + return self._key == other + + def __str__(self): + return self.__unicode__() + + def __unicode__(self): + return smart_str(self._key) + class CacheClass(BaseCache): def __init__(self, server, params): @@ -49,8 +60,8 @@ def make_key(self, key, version=None): Returns the utf-8 encoded bytestring of the given key as a CacheKey instance to be able to check if it was "made" before. """ - if isinstance(key, CacheKey): - key = CacheKey(smart_str(key)) + if not isinstance(key, CacheKey): + key = CacheKey(key) return key def add(self, key, value, timeout=None, version=None): @@ -93,12 +104,13 @@ def expire(self, key, timeout=None, version=None): Set content expiration, if necessary """ key = self.make_key(key, version=version) - if timeout == 0: + if timeout is None: + timeout = self.default_timeout + if timeout <= 0: # force the key to be non-volatile result = self._cache.get(key) self._cache.set(key, result) else: - timeout = timeout or self.default_timeout # If the expiration command returns false, we need to reset the key # with the new expiration if not self._cache.expire(key, timeout): @@ -130,9 +142,7 @@ def unpickle(self, value): """ Unpickles the given value. """ - # pickle doesn't want a unicode! value = smart_str(value) - # hydrate that pickle return pickle.loads(value) def get_many(self, keys, version=None): @@ -178,22 +188,30 @@ class RedisCache(CacheClass): """ A subclass that is supposed to be used on Django >= 1.3. """ + def make_key(self, key, version=None): if not isinstance(key, CacheKey): - key = CacheKey(smart_str(super(CacheClass, self).make_key(key, version))) + key = CacheKey(super(CacheClass, self).make_key(key, version)) return key def incr_version(self, key, delta=1, version=None): """ Adds delta to the cache version for the supplied key. Returns the new version. + + Note: In Redis 2.0 you cannot rename a volitle key, so we have to move + the value from the old key to the new key and maintain the ttl. """ if version is None: version = self.version - key = self.make_key(key, version) - value = self.get(key, version=version) + old_key = self.make_key(key, version) + value = self.get(old_key, version=version) + ttl = self._cache.ttl(old_key) if value is None: raise ValueError("Key '%s' not found" % key) - incr_key = self.make_key(key, version=version+delta) - self._cache.rename(key, incr_key) + new_key = self.make_key(key, version=version+delta) + # TODO: See if we can check the version of Redis, since 2.2 will be able + # to rename volitile keys. + self.set(new_key, value, timeout=ttl) + self.delete(old_key) return version + delta diff --git a/runtests.py b/runtests.py index d685ce46..bfdca97e 100644 --- a/runtests.py +++ b/runtests.py @@ -12,7 +12,17 @@ }, INSTALLED_APPS = [ 'tests.testapp', - ] + ], + CACHES = { + 'default': { + 'BACKEND': 'redis_cache.RedisCache', + 'LOCATION': '127.0.0.1:6379', + 'OPTIONS': { + 'DB': 15, + 'PASSWORD': 'yadayada', + }, + }, + } ) from django.test.simple import DjangoTestSuiteRunner @@ -27,4 +37,4 @@ def runtests(*test_args): sys.exit(failures) if __name__ == '__main__': - runtests(*sys.argv[1:]) \ No newline at end of file + runtests(*sys.argv[1:]) diff --git a/tests/settings.py b/tests/settings.py index 5ba6298a..ebb6a497 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -9,3 +9,14 @@ INSTALLED_APPS = [ 'tests.testapp', ] + +CACHES = { + 'default': { + 'BACKEND': 'redis_cache.RedisCache', + 'LOCATION': '127.0.0.1:6379', + 'OPTIONS': { # optional + 'DB': 15, + 'PASSWORD': 'yadayada', + }, + }, +} diff --git a/tests/testapp/tests.py b/tests/testapp/tests.py index d0a43bdc..6af96071 100644 --- a/tests/testapp/tests.py +++ b/tests/testapp/tests.py @@ -7,9 +7,10 @@ import cPickle as pickle except ImportError: import pickle - +from django import VERSION from django.core.cache import get_cache from models import Poll, expensive_calculation +from redis_cache.cache import RedisCache # functions/classes for complex data type tests def f(): @@ -25,10 +26,32 @@ class RedisCacheTests(unittest.TestCase): """ def setUp(self): # use DB 16 for testing and hope there isn't any important data :-> - self.cache = get_cache('redis_cache.cache://127.0.0.1:6379?db=15') + self.cache = self.get_cache() def tearDown(self): self.cache.clear() + self.cache.close() + + def get_cache(self, backend=None): + if VERSION[0] == 1 and VERSION[1] < 3: + cache = get_cache(backend or 'redis_cache.cache://127.0.0.1:6379?db=15') + elif VERSION[0] == 1 and VERSION[1] >= 3: + cache = get_cache(backend or 'default') + return cache + + def test_bad_db_initialization(self): + self.cache = self.get_cache('redis_cache.cache://127.0.0.1:6379?db=not_a_number') + self.assertEqual(self.cache._cache.db, 1) + + def test_bad_port_initialization(self): + self.cache = self.get_cache('redis_cache.cache://127.0.0.1:not_a_number?db=15') + self.assertEqual(self.cache._cache.port, 6379) + + def test_default_initialization(self): + self.cache = self.get_cache('redis_cache.cache://127.0.0.1') + self.assertEqual(self.cache._cache.host, '127.0.0.1') + self.assertEqual(self.cache._cache.db, 1) + self.assertEqual(self.cache._cache.port, 6379) def test_simple(self): # Simple cache set/get works @@ -166,12 +189,12 @@ def test_expiration(self): self.assertEqual(self.cache.has_key("expire3"), False) def test_set_expiration_timeout_None(self): - key, value = 'key', 'value' + key, value = self.cache.make_key('key'), 'value' self.cache.set(key, value); self.assertTrue(self.cache._cache.ttl(key) > 0) def test_set_expiration_timeout_0(self): - key, value = 'key', 'value' + key, value = self.cache.make_key('key'), 'value' self.cache.set(key, value) self.assertTrue(self.cache._cache.ttl(key) > 0) self.cache.expire(key, 0) @@ -268,5 +291,18 @@ def test_long_timeout(self): self.assertEqual(self.cache.get('key3'), 'sausage') self.assertEqual(self.cache.get('key4'), 'lobster bisque') + def test_incr_version(self): + if isinstance(self.cache, RedisCache): + old_key = "key1" + self.cache.set(old_key, "spam", version=1) + self.assertEqual(self.cache.make_key(old_key), ':1:key1') + new_version = self.cache.incr_version(old_key, 1) + self.assertEqual(new_version, 2) + new_key = self.cache.make_key(old_key, version=new_version) + self.assertEqual(new_key, ':2:key1') + self.assertEqual(self.cache.get(old_key), None) + self.assertEqual(self.cache.get(new_key), 'spam') + + if __name__ == '__main__': unittest.main() From 0f9cb5311fea9b1989b407673bde96f4bb86aa39 Mon Sep 17 00:00:00 2001 From: Sean Bleier Date: Sat, 22 Jan 2011 13:37:11 +0800 Subject: [PATCH 4/6] Bumped version --- setup.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index a66d063c..4a9e2691 100644 --- a/setup.py +++ b/setup.py @@ -2,10 +2,10 @@ setup( name = "django-redis-cache", - url = "http://github.com/blackbrrr/django-redis-cache/", - author = "Matt Dennewitz", - author_email = "mattdennewitz@gmail.com", - version = "0.2a10", + url = "http://github.com/sebleier/django-redis-cache/", + author = "Sean Bleier", + author_email = "sebleier@gmail.com", + version = "0.2a11", packages = ["redis_cache"], description = "Redis Cache Backend for Django", classifiers = [ @@ -17,4 +17,3 @@ "Framework :: Django", ], ) - From 9cb5fa5f9065f6eed431a388fe2f9ab90dd27f6f Mon Sep 17 00:00:00 2001 From: twidi Date: Tue, 24 Apr 2012 01:29:29 +0200 Subject: [PATCH 5/6] Oopss, merge corrected --- redis_cache/cache.py | 34 ---------------------------------- tests/testapp/tests.py | 22 ---------------------- 2 files changed, 56 deletions(-) diff --git a/redis_cache/cache.py b/redis_cache/cache.py index fd755d8d..b6526e22 100644 --- a/redis_cache/cache.py +++ b/redis_cache/cache.py @@ -65,23 +65,6 @@ def get_connection_pool(self, host='127.0.0.1', port=6379, db=1, pool = CacheConnectionPool() -class CacheKey(object): - """ - A stub string class that we can use to check if a key was created already. - """ - def __init__(self, key): - self._key = key - - def __eq__(self, other): - return self._key == other - - def __str__(self): - return self.__unicode__() - - def __unicode__(self): - return smart_str(self._key) - - class CacheClass(BaseCache): def __init__(self, server, params): """ @@ -258,13 +241,6 @@ def unpickle(self, value): value = smart_str(value) return pickle.loads(value) - def unpickle(self, value): - """ - Unpickles the given value. - """ - value = smart_str(value) - return pickle.loads(value) - def get_many(self, keys, version=None): """ Retrieve many keys. @@ -317,16 +293,6 @@ def incr(self, key, delta=1, version=None): return value -class RedisCache(CacheClass): - """ - A subclass that is supposed to be used on Django >= 1.3. - """ - - def make_key(self, key, version=None): - if not isinstance(key, CacheKey): - key = CacheKey(super(CacheClass, self).make_key(key, version)) - return key - class RedisCache(CacheClass): """ A subclass that is supposed to be used on Django >= 1.3. diff --git a/tests/testapp/tests.py b/tests/testapp/tests.py index 221dddba..f8568995 100644 --- a/tests/testapp/tests.py +++ b/tests/testapp/tests.py @@ -32,28 +32,6 @@ def setUp(self): def tearDown(self): self.cache.clear() - self.cache.close() - - def get_cache(self, backend=None): - if VERSION[0] == 1 and VERSION[1] < 3: - cache = get_cache(backend or 'redis_cache.cache://127.0.0.1:6379?db=15') - elif VERSION[0] == 1 and VERSION[1] >= 3: - cache = get_cache(backend or 'default') - return cache - - def test_bad_db_initialization(self): - self.cache = self.get_cache('redis_cache.cache://127.0.0.1:6379?db=not_a_number') - self.assertEqual(self.cache._cache.db, 1) - - def test_bad_port_initialization(self): - self.cache = self.get_cache('redis_cache.cache://127.0.0.1:not_a_number?db=15') - self.assertEqual(self.cache._cache.port, 6379) - - def test_default_initialization(self): - self.cache = self.get_cache('redis_cache.cache://127.0.0.1') - self.assertEqual(self.cache._cache.host, '127.0.0.1') - self.assertEqual(self.cache._cache.db, 1) - self.assertEqual(self.cache._cache.port, 6379) def reset_pool(self): if hasattr(self, 'cache'): From 837fa271d16e22bbb7375d4b49e5216a5b99b156 Mon Sep 17 00:00:00 2001 From: twidi Date: Tue, 24 Apr 2012 01:46:41 +0200 Subject: [PATCH 6/6] Add a way to choose the protocol for pickle. Default is 0, and change it by passing another protocol (1 or 2) in the OPTIONS dict of your cache declaration Based on https://github.com/arshaver/django-redis/commit/204216808e31338023878dd8055689f0b9674e96 --- README.rst | 6 ++++-- redis_cache/cache.py | 23 ++++++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index e1ef9fe7..80ca6e25 100644 --- a/README.rst +++ b/README.rst @@ -51,7 +51,8 @@ On Django >= 1.3:: 'OPTIONS': { 'DB': 1, 'PASSWORD': 'yadayada', - 'PARSER_CLASS': 'redis.connection.HiredisParser' + 'PARSER_CLASS': 'redis.connection.HiredisParser', + 'PICKLE_VERSION': 2 # optional, default to 0 }, }, } @@ -66,7 +67,8 @@ On Django >= 1.3:: 'OPTIONS': { 'DB': 1, 'PASSWORD': 'yadayada', - 'PARSER_CLASS': 'redis.connection.HiredisParser' + 'PARSER_CLASS': 'redis.connection.HiredisParser', + 'PICKLE_VERSION': 2 # optional, default to 0 }, }, } diff --git a/redis_cache/cache.py b/redis_cache/cache.py index b6526e22..ffbc16cd 100644 --- a/redis_cache/cache.py +++ b/redis_cache/cache.py @@ -76,6 +76,7 @@ def _init(self, server, params): super(CacheClass, self).__init__(params) self._server = server self._params = params + self._pickle_version = None unix_socket_path = None if ':' in self.server: @@ -142,6 +143,20 @@ def parser_class(self): raise ImproperlyConfigured("Could not find parser class '%s'" % parser_class) return parser_class + @property + def pickle_version(self): + """ + Get the pickle version from the settings and save it for future use + """ + if self._pickle_version is None: + _pickle_version = self.options.get('PICKLE_VERSION', 0) + try: + _pickle_version = int(_pickle_version) + except (ValueError, TypeError): + raise ImproperlyConfigured("pickle version value must be an integer") + self._pickle_version = _pickle_version + return self._pickle_version + def __getstate__(self): return {'params': self._params, 'server': self._server} @@ -207,7 +222,7 @@ def set(self, key, value, timeout=None, version=None, client=None): if int(value) != value: raise TypeError except (ValueError, TypeError): - result = self._set(key, pickle.dumps(value), int(timeout), client) + result = self._set(key, self.pickle(value), int(timeout), client) else: result = self._set(key, int(value), int(timeout), client) # result is a boolean @@ -241,6 +256,12 @@ def unpickle(self, value): value = smart_str(value) return pickle.loads(value) + def pickle(self, value): + """ + Pickle the given value. + """ + return pickle.dumps(value, self.pickle_version) + def get_many(self, keys, version=None): """ Retrieve many keys.