From e5faa12e91d43b66b6e9c666059d23e03d3eb9bf Mon Sep 17 00:00:00 2001 From: opapy Date: Mon, 10 Apr 2017 19:04:36 +0900 Subject: [PATCH 01/57] replace pylibc to pymemcache --- .../__init__.py | 0 django_elasticache_pymemcache/client.py | 5 ++ .../cluster_utils.py | 0 .../memcached.py | 75 +++++++------------ setup.py | 12 +-- 5 files changed, 38 insertions(+), 54 deletions(-) rename {django_elasticache => django_elasticache_pymemcache}/__init__.py (100%) create mode 100644 django_elasticache_pymemcache/client.py rename {django_elasticache => django_elasticache_pymemcache}/cluster_utils.py (100%) rename {django_elasticache => django_elasticache_pymemcache}/memcached.py (52%) diff --git a/django_elasticache/__init__.py b/django_elasticache_pymemcache/__init__.py similarity index 100% rename from django_elasticache/__init__.py rename to django_elasticache_pymemcache/__init__.py diff --git a/django_elasticache_pymemcache/client.py b/django_elasticache_pymemcache/client.py new file mode 100644 index 0000000..139a789 --- /dev/null +++ b/django_elasticache_pymemcache/client.py @@ -0,0 +1,5 @@ +from pymemcache.client.hash import HashClient + + +class Client(HashClient): + pass diff --git a/django_elasticache/cluster_utils.py b/django_elasticache_pymemcache/cluster_utils.py similarity index 100% rename from django_elasticache/cluster_utils.py rename to django_elasticache_pymemcache/cluster_utils.py diff --git a/django_elasticache/memcached.py b/django_elasticache_pymemcache/memcached.py similarity index 52% rename from django_elasticache/memcached.py rename to django_elasticache_pymemcache/memcached.py index f7c19fc..1fcb5a8 100644 --- a/django_elasticache/memcached.py +++ b/django_elasticache_pymemcache/memcached.py @@ -3,8 +3,11 @@ """ import socket from functools import wraps + from django.core.cache import InvalidCacheBackendError -from django.core.cache.backends.memcached import PyLibMCCache +from django.core.cache.backends.memcached import BaseMemcachedCache + +from . import client as pymemcache_client from .cluster_utils import get_cluster_info @@ -22,14 +25,17 @@ def wrapper(self, *args, **kwds): return wrapper -class ElastiCache(PyLibMCCache): +class PyMemcacheElastiCache(BaseMemcachedCache): """ backend for Amazon ElastiCache (memcached) with auto discovery mode - it used pylibmc in binary mode + it used pymemcache """ def __init__(self, server, params): - self.update_params(params) - super(ElastiCache, self).__init__(server, params) + super(PyMemcacheElastiCache, self).__init__( + server, + params, + library=pymemcache_client, + value_not_found_exception=ValueError) if len(self._servers) > 1: raise InvalidCacheBackendError( 'ElastiCache should be configured with only one server ' @@ -41,22 +47,6 @@ def __init__(self, server, params): self._ignore_cluster_errors = self._options.get( 'IGNORE_CLUSTER_ERRORS', False) - def update_params(self, params): - """ - update connection params to maximize performance - """ - if not params.get('BINARY', True): - raise Warning('To increase performance please use ElastiCache' - ' in binary mode') - else: - params['BINARY'] = True # patch params, set binary mode - if 'OPTIONS' not in params: - # set special 'behaviors' pylibmc attributes - params['OPTIONS'] = { - 'tcp_nodelay': True, - 'ketama': True - } - def clear_cluster_nodes_cache(self): """clear internal cache with list of nodes in cluster""" if hasattr(self, '_cluster_nodes_cache'): @@ -69,9 +59,16 @@ def get_cluster_nodes(self): if not hasattr(self, '_cluster_nodes_cache'): server, port = self._servers[0].split(':') try: - self._cluster_nodes_cache = ( - get_cluster_info(server, port, - self._ignore_cluster_errors)['nodes']) + nodes = get_cluster_info( + server, + port, + self._ignore_cluster_errors + )['nodes'] + self._cluster_nodes_cache = [ + (i.split(':')[0], int(i.split(':')[1])) + for i in nodes + ] + print(self._cluster_nodes_cache) except (socket.gaierror, socket.timeout) as err: raise Exception('Cannot connect to cluster {} ({})'.format( self._servers[0], err @@ -80,42 +77,24 @@ def get_cluster_nodes(self): @property def _cache(self): - # PylibMC uses cache options as the 'behaviors' attribute. - # It also needs to use threadlocals, because some versions of - # PylibMC don't play well with the GIL. - - # instance to store cached version of client - # in Django 1.7 use self - # in Django < 1.7 use thread local - container = getattr(self, '_local', self) - client = getattr(container, '_client', None) - if client: - return client - - client = self._lib.Client(self.get_cluster_nodes()) - if self._options: - client.behaviors = self._options - - container._client = client - - return client + return self._lib.Client(self.get_cluster_nodes()) @invalidate_cache_after_error def get(self, *args, **kwargs): - return super(ElastiCache, self).get(*args, **kwargs) + return super(PyMemcacheElastiCache, self).get(*args, **kwargs) @invalidate_cache_after_error def get_many(self, *args, **kwargs): - return super(ElastiCache, self).get_many(*args, **kwargs) + return super(PyMemcacheElastiCache, self).get_many(*args, **kwargs) @invalidate_cache_after_error def set(self, *args, **kwargs): - return super(ElastiCache, self).set(*args, **kwargs) + return super(PyMemcacheElastiCache, self).set(*args, **kwargs) @invalidate_cache_after_error def set_many(self, *args, **kwargs): - return super(ElastiCache, self).set_many(*args, **kwargs) + return super(PyMemcacheElastiCache, self).set_many(*args, **kwargs) @invalidate_cache_after_error def delete(self, *args, **kwargs): - return super(ElastiCache, self).delete(*args, **kwargs) + return super(PyMemcacheElastiCache, self).delete(*args, **kwargs) diff --git a/setup.py b/setup.py index f65143c..2ac6848 100644 --- a/setup.py +++ b/setup.py @@ -1,21 +1,21 @@ from setuptools import setup -import django_elasticache +import django_elasticache_pymemcache setup( - name='django-elasticache', + name='django-elasticache-pymemcache', version=django_elasticache.__version__, description='Django cache backend for Amazon ElastiCache (memcached)', long_description=open('README.rst').read(), author='Danil Gusev', platforms='any', - author_email='danil.gusev@gmail.com', - url='http://github.com/gusdan/django-elasticache', + author_email='info@uncovertruth.jp', + url='http://github.com/uncovertruth/django-elasticache-pymemcache', license='MIT', - keywords='elasticache amazon cache pylibmc memcached aws', + keywords='elasticache amazon cache pymemcache memcached aws', packages=['django_elasticache'], - install_requires=['pylibmc', 'Django>=1.3'], + install_requires=['pymemcache', 'Django>=1.7'], classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', From 532d28a5290b4fd8cc89ec58911e4bd37ed92b41 Mon Sep 17 00:00:00 2001 From: opapy Date: Mon, 10 Apr 2017 19:24:44 +0900 Subject: [PATCH 02/57] add ignore_exc parameter --- django_elasticache_pymemcache/memcached.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_elasticache_pymemcache/memcached.py b/django_elasticache_pymemcache/memcached.py index 1fcb5a8..f76fe63 100644 --- a/django_elasticache_pymemcache/memcached.py +++ b/django_elasticache_pymemcache/memcached.py @@ -77,7 +77,7 @@ def get_cluster_nodes(self): @property def _cache(self): - return self._lib.Client(self.get_cluster_nodes()) + return self._lib.Client(self.get_cluster_nodes(), ignore_exc=self._ignore_cluster_errors) @invalidate_cache_after_error def get(self, *args, **kwargs): From 2ecc21c723d6372c488405f0164a59849efc8321 Mon Sep 17 00:00:00 2001 From: tsuboi Date: Tue, 11 Apr 2017 13:35:15 +0900 Subject: [PATCH 03/57] rename module name --- .../__init__.py | 2 +- .../client.py | 0 .../cluster_utils.py | 0 .../memcached.py | 22 +++++++++---------- setup.py | 10 ++++----- tests/test_backend.py | 18 +++++++-------- tests/test_protocol.py | 16 +++++++------- 7 files changed, 34 insertions(+), 34 deletions(-) rename {django_elasticache_pymemcache => django_elastipymemcache}/__init__.py (67%) rename {django_elasticache_pymemcache => django_elastipymemcache}/client.py (100%) rename {django_elasticache_pymemcache => django_elastipymemcache}/cluster_utils.py (100%) rename {django_elasticache_pymemcache => django_elastipymemcache}/memcached.py (82%) diff --git a/django_elasticache_pymemcache/__init__.py b/django_elastipymemcache/__init__.py similarity index 67% rename from django_elasticache_pymemcache/__init__.py rename to django_elastipymemcache/__init__.py index e3114ec..9603d9e 100644 --- a/django_elasticache_pymemcache/__init__.py +++ b/django_elastipymemcache/__init__.py @@ -1,2 +1,2 @@ -VERSION = (1, 0, 1) +VERSION = (0, 0, 1) __version__ = '.'.join(map(str, VERSION)) diff --git a/django_elasticache_pymemcache/client.py b/django_elastipymemcache/client.py similarity index 100% rename from django_elasticache_pymemcache/client.py rename to django_elastipymemcache/client.py diff --git a/django_elasticache_pymemcache/cluster_utils.py b/django_elastipymemcache/cluster_utils.py similarity index 100% rename from django_elasticache_pymemcache/cluster_utils.py rename to django_elastipymemcache/cluster_utils.py diff --git a/django_elasticache_pymemcache/memcached.py b/django_elastipymemcache/memcached.py similarity index 82% rename from django_elasticache_pymemcache/memcached.py rename to django_elastipymemcache/memcached.py index f76fe63..8a8549d 100644 --- a/django_elasticache_pymemcache/memcached.py +++ b/django_elastipymemcache/memcached.py @@ -7,7 +7,7 @@ from django.core.cache import InvalidCacheBackendError from django.core.cache.backends.memcached import BaseMemcachedCache -from . import client as pymemcache_client +from . import client as pyMemcache_client from .cluster_utils import get_cluster_info @@ -25,16 +25,16 @@ def wrapper(self, *args, **kwds): return wrapper -class PyMemcacheElastiCache(BaseMemcachedCache): +class ElastiPyMemCache(BaseMemcachedCache): """ backend for Amazon ElastiCache (memcached) with auto discovery mode - it used pymemcache + it used pyMemcache """ def __init__(self, server, params): - super(PyMemcacheElastiCache, self).__init__( + super(ElastiPyMemCache, self).__init__( server, params, - library=pymemcache_client, + library=pyMemcache_client, value_not_found_exception=ValueError) if len(self._servers) > 1: raise InvalidCacheBackendError( @@ -64,7 +64,7 @@ def get_cluster_nodes(self): port, self._ignore_cluster_errors )['nodes'] - self._cluster_nodes_cache = [ + self._cluster_nodes_cache = [ (i.split(':')[0], int(i.split(':')[1])) for i in nodes ] @@ -81,20 +81,20 @@ def _cache(self): @invalidate_cache_after_error def get(self, *args, **kwargs): - return super(PyMemcacheElastiCache, self).get(*args, **kwargs) + return super(ElastiPyMemCache, self).get(*args, **kwargs) @invalidate_cache_after_error def get_many(self, *args, **kwargs): - return super(PyMemcacheElastiCache, self).get_many(*args, **kwargs) + return super(ElastiPyMemCache, self).get_many(*args, **kwargs) @invalidate_cache_after_error def set(self, *args, **kwargs): - return super(PyMemcacheElastiCache, self).set(*args, **kwargs) + return super(ElastiPyMemCache, self).set(*args, **kwargs) @invalidate_cache_after_error def set_many(self, *args, **kwargs): - return super(PyMemcacheElastiCache, self).set_many(*args, **kwargs) + return super(ElastiPyMemCache, self).set_many(*args, **kwargs) @invalidate_cache_after_error def delete(self, *args, **kwargs): - return super(PyMemcacheElastiCache, self).delete(*args, **kwargs) + return super(ElastiPyMemCache, self).delete(*args, **kwargs) diff --git a/setup.py b/setup.py index 2ac6848..7f8f470 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,20 @@ from setuptools import setup -import django_elasticache_pymemcache +import django_elastipymemcache setup( - name='django-elasticache-pymemcache', - version=django_elasticache.__version__, + name='django-elastipymemcache', + version=django_elastipymemcache.__version__, description='Django cache backend for Amazon ElastiCache (memcached)', long_description=open('README.rst').read(), author='Danil Gusev', platforms='any', author_email='info@uncovertruth.jp', - url='http://github.com/uncovertruth/django-elasticache-pymemcache', + url='http://github.com/uncovertruth/django-elastipymemcache', license='MIT', keywords='elasticache amazon cache pymemcache memcached aws', - packages=['django_elasticache'], + packages=['django_elastipymemcache'], install_requires=['pymemcache', 'Django>=1.7'], classifiers=[ 'Development Status :: 4 - Beta', diff --git a/tests/test_backend.py b/tests/test_backend.py index 9b6f425..fa9f912 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -14,7 +14,7 @@ @patch('django.conf.settings', global_settings) def test_patch_params(): - from django_elasticache.memcached import ElastiCache + from django_elastipymemcache.memcached import ElastiCache params = {} ElastiCache('qew:12', params) eq_(params['BINARY'], True) @@ -25,21 +25,21 @@ def test_patch_params(): @raises(Exception) @patch('django.conf.settings', global_settings) def test_wrong_params(): - from django_elasticache.memcached import ElastiCache + from django_elastipymemcache.memcached import ElastiCache ElastiCache('qew', {}) @raises(Warning) @patch('django.conf.settings', global_settings) def test_wrong_params_warning(): - from django_elasticache.memcached import ElastiCache + from django_elastipymemcache.memcached import ElastiCache ElastiCache('qew', {'BINARY': False}) @patch('django.conf.settings', global_settings) -@patch('django_elasticache.memcached.get_cluster_info') +@patch('django_elastipymemcache.memcached.get_cluster_info') def test_split_servers(get_cluster_info): - from django_elasticache.memcached import ElastiCache + from django_elastipymemcache.memcached import ElastiCache backend = ElastiCache('h:0', {}) servers = ['h1:p', 'h2:p'] get_cluster_info.return_value = { @@ -52,9 +52,9 @@ def test_split_servers(get_cluster_info): @patch('django.conf.settings', global_settings) -@patch('django_elasticache.memcached.get_cluster_info') +@patch('django_elastipymemcache.memcached.get_cluster_info') def test_node_info_cache(get_cluster_info): - from django_elasticache.memcached import ElastiCache + from django_elastipymemcache.memcached import ElastiCache servers = ['h1:p', 'h2:p'] get_cluster_info.return_value = { 'nodes': servers @@ -74,9 +74,9 @@ def test_node_info_cache(get_cluster_info): @patch('django.conf.settings', global_settings) -@patch('django_elasticache.memcached.get_cluster_info') +@patch('django_elastipymemcache.memcached.get_cluster_info') def test_invalidate_cache(get_cluster_info): - from django_elasticache.memcached import ElastiCache + from django_elastipymemcache.memcached import ElastiCache servers = ['h1:p', 'h2:p'] get_cluster_info.return_value = { 'nodes': servers diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 7232a1a..3a752df 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -1,4 +1,4 @@ -from django_elasticache.cluster_utils import ( +from django_elastipymemcache.cluster_utils import ( get_cluster_info, WrongProtocolData) from nose.tools import eq_, raises import sys @@ -41,7 +41,7 @@ ] -@patch('django_elasticache.cluster_utils.Telnet') +@patch('django_elastipymemcache.cluster_utils.Telnet') def test_happy_path(Telnet): client = Telnet.return_value client.read_until.side_effect = TEST_PROTOCOL_1_READ_UNTIL @@ -52,12 +52,12 @@ def test_happy_path(Telnet): @raises(WrongProtocolData) -@patch('django_elasticache.cluster_utils.Telnet', MagicMock()) +@patch('django_elastipymemcache.cluster_utils.Telnet', MagicMock()) def test_bad_protocol(): get_cluster_info('', 0) -@patch('django_elasticache.cluster_utils.Telnet') +@patch('django_elastipymemcache.cluster_utils.Telnet') def test_last_versions(Telnet): client = Telnet.return_value client.read_until.side_effect = TEST_PROTOCOL_1_READ_UNTIL @@ -69,7 +69,7 @@ def test_last_versions(Telnet): ]) -@patch('django_elasticache.cluster_utils.Telnet') +@patch('django_elastipymemcache.cluster_utils.Telnet') def test_prev_versions(Telnet): client = Telnet.return_value client.read_until.side_effect = TEST_PROTOCOL_2_READ_UNTIL @@ -81,7 +81,7 @@ def test_prev_versions(Telnet): ]) -@patch('django_elasticache.cluster_utils.Telnet') +@patch('django_elastipymemcache.cluster_utils.Telnet') def test_ubuntu_protocol(Telnet): client = Telnet.return_value client.read_until.side_effect = TEST_PROTOCOL_3_READ_UNTIL @@ -98,7 +98,7 @@ def test_ubuntu_protocol(Telnet): ]) -@patch('django_elasticache.cluster_utils.Telnet') +@patch('django_elastipymemcache.cluster_utils.Telnet') def test_no_configuration_protocol_support_with_errors_ignored(Telnet): client = Telnet.return_value client.read_until.side_effect = TEST_PROTOCOL_4_READ_UNTIL @@ -113,7 +113,7 @@ def test_no_configuration_protocol_support_with_errors_ignored(Telnet): @raises(WrongProtocolData) -@patch('django_elasticache.cluster_utils.Telnet') +@patch('django_elastipymemcache.cluster_utils.Telnet') def test_no_configuration_protocol_support_with_errors(Telnet): client = Telnet.return_value client.read_until.side_effect = TEST_PROTOCOL_4_READ_UNTIL From e1f0ba1b630f75ea06c5e24b9ea9a5968b755fcf Mon Sep 17 00:00:00 2001 From: tsuboi Date: Tue, 11 Apr 2017 14:27:18 +0900 Subject: [PATCH 04/57] rename readme --- README.md | 48 ++++++++++++++++++++ README.rst | 131 ----------------------------------------------------- 2 files changed, 48 insertions(+), 131 deletions(-) create mode 100644 README.md delete mode 100644 README.rst diff --git a/README.md b/README.md new file mode 100644 index 0000000..e80c6aa --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# django-elastipymemcache + +This project is forked [django-elasticache](https://github.com/gusdan/django-elasticache) + +Simple Django cache backend for Amazon ElastiCache (memcached based). It uses +[pymemcache](https://github.com/pinterest/pymemcache>) and sets up a connection to each +node in the cluster using +[auto discovery](http://docs.aws.amazon.com/AmazonElastiCache/latest/UserGuide/AutoDiscovery.html>) + + +## Requirements + +* pymemcache +* Django 1.7+. + +It was written and tested on Python 2.7 and 3.5. + +## Installation + +Get it from [pypi](http://pypi.python.org/pypi/django-elastipymemcache) + +```bash +pip install django-elastipymemcache +``` + +## Usage + +Your cache backend should look something like this + +```python +CACHES = { + 'default': { + 'BACKEND': 'django_elastipymemcache.memcached.ElastiPyMemCache', + 'LOCATION': '[configuration endpoint].com:11211', + 'OPTIONS' { + 'IGNORE_CLUSTER_ERRORS': [True,False], + }, + } +} +``` + +## Testing + +Run the tests like this + +```bash +nosetests +``` diff --git a/README.rst b/README.rst deleted file mode 100644 index 644a037..0000000 --- a/README.rst +++ /dev/null @@ -1,131 +0,0 @@ -Amazon ElastiCache backend for Django -===================================== - -Simple Django cache backend for Amazon ElastiCache (memcached based). It uses -`pylibmc `_ and sets up a connection to each -node in the cluster using -`auto discovery `_. - - -Requirements ------------- - -* pylibmc -* Django 1.5+. - -It was written and tested on Python 2.7 and 3.4. - -Installation ------------- - -Get it from `pypi `_:: - - pip install django-elasticache - -or `github `_:: - - pip install -e git://github.com/gusdan/django-elasticache.git#egg=django-elasticache - - -Usage ------ - -Your cache backend should look something like this:: - - CACHES = { - 'default': { - 'BACKEND': 'django_elasticache.memcached.ElastiCache', - 'LOCATION': 'cache-c.draaaf.cfg.use1.cache.amazonaws.com:11211', - 'OPTIONS' { - 'IGNORE_CLUSTER_ERRORS': [True,False], - }, - } - } - -By the first call to cache it connects to cluster (using ``LOCATION`` param), -gets list of all nodes and setup pylibmc client using full -list of nodes. As result your cache will work with all nodes in cluster and -automatically detect new nodes in cluster. List of nodes are stored in class-level -cached, so any changes in cluster take affect only after restart of working process. -But if you're using gunicorn or mod_wsgi you usually have max_request settings which -restart process after some count of processed requests, so auto discovery will work -fine. - -The ``IGNORE_CLUSTER_ERRORS`` option is useful when ``LOCATION`` doesn't have support -for ``config get cluster``. When set to ``True``, and ``config get cluster`` fails, -it returns a list of a single node with the same endpoint supplied to ``LOCATION``. - -Django-elasticache changes default pylibmc params to increase performance. - -Another solutions ------------------ - -ElastiCache provides memcached interface so there are three solution of using it: - -1. Memcached configured with location = Configuration Endpoint -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In this case your application -will randomly connect to nodes in cluster and cache will be used with not optimal -way. At some moment you will be connected to first node and set item. Minute later -you will be connected to another node and will not able to get this item. - - :: - - CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache', - 'LOCATION': 'cache.gasdbp.cfg.use1.cache.amazonaws.com:11211', - } - } - - -2. Memcached configured with all nodes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -It will work fine, memcache client will -separate items between all nodes and will balance loading on client side. You will -have problems only after adding new nodes or delete old nodes. In this case you should -add new nodes manually and don't forget update your app after all changes on AWS. - - :: - - CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache', - 'LOCATION': [ - 'cache.gqasdbp.0001.use1.cache.amazonaws.com:11211', - 'cache.gqasdbp.0002.use1.cache.amazonaws.com:11211', - ] - } - } - - -3. Use django-elasticache -~~~~~~~~~~~~~~~~~~~~~~~~~ - -It will connect to cluster and retrieve ip addresses -of all nodes and configure memcached to use all nodes. - - :: - - CACHES = { - 'default': { - 'BACKEND': 'django_elasticache.memcached.ElastiCache', - 'LOCATION': 'cache-c.draaaf.cfg.use1.cache.amazonaws.com:11211', - } - } - - -Difference between setup with nodes list (django-elasticache) and -connection to only one configuration Endpoint (using dns routing) you can see on -this graph: - -.. image:: https://raw.github.com/gusdan/django-elasticache/master/docs/images/get%20operation%20in%20cluster.png - -Testing -------- - -Run the tests like this:: - - nosetests From bea9225e8ad8d2c389ccf2ae3b77b443dea8a3e9 Mon Sep 17 00:00:00 2001 From: tsuboi Date: Tue, 11 Apr 2017 14:52:54 +0900 Subject: [PATCH 05/57] add options --- django_elastipymemcache/cluster_utils.py | 5 +---- django_elastipymemcache/memcached.py | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/django_elastipymemcache/cluster_utils.py b/django_elastipymemcache/cluster_utils.py index 740e4cf..92ac72a 100644 --- a/django_elastipymemcache/cluster_utils.py +++ b/django_elastipymemcache/cluster_utils.py @@ -49,10 +49,7 @@ def get_cluster_info(host, port, ignore_cluster_errors=False): if res == b'ERROR\r\n' and ignore_cluster_errors: return { 'version': version, - 'nodes': [ - '{}:{}'.format(smart_text(host), - smart_text(port)) - ] + 'nodes': [] } ls = list(filter(None, re.compile(br'\r?\n').split(res))) diff --git a/django_elastipymemcache/memcached.py b/django_elastipymemcache/memcached.py index 8a8549d..b4e93a2 100644 --- a/django_elastipymemcache/memcached.py +++ b/django_elastipymemcache/memcached.py @@ -45,7 +45,7 @@ def __init__(self, server, params): 'Server configuration should be in format IP:port') self._ignore_cluster_errors = self._options.get( - 'IGNORE_CLUSTER_ERRORS', False) + 'ignore_exc', False) def clear_cluster_nodes_cache(self): """clear internal cache with list of nodes in cluster""" @@ -77,7 +77,7 @@ def get_cluster_nodes(self): @property def _cache(self): - return self._lib.Client(self.get_cluster_nodes(), ignore_exc=self._ignore_cluster_errors) + return self._lib.Client(self.get_cluster_nodes(), **self._options) @invalidate_cache_after_error def get(self, *args, **kwargs): From 8c3c15aca9bf78051b684ac46b72aff79f6640d3 Mon Sep 17 00:00:00 2001 From: tsuboi Date: Tue, 11 Apr 2017 14:53:49 +0900 Subject: [PATCH 06/57] fix README path --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7f8f470..b59bdfc 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ name='django-elastipymemcache', version=django_elastipymemcache.__version__, description='Django cache backend for Amazon ElastiCache (memcached)', - long_description=open('README.rst').read(), + long_description=open('README.md').read(), author='Danil Gusev', platforms='any', author_email='info@uncovertruth.jp', From cc8e30e8a465deb74ca129df5b689a5eb8b3d621 Mon Sep 17 00:00:00 2001 From: tsuboi Date: Tue, 11 Apr 2017 14:56:22 +0900 Subject: [PATCH 07/57] rm png --- docs/images/get operation in cluster.png | Bin 90644 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/images/get operation in cluster.png diff --git a/docs/images/get operation in cluster.png b/docs/images/get operation in cluster.png deleted file mode 100644 index a85f01d5b503519fa1a5fe9b2a6c26c182b570e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90644 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 C0qI!) From 64ec2a17c44f67b35edc09659fdedef0c92db361 Mon Sep 17 00:00:00 2001 From: tsuboi Date: Tue, 11 Apr 2017 17:18:51 +0900 Subject: [PATCH 08/57] memo self._client --- django_elastipymemcache/cluster_utils.py | 8 +++-- django_elastipymemcache/memcached.py | 37 +++++++++----------- tests/test_backend.py | 44 ++++++------------------ tests/test_protocol.py | 19 +++++----- 4 files changed, 41 insertions(+), 67 deletions(-) diff --git a/django_elastipymemcache/cluster_utils.py b/django_elastipymemcache/cluster_utils.py index 92ac72a..a3f27d7 100644 --- a/django_elastipymemcache/cluster_utils.py +++ b/django_elastipymemcache/cluster_utils.py @@ -32,6 +32,7 @@ def get_cluster_info(host, port, ignore_cluster_errors=False): client.write(b'version\n') res = client.read_until(b'\r\n').strip() version_list = res.split(b' ') + print(version_list) if len(version_list) not in [2, 3] or version_list[0] != b'VERSION': raise WrongProtocolData('version', res) version = version_list[1] @@ -49,7 +50,9 @@ def get_cluster_info(host, port, ignore_cluster_errors=False): if res == b'ERROR\r\n' and ignore_cluster_errors: return { 'version': version, - 'nodes': [] + 'nodes': [ + (smart_text(host), int(port)) + ] } ls = list(filter(None, re.compile(br'\r?\n').split(res))) @@ -64,8 +67,7 @@ def get_cluster_info(host, port, ignore_cluster_errors=False): try: for node in ls[2].split(b' '): host, ip, port = node.split(b'|') - nodes.append('{}:{}'.format(smart_text(ip or host), - smart_text(port))) + nodes.append((smart_text(ip or host), int(port))) except ValueError: raise WrongProtocolData(cmd, res) return { diff --git a/django_elastipymemcache/memcached.py b/django_elastipymemcache/memcached.py index b4e93a2..8397261 100644 --- a/django_elastipymemcache/memcached.py +++ b/django_elastipymemcache/memcached.py @@ -49,35 +49,30 @@ def __init__(self, server, params): def clear_cluster_nodes_cache(self): """clear internal cache with list of nodes in cluster""" - if hasattr(self, '_cluster_nodes_cache'): - del self._cluster_nodes_cache + if hasattr(self, '_client'): + del self._client def get_cluster_nodes(self): """ return list with all nodes in cluster """ - if not hasattr(self, '_cluster_nodes_cache'): - server, port = self._servers[0].split(':') - try: - nodes = get_cluster_info( - server, - port, - self._ignore_cluster_errors - )['nodes'] - self._cluster_nodes_cache = [ - (i.split(':')[0], int(i.split(':')[1])) - for i in nodes - ] - print(self._cluster_nodes_cache) - except (socket.gaierror, socket.timeout) as err: - raise Exception('Cannot connect to cluster {} ({})'.format( - self._servers[0], err - )) - return self._cluster_nodes_cache + server, port = self._servers[0].split(':') + try: + return get_cluster_info( + server, + port, + self._ignore_cluster_errors + )['nodes'] + except (socket.gaierror, socket.timeout) as err: + raise Exception('Cannot connect to cluster {} ({})'.format( + self._servers[0], err + )) @property def _cache(self): - return self._lib.Client(self.get_cluster_nodes(), **self._options) + if getattr(self, '_client', None) is None: + self._client = self._lib.Client(self.get_cluster_nodes(), **self._options) + return self._client @invalidate_cache_after_error def get(self, *args, **kwargs): diff --git a/tests/test_backend.py b/tests/test_backend.py index fa9f912..da1454d 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -12,36 +12,12 @@ global_settings.configured = True -@patch('django.conf.settings', global_settings) -def test_patch_params(): - from django_elastipymemcache.memcached import ElastiCache - params = {} - ElastiCache('qew:12', params) - eq_(params['BINARY'], True) - eq_(params['OPTIONS']['tcp_nodelay'], True) - eq_(params['OPTIONS']['ketama'], True) - - -@raises(Exception) -@patch('django.conf.settings', global_settings) -def test_wrong_params(): - from django_elastipymemcache.memcached import ElastiCache - ElastiCache('qew', {}) - - -@raises(Warning) -@patch('django.conf.settings', global_settings) -def test_wrong_params_warning(): - from django_elastipymemcache.memcached import ElastiCache - ElastiCache('qew', {'BINARY': False}) - - @patch('django.conf.settings', global_settings) @patch('django_elastipymemcache.memcached.get_cluster_info') def test_split_servers(get_cluster_info): - from django_elastipymemcache.memcached import ElastiCache - backend = ElastiCache('h:0', {}) - servers = ['h1:p', 'h2:p'] + from django_elastipymemcache.memcached import ElastiPyMemCache + backend = ElastiPyMemCache('h:0', {}) + servers = [('h1', 0), ('h2', 0)] get_cluster_info.return_value = { 'nodes': servers } @@ -54,13 +30,13 @@ def test_split_servers(get_cluster_info): @patch('django.conf.settings', global_settings) @patch('django_elastipymemcache.memcached.get_cluster_info') def test_node_info_cache(get_cluster_info): - from django_elastipymemcache.memcached import ElastiCache - servers = ['h1:p', 'h2:p'] + from django_elastipymemcache.memcached import ElastiPyMemCache + servers = [('h1', 0), ('h2', 0)] get_cluster_info.return_value = { 'nodes': servers } - backend = ElastiCache('h:0', {}) + backend = ElastiPyMemCache('h:0', {}) backend._lib.Client = Mock() backend.set('key1', 'val') backend.get('key1') @@ -76,13 +52,13 @@ def test_node_info_cache(get_cluster_info): @patch('django.conf.settings', global_settings) @patch('django_elastipymemcache.memcached.get_cluster_info') def test_invalidate_cache(get_cluster_info): - from django_elastipymemcache.memcached import ElastiCache - servers = ['h1:p', 'h2:p'] + from django_elastipymemcache.memcached import ElastiPyMemCache + servers = [('h1', 0), ('h2', 0)] get_cluster_info.return_value = { 'nodes': servers } - backend = ElastiCache('h:0', {}) + backend = ElastiPyMemCache('h:0', {}) backend._lib.Client = Mock() assert backend._cache backend._cache.get = Mock() @@ -99,4 +75,4 @@ def test_invalidate_cache(get_cluster_info): except Exception: pass eq_(backend._cache.get.call_count, 2) - eq_(get_cluster_info.call_count, 2) + eq_(get_cluster_info.call_count, 3) diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 3a752df..359b7d5 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -13,7 +13,7 @@ ] TEST_PROTOCOL_1_EXPECT = [ - (0, None, b'CONFIG cluster 0 138\r\n1\nhost|ip|port host||port\n\r\nEND\r\n'), # NOQA + (0, None, b'CONFIG cluster 0 138\r\n1\nhost|ip|11211 host||11211\n\r\nEND\r\n'), # NOQA ] TEST_PROTOCOL_2_READ_UNTIL = [ @@ -21,7 +21,7 @@ ] TEST_PROTOCOL_2_EXPECT = [ - (0, None, b'CONFIG cluster 0 138\r\n1\nhost|ip|port host||port\n\r\nEND\r\n'), # NOQA + (0, None, b'CONFIG cluster 0 138\r\n1\nhost|ip|11211 host||11211\n\r\nEND\r\n'), # NOQA ] TEST_PROTOCOL_3_READ_UNTIL = [ @@ -29,7 +29,7 @@ ] TEST_PROTOCOL_3_EXPECT = [ - (0, None, b'CONFIG cluster 0 138\r\n1\nhost|ip|port host||port\n\r\nEND\r\n'), # NOQA + (0, None, b'CONFIG cluster 0 138\r\n1\nhost|ip|11211 host||11211\n\r\nEND\r\n'), # NOQA ] TEST_PROTOCOL_4_READ_UNTIL = [ @@ -48,7 +48,7 @@ def test_happy_path(Telnet): client.expect.side_effect = TEST_PROTOCOL_1_EXPECT info = get_cluster_info('', 0) eq_(info['version'], 1) - eq_(info['nodes'], ['ip:port', 'host:port']) + eq_(info['nodes'], [('ip', 11211), ('host', 11211)]) @raises(WrongProtocolData) @@ -87,10 +87,11 @@ def test_ubuntu_protocol(Telnet): client.read_until.side_effect = TEST_PROTOCOL_3_READ_UNTIL client.expect.side_effect = TEST_PROTOCOL_3_EXPECT - try: - get_cluster_info('', 0) - except WrongProtocolData: - raise AssertionError('Raised WrongProtocolData with Ubuntu version.') + #try: + # get_cluster_info('', 0) + #except WrongProtocolData: + # raise AssertionError('Raised WrongProtocolData with Ubuntu version.') + get_cluster_info('', 0) client.write.assert_has_calls([ call(b'version\n'), @@ -109,7 +110,7 @@ def test_no_configuration_protocol_support_with_errors_ignored(Telnet): call(b'config get cluster\n'), ]) eq_(info['version'], '1.4.34') - eq_(info['nodes'], ['test:0']) + eq_(info['nodes'], [('test', 0)]) @raises(WrongProtocolData) From e90da03eea6c14d02d8a5e65f5b3c5abb2fc66fe Mon Sep 17 00:00:00 2001 From: tsuboi Date: Tue, 11 Apr 2017 17:21:03 +0900 Subject: [PATCH 09/57] rm options from example --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index e80c6aa..1f7d57c 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,6 @@ CACHES = { 'default': { 'BACKEND': 'django_elastipymemcache.memcached.ElastiPyMemCache', 'LOCATION': '[configuration endpoint].com:11211', - 'OPTIONS' { - 'IGNORE_CLUSTER_ERRORS': [True,False], - }, } } ``` From 0dd016086af23e5611ed7e055f3b0f391eb97f3e Mon Sep 17 00:00:00 2001 From: tsuboi Date: Tue, 11 Apr 2017 17:22:42 +0900 Subject: [PATCH 10/57] rm print --- django_elastipymemcache/cluster_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/django_elastipymemcache/cluster_utils.py b/django_elastipymemcache/cluster_utils.py index a3f27d7..b5c042c 100644 --- a/django_elastipymemcache/cluster_utils.py +++ b/django_elastipymemcache/cluster_utils.py @@ -32,7 +32,6 @@ def get_cluster_info(host, port, ignore_cluster_errors=False): client.write(b'version\n') res = client.read_until(b'\r\n').strip() version_list = res.split(b' ') - print(version_list) if len(version_list) not in [2, 3] or version_list[0] != b'VERSION': raise WrongProtocolData('version', res) version = version_list[1] From f630816c1ea975d038085a9595d6bada38a7e7cc Mon Sep 17 00:00:00 2001 From: opapy Date: Tue, 11 Apr 2017 17:44:42 +0900 Subject: [PATCH 11/57] Update LICENSE --- LICENSE | 1 + 1 file changed, 1 insertion(+) diff --git a/LICENSE b/LICENSE index 9b5f8a5..e4835f6 100644 --- a/LICENSE +++ b/LICENSE @@ -18,3 +18,4 @@ 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. + From 9edd161ef1aca9771c1189e9f1738991dab15ce4 Mon Sep 17 00:00:00 2001 From: tsuboi Date: Tue, 11 Apr 2017 17:46:56 +0900 Subject: [PATCH 12/57] fix README --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1f7d57c..2a21b97 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,10 @@ # django-elastipymemcache -This project is forked [django-elasticache](https://github.com/gusdan/django-elasticache) - Simple Django cache backend for Amazon ElastiCache (memcached based). It uses [pymemcache](https://github.com/pinterest/pymemcache>) and sets up a connection to each node in the cluster using [auto discovery](http://docs.aws.amazon.com/AmazonElastiCache/latest/UserGuide/AutoDiscovery.html>) - ## Requirements * pymemcache @@ -43,3 +40,7 @@ Run the tests like this ```bash nosetests ``` + +## Thx + +Originally forked from [django-elasticache](https://github.com/gusdan/django-elasticache) From 0715186593c5a2f1a2436b3fc8b8c031ff9ab76f Mon Sep 17 00:00:00 2001 From: tsuboi Date: Tue, 11 Apr 2017 17:48:06 +0900 Subject: [PATCH 13/57] fix markdown --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2a21b97..6bc58d4 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # django-elastipymemcache Simple Django cache backend for Amazon ElastiCache (memcached based). It uses -[pymemcache](https://github.com/pinterest/pymemcache>) and sets up a connection to each +[pymemcache](https://github.com/pinterest/pymemcache) and sets up a connection to each node in the cluster using -[auto discovery](http://docs.aws.amazon.com/AmazonElastiCache/latest/UserGuide/AutoDiscovery.html>) +[auto discovery](http://docs.aws.amazon.com/AmazonElastiCache/latest/UserGuide/AutoDiscovery.html) ## Requirements From 6255a8b01a49d9f6d39d991af528d9d29f270f73 Mon Sep 17 00:00:00 2001 From: tsuboi Date: Tue, 11 Apr 2017 17:49:28 +0900 Subject: [PATCH 14/57] fix manifest.in --- MANIFEST.in | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 4c4c31e..0140813 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include README.rst +include README.md include MANIFEST.in -include setup.py \ No newline at end of file +include setup.py From 027b83d625798e671c39a9f1ff8daa5226a816c5 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 11 Apr 2017 18:27:55 +0900 Subject: [PATCH 15/57] Re-generate via gibo --- .gitignore | 104 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 84 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index b288303..9adce58 100644 --- a/.gitignore +++ b/.gitignore @@ -1,37 +1,101 @@ +### https://raw.github.com/github/gitignore/f57304e9762876ae4c9b02867ed0cb887316387e/python.gitignore + +# Byte-compiled / optimized / DLL files +__pycache__/ *.py[cod] +*$py.class # C extensions *.so -# Packages -*.egg -*.egg-info -dist -build -eggs -parts -bin -var -sdist -develop-eggs +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ .installed.cfg -lib -lib64 -__pycache__ +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec # Installer logs pip-log.txt +pip-delete-this-directory.txt # Unit test / coverage reports +htmlcov/ +.tox/ .coverage -.tox +.coverage.* +.cache nosetests.xml +coverage.xml +*,cover +.hypothesis/ # Translations *.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + -# Mr Developer -.mr.developer.cfg -.project -.pydevproject -.idea From aab80f7167429a0da2915a8d4c3ed2f5993b58ca Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 11 Apr 2017 18:28:10 +0900 Subject: [PATCH 16/57] Set up Travis-CI --- .travis.yml | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++ tox.ini | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 .travis.yml create mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6ede967 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,54 @@ +sudo: false +language: python +cache: pip +matrix: + fast_finish: true + include: + - python: 2.7 + env: TOXENV=py27-dj18 + - python: 3.3 + env: TOXENV=py33-dj18 + - python: 3.4 + env: TOXENV=py34-dj18 + - python: 3.5 + env: TOXENV=py34-dj18 + - python: 2.7 + env: TOXENV=py27-dj19 + - python: 3.4 + env: TOXENV=py34-dj19 + - python: 3.5 + env: TOXENV=py35-dj19 + - python: 2.7 + env: TOXENV=py27-dj110 + - python: 3.4 + env: TOXENV=py34-dj110 + - python: 3.5 + env: TOXENV=py35-dj110 + - python: 2.7 + env: TOXENV=py27-dj111 + - python: 3.4 + env: TOXENV=py34-dj111 + - python: 3.5 + env: TOXENV=py35-dj111 + - python: 3.6 + env: TOXENV=py36-dj111 + - python: 3.5 + env: TOXENV=py35-djdev + - python: 3.6 + env: TOXENV=py36-djdev + - python: 3.6 + env: TOXENV=flake8 + - python: 3.6 + env: TOXENV=isort + - python: 3.6 + env: TOXENV=readme + allow_failures: + - env: TOX_ENV=py35-djdev + - env: TOX_ENV=py36-djdev + +install: +- pip install tox codecov +script: +- tox -v +after_success: +- codecov diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..6f3f105 --- /dev/null +++ b/tox.ini @@ -0,0 +1,39 @@ +[tox] +envlist = + py{27,33,34,35,36}-dj{18,19,110,111,dev} + flake8, + isort, + readme + +[testenv] +basepython = + py27: python2.7 + py33: python3.3 + py34: python3.4 + py35: python3.5 + py36: python3.6 +deps = + dj18: Django>=1.8,<1.9 + dj19: Django>=1.9,<1.10 + dj110: Django>=1.10,<1.11 + dj111: Django>=1.11,<2.0 + djdev: https://github.com/django/django/archive/master.tar.gz + coverage +setenv = + PYTHONPATH = {toxinidir} +commands = coverage run --source=django_elastipymemcache -a setup.py test + +[testenv:flake8] +basepython = python3.6 +commands = make flake8 +deps = flake8 + +[testenv:isort] +basepython = python3.6 +commands = make isort_check_only +deps = isort + +[testenv:readme] +basepython = python3.6 +commands = python setup.py check -r -s +deps = readme_renderer From abb81fe6d20da26617831931ed5427f255f8ae4a Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 11 Apr 2017 18:31:13 +0900 Subject: [PATCH 17/57] Remove support for Django 1.7 --- README.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6bc58d4..0ce71d9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ node in the cluster using ## Requirements * pymemcache -* Django 1.7+. +* Django >= 1.8 It was written and tested on Python 2.7 and 3.5. diff --git a/setup.py b/setup.py index b59bdfc..12c8e78 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ license='MIT', keywords='elasticache amazon cache pymemcache memcached aws', packages=['django_elastipymemcache'], - install_requires=['pymemcache', 'Django>=1.7'], + install_requires=['pymemcache', 'Django>=1.8'], classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', From 4c90b99f1f1fb3958efa1845ff438fcd2c6e3a8a Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 11 Apr 2017 18:40:59 +0900 Subject: [PATCH 18/57] Add dependencies for test --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 6f3f105..e1691a1 100644 --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,7 @@ deps = dj110: Django>=1.10,<1.11 dj111: Django>=1.11,<2.0 djdev: https://github.com/django/django/archive/master.tar.gz + nose coverage setenv = PYTHONPATH = {toxinidir} @@ -25,12 +26,12 @@ commands = coverage run --source=django_elastipymemcache -a setup.py test [testenv:flake8] basepython = python3.6 -commands = make flake8 +commands = flake8 deps = flake8 [testenv:isort] basepython = python3.6 -commands = make isort_check_only +commands = isort --check-only deps = isort [testenv:readme] From 99d800d9bb491347e52b03555cecd333e8185e62 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 11 Apr 2017 18:54:26 +0900 Subject: [PATCH 19/57] Update setup.py - fix license etc. --- setup.py | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/setup.py b/setup.py index 12c8e78..6706657 100644 --- a/setup.py +++ b/setup.py @@ -6,25 +6,41 @@ setup( name='django-elastipymemcache', version=django_elastipymemcache.__version__, + url='http://github.com/uncovertruth/django-elastipymemcache', + author='UNCOVER TRUTH Inc.', + author_email='dev@uncovertruth.co.jp', description='Django cache backend for Amazon ElastiCache (memcached)', long_description=open('README.md').read(), - author='Danil Gusev', - platforms='any', - author_email='info@uncovertruth.jp', - url='http://github.com/uncovertruth/django-elastipymemcache', - license='MIT', keywords='elasticache amazon cache pymemcache memcached aws', - packages=['django_elastipymemcache'], - install_requires=['pymemcache', 'Django>=1.8'], + license='MIT', + packages=[ + 'django_elastipymemcache', + ], + install_requires=[ + 'pymemcache', + 'Django>=1.8', + ], + extras_require={ + 'dev': ['check-manifest'], + 'test': ['nose', 'coverage', 'flake8', 'isort', 'readme_renderer'], + }, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', - 'Environment :: Web Environment :: Mozilla', - 'Framework :: Django', + 'Framework :: Django :: 1.8', + 'Framework :: Django :: 1.9', + 'Framework :: Django :: 1.10', + 'Framework :: Django :: 1.11', 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', + 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', - 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Software Development :: Libraries :: Python Modules', ], ) From 0be478dea378e390e01673ea2a01e429b23eed49 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 11 Apr 2017 18:57:20 +0900 Subject: [PATCH 20/57] Fix flake8 bugs --- django_elastipymemcache/memcached.py | 3 ++- tests/test_backend.py | 2 +- tests/test_protocol.py | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/django_elastipymemcache/memcached.py b/django_elastipymemcache/memcached.py index 8397261..114361d 100644 --- a/django_elastipymemcache/memcached.py +++ b/django_elastipymemcache/memcached.py @@ -71,7 +71,8 @@ def get_cluster_nodes(self): @property def _cache(self): if getattr(self, '_client', None) is None: - self._client = self._lib.Client(self.get_cluster_nodes(), **self._options) + self._client = self._lib.Client( + self.get_cluster_nodes(), **self._options) return self._client @invalidate_cache_after_error diff --git a/tests/test_backend.py b/tests/test_backend.py index da1454d..e831924 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -1,5 +1,5 @@ from django.conf import global_settings, settings -from nose.tools import eq_, raises +from nose.tools import eq_ import sys if sys.version < '3': from mock import patch, Mock diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 359b7d5..68dbc09 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -87,10 +87,10 @@ def test_ubuntu_protocol(Telnet): client.read_until.side_effect = TEST_PROTOCOL_3_READ_UNTIL client.expect.side_effect = TEST_PROTOCOL_3_EXPECT - #try: - # get_cluster_info('', 0) - #except WrongProtocolData: - # raise AssertionError('Raised WrongProtocolData with Ubuntu version.') + # try: + # get_cluster_info('', 0) + # except WrongProtocolData: + # raise AssertionError('Raised WrongProtocolData with Ubuntu version.') get_cluster_info('', 0) client.write.assert_has_calls([ From e419a9069c55662c2f7f8d5858d3dee7d2eae621 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 11 Apr 2017 19:28:55 +0900 Subject: [PATCH 21/57] Fix flake8, check-manifest and isort bugs --- .travis.yml | 2 ++ MANIFEST.in | 7 +++-- README.md => README.rst | 0 django_elastipymemcache/cluster_utils.py | 5 ++-- requirements.txt | 6 ++++ setup.cfg | 15 ++++++++++ setup.py | 35 ++++++++++++------------ tests/test_backend.py | 4 ++- tests/test_protocol.py | 8 ++++-- tox.ini | 8 +++++- 10 files changed, 64 insertions(+), 26 deletions(-) rename README.md => README.rst (100%) create mode 100644 requirements.txt create mode 100644 setup.cfg diff --git a/.travis.yml b/.travis.yml index 6ede967..1937a6c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,6 +42,8 @@ matrix: env: TOXENV=isort - python: 3.6 env: TOXENV=readme + - python: 3.6 + env: TOXENV=check-manifest allow_failures: - env: TOX_ENV=py35-djdev - env: TOX_ENV=py36-djdev diff --git a/MANIFEST.in b/MANIFEST.in index 0140813..2be18b6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ -include README.md -include MANIFEST.in -include setup.py +graft django_elastipymemcache +graft tests +include README.rst AUTHORS LICENSE CHANGELOG.rst setup.py setup.cfg requirements.txt tox.ini +global-exclude __pycache__ *.pyc diff --git a/README.md b/README.rst similarity index 100% rename from README.md rename to README.rst diff --git a/django_elastipymemcache/cluster_utils.py b/django_elastipymemcache/cluster_utils.py index b5c042c..cf7c04d 100644 --- a/django_elastipymemcache/cluster_utils.py +++ b/django_elastipymemcache/cluster_utils.py @@ -1,11 +1,12 @@ """ utils for discovery cluster """ -from distutils.version import StrictVersion -from django.utils.encoding import smart_text import re +from distutils.version import StrictVersion from telnetlib import Telnet +from django.utils.encoding import smart_text + class WrongProtocolData(ValueError): """ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2520984 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +nose +coverage +flake8 +isort +readme_renderer +check-manifest diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..4c761ef --- /dev/null +++ b/setup.cfg @@ -0,0 +1,15 @@ +[wheel] +universal = 1 + +[isort] +line_length=80 +known_first_party=django_elastipymemcache +multi_line_output=3 + +[check-manifest] +ignore = + *.swp + +[coverage:run] +branch = True +omit = tests/* diff --git a/setup.py b/setup.py index 6706657..876bc43 100644 --- a/setup.py +++ b/setup.py @@ -1,29 +1,24 @@ -from setuptools import setup +#!/usr/bin/env python +# -*- encoding: utf-8 -*- -import django_elastipymemcache +import io + +from setuptools import find_packages, setup +import django_elastipymemcache setup( name='django-elastipymemcache', version=django_elastipymemcache.__version__, - url='http://github.com/uncovertruth/django-elastipymemcache', - author='UNCOVER TRUTH Inc.', - author_email='dev@uncovertruth.co.jp', description='Django cache backend for Amazon ElastiCache (memcached)', - long_description=open('README.md').read(), keywords='elasticache amazon cache pymemcache memcached aws', + author='UNCOVER TRUTH Inc.', + author_email='dev@uncovertruth.co.jp', + url='http://github.com/uncovertruth/django-elastipymemcache', license='MIT', - packages=[ - 'django_elastipymemcache', - ], - install_requires=[ - 'pymemcache', - 'Django>=1.8', - ], - extras_require={ - 'dev': ['check-manifest'], - 'test': ['nose', 'coverage', 'flake8', 'isort', 'readme_renderer'], - }, + long_description=io.open('README.rst').read(), + platforms='any', + zip_safe=False, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', @@ -43,4 +38,10 @@ 'Programming Language :: Python :: 3.6', 'Topic :: Software Development :: Libraries :: Python Modules', ], + packages=find_packages(exclude=('tests',)), + include_package_data=True, + install_requires=[ + 'pymemcache', + 'Django>=1.8', + ], ) diff --git a/tests/test_backend.py b/tests/test_backend.py index e831924..9c9374a 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -1,6 +1,8 @@ +import sys + from django.conf import global_settings, settings from nose.tools import eq_ -import sys + if sys.version < '3': from mock import patch, Mock else: diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 68dbc09..40e3b11 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -1,7 +1,11 @@ +import sys + from django_elastipymemcache.cluster_utils import ( - get_cluster_info, WrongProtocolData) + WrongProtocolData, + get_cluster_info +) from nose.tools import eq_, raises -import sys + if sys.version < '3': from mock import patch, call, MagicMock else: diff --git a/tox.ini b/tox.ini index e1691a1..8c31eef 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,7 @@ envlist = flake8, isort, readme + check-manifest [testenv] basepython = @@ -31,10 +32,15 @@ deps = flake8 [testenv:isort] basepython = python3.6 -commands = isort --check-only +commands = isort --verbose --check-only --diff deps = isort [testenv:readme] basepython = python3.6 commands = python setup.py check -r -s deps = readme_renderer + +[testenv:check-manifest] +basepython = python3.6 +commands = python setup.py check -r -s +deps = check-manifest {toxinidir} From 4d2685df20a9436580b5eb90449b37e98f5a377b Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 11 Apr 2017 19:42:23 +0900 Subject: [PATCH 22/57] Use rest format --- README.rst | 66 ++++++++++++++++++++++++++++++------------------------ setup.py | 2 +- 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/README.rst b/README.rst index 0ce71d9..cf277b6 100644 --- a/README.rst +++ b/README.rst @@ -1,46 +1,54 @@ -# django-elastipymemcache +======================= +django-elastipymemcache +======================= + +:Info: Simple Django cache backend for Amazon ElastiCache (memcached based). +:Author: UNCOVER TRUTH Inc. +:Copyright: © UNCOVER TRUTH Inc. +:Date: 2017-04-11 +:Version: 0.0.1 + +.. index: README +.. image:: https://travis-ci.org/uncovertruth/django-elastipymemcache.svg?branch=master + :target: https://travis-ci.org/uncovertruth/django-elastipymemcache + +Purpose +------- Simple Django cache backend for Amazon ElastiCache (memcached based). It uses [pymemcache](https://github.com/pinterest/pymemcache) and sets up a connection to each node in the cluster using -[auto discovery](http://docs.aws.amazon.com/AmazonElastiCache/latest/UserGuide/AutoDiscovery.html) +[auto discovery](http://docs.aws.amazon.com/AmazonElastiCache/latest/UserGuide/AutoDiscovery.html). +Originally forked from [django-elasticache](https://github.com/gusdan/django-elasticache). -## Requirements +Requirements +------------ * pymemcache -* Django >= 1.8 +* Django>=1.8 -It was written and tested on Python 2.7 and 3.5. +Installation +------------ -## Installation +Get it from [pypi](http://pypi.python.org/pypi/django-elastipymemcache):: -Get it from [pypi](http://pypi.python.org/pypi/django-elastipymemcache) + pip install django-elastipymemcache -```bash -pip install django-elastipymemcache -``` +Usage +----- -## Usage +Your cache backend should look something like this:: -Your cache backend should look something like this - -```python -CACHES = { - 'default': { - 'BACKEND': 'django_elastipymemcache.memcached.ElastiPyMemCache', - 'LOCATION': '[configuration endpoint].com:11211', + CACHES = { + 'default': { + 'BACKEND': 'django_elastipymemcache.memcached.ElastiPyMemCache', + 'LOCATION': '[configuration endpoint].com:11211', + } } -} -``` - -## Testing - -Run the tests like this -```bash -nosetests -``` +Testing +------- -## Thx +Run the tests like this:: -Originally forked from [django-elasticache](https://github.com/gusdan/django-elasticache) + nosetests diff --git a/setup.py b/setup.py index 876bc43..71b8992 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ description='Django cache backend for Amazon ElastiCache (memcached)', keywords='elasticache amazon cache pymemcache memcached aws', author='UNCOVER TRUTH Inc.', - author_email='dev@uncovertruth.co.jp', + author_email='develop@uncovertruth.co.jp', url='http://github.com/uncovertruth/django-elastipymemcache', license='MIT', long_description=io.open('README.rst').read(), From cb60e0a3c846d7e27ddba22743663dd9a2ae647c Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 11 Apr 2017 19:45:18 +0900 Subject: [PATCH 23/57] Add codecov badge --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index cf277b6..b09edaa 100644 --- a/README.rst +++ b/README.rst @@ -11,6 +11,8 @@ django-elastipymemcache .. index: README .. image:: https://travis-ci.org/uncovertruth/django-elastipymemcache.svg?branch=master :target: https://travis-ci.org/uncovertruth/django-elastipymemcache +.. image:: https://codecov.io/gh/uncovertruth/django-elastipymemcache/branch/master/graph/badge.svg + :target: https://codecov.io/gh/uncovertruth/django-elastipymemcache Purpose ------- From 156630c58aced0121dc31d6f9b7ff54f17fcbef5 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 11 Apr 2017 20:02:09 +0900 Subject: [PATCH 24/57] Enable include_trailing_comma option --- setup.cfg | 4 +++- tests/test_protocol.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4c761ef..ca864d7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,9 +2,11 @@ universal = 1 [isort] +include_trailing_comma=True line_length=80 -known_first_party=django_elastipymemcache multi_line_output=3 +not_skip=__init__.py +known_first_party=django_elastipymemcache [check-manifest] ignore = diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 40e3b11..1bcd198 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -2,7 +2,7 @@ from django_elastipymemcache.cluster_utils import ( WrongProtocolData, - get_cluster_info + get_cluster_info, ) from nose.tools import eq_, raises From 242b83cf9c828758905c34be6a79af39ce5be795 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 11 Apr 2017 20:04:03 +0900 Subject: [PATCH 25/57] Test only project files --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 8c31eef..5e47936 100644 --- a/tox.ini +++ b/tox.ini @@ -32,7 +32,7 @@ deps = flake8 [testenv:isort] basepython = python3.6 -commands = isort --verbose --check-only --diff +commands = isort --verbose --check-only --diff django_elastipymemcache tests setup.py deps = isort [testenv:readme] From f314e6c176c17c997f3c485b34bf1bc72f2ae96d Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 11 Apr 2017 20:07:25 +0900 Subject: [PATCH 26/57] Fix wrong commands --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 5e47936..725ad30 100644 --- a/tox.ini +++ b/tox.ini @@ -42,5 +42,5 @@ deps = readme_renderer [testenv:check-manifest] basepython = python3.6 -commands = python setup.py check -r -s -deps = check-manifest {toxinidir} +commands = check-manifest {toxinidir} +deps = check-manifest From 87764a31eacf7264ceddfd5780663e9a57bb0257 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 11 Apr 2017 20:20:05 +0900 Subject: [PATCH 27/57] Fix rest format --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index b09edaa..efcb33b 100644 --- a/README.rst +++ b/README.rst @@ -18,10 +18,10 @@ Purpose ------- Simple Django cache backend for Amazon ElastiCache (memcached based). It uses -[pymemcache](https://github.com/pinterest/pymemcache) and sets up a connection to each +`pymemcache `_ and sets up a connection to each node in the cluster using -[auto discovery](http://docs.aws.amazon.com/AmazonElastiCache/latest/UserGuide/AutoDiscovery.html). -Originally forked from [django-elasticache](https://github.com/gusdan/django-elasticache). +`auto discovery `_. +Originally forked from `django-elasticache `_. Requirements ------------ @@ -32,7 +32,7 @@ Requirements Installation ------------ -Get it from [pypi](http://pypi.python.org/pypi/django-elastipymemcache):: +Get it from `pypix `_:: pip install django-elastipymemcache From eb00cfeb20c497093dd899f464aaa54a5e22021b Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Wed, 12 Apr 2017 12:16:37 +0900 Subject: [PATCH 28/57] Add deployment for pypi --- .travis.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1937a6c..3ebb72e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,12 +45,20 @@ matrix: - python: 3.6 env: TOXENV=check-manifest allow_failures: - - env: TOX_ENV=py35-djdev - - env: TOX_ENV=py36-djdev - + - env: TOX_ENV=py35-djdev + - env: TOX_ENV=py36-djdev install: - pip install tox codecov script: - tox -v after_success: - codecov +deploy: + provider: pypi + user: uncovertruth + password: + secure: FtNY+8/F79RYpJzfusYBC9HFfDWlevUbS4J5TECm8tyMxAxeHMpLOaL4dd/wPkRkuctUM5LyXux5rpVQTfMA4Dvjdufc7BqexCt056GznJPFRjHW19ExeguTHXW810RIVrFmo5Dk1/AlMpGEeiL3OXoI041h7/vRZe6QWP6P0NCz9UrkZ/Qc9JDZqkXBmhiwvt4P3gu5sbxaiM4IqxhSQ/4DoI87SQlD3nUfgRT2TkqN/Jx0ME0H7l+40HeqZvVq6sh9Muc5/XhDEx5HWt9BSgymeXXG6mT3sewBfinJlQ5/1Rrm+IHa48m/3mLJkfYEs15BQCizvDc6/dRfdtCc/9jYl3e67yEM6Akn+52pTOU4Oa727ZUpAYNCzfY+pHc3E9oI0YFuOl+WYNo5bWUh3IlWAl/eXDGWOvRMF4waUbyjUMiZ+C4pRmRKznYZB9xgbPxHDlR6CMn8P5F5j3ZVbW0BxUEjlDFcDT65gflBhKS1eWTOQRMobR+yjQKlYMlIeCgITbuEYbn2GbL84aN42U910rSQ4SryzqjaOIyxtZG53cGDw2NMm4q7bIwRkj6gbvliA36HIFaRQrI/wtrmr3guy4raPRpEGi7OhFoaPxaqkEs+yF55MEafNM4eJc9swM/K/mbN6FFKIWoWVyXUCUDIWvcPYLWPR/kLidWCNzw= + on: + tags: true + distributions: sdist bdist_wheel + repo: uncovertruth/django-elastipymemcache From 3f95c3725b943c19b53ae359c095975729c191a8 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Wed, 12 Apr 2017 12:31:01 +0900 Subject: [PATCH 29/57] Add pypi badge --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index efcb33b..21393f5 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,11 @@ django-elastipymemcache .. image:: https://travis-ci.org/uncovertruth/django-elastipymemcache.svg?branch=master :target: https://travis-ci.org/uncovertruth/django-elastipymemcache .. image:: https://codecov.io/gh/uncovertruth/django-elastipymemcache/branch/master/graph/badge.svg - :target: https://codecov.io/gh/uncovertruth/django-elastipymemcache + :target: https://codecov.io/gh/uncovertruth/django-elastipymemcache +.. image:: https://requires.io/github/uncovertruth/django-elastipymemcache/requirements.svg?branch=master + :target: https://requires.io/github/uncovertruth/django-elastipymemcache/requirements/?branch=master +.. image:: https://badge.fury.io/py/django-elastipymemcache.svg + :target: https://badge.fury.io/py/django-elastipymemcache Purpose ------- From dc9a1d2b793a63a1ee109f6c23b5f70dcadc997b Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Wed, 12 Apr 2017 13:19:15 +0900 Subject: [PATCH 30/57] Deploy only specific env --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3ebb72e..206fb1f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -59,6 +59,7 @@ deploy: password: secure: FtNY+8/F79RYpJzfusYBC9HFfDWlevUbS4J5TECm8tyMxAxeHMpLOaL4dd/wPkRkuctUM5LyXux5rpVQTfMA4Dvjdufc7BqexCt056GznJPFRjHW19ExeguTHXW810RIVrFmo5Dk1/AlMpGEeiL3OXoI041h7/vRZe6QWP6P0NCz9UrkZ/Qc9JDZqkXBmhiwvt4P3gu5sbxaiM4IqxhSQ/4DoI87SQlD3nUfgRT2TkqN/Jx0ME0H7l+40HeqZvVq6sh9Muc5/XhDEx5HWt9BSgymeXXG6mT3sewBfinJlQ5/1Rrm+IHa48m/3mLJkfYEs15BQCizvDc6/dRfdtCc/9jYl3e67yEM6Akn+52pTOU4Oa727ZUpAYNCzfY+pHc3E9oI0YFuOl+WYNo5bWUh3IlWAl/eXDGWOvRMF4waUbyjUMiZ+C4pRmRKznYZB9xgbPxHDlR6CMn8P5F5j3ZVbW0BxUEjlDFcDT65gflBhKS1eWTOQRMobR+yjQKlYMlIeCgITbuEYbn2GbL84aN42U910rSQ4SryzqjaOIyxtZG53cGDw2NMm4q7bIwRkj6gbvliA36HIFaRQrI/wtrmr3guy4raPRpEGi7OhFoaPxaqkEs+yF55MEafNM4eJc9swM/K/mbN6FFKIWoWVyXUCUDIWvcPYLWPR/kLidWCNzw= on: + repo: uncovertruth/django-elastipymemcache tags: true distributions: sdist bdist_wheel - repo: uncovertruth/django-elastipymemcache + condition: $TOX_ENV = py36-dj111 From c68373a81ba5d71f21b1cd4e40289ad4f0e71a02 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Wed, 12 Apr 2017 13:50:07 +0900 Subject: [PATCH 31/57] Fix wrong env name --- .travis.yml | 40 ++++++++++++++++++++-------------------- requirements.txt | 6 ++++-- tox.ini | 5 ++--- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/.travis.yml b/.travis.yml index 206fb1f..1612c83 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,45 +5,45 @@ matrix: fast_finish: true include: - python: 2.7 - env: TOXENV=py27-dj18 + env: TOX_ENV=py27-dj18 - python: 3.3 - env: TOXENV=py33-dj18 + env: TOX_ENV=py33-dj18 - python: 3.4 - env: TOXENV=py34-dj18 + env: TOX_ENV=py34-dj18 - python: 3.5 - env: TOXENV=py34-dj18 + env: TOX_ENV=py34-dj18 - python: 2.7 - env: TOXENV=py27-dj19 + env: TOX_ENV=py27-dj19 - python: 3.4 - env: TOXENV=py34-dj19 + env: TOX_ENV=py34-dj19 - python: 3.5 - env: TOXENV=py35-dj19 + env: TOX_ENV=py35-dj19 - python: 2.7 - env: TOXENV=py27-dj110 + env: TOX_ENV=py27-dj110 - python: 3.4 - env: TOXENV=py34-dj110 + env: TOX_ENV=py34-dj110 - python: 3.5 - env: TOXENV=py35-dj110 + env: TOX_ENV=py35-dj110 - python: 2.7 - env: TOXENV=py27-dj111 + env: TOX_ENV=py27-dj111 - python: 3.4 - env: TOXENV=py34-dj111 + env: TOX_ENV=py34-dj111 - python: 3.5 - env: TOXENV=py35-dj111 + env: TOX_ENV=py35-dj111 - python: 3.6 - env: TOXENV=py36-dj111 + env: TOX_ENV=py36-dj111 - python: 3.5 - env: TOXENV=py35-djdev + env: TOX_ENV=py35-djdev - python: 3.6 - env: TOXENV=py36-djdev + env: TOX_ENV=py36-djdev - python: 3.6 - env: TOXENV=flake8 + env: TOX_ENV=flake8 - python: 3.6 - env: TOXENV=isort + env: TOX_ENV=isort - python: 3.6 - env: TOXENV=readme + env: TOX_ENV=readme - python: 3.6 - env: TOXENV=check-manifest + env: TOX_ENV=check-manifest allow_failures: - env: TOX_ENV=py35-djdev - env: TOX_ENV=py36-djdev diff --git a/requirements.txt b/requirements.txt index 2520984..4b67c46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ -nose +check-manifest coverage flake8 isort +mock +nose +pymemcache readme_renderer -check-manifest diff --git a/tox.ini b/tox.ini index 725ad30..41e2fa2 100644 --- a/tox.ini +++ b/tox.ini @@ -19,11 +19,10 @@ deps = dj110: Django>=1.10,<1.11 dj111: Django>=1.11,<2.0 djdev: https://github.com/django/django/archive/master.tar.gz - nose - coverage + -r requirements.txt setenv = PYTHONPATH = {toxinidir} -commands = coverage run --source=django_elastipymemcache -a setup.py test +commands = coverage run --source=django_elastipymemcache -m nose [testenv:flake8] basepython = python3.6 From 4f04a7f76fb74af312dae20731581f05411698ee Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Wed, 12 Apr 2017 14:02:23 +0900 Subject: [PATCH 32/57] Fix wrong requirements file path --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 41e2fa2..fa25889 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,7 @@ deps = dj110: Django>=1.10,<1.11 dj111: Django>=1.11,<2.0 djdev: https://github.com/django/django/archive/master.tar.gz - -r requirements.txt + -r{toxinidir}/requirements.txt setenv = PYTHONPATH = {toxinidir} commands = coverage run --source=django_elastipymemcache -m nose From 651d1bddeae0ce435236fe9a858498b34e3a86bd Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Wed, 12 Apr 2017 14:14:35 +0900 Subject: [PATCH 33/57] Run only specific env --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1612c83..96a20a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,7 +50,7 @@ matrix: install: - pip install tox codecov script: -- tox -v +- tox -e "$TOX_ENV" after_success: - codecov deploy: From 7cb7c50c87412fe01d98fc404d434821adf68c3e Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Wed, 12 Apr 2017 14:20:33 +0900 Subject: [PATCH 34/57] Fix wrong type --- tests/test_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 1bcd198..15ed724 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -113,7 +113,7 @@ def test_no_configuration_protocol_support_with_errors_ignored(Telnet): call(b'version\n'), call(b'config get cluster\n'), ]) - eq_(info['version'], '1.4.34') + eq_(info['version'], b'1.4.34') eq_(info['nodes'], [('test', 0)]) From a7cd6209bd1b975fcc26bba337b7d14c7a4749d3 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Wed, 12 Apr 2017 14:26:40 +0900 Subject: [PATCH 35/57] Apply patch for django<1.11 --- django_elastipymemcache/memcached.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/django_elastipymemcache/memcached.py b/django_elastipymemcache/memcached.py index 114361d..1fb5b5f 100644 --- a/django_elastipymemcache/memcached.py +++ b/django_elastipymemcache/memcached.py @@ -44,6 +44,8 @@ def __init__(self, server, params): raise InvalidCacheBackendError( 'Server configuration should be in format IP:port') + # Patch for django<1.11 + self._options = self._options or dict() self._ignore_cluster_errors = self._options.get( 'ignore_exc', False) From feda4556271d23f7c8c117b9e47288dff0595640 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Wed, 12 Apr 2017 14:30:06 +0900 Subject: [PATCH 36/57] Codecov in specific version --- .travis.yml | 4 +--- tox.ini | 41 +++++++++++++++++++++++------------------ 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index 96a20a9..9e1b21d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,11 +48,9 @@ matrix: - env: TOX_ENV=py35-djdev - env: TOX_ENV=py36-djdev install: -- pip install tox codecov +- pip install tox script: - tox -e "$TOX_ENV" -after_success: -- codecov deploy: provider: pypi user: uncovertruth diff --git a/tox.ini b/tox.ini index fa25889..56f01c2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,28 +1,33 @@ [tox] envlist = - py{27,33,34,35,36}-dj{18,19,110,111,dev} - flake8, - isort, - readme - check-manifest + py{27,33,34,35,36}-dj{18,19,110,111,dev} + flake8, + isort, + readme + check-manifest [testenv] basepython = - py27: python2.7 - py33: python3.3 - py34: python3.4 - py35: python3.5 - py36: python3.6 + py27: python2.7 + py33: python3.3 + py34: python3.4 + py35: python3.5 + py36: python3.6 deps = - dj18: Django>=1.8,<1.9 - dj19: Django>=1.9,<1.10 - dj110: Django>=1.10,<1.11 - dj111: Django>=1.11,<2.0 - djdev: https://github.com/django/django/archive/master.tar.gz - -r{toxinidir}/requirements.txt + dj18: Django>=1.8,<1.9 + dj19: Django>=1.9,<1.10 + dj110: Django>=1.10,<1.11 + dj111: Django>=1.11,<2.0 + djdev: https://github.com/django/django/archive/master.tar.gz + -r{toxinidir}/requirements.txt + py36-dj111: codecov setenv = - PYTHONPATH = {toxinidir} -commands = coverage run --source=django_elastipymemcache -m nose + PYTHONPATH = {toxinidir} +commands = + coverage run --source=django_elastipymemcache -m nose + py36-dj111: coverage report + py36-dj111: coverage xml + py36-dj111: codecov [testenv:flake8] basepython = python3.6 From f01f28bdc25db19f50a5d9c5355b948bed5bf817 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Wed, 12 Apr 2017 14:47:41 +0900 Subject: [PATCH 37/57] Fix typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 21393f5..2efb3c6 100644 --- a/README.rst +++ b/README.rst @@ -36,7 +36,7 @@ Requirements Installation ------------ -Get it from `pypix `_:: +Get it from `pypi `_:: pip install django-elastipymemcache From 6fb2ba4747a8a9c88955b6b6fb9a13274f9546fc Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Wed, 12 Apr 2017 14:51:59 +0900 Subject: [PATCH 38/57] Skip upload docs --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9e1b21d..df3ce3f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -56,8 +56,9 @@ deploy: user: uncovertruth password: secure: FtNY+8/F79RYpJzfusYBC9HFfDWlevUbS4J5TECm8tyMxAxeHMpLOaL4dd/wPkRkuctUM5LyXux5rpVQTfMA4Dvjdufc7BqexCt056GznJPFRjHW19ExeguTHXW810RIVrFmo5Dk1/AlMpGEeiL3OXoI041h7/vRZe6QWP6P0NCz9UrkZ/Qc9JDZqkXBmhiwvt4P3gu5sbxaiM4IqxhSQ/4DoI87SQlD3nUfgRT2TkqN/Jx0ME0H7l+40HeqZvVq6sh9Muc5/XhDEx5HWt9BSgymeXXG6mT3sewBfinJlQ5/1Rrm+IHa48m/3mLJkfYEs15BQCizvDc6/dRfdtCc/9jYl3e67yEM6Akn+52pTOU4Oa727ZUpAYNCzfY+pHc3E9oI0YFuOl+WYNo5bWUh3IlWAl/eXDGWOvRMF4waUbyjUMiZ+C4pRmRKznYZB9xgbPxHDlR6CMn8P5F5j3ZVbW0BxUEjlDFcDT65gflBhKS1eWTOQRMobR+yjQKlYMlIeCgITbuEYbn2GbL84aN42U910rSQ4SryzqjaOIyxtZG53cGDw2NMm4q7bIwRkj6gbvliA36HIFaRQrI/wtrmr3guy4raPRpEGi7OhFoaPxaqkEs+yF55MEafNM4eJc9swM/K/mbN6FFKIWoWVyXUCUDIWvcPYLWPR/kLidWCNzw= + distributions: sdist bdist_wheel + skip_upload_docs: true on: repo: uncovertruth/django-elastipymemcache tags: true - distributions: sdist bdist_wheel condition: $TOX_ENV = py36-dj111 From 1e191683a0b0e023edabe5ad71a9b41789d5c76c Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Wed, 12 Apr 2017 14:59:23 +0900 Subject: [PATCH 39/57] Add tox env for pypy pypy3 --- .travis.yml | 4 ++++ tox.ini | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index df3ce3f..ffa4d1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,12 +26,16 @@ matrix: env: TOX_ENV=py35-dj110 - python: 2.7 env: TOX_ENV=py27-dj111 + - python: pypy + env: TOX_ENV=pypy-dj111 - python: 3.4 env: TOX_ENV=py34-dj111 - python: 3.5 env: TOX_ENV=py35-dj111 - python: 3.6 env: TOX_ENV=py36-dj111 + - python: pypy3 + env: TOX_ENV=pypy3-dj111 - python: 3.5 env: TOX_ENV=py35-djdev - python: 3.6 diff --git a/tox.ini b/tox.ini index 56f01c2..7f6ced2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{27,33,34,35,36}-dj{18,19,110,111,dev} + py{py3,py2,27,33,34,35,36}-dj{18,19,110,111,dev} flake8, isort, readme @@ -13,6 +13,8 @@ basepython = py34: python3.4 py35: python3.5 py36: python3.6 + pypy2: pypy2 + pypy3: pypy3 deps = dj18: Django>=1.8,<1.9 dj19: Django>=1.9,<1.10 From 4976e945b42f79159b2f9c99907cb571b72f8a06 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Wed, 12 Apr 2017 14:59:34 +0900 Subject: [PATCH 40/57] Bump version --- README.rst | 2 +- django_elastipymemcache/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 2efb3c6..cebf5b5 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,7 @@ django-elastipymemcache :Author: UNCOVER TRUTH Inc. :Copyright: © UNCOVER TRUTH Inc. :Date: 2017-04-11 -:Version: 0.0.1 +:Version: 0.0.2 .. index: README .. image:: https://travis-ci.org/uncovertruth/django-elastipymemcache.svg?branch=master diff --git a/django_elastipymemcache/__init__.py b/django_elastipymemcache/__init__.py index 9603d9e..81f39e9 100644 --- a/django_elastipymemcache/__init__.py +++ b/django_elastipymemcache/__init__.py @@ -1,2 +1,2 @@ -VERSION = (0, 0, 1) +VERSION = (0, 0, 2) __version__ = '.'.join(map(str, VERSION)) From 76e71d11ec1fbd52eadce82ba5d411b690cab096 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Wed, 12 Apr 2017 15:04:36 +0900 Subject: [PATCH 41/57] Set codecov token --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 7f6ced2..5f2f373 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,7 @@ deps = py36-dj111: codecov setenv = PYTHONPATH = {toxinidir} + CODECOV_TOKEN="8ea69bfe-d9b5-45fc-97f7-ef843bd3d9d2" commands = coverage run --source=django_elastipymemcache -m nose py36-dj111: coverage report From 42e0eccc06436de4c257ea7b974f05f39349384d Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Wed, 12 Apr 2017 15:07:30 +0900 Subject: [PATCH 42/57] Allow failure pypy3 --- .travis.yml | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ffa4d1c..4edb0df 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,6 +51,7 @@ matrix: allow_failures: - env: TOX_ENV=py35-djdev - env: TOX_ENV=py36-djdev + - env: TOX_ENV=pypy3-dj111 install: - pip install tox script: diff --git a/tox.ini b/tox.ini index 5f2f373..b13c468 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ basepython = py34: python3.4 py35: python3.5 py36: python3.6 - pypy2: pypy2 + pypy2: pypy pypy3: pypy3 deps = dj18: Django>=1.8,<1.9 From 2dbb39f9b8ab0e3003d13b4c026fc3fb150827eb Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Wed, 12 Apr 2017 15:14:52 +0900 Subject: [PATCH 43/57] Fix worng env --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index b13c468..72aeba4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{py3,py2,27,33,34,35,36}-dj{18,19,110,111,dev} + py{py,py3,27,33,34,35,36}-dj{18,19,110,111,dev} flake8, isort, readme @@ -13,7 +13,7 @@ basepython = py34: python3.4 py35: python3.5 py36: python3.6 - pypy2: pypy + pypy: pypy pypy3: pypy3 deps = dj18: Django>=1.8,<1.9 From 562fbfca6230289b1aac6bf4a5e341d60a6ee6ed Mon Sep 17 00:00:00 2001 From: opapy Date: Wed, 12 Apr 2017 17:47:24 +0900 Subject: [PATCH 44/57] ignore error when get cluster info --- django_elastipymemcache/cluster_utils.py | 10 +--------- django_elastipymemcache/memcached.py | 20 ++++++++++++-------- tests/test_backend.py | 4 ++-- tests/test_protocol.py | 16 +--------------- 4 files changed, 16 insertions(+), 34 deletions(-) diff --git a/django_elastipymemcache/cluster_utils.py b/django_elastipymemcache/cluster_utils.py index cf7c04d..26c623a 100644 --- a/django_elastipymemcache/cluster_utils.py +++ b/django_elastipymemcache/cluster_utils.py @@ -18,7 +18,7 @@ def __init__(self, cmd, response): 'Unexpected response {} for command {}'.format(response, cmd)) -def get_cluster_info(host, port, ignore_cluster_errors=False): +def get_cluster_info(host, port): """ return dict with info about nodes in cluster and current version { @@ -47,14 +47,6 @@ def get_cluster_info(host, port, ignore_cluster_errors=False): ]) client.close() - if res == b'ERROR\r\n' and ignore_cluster_errors: - return { - 'version': version, - 'nodes': [ - (smart_text(host), int(port)) - ] - } - ls = list(filter(None, re.compile(br'\r?\n').split(res))) if len(ls) != 4: raise WrongProtocolData(cmd, res) diff --git a/django_elastipymemcache/memcached.py b/django_elastipymemcache/memcached.py index 1fb5b5f..8556bb9 100644 --- a/django_elastipymemcache/memcached.py +++ b/django_elastipymemcache/memcached.py @@ -1,6 +1,7 @@ """ Backend for django cache """ +import logging import socket from functools import wraps @@ -11,6 +12,9 @@ from .cluster_utils import get_cluster_info +logger = logging.getLogger(__name__) + + def invalidate_cache_after_error(f): """ catch any exception and invalidate internal cache with list of nodes @@ -46,8 +50,6 @@ def __init__(self, server, params): # Patch for django<1.11 self._options = self._options or dict() - self._ignore_cluster_errors = self._options.get( - 'ignore_exc', False) def clear_cluster_nodes_cache(self): """clear internal cache with list of nodes in cluster""" @@ -62,13 +64,15 @@ def get_cluster_nodes(self): try: return get_cluster_info( server, - port, - self._ignore_cluster_errors + port )['nodes'] - except (socket.gaierror, socket.timeout) as err: - raise Exception('Cannot connect to cluster {} ({})'.format( - self._servers[0], err - )) + except (OSError, socket.gaierror, socket.timeout) as err: + logger.debug( + 'Cannot connect to cluster %s, err: %s', + self._servers[0], + err + ) + return [(server, int(port))] @property def _cache(self): diff --git a/tests/test_backend.py b/tests/test_backend.py index 9c9374a..c7c2a4c 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -25,7 +25,7 @@ def test_split_servers(get_cluster_info): } backend._lib.Client = Mock() assert backend._cache - get_cluster_info.assert_called_once_with('h', '0', False) + get_cluster_info.assert_called_once_with('h', '0') backend._lib.Client.assert_called_once_with(servers) @@ -48,7 +48,7 @@ def test_node_info_cache(get_cluster_info): eq_(backend._cache.get.call_count, 2) eq_(backend._cache.set.call_count, 2) - get_cluster_info.assert_called_once_with('h', '0', False) + get_cluster_info.assert_called_once_with('h', '0') @patch('django.conf.settings', global_settings) diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 15ed724..a536238 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -103,27 +103,13 @@ def test_ubuntu_protocol(Telnet): ]) -@patch('django_elastipymemcache.cluster_utils.Telnet') -def test_no_configuration_protocol_support_with_errors_ignored(Telnet): - client = Telnet.return_value - client.read_until.side_effect = TEST_PROTOCOL_4_READ_UNTIL - client.expect.side_effect = TEST_PROTOCOL_4_EXPECT - info = get_cluster_info('test', 0, ignore_cluster_errors=True) - client.write.assert_has_calls([ - call(b'version\n'), - call(b'config get cluster\n'), - ]) - eq_(info['version'], b'1.4.34') - eq_(info['nodes'], [('test', 0)]) - - @raises(WrongProtocolData) @patch('django_elastipymemcache.cluster_utils.Telnet') def test_no_configuration_protocol_support_with_errors(Telnet): client = Telnet.return_value client.read_until.side_effect = TEST_PROTOCOL_4_READ_UNTIL client.expect.side_effect = TEST_PROTOCOL_4_EXPECT - get_cluster_info('test', 0, ignore_cluster_errors=False) + get_cluster_info('test', 0) client.write.assert_has_calls([ call(b'version\n'), call(b'config get cluster\n'), From 7db72bd0755dbeaffb3f5a6ef9219b86365f0615 Mon Sep 17 00:00:00 2001 From: opapy Date: Wed, 12 Apr 2017 18:24:13 +0900 Subject: [PATCH 45/57] set empty server when get cluster info --- django_elastipymemcache/memcached.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_elastipymemcache/memcached.py b/django_elastipymemcache/memcached.py index 8556bb9..2913d53 100644 --- a/django_elastipymemcache/memcached.py +++ b/django_elastipymemcache/memcached.py @@ -72,7 +72,7 @@ def get_cluster_nodes(self): self._servers[0], err ) - return [(server, int(port))] + return [] @property def _cache(self): From e09547697075e18556284da01cb45aca1ec0dc4a Mon Sep 17 00:00:00 2001 From: opapy Date: Wed, 12 Apr 2017 18:39:27 +0900 Subject: [PATCH 46/57] add timeout params --- django_elastipymemcache/cluster_utils.py | 4 ++-- django_elastipymemcache/memcached.py | 13 +++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/django_elastipymemcache/cluster_utils.py b/django_elastipymemcache/cluster_utils.py index 26c623a..cd3b7e2 100644 --- a/django_elastipymemcache/cluster_utils.py +++ b/django_elastipymemcache/cluster_utils.py @@ -18,7 +18,7 @@ def __init__(self, cmd, response): 'Unexpected response {} for command {}'.format(response, cmd)) -def get_cluster_info(host, port): +def get_cluster_info(host, port, timeout=None): """ return dict with info about nodes in cluster and current version { @@ -29,7 +29,7 @@ def get_cluster_info(host, port): 'version': '1.4.4' } """ - client = Telnet(host, int(port)) + client = Telnet(host, int(port), timeout=None) client.write(b'version\n') res = client.read_until(b'\r\n').strip() version_list = res.split(b' ') diff --git a/django_elastipymemcache/memcached.py b/django_elastipymemcache/memcached.py index 2913d53..e1d6ab8 100644 --- a/django_elastipymemcache/memcached.py +++ b/django_elastipymemcache/memcached.py @@ -50,6 +50,7 @@ def __init__(self, server, params): # Patch for django<1.11 self._options = self._options or dict() + self._cluster_timeout = self._options.get('cluster_timeout') def clear_cluster_nodes_cache(self): """clear internal cache with list of nodes in cluster""" @@ -64,7 +65,8 @@ def get_cluster_nodes(self): try: return get_cluster_info( server, - port + port, + self._cluster_timeout )['nodes'] except (OSError, socket.gaierror, socket.timeout) as err: logger.debug( @@ -76,9 +78,16 @@ def get_cluster_nodes(self): @property def _cache(self): + if getattr(self, '_client', None) is None: + + options = self._options + options.setdefault('ignore_exc', True) + options.pop('cluster_timeout', None) + self._client = self._lib.Client( - self.get_cluster_nodes(), **self._options) + self.get_cluster_nodes(), **options) + return self._client @invalidate_cache_after_error From 33d8dbe49568c937d7258c1b675607d2e3c435b1 Mon Sep 17 00:00:00 2001 From: opapy Date: Wed, 12 Apr 2017 18:51:36 +0900 Subject: [PATCH 47/57] edit README --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index cebf5b5..abe4c8f 100644 --- a/README.rst +++ b/README.rst @@ -48,7 +48,11 @@ Your cache backend should look something like this:: CACHES = { 'default': { 'BACKEND': 'django_elastipymemcache.memcached.ElastiPyMemCache', - 'LOCATION': '[configuration endpoint].com:11211', + 'LOCATION': '[configuration endpoint]:11211', + 'OPTIONS': { + 'cluster_timeout': 1, # its used when get cluster info + 'ignore_exc': True, # pymemcache Client params + } } } From 0d8975230b6536afb791ca138a2e9f0409685c39 Mon Sep 17 00:00:00 2001 From: opapy Date: Thu, 13 Apr 2017 13:53:35 +0900 Subject: [PATCH 48/57] fix test --- tests/test_backend.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_backend.py b/tests/test_backend.py index c7c2a4c..fd0f590 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -25,8 +25,8 @@ def test_split_servers(get_cluster_info): } backend._lib.Client = Mock() assert backend._cache - get_cluster_info.assert_called_once_with('h', '0') - backend._lib.Client.assert_called_once_with(servers) + get_cluster_info.assert_called_once_with('h', '0', None) + backend._lib.Client.assert_called_once_with(servers, ignore_exc=True) @patch('django.conf.settings', global_settings) @@ -44,11 +44,11 @@ def test_node_info_cache(get_cluster_info): backend.get('key1') backend.set('key2', 'val') backend.get('key2') - backend._lib.Client.assert_called_once_with(servers) + backend._lib.Client.assert_called_once_with(servers, ignore_exc=True) eq_(backend._cache.get.call_count, 2) eq_(backend._cache.set.call_count, 2) - get_cluster_info.assert_called_once_with('h', '0') + get_cluster_info.assert_called_once_with('h', '0', None) @patch('django.conf.settings', global_settings) From 12b2276d3ab0e19f9165004bf2b65f0a5b612075 Mon Sep 17 00:00:00 2001 From: opapy Date: Thu, 13 Apr 2017 15:22:09 +0900 Subject: [PATCH 49/57] set socket._GLOBAL_DEFAULT_TIMEOUT as default value for telnet --- django_elastipymemcache/cluster_utils.py | 5 +++-- django_elastipymemcache/memcached.py | 3 ++- tests/test_backend.py | 5 +++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/django_elastipymemcache/cluster_utils.py b/django_elastipymemcache/cluster_utils.py index cd3b7e2..891e051 100644 --- a/django_elastipymemcache/cluster_utils.py +++ b/django_elastipymemcache/cluster_utils.py @@ -3,6 +3,7 @@ """ import re from distutils.version import StrictVersion +import socket from telnetlib import Telnet from django.utils.encoding import smart_text @@ -18,7 +19,7 @@ def __init__(self, cmd, response): 'Unexpected response {} for command {}'.format(response, cmd)) -def get_cluster_info(host, port, timeout=None): +def get_cluster_info(host, port, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): """ return dict with info about nodes in cluster and current version { @@ -29,7 +30,7 @@ def get_cluster_info(host, port, timeout=None): 'version': '1.4.4' } """ - client = Telnet(host, int(port), timeout=None) + client = Telnet(host, int(port), timeout=timeout) client.write(b'version\n') res = client.read_until(b'\r\n').strip() version_list = res.split(b' ') diff --git a/django_elastipymemcache/memcached.py b/django_elastipymemcache/memcached.py index e1d6ab8..e100c25 100644 --- a/django_elastipymemcache/memcached.py +++ b/django_elastipymemcache/memcached.py @@ -50,7 +50,8 @@ def __init__(self, server, params): # Patch for django<1.11 self._options = self._options or dict() - self._cluster_timeout = self._options.get('cluster_timeout') + self._cluster_timeout = self._options.get( + 'cluster_timeout', socket._GLOBAL_DEFAULT_TIMEOUT) def clear_cluster_nodes_cache(self): """clear internal cache with list of nodes in cluster""" diff --git a/tests/test_backend.py b/tests/test_backend.py index fd0f590..e646ac6 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -1,3 +1,4 @@ +import socket import sys from django.conf import global_settings, settings @@ -25,7 +26,7 @@ def test_split_servers(get_cluster_info): } backend._lib.Client = Mock() assert backend._cache - get_cluster_info.assert_called_once_with('h', '0', None) + get_cluster_info.assert_called_once_with('h', '0', socket._GLOBAL_DEFAULT_TIMEOUT) backend._lib.Client.assert_called_once_with(servers, ignore_exc=True) @@ -48,7 +49,7 @@ def test_node_info_cache(get_cluster_info): eq_(backend._cache.get.call_count, 2) eq_(backend._cache.set.call_count, 2) - get_cluster_info.assert_called_once_with('h', '0', None) + get_cluster_info.assert_called_once_with('h', '0', socket._GLOBAL_DEFAULT_TIMEOUT) @patch('django.conf.settings', global_settings) From f4e8c7c89f9ac88d2db9ebd4c38ace391a18ae5c Mon Sep 17 00:00:00 2001 From: opapy Date: Thu, 13 Apr 2017 15:29:14 +0900 Subject: [PATCH 50/57] fix pep8 --- tests/test_backend.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_backend.py b/tests/test_backend.py index e646ac6..be93fbf 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -26,7 +26,8 @@ def test_split_servers(get_cluster_info): } backend._lib.Client = Mock() assert backend._cache - get_cluster_info.assert_called_once_with('h', '0', socket._GLOBAL_DEFAULT_TIMEOUT) + get_cluster_info.assert_called_once_with( + 'h', '0', socket._GLOBAL_DEFAULT_TIMEOUT) backend._lib.Client.assert_called_once_with(servers, ignore_exc=True) @@ -49,7 +50,8 @@ def test_node_info_cache(get_cluster_info): eq_(backend._cache.get.call_count, 2) eq_(backend._cache.set.call_count, 2) - get_cluster_info.assert_called_once_with('h', '0', socket._GLOBAL_DEFAULT_TIMEOUT) + get_cluster_info.assert_called_once_with( + 'h', '0', socket._GLOBAL_DEFAULT_TIMEOUT) @patch('django.conf.settings', global_settings) From 2e397b9beb61fac9f1919e6ea0ad57d655c6e563 Mon Sep 17 00:00:00 2001 From: opapy Date: Thu, 13 Apr 2017 18:32:26 +0900 Subject: [PATCH 51/57] bump version --- README.rst | 2 +- django_elastipymemcache/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index abe4c8f..98f45e2 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,7 @@ django-elastipymemcache :Author: UNCOVER TRUTH Inc. :Copyright: © UNCOVER TRUTH Inc. :Date: 2017-04-11 -:Version: 0.0.2 +:Version: 0.0.3 .. index: README .. image:: https://travis-ci.org/uncovertruth/django-elastipymemcache.svg?branch=master diff --git a/django_elastipymemcache/__init__.py b/django_elastipymemcache/__init__.py index 81f39e9..ad865ad 100644 --- a/django_elastipymemcache/__init__.py +++ b/django_elastipymemcache/__init__.py @@ -1,2 +1,2 @@ -VERSION = (0, 0, 2) +VERSION = (0, 0, 3) __version__ = '.'.join(map(str, VERSION)) From 87ee7c4c90ec6170db02c01b0a8ae8b6980aaf10 Mon Sep 17 00:00:00 2001 From: opapy Date: Wed, 10 May 2017 16:32:51 +0900 Subject: [PATCH 52/57] add close method --- django_elastipymemcache/memcached.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/django_elastipymemcache/memcached.py b/django_elastipymemcache/memcached.py index e100c25..84a1d81 100644 --- a/django_elastipymemcache/memcached.py +++ b/django_elastipymemcache/memcached.py @@ -91,6 +91,11 @@ def _cache(self): return self._client + def close(self, **kwargs): + # libmemcached manages its own connections. Don't call disconnect_all() + # as it resets the failover state and creates unnecessary reconnects. + pass + @invalidate_cache_after_error def get(self, *args, **kwargs): return super(ElastiPyMemCache, self).get(*args, **kwargs) From 2ca478aab8832a9a6fef16d3322fa07505f802a7 Mon Sep 17 00:00:00 2001 From: opapy Date: Wed, 10 May 2017 18:40:54 +0900 Subject: [PATCH 53/57] replace serializer --- django_elastipymemcache/memcached.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/django_elastipymemcache/memcached.py b/django_elastipymemcache/memcached.py index 84a1d81..0abf2cd 100644 --- a/django_elastipymemcache/memcached.py +++ b/django_elastipymemcache/memcached.py @@ -5,6 +5,11 @@ import socket from functools import wraps +try: + import cPickle as pickle +except ImportError: + import pickle + from django.core.cache import InvalidCacheBackendError from django.core.cache.backends.memcached import BaseMemcachedCache @@ -15,6 +20,19 @@ logger = logging.getLogger(__name__) +def serialize_pickle(key, value): + if isinstance(value, str): + return value, 1 + return pickle.dumps(value), 2 + + +def deserialize_pickle(key, value, flags): + if flags == 1: + return value + if flags == 2: + return pickle.loads(value) + + def invalidate_cache_after_error(f): """ catch any exception and invalidate internal cache with list of nodes @@ -83,6 +101,8 @@ def _cache(self): if getattr(self, '_client', None) is None: options = self._options + options['serializer'] = serialize_pickle + options['deserializer'] = deserialize_pickle options.setdefault('ignore_exc', True) options.pop('cluster_timeout', None) From 08e38f6c214728d7cf38cb4fa668302ffd31161e Mon Sep 17 00:00:00 2001 From: opapy Date: Wed, 10 May 2017 19:56:07 +0900 Subject: [PATCH 54/57] fix test --- tests/test_backend.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/test_backend.py b/tests/test_backend.py index be93fbf..2005d0c 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -18,7 +18,10 @@ @patch('django.conf.settings', global_settings) @patch('django_elastipymemcache.memcached.get_cluster_info') def test_split_servers(get_cluster_info): - from django_elastipymemcache.memcached import ElastiPyMemCache + from django_elastipymemcache.memcached import ( + ElastiPyMemCache, + deserialize_pickle, + ) backend = ElastiPyMemCache('h:0', {}) servers = [('h1', 0), ('h2', 0)] get_cluster_info.return_value = { @@ -28,13 +31,20 @@ def test_split_servers(get_cluster_info): assert backend._cache get_cluster_info.assert_called_once_with( 'h', '0', socket._GLOBAL_DEFAULT_TIMEOUT) - backend._lib.Client.assert_called_once_with(servers, ignore_exc=True) + backend._lib.Client.assert_called_once_with( + servers, + deserializer=deserialize_pickle, + ignore_exc=True + ) @patch('django.conf.settings', global_settings) @patch('django_elastipymemcache.memcached.get_cluster_info') def test_node_info_cache(get_cluster_info): - from django_elastipymemcache.memcached import ElastiPyMemCache + from django_elastipymemcache.memcached import ( + ElastiPyMemCache, + deserialize_pickle, + ) servers = [('h1', 0), ('h2', 0)] get_cluster_info.return_value = { 'nodes': servers @@ -46,7 +56,11 @@ def test_node_info_cache(get_cluster_info): backend.get('key1') backend.set('key2', 'val') backend.get('key2') - backend._lib.Client.assert_called_once_with(servers, ignore_exc=True) + backend._lib.Client.assert_called_once_with( + servers, + deserializer=deserialize_pickle, + ignore_exc=True + ) eq_(backend._cache.get.call_count, 2) eq_(backend._cache.set.call_count, 2) From 45158a1f3c002b638b61791b1e2a62ea185a364a Mon Sep 17 00:00:00 2001 From: opapy Date: Wed, 10 May 2017 19:59:11 +0900 Subject: [PATCH 55/57] fix test --- tests/test_backend.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_backend.py b/tests/test_backend.py index 2005d0c..d4a68bf 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -21,6 +21,7 @@ def test_split_servers(get_cluster_info): from django_elastipymemcache.memcached import ( ElastiPyMemCache, deserialize_pickle, + serialize_pickle, ) backend = ElastiPyMemCache('h:0', {}) servers = [('h1', 0), ('h2', 0)] @@ -34,7 +35,8 @@ def test_split_servers(get_cluster_info): backend._lib.Client.assert_called_once_with( servers, deserializer=deserialize_pickle, - ignore_exc=True + ignore_exc=True, + serializer=serialize_pickle ) @@ -44,6 +46,7 @@ def test_node_info_cache(get_cluster_info): from django_elastipymemcache.memcached import ( ElastiPyMemCache, deserialize_pickle, + serialize_pickle, ) servers = [('h1', 0), ('h2', 0)] get_cluster_info.return_value = { @@ -59,7 +62,8 @@ def test_node_info_cache(get_cluster_info): backend._lib.Client.assert_called_once_with( servers, deserializer=deserialize_pickle, - ignore_exc=True + ignore_exc=True, + serializer=serialize_pickle ) eq_(backend._cache.get.call_count, 2) eq_(backend._cache.set.call_count, 2) From e986e4a79139320fcbb0febb1ce901b50324a840 Mon Sep 17 00:00:00 2001 From: opapy Date: Thu, 11 May 2017 12:13:47 +0900 Subject: [PATCH 56/57] bump version --- README.rst | 2 +- django_elastipymemcache/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 98f45e2..42b3191 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,7 @@ django-elastipymemcache :Author: UNCOVER TRUTH Inc. :Copyright: © UNCOVER TRUTH Inc. :Date: 2017-04-11 -:Version: 0.0.3 +:Version: 0.0.5 .. index: README .. image:: https://travis-ci.org/uncovertruth/django-elastipymemcache.svg?branch=master diff --git a/django_elastipymemcache/__init__.py b/django_elastipymemcache/__init__.py index ad865ad..d9e9c97 100644 --- a/django_elastipymemcache/__init__.py +++ b/django_elastipymemcache/__init__.py @@ -1,2 +1,2 @@ -VERSION = (0, 0, 3) +VERSION = (0, 0, 5) __version__ = '.'.join(map(str, VERSION)) From 7fee4da5077c1d151fbb73c378d0767c6044342e Mon Sep 17 00:00:00 2001 From: opapy Date: Mon, 15 May 2017 14:36:03 +0900 Subject: [PATCH 57/57] add ignore_cluster_errors params --- README.rst | 1 + django_elastipymemcache/cluster_utils.py | 10 +++++++++- django_elastipymemcache/memcached.py | 4 ++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 42b3191..7b462d3 100644 --- a/README.rst +++ b/README.rst @@ -52,6 +52,7 @@ Your cache backend should look something like this:: 'OPTIONS': { 'cluster_timeout': 1, # its used when get cluster info 'ignore_exc': True, # pymemcache Client params + 'ignore_cluster_errors': True, # ignore get cluster info error } } } diff --git a/django_elastipymemcache/cluster_utils.py b/django_elastipymemcache/cluster_utils.py index 891e051..d3d39eb 100644 --- a/django_elastipymemcache/cluster_utils.py +++ b/django_elastipymemcache/cluster_utils.py @@ -19,7 +19,7 @@ def __init__(self, cmd, response): 'Unexpected response {} for command {}'.format(response, cmd)) -def get_cluster_info(host, port, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): +def get_cluster_info(host, port, ignore_cluster_errors=False, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): """ return dict with info about nodes in cluster and current version { @@ -48,6 +48,14 @@ def get_cluster_info(host, port, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): ]) client.close() + if res == b'ERROR\r\n' and ignore_cluster_errors: + return { + 'version': version, + 'nodes': [ + (smart_text(host), int(port)) + ] + } + ls = list(filter(None, re.compile(br'\r?\n').split(res))) if len(ls) != 4: raise WrongProtocolData(cmd, res) diff --git a/django_elastipymemcache/memcached.py b/django_elastipymemcache/memcached.py index 0abf2cd..cf59f77 100644 --- a/django_elastipymemcache/memcached.py +++ b/django_elastipymemcache/memcached.py @@ -70,6 +70,8 @@ def __init__(self, server, params): self._options = self._options or dict() self._cluster_timeout = self._options.get( 'cluster_timeout', socket._GLOBAL_DEFAULT_TIMEOUT) + self._ignore_cluster_errors = self._options.get( + 'ignore_cluster_errors', False) def clear_cluster_nodes_cache(self): """clear internal cache with list of nodes in cluster""" @@ -85,6 +87,7 @@ def get_cluster_nodes(self): return get_cluster_info( server, port, + self._ignore_cluster_errors, self._cluster_timeout )['nodes'] except (OSError, socket.gaierror, socket.timeout) as err: @@ -105,6 +108,7 @@ def _cache(self): options['deserializer'] = deserialize_pickle options.setdefault('ignore_exc', True) options.pop('cluster_timeout', None) + options.pop('ignore_cluster_errors', None) self._client = self._lib.Client( self.get_cluster_nodes(), **options)