├── .gitignore ├── MANIFEST ├── nsmemcached ├── __init__.py ├── tests.py └── client.py ├── setup.py ├── LICENSE.txt └── README.rst /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | dist 3 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.py 3 | nsmemcached/__init__.py 4 | nsmemcached/client.py 5 | nsmemcached/tests.py 6 | -------------------------------------------------------------------------------- /nsmemcached/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import memcache 4 | 5 | from client import NamespacedClient 6 | 7 | VERSION = (0, 2, 1) 8 | __version__ = '.'.join([str(x) for x in VERSION]) 9 | 10 | 11 | class Client(NamespacedClient): 12 | """ Just a convenient Client class to ease import and instantiation:: 13 | 14 | >>> from nsmemcached import Client 15 | >>> ns_client = Client(['127.0.0.1:11211']) 16 | """ 17 | def __init__(self, *args, **kwargs): 18 | super(Client, self).__init__(memcache.Client(*args, **kwargs)) 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import nsmemcached 4 | 5 | from distutils.core import setup 6 | 7 | 8 | setup( 9 | name='NSMemcached', 10 | version=nsmemcached.__version__, 11 | description='A simple implementation of a namespaced memcached client', 12 | author='Nicolas Perriault', 13 | author_email='np@akei.com', 14 | url='https://github.com/n1k0/NSMemcached', 15 | packages=['nsmemcached', ], 16 | requires=['python_memcached (>=1.47, <2.0)', ], 17 | license='MIT', 18 | long_description=open('README.rst').read(), 19 | ) 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 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: 2 | 3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4 | 5 | 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 SIMON TATHAM 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. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | README: NSMemcached 0.2.1 3 | =========================== 4 | 5 | :Author: Nicolas Perriault 6 | :Contact: np at akei com 7 | 8 | .. contents:: 9 | 10 | Abstract 11 | ======== 12 | 13 | NSMemcached_ is simple yet efficient Python implementation of a 14 | `namespaced client`_ for memcached_ on top of the python-memcached_ package. 15 | 16 | Quick Start 17 | =========== 18 | 19 | Sample usage: 20 | 21 | >>> from nsmemcached import Client 22 | >>> ns_client = Client(['127.0.0.1:11211']) 23 | >>> ns_client.set('foo', 'bar', ns='barspace') 24 | True 25 | >>> ns_client.set('zoo', 'baz', ns='barspace') 26 | True 27 | >>> ns_client.get('foo', ns='barspace') 28 | 'bar' 29 | >>> ns_client.get('zoo', ns='barspace') 30 | 'baz' 31 | >>> ns_client.get('foo') 32 | >>> ns_client.get('zoo') 33 | >>> ns_client.clear_ns('barspace') 34 | True 35 | >>> ns_client.get('foo', ns='barspace') 36 | >>> ns_client.get('zoo', ns='barspace') 37 | 38 | Yes, that simple. Other python-memcached_ client methods are supported as well, 39 | sharing the same signature but with a supplementary ``ns`` named argument 40 | available to optionaly declare a namespace to perform the query within:: 41 | 42 | >>> ns_client.set('foo', 1, ns='bar') 43 | True 44 | >>> ns_client.incr('foo', ns='bar') 45 | 2 46 | >>> ns_client.decr('foo', ns='bar') 47 | 1 48 | >>> ns_client.set('foo', 'bar, ns='bar') 49 | True 50 | >>> ns_client.append('foo', '!!!', ns='bar') 51 | True 52 | >>> ns_client.get('foo', ns='bar') 53 | 'bar!!!' 54 | >>> ns_client.delete('foo', ns='bar') 55 | True 56 | >>> ns_client.get('foo', ns='bar') 57 | 58 | The best way to learn about the usage of all the available methods from the API 59 | is probably to dig into the `test suite code`_ which covers 100% of them. 60 | 61 | Caveats 62 | ======= 63 | 64 | Namespace keys are stored in dedicated keys, so every time you request a 65 | namespaced item you'll make two queries to the memcached server instead of one, 66 | so expect a tiny slowdown compared to the way of using the standard, 67 | non-namespaced `memcached API`_. 68 | 69 | Also, python-memcached_ client's ``get_multi()`` and ``set_multi()`` are not 70 | currently supported (yet). 71 | 72 | Dependencies and Compatibility 73 | ============================== 74 | 75 | NSMemcached_ requires the use of Python 2.4 or more recent. 76 | 77 | Installing python-memcached_ package is required in order to use this library, 78 | as well as a working memcached_ server instance, obviously. 79 | 80 | NSMemcached_ is fully compatible with the API of the standard python-memcached_ 81 | client. 82 | 83 | License 84 | ======= 85 | 86 | This code is released under the terms of the `MIT License`_. 87 | 88 | Author 89 | ====== 90 | 91 | Nicolas Perriault, AKEI_, ```` 92 | 93 | .. _namespaced client: http://code.google.com/p/memcached/wiki/FAQ#Deleting_by_Namespace 94 | .. _memcached: http://memcached.org/ 95 | .. _memcached API: http://code.google.com/p/memcached/wiki/NewCommands 96 | .. _NSMemcached: http://pypi.python.org/pypi/NSMemcached 97 | .. _python-memcached: http://pypi.python.org/pypi/python-memcached/ 98 | .. _test suite code: https://github.com/n1k0/NSMemcached/blob/master/nsmemcached/tests.py 99 | .. _MIT License: http://en.wikipedia.org/wiki/MIT_License 100 | .. _AKEI: http://akei.com/ 101 | -------------------------------------------------------------------------------- /nsmemcached/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | 5 | from __init__ import Client 6 | 7 | 8 | class CacheTest(unittest.TestCase): 9 | client = None 10 | 11 | def setUp(self): 12 | self.client = Client(['127.0.0.1:11211'], debug=1) 13 | if len(self.client.get_stats()) < 1: 14 | raise RuntimeError('memcached server seems down, cannot run tests') 15 | 16 | def test_add(self): 17 | self.client.flush_all() 18 | self.assertEquals(self.client.add('foo', 'plop', ns='bar'), True) 19 | self.assertEquals(self.client.get('foo'), None) 20 | self.assertEquals(self.client.get('foo', ns='bar'), 'plop') 21 | # namespace clear 22 | self.client.clear_ns('bar') 23 | self.assertEquals(self.client.get('foo', ns='bar'), None) 24 | 25 | def test_append_prepend(self): 26 | self.client.flush_all() 27 | self.client.add('foo', 'plop', ns='bar') 28 | self.assertEquals(self.client.append('foo', '!!!', ns='bar'), True) 29 | self.assertEquals(self.client.get('foo', ns='bar'), 'plop!!!') 30 | self.assertEquals(self.client.prepend('foo', '!!!', ns='bar'), True) 31 | self.assertEquals(self.client.get('foo', ns='bar'), '!!!plop!!!') 32 | 33 | def test_cas(self): 34 | self.client.flush_all() 35 | self.client.add('foo', 'plop', ns='bar') 36 | self.client.get('foo', ns='bar') 37 | self.assertEquals(self.client.cas('foo', 'baz', ns='bar'), True) 38 | self.assertEquals(self.client.get('foo', ns='bar'), 'baz') 39 | 40 | def test_get_set_gets_delete_clear(self): 41 | self.client.flush_all() 42 | self.assertEquals(self.client.get('blah'), None) 43 | self.client.set('blah', 2) 44 | self.assertEquals(self.client.get('blah'), 2) 45 | self.assertEquals(self.client.get('blah', ns='foo'), None) 46 | self.client.set('blah', 8, ns='foo') 47 | self.client.set('blah', 12, ns='bar') 48 | self.assertEquals(self.client.get('blah', ns='foo'), 8) 49 | self.assertEquals(self.client.get('blah', ns='bar'), 12) 50 | self.assertEquals(self.client.get('blah'), 2) 51 | # namespace clear 52 | self.client.clear_ns('foo') 53 | self.assertEquals(self.client.get('blah', ns='foo'), None) 54 | self.assertEquals(self.client.get('blah', ns='bar'), 12) 55 | self.assertEquals(self.client.get('blah'), 2) 56 | # namespaced key deletion 57 | self.client.delete('blah', ns='bar') 58 | self.assertEquals(self.client.get('blah', ns='bar'), None) 59 | # gets 60 | self.client.set('foo', 2, ns='bar') 61 | self.assertEquals(self.client.gets('foo', ns='bar'), 2) 62 | 63 | def test_incr_decr(self): 64 | self.client.flush_all() 65 | self.client.set('foo', 1) 66 | self.client.set('foo', 10, ns='bar') 67 | self.assertEquals(self.client.incr('foo'), 2) 68 | self.assertEquals(self.client.get('foo'), 2) 69 | self.assertEquals(self.client.incr('foo', ns='bar'), 11) 70 | self.assertEquals(self.client.get('foo', ns='bar'), 11) 71 | self.assertEquals(self.client.decr('foo'), 1) 72 | self.assertEquals(self.client.get('foo'), 1) 73 | self.assertEquals(self.client.decr('foo', ns='bar'), 10) 74 | self.assertEquals(self.client.get('foo', ns='bar'), 10) 75 | # namespace clear 76 | self.client.clear_ns('bar') 77 | self.assertEquals(self.client.get('foo'), 1) 78 | self.assertEquals(self.client.get('foo', ns='bar'), None) 79 | 80 | def test_replace(self): 81 | self.client.flush_all() 82 | self.client.set('foo', 1, ns='bar') 83 | self.assertEquals(self.client.replace('foo', 2, ns='bar'), True) 84 | self.assertEquals(self.client.get('foo', ns='bar'), 2) 85 | 86 | if __name__ == '__main__': 87 | unittest.main() 88 | -------------------------------------------------------------------------------- /nsmemcached/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import memcache 4 | import random 5 | 6 | MAX_KEY = 1000000 7 | 8 | 9 | class NamespacedClient(object): 10 | """ An implementation of namespaces using Memcache. 11 | See: http://code.google.com/p/memcached/wiki/FAQ#Deleting_by_Namespace 12 | 13 | Sample usage:: 14 | >>> std_client = memcache.Client(['127.0.0.1:11211']) 15 | >>> ns_client = NamespacedClient(std_client) 16 | >>> ns_client.set('foo', 'bar', ns='barspace') 17 | True 18 | >>> ns_client.get('foo', ns='barspace') 19 | bar 20 | >>> ns_client.get('foo') 21 | None 22 | >>> ns_client.clear_ns('barspace') 23 | >>> ns_client.get('foo', ns='barspace') 24 | None 25 | """ 26 | mc = None 27 | 28 | def __init__(self, mc): 29 | """ Constructor, accepting a ``memcache.Client`` instance as first arg. 30 | """ 31 | if not isinstance(mc, memcache.Client): 32 | raise ValueError('mc must be an instance of memcache.Client') 33 | self.mc = mc 34 | 35 | def __getattr__(self, name): 36 | """ If unknown attribute, fallbacks to the memcache standard client 37 | instance one. 38 | """ 39 | return getattr(self.mc, name) 40 | 41 | def add(self, key, val, time=0, min_compress_len=0, ns=None): 42 | """ Stores a value in an optionaly namespaced key. 43 | """ 44 | return self.mc.add(self._compute_key(key, ns), val, time=time, 45 | min_compress_len=min_compress_len) 46 | 47 | def append(self, key, val, time=0, min_compress_len=0, ns=None): 48 | """ Appends the value to the end of the existing optinaly namespaced 49 | key's value. 50 | """ 51 | return self.mc.append(self._compute_key(key, ns), val, time=time, 52 | min_compress_len=min_compress_len) 53 | 54 | def cas(self, key, val, time=0, min_compress_len=0, ns=None): 55 | """ Sets an optionaly namespaced key to a given value in the memcache 56 | if it hasn't been altered since last fetched. 57 | """ 58 | return self.mc.cas(self._compute_key(key, ns), val, time=time, 59 | min_compress_len=min_compress_len) 60 | 61 | def clear_ns(self, ns): 62 | """ Cleans all cached values for all keys within the given namespace. 63 | """ 64 | return self.mc.incr(self._compute_ns_key(ns)) > 0 65 | 66 | def decr(self, key, delta=1, ns=None): 67 | """ Decrements a value stored in an optionaly namspaced key. 68 | """ 69 | return self.mc.decr(self._compute_key(key, ns), delta=delta) 70 | 71 | def delete(self, key, ns=None, time=0): 72 | """ Deletes a cached value by its key and an optional namespace. 73 | """ 74 | return self.mc.delete(self._compute_key(key, ns), time=time) 75 | 76 | def get(self, key, ns=None): 77 | """ Retrieve a stored value by its key, optionnaly within a given 78 | namespace. 79 | """ 80 | return self.mc.get(self._compute_key(key, ns)) 81 | 82 | def gets(self, key, ns=None): 83 | """ Retrieves a value stored in an optionaly namespaced key. 84 | """ 85 | return self.mc.gets(self._compute_key(key, ns)) 86 | 87 | def get_ns_key(self, ns): 88 | """ Retrieves the stored namespace key name, creates it if it doesn't 89 | exist. 90 | """ 91 | ns_key = self.mc.get(self._compute_ns_key(ns)) 92 | if not ns_key: 93 | ns_key = random.randint(1, MAX_KEY) 94 | self.mc.set(self._compute_ns_key(ns), ns_key) 95 | return ns_key 96 | 97 | def incr(self, key, delta=1, ns=None): 98 | """ Increments a value stored in an optionaly namspaced key. 99 | """ 100 | return self.mc.incr(self._compute_key(key, ns), delta=delta) 101 | 102 | def prepend(self, key, val, time=0, min_compress_len=0, ns=None): 103 | """ Prepends the value at the beginning of the existing optinaly 104 | namespaced key's value. 105 | """ 106 | return self.mc.prepend(self._compute_key(key, ns), val, time=time, 107 | min_compress_len=min_compress_len) 108 | 109 | def replace(self, key, val, time=0, min_compress_len=0, ns=None): 110 | """ Replaces existing optoinaly namespaced key with value. 111 | """ 112 | return self.mc.replace(self._compute_key(key, ns), val, time=time, 113 | min_compress_len=min_compress_len) 114 | 115 | def set(self, key, val, ns=None, **kwargs): 116 | """ Stores a values with given key, optionnaly within a given 117 | namespace. 118 | """ 119 | return self.mc.set(self._compute_key(key, ns), val, **kwargs) 120 | 121 | def _compute_key(self, key, ns=None): 122 | """ Computes key name, depending if it's tied to a namespace. 123 | """ 124 | if not isinstance(key, basestring): 125 | raise NotImplementedError('nsmemcached only handles string keys') 126 | if ns: 127 | key = '%s_%d_%s' % (ns, self.get_ns_key(ns), str(key),) 128 | return str(key) 129 | 130 | def _compute_ns_key(self, ns): 131 | """ Computes a namespace key name. 132 | """ 133 | if not isinstance(ns, basestring): 134 | raise NotImplementedError('nsmemcached only handles string ns') 135 | return str('%s_ns_key' % ns) 136 | --------------------------------------------------------------------------------