From e5faa12e91d43b66b6e9c666059d23e03d3eb9bf Mon Sep 17 00:00:00 2001 From: opapy Date: Mon, 10 Apr 2017 19:04:36 +0900 Subject: [PATCH 001/149] 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 002/149] 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 003/149] 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 004/149] 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 005/149] 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 006/149] 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 007/149] 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 008/149] 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 009/149] 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 010/149] 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 011/149] 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 012/149] 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 013/149] 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 014/149] 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 015/149] 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 016/149] 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 017/149] 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 018/149] 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 019/149] 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 020/149] 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 021/149] 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 022/149] 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 023/149] 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 024/149] 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 025/149] 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 026/149] 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 027/149] 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 028/149] 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 029/149] 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 030/149] 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 031/149] 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 032/149] 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 033/149] 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 034/149] 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 035/149] 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 036/149] 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 037/149] 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 038/149] 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 039/149] 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 040/149] 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 041/149] 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 042/149] 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 043/149] 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 044/149] 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 045/149] 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 046/149] 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 047/149] 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 048/149] 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 049/149] 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 050/149] 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 051/149] 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 052/149] 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 053/149] 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 054/149] 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 055/149] 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 056/149] 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 057/149] 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) From bca3fa6db4ff6112f5a4962d0dc6309398c1aff8 Mon Sep 17 00:00:00 2001 From: opapy Date: Mon, 15 May 2017 14:53:54 +0900 Subject: [PATCH 058/149] fix test --- tests/test_backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_backend.py b/tests/test_backend.py index d4a68bf..805e62e 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -31,7 +31,7 @@ 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) + 'h', '0', False, socket._GLOBAL_DEFAULT_TIMEOUT) backend._lib.Client.assert_called_once_with( servers, deserializer=deserialize_pickle, @@ -69,7 +69,7 @@ def test_node_info_cache(get_cluster_info): eq_(backend._cache.set.call_count, 2) get_cluster_info.assert_called_once_with( - 'h', '0', socket._GLOBAL_DEFAULT_TIMEOUT) + 'h', '0', False, socket._GLOBAL_DEFAULT_TIMEOUT) @patch('django.conf.settings', global_settings) From ebd2a6172aa929f2316d6fe9d3262d9bd8da7f1e Mon Sep 17 00:00:00 2001 From: opapy Date: Mon, 15 May 2017 15:01:59 +0900 Subject: [PATCH 059/149] fix flake8 --- django_elastipymemcache/cluster_utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/django_elastipymemcache/cluster_utils.py b/django_elastipymemcache/cluster_utils.py index d3d39eb..15f2d9f 100644 --- a/django_elastipymemcache/cluster_utils.py +++ b/django_elastipymemcache/cluster_utils.py @@ -19,7 +19,11 @@ def __init__(self, cmd, response): 'Unexpected response {} for command {}'.format(response, cmd)) -def get_cluster_info(host, port, ignore_cluster_errors=False, 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 { From 665651c29ca6bd77d8af796fc942a5d9ca2255d6 Mon Sep 17 00:00:00 2001 From: opapy Date: Mon, 15 May 2017 17:35:13 +0900 Subject: [PATCH 060/149] 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 7b462d3..09889a1 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.5 +:Version: 0.0.6 .. 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 d9e9c97..d7e04e4 100644 --- a/django_elastipymemcache/__init__.py +++ b/django_elastipymemcache/__init__.py @@ -1,2 +1,2 @@ -VERSION = (0, 0, 5) +VERSION = (0, 0, 6) __version__ = '.'.join(map(str, VERSION)) From 2b3b10d660c809a037fea604fa926ef67961f9e5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 15 Jun 2017 11:53:06 +0900 Subject: [PATCH 061/149] Pin check-manifest to latest version 0.35 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4b67c46..ba74dcc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -check-manifest +check-manifest==0.35 coverage flake8 isort From 319f5805683b4555958c8bba34b5d7011320650f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 15 Jun 2017 11:53:08 +0900 Subject: [PATCH 062/149] Pin coverage to latest version 4.4.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ba74dcc..a40f055 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ check-manifest==0.35 -coverage +coverage==4.4.1 flake8 isort mock From 2fb3be67f2f279a3bb46c7edf345cad41b1c62a2 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 15 Jun 2017 11:53:09 +0900 Subject: [PATCH 063/149] Pin flake8 to latest version 3.3.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a40f055..e6569b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ check-manifest==0.35 coverage==4.4.1 -flake8 +flake8==3.3.0 isort mock nose From 6b047d328237336a4fa875f50bba3ced5fcc053c Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 15 Jun 2017 11:53:10 +0900 Subject: [PATCH 064/149] Pin isort to latest version 4.2.15 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e6569b2..5c75e95 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ check-manifest==0.35 coverage==4.4.1 flake8==3.3.0 -isort +isort==4.2.15 mock nose pymemcache From 843df58f2e6e12b7085ae8d9d1df73727c8614ef Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 15 Jun 2017 11:53:12 +0900 Subject: [PATCH 065/149] Pin mock to latest version 2.0.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5c75e95..7f942fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ check-manifest==0.35 coverage==4.4.1 flake8==3.3.0 isort==4.2.15 -mock +mock==2.0.0 nose pymemcache readme_renderer From 36070e998334686785ba8cff205007cc0eb5cba7 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 15 Jun 2017 11:53:13 +0900 Subject: [PATCH 066/149] Pin nose to latest version 1.3.7 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7f942fd..4dce084 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,6 @@ coverage==4.4.1 flake8==3.3.0 isort==4.2.15 mock==2.0.0 -nose +nose==1.3.7 pymemcache readme_renderer From dd802c47852b2c0c3ebb5effee2e4461f383937e Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 15 Jun 2017 11:53:15 +0900 Subject: [PATCH 067/149] Pin pymemcache to latest version 1.4.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4dce084..c7d2272 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ flake8==3.3.0 isort==4.2.15 mock==2.0.0 nose==1.3.7 -pymemcache +pymemcache==1.4.3 readme_renderer From dd2949011162f3b2f340a3530c76d7874d81a1e5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 15 Jun 2017 11:53:16 +0900 Subject: [PATCH 068/149] Pin readme-renderer to latest version 17.2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c7d2272..a537e5b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ isort==4.2.15 mock==2.0.0 nose==1.3.7 pymemcache==1.4.3 -readme_renderer +readme-renderer==17.2 From 844b190759cffd108ae4cf49f9b937b9c443a6c1 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Mon, 31 Jul 2017 03:42:09 +0200 Subject: [PATCH 069/149] Update flake8 from 3.3.0 to 3.4.1 (#16) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a537e5b..f8f55ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ check-manifest==0.35 coverage==4.4.1 -flake8==3.3.0 +flake8==3.4.1 isort==4.2.15 mock==2.0.0 nose==1.3.7 From 39f09c460dff9fdf70a3e2cc15e21cea41dc54ce Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Tue, 24 Oct 2017 03:26:01 +0200 Subject: [PATCH 070/149] Update flake8 from 3.4.1 to 3.5.0 (#17) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f8f55ce..e029246 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ check-manifest==0.35 coverage==4.4.1 -flake8==3.4.1 +flake8==3.5.0 isort==4.2.15 mock==2.0.0 nose==1.3.7 From 439b4676f57e5d3f13ae1175119af7392506979e Mon Sep 17 00:00:00 2001 From: Shinya Ohyanagi Date: Wed, 1 Nov 2017 16:29:52 +0900 Subject: [PATCH 071/149] Fix ignore pymemcache's get_many returns False value In some case, pymemcache returns {'key': False} if client does not found. It raises exception at Django-Cachalot. Django-Cachalot's author said get_many should not contain a key that's not in the cache, as stated in Django. So we try to imprements get_many method and ignore if False were included. --- django_elastipymemcache/client.py | 8 +++++- tests/test_backend.py | 46 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/django_elastipymemcache/client.py b/django_elastipymemcache/client.py index 139a789..7756ff9 100644 --- a/django_elastipymemcache/client.py +++ b/django_elastipymemcache/client.py @@ -2,4 +2,10 @@ class Client(HashClient): - pass + def get_many(self, keys, gets=False, *args, **kwargs): + # pymemcache's HashClient may returns {'key': False} + end = super(Client, self).get_many(keys, gets, args, kwargs) + + return {key: end[key] for key in end if end[key]} + + get_multi = get_many diff --git a/tests/test_backend.py b/tests/test_backend.py index 805e62e..df3a664 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -99,3 +99,49 @@ def test_invalidate_cache(get_cluster_info): pass eq_(backend._cache.get.call_count, 2) eq_(get_cluster_info.call_count, 3) + + +@patch('django.conf.settings', global_settings) +@patch('django_elastipymemcache.memcached.get_cluster_info') +def test_client_get_many(get_cluster_info): + from django_elastipymemcache.memcached import ElastiPyMemCache + + servers = [('h1', 0), ('h2', 0)] + get_cluster_info.return_value = { + 'nodes': servers + } + + backend = ElastiPyMemCache('h:0', {}) + ret = backend.get_many(['key1']) + eq_(ret, {}) + + # When server does not found... + with patch('pymemcache.client.hash.HashClient._get_client') as p: + p.return_value = None + ret = backend.get_many(['key2']) + eq_(ret, {}) + + with patch('django_elastipymemcache.client.Client.get_many'): + with patch('pymemcache.client.hash.HashClient._safely_run_func') as p2: + p2.return_value = { + ':1:key3': 1509111630.048594 + } + + ret = backend.get_many(['key3']) + eq_(ret, {'key3': 1509111630.048594}) + + # If False value is included, ignore it. + with patch('pymemcache.client.hash.HashClient.get_many') as p: + p.return_value = { + ':1:key1': 1509111630.048594, + ':1:key2': False, + ':1:key3': 1509111630.058594, + } + ret = backend.get_many(['key1', 'key2', 'key3']) + eq_( + ret, + { + 'key1': 1509111630.048594, + 'key3': 1509111630.058594 + }, + ) From e966ddd804eee2f1b053de6f0bbf943d80dccc59 Mon Sep 17 00:00:00 2001 From: Shinya Ohyanagi Date: Wed, 1 Nov 2017 17:41:13 +0900 Subject: [PATCH 072/149] Fix get value more safe --- django_elastipymemcache/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_elastipymemcache/client.py b/django_elastipymemcache/client.py index 7756ff9..6fce8c9 100644 --- a/django_elastipymemcache/client.py +++ b/django_elastipymemcache/client.py @@ -6,6 +6,6 @@ def get_many(self, keys, gets=False, *args, **kwargs): # pymemcache's HashClient may returns {'key': False} end = super(Client, self).get_many(keys, gets, args, kwargs) - return {key: end[key] for key in end if end[key]} + return {key: end.get(key) for key in end if end.get(key)} get_multi = get_many From f12c7bb0ca9c4c80e6c5cd625024821362620a49 Mon Sep 17 00:00:00 2001 From: Shinya Ohyanagi Date: Wed, 1 Nov 2017 17:41:58 +0900 Subject: [PATCH 073/149] Fix nested with statement to flatten --- tests/test_backend.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/test_backend.py b/tests/test_backend.py index df3a664..3da3830 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -121,14 +121,14 @@ def test_client_get_many(get_cluster_info): ret = backend.get_many(['key2']) eq_(ret, {}) - with patch('django_elastipymemcache.client.Client.get_many'): - with patch('pymemcache.client.hash.HashClient._safely_run_func') as p2: - p2.return_value = { - ':1:key3': 1509111630.048594 - } + with patch('django_elastipymemcache.client.Client.get_many'), \ + patch('pymemcache.client.hash.HashClient._safely_run_func') as p2: + p2.return_value = { + ':1:key3': 1509111630.048594 + } - ret = backend.get_many(['key3']) - eq_(ret, {'key3': 1509111630.048594}) + ret = backend.get_many(['key3']) + eq_(ret, {'key3': 1509111630.048594}) # If False value is included, ignore it. with patch('pymemcache.client.hash.HashClient.get_many') as p: @@ -145,3 +145,17 @@ def test_client_get_many(get_cluster_info): 'key3': 1509111630.058594 }, ) + + with patch('pymemcache.client.hash.HashClient.get_many') as p: + p.return_value = { + ':1:key1': None, + ':1:key2': 1509111630.048594, + ':1:key3': False, + } + ret = backend.get_many(['key1', 'key2', 'key3']) + eq_( + ret, + { + 'key2': 1509111630.048594, + }, + ) From 35e4d52f5a4ef246b13d898b47a0575d8dee12ba Mon Sep 17 00:00:00 2001 From: Shinya Ohyanagi Date: Wed, 1 Nov 2017 18:18:12 +0900 Subject: [PATCH 074/149] Release 1.0.0 --- README.rst | 2 +- django_elastipymemcache/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 09889a1..4519841 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.6 +:Version: 1.0.0 .. 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 d7e04e4..bc8219a 100644 --- a/django_elastipymemcache/__init__.py +++ b/django_elastipymemcache/__init__.py @@ -1,2 +1,2 @@ -VERSION = (0, 0, 6) +VERSION = (1, 0, 0) __version__ = '.'.join(map(str, VERSION)) From e9dcbfa278f30ffad50836b061ca432470f124b8 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Mon, 6 Nov 2017 04:07:36 +0100 Subject: [PATCH 075/149] Update coverage from 4.4.1 to 4.4.2 (#21) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e029246..f52236e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ check-manifest==0.35 -coverage==4.4.1 +coverage==4.4.2 flake8==3.5.0 isort==4.2.15 mock==2.0.0 From b22a69427d9c4a265c1ed795a616bf6dd76a7b83 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Wed, 22 Nov 2017 03:58:14 +0100 Subject: [PATCH 076/149] Update check-manifest from 0.35 to 0.36 (#22) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f52236e..a8a6ca2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -check-manifest==0.35 +check-manifest==0.36 coverage==4.4.2 flake8==3.5.0 isort==4.2.15 From 8cded73773dd34b0d1cb2fd7e34f739b6d9f5384 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Wed, 24 Jan 2018 10:43:47 +0100 Subject: [PATCH 077/149] Update pymemcache from 1.4.3 to 1.4.4 (#23) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a8a6ca2..92124f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ flake8==3.5.0 isort==4.2.15 mock==2.0.0 nose==1.3.7 -pymemcache==1.4.3 +pymemcache==1.4.4 readme-renderer==17.2 From b333296ac21e8b0da7a8e77c7d5a74cf28ca71ba Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Fri, 2 Feb 2018 19:58:52 +0900 Subject: [PATCH 078/149] Update support versions (#25) --- .travis.yml | 17 +++++++---------- setup.py | 4 ++-- tox.ini | 12 ++++++------ 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4edb0df..46c6bf6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,14 +4,6 @@ cache: pip matrix: fast_finish: true include: - - python: 2.7 - env: TOX_ENV=py27-dj18 - - python: 3.3 - env: TOX_ENV=py33-dj18 - - python: 3.4 - env: TOX_ENV=py34-dj18 - - python: 3.5 - env: TOX_ENV=py34-dj18 - python: 2.7 env: TOX_ENV=py27-dj19 - python: 3.4 @@ -36,6 +28,12 @@ matrix: env: TOX_ENV=py36-dj111 - python: pypy3 env: TOX_ENV=pypy3-dj111 + - python: 3.4 + env: TOX_ENV=py34-dj20 + - python: 3.5 + env: TOX_ENV=py35-dj20 + - python: 3.6 + env: TOX_ENV=py36-dj20 - python: 3.5 env: TOX_ENV=py35-djdev - python: 3.6 @@ -51,7 +49,6 @@ matrix: allow_failures: - env: TOX_ENV=py35-djdev - env: TOX_ENV=py36-djdev - - env: TOX_ENV=pypy3-dj111 install: - pip install tox script: @@ -66,4 +63,4 @@ deploy: on: repo: uncovertruth/django-elastipymemcache tags: true - condition: $TOX_ENV = py36-dj111 + condition: $TOX_ENV = py36-dj20 diff --git a/setup.py b/setup.py index 71b8992..b4a09a2 100644 --- a/setup.py +++ b/setup.py @@ -22,10 +22,10 @@ classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', - 'Framework :: Django :: 1.8', 'Framework :: Django :: 1.9', 'Framework :: Django :: 1.10', 'Framework :: Django :: 1.11', + 'Framework :: Django :: 1.20', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', @@ -42,6 +42,6 @@ include_package_data=True, install_requires=[ 'pymemcache', - 'Django>=1.8', + 'Django>=1.9', ], ) diff --git a/tox.ini b/tox.ini index 72aeba4..2e8730e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{py,py3,27,33,34,35,36}-dj{18,19,110,111,dev} + py{py,py3,27,33,34,35,36}-dj{19,110,111,20,dev} flake8, isort, readme @@ -16,21 +16,21 @@ basepython = pypy: pypy pypy3: pypy3 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 + dj20: Django>=2.0,<2.1 djdev: https://github.com/django/django/archive/master.tar.gz -r{toxinidir}/requirements.txt - py36-dj111: codecov + py36-dj20: codecov setenv = PYTHONPATH = {toxinidir} CODECOV_TOKEN="8ea69bfe-d9b5-45fc-97f7-ef843bd3d9d2" commands = coverage run --source=django_elastipymemcache -m nose - py36-dj111: coverage report - py36-dj111: coverage xml - py36-dj111: codecov + py36-dj20: coverage report + py36-dj20: coverage xml + py36-dj20: codecov [testenv:flake8] basepython = python3.6 From 39d521d5f799d27e2d8ed3166e4d4ece89df076e Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Fri, 2 Feb 2018 12:17:15 +0100 Subject: [PATCH 079/149] Update isort from 4.2.15 to 4.3.0 (#26) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 92124f3..2263c9c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ check-manifest==0.36 coverage==4.4.2 flake8==3.5.0 -isort==4.2.15 +isort==4.3.0 mock==2.0.0 nose==1.3.7 pymemcache==1.4.4 From 78d41f64bd099c1ae5e4f7f91ed61fb53e42e470 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Sat, 3 Feb 2018 01:14:37 +0100 Subject: [PATCH 080/149] Update isort from 4.3.0 to 4.3.1 (#27) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2263c9c..19925c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ check-manifest==0.36 coverage==4.4.2 flake8==3.5.0 -isort==4.3.0 +isort==4.3.1 mock==2.0.0 nose==1.3.7 pymemcache==1.4.4 From 10619ae26f4d35955e8a51078e3e50a1fbccebb6 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Mon, 5 Feb 2018 03:40:28 +0100 Subject: [PATCH 081/149] Update coverage from 4.4.2 to 4.5 (#28) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 19925c6..f6f8485 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ check-manifest==0.36 -coverage==4.4.2 +coverage==4.5 flake8==3.5.0 isort==4.3.1 mock==2.0.0 From 9846160681430e3ba55313acff74bf1acf279c15 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Mon, 5 Feb 2018 08:01:14 +0100 Subject: [PATCH 082/149] Update isort from 4.3.1 to 4.3.2 (#29) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f6f8485..2ef54d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ check-manifest==0.36 coverage==4.5 flake8==3.5.0 -isort==4.3.1 +isort==4.3.2 mock==2.0.0 nose==1.3.7 pymemcache==1.4.4 From 9fad3374534be7fb77d5033bca90e489b3f99be8 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Tue, 6 Feb 2018 11:08:57 +0100 Subject: [PATCH 083/149] Update isort from 4.3.2 to 4.3.3 (#30) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2ef54d3..66e059c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ check-manifest==0.36 coverage==4.5 flake8==3.5.0 -isort==4.3.2 +isort==4.3.3 mock==2.0.0 nose==1.3.7 pymemcache==1.4.4 From 1c1acbed3952ff976cdaf2fa25cc26911bceb3b1 Mon Sep 17 00:00:00 2001 From: Keisuke Kan Date: Wed, 7 Feb 2018 11:34:31 +0900 Subject: [PATCH 084/149] refacotor(travis) skip email --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 46c6bf6..7d61ebf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -64,3 +64,5 @@ deploy: repo: uncovertruth/django-elastipymemcache tags: true condition: $TOX_ENV = py36-dj20 +notifications: + email: false From 99d610f7ae6d8749d8bd487b96bcec672f86c35c Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Mon, 12 Feb 2018 01:48:06 +0100 Subject: [PATCH 085/149] Update coverage from 4.5 to 4.5.1 (#32) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 66e059c..5c73d69 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ check-manifest==0.36 -coverage==4.5 +coverage==4.5.1 flake8==3.5.0 isort==4.3.3 mock==2.0.0 From 1f2bd00dddd9908fe948404a64dbeb33fd8460ef Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Tue, 13 Feb 2018 04:22:07 +0100 Subject: [PATCH 086/149] Update isort from 4.3.3 to 4.3.4 (#33) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5c73d69..9bcf327 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ check-manifest==0.36 coverage==4.5.1 flake8==3.5.0 -isort==4.3.3 +isort==4.3.4 mock==2.0.0 nose==1.3.7 pymemcache==1.4.4 From c56c5bb8184a640b8f4f3cdc626c61c93052dc80 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Mon, 12 Mar 2018 04:06:07 +0100 Subject: [PATCH 087/149] Update readme-renderer from 17.2 to 17.3 (#34) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9bcf327..a2d1d31 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ isort==4.3.4 mock==2.0.0 nose==1.3.7 pymemcache==1.4.4 -readme-renderer==17.2 +readme-renderer==17.3 From 54a8c1f93f931c034d3dcbeb7fefcae030d8d806 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Wed, 28 Mar 2018 02:53:41 +0200 Subject: [PATCH 088/149] Update readme-renderer from 17.3 to 17.4 (#35) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a2d1d31..6b54e61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ isort==4.3.4 mock==2.0.0 nose==1.3.7 pymemcache==1.4.4 -readme-renderer==17.3 +readme-renderer==17.4 From a9dc6af23b1e1298c229d03436f14721d5e56eb5 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 2 Apr 2018 08:26:15 +0900 Subject: [PATCH 089/149] Update readme-renderer from 17.4 to 18.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6b54e61..f7a4ca9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ isort==4.3.4 mock==2.0.0 nose==1.3.7 pymemcache==1.4.4 -readme-renderer==17.4 +readme-renderer==18.1 From 8560bb9311e8dab0ce814c2722a8c881285741eb Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 12 Apr 2018 22:53:25 +0900 Subject: [PATCH 090/149] Update check-manifest from 0.36 to 0.37 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f7a4ca9..22cca1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -check-manifest==0.36 +check-manifest==0.37 coverage==4.5.1 flake8==3.5.0 isort==4.3.4 From 513309aa8ebc76221480150b439926fb9b03b08e Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Wed, 30 May 2018 03:02:12 +0200 Subject: [PATCH 091/149] Update readme-renderer from 18.1 to 21.0 (#42) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 22cca1f..3ebe10e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ isort==4.3.4 mock==2.0.0 nose==1.3.7 pymemcache==1.4.4 -readme-renderer==18.1 +readme-renderer==21.0 From d57eaba327df0125d674d714349b78ba5ef01b5e Mon Sep 17 00:00:00 2001 From: Chihiro Kaneko Date: Tue, 19 Jun 2018 17:50:48 +0900 Subject: [PATCH 092/149] Drop old django version --- .travis.yml | 12 ------------ README.rst | 6 +++--- django_elastipymemcache/__init__.py | 2 +- setup.py | 7 ++----- tox.ini | 5 +---- 5 files changed, 7 insertions(+), 25 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7d61ebf..1d18bb9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,18 +4,6 @@ cache: pip matrix: fast_finish: true include: - - python: 2.7 - env: TOX_ENV=py27-dj19 - - python: 3.4 - env: TOX_ENV=py34-dj19 - - python: 3.5 - env: TOX_ENV=py35-dj19 - - python: 2.7 - env: TOX_ENV=py27-dj110 - - python: 3.4 - env: TOX_ENV=py34-dj110 - - python: 3.5 - env: TOX_ENV=py35-dj110 - python: 2.7 env: TOX_ENV=py27-dj111 - python: pypy diff --git a/README.rst b/README.rst index 4519841..5e5d2f1 100644 --- a/README.rst +++ b/README.rst @@ -5,8 +5,8 @@ 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: 1.0.0 +:Date: 2018-06-19 +:Version: 1.1.0 .. index: README .. image:: https://travis-ci.org/uncovertruth/django-elastipymemcache.svg?branch=master @@ -31,7 +31,7 @@ Requirements ------------ * pymemcache -* Django>=1.8 +* Django>=1.11 Installation ------------ diff --git a/django_elastipymemcache/__init__.py b/django_elastipymemcache/__init__.py index bc8219a..c0ef4db 100644 --- a/django_elastipymemcache/__init__.py +++ b/django_elastipymemcache/__init__.py @@ -1,2 +1,2 @@ -VERSION = (1, 0, 0) +VERSION = (1, 1, 0) __version__ = '.'.join(map(str, VERSION)) diff --git a/setup.py b/setup.py index b4a09a2..fe3154f 100644 --- a/setup.py +++ b/setup.py @@ -22,17 +22,14 @@ classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', - 'Framework :: Django :: 1.9', - 'Framework :: Django :: 1.10', 'Framework :: Django :: 1.11', - 'Framework :: Django :: 1.20', + 'Framework :: Django :: 2.0', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', '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', @@ -42,6 +39,6 @@ include_package_data=True, install_requires=[ 'pymemcache', - 'Django>=1.9', + 'Django>=1.11', ], ) diff --git a/tox.ini b/tox.ini index 2e8730e..5b2b04b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{py,py3,27,33,34,35,36}-dj{19,110,111,20,dev} + py{py,27}-dj111,py{py3,34,35,36}-dj{111,20,dev} flake8, isort, readme @@ -9,15 +9,12 @@ envlist = [testenv] basepython = py27: python2.7 - py33: python3.3 py34: python3.4 py35: python3.5 py36: python3.6 pypy: pypy pypy3: pypy3 deps = - dj19: Django>=1.9,<1.10 - dj110: Django>=1.10,<1.11 dj111: Django>=1.11,<2.0 dj20: Django>=2.0,<2.1 djdev: https://github.com/django/django/archive/master.tar.gz From 6c85ff997c4b6357fd336055705b1f837dd64081 Mon Sep 17 00:00:00 2001 From: hirokinko Date: Wed, 20 Jun 2018 12:07:39 +0900 Subject: [PATCH 093/149] Drop py34 in djdev case. --- tests/test_backend.py | 1 - tox.ini | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_backend.py b/tests/test_backend.py index 3da3830..455c7e1 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -10,7 +10,6 @@ from unittest.mock import patch, Mock -# Initialize django 1.7 settings.configure() global_settings.configured = True diff --git a/tox.ini b/tox.ini index 5b2b04b..edce7e5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{py,27}-dj111,py{py3,34,35,36}-dj{111,20,dev} + py{py,27}-dj111,py{py3,34,35,36}-dj{111,20},py{py3,35,36}-dj{dev} flake8, isort, readme From 6a4fe086931ec11a3a245e3a999ba261582536d6 Mon Sep 17 00:00:00 2001 From: hirokinko Date: Wed, 20 Jun 2018 19:22:34 +0900 Subject: [PATCH 094/149] Remove unnecessary patches. --- django_elastipymemcache/memcached.py | 2 -- tests/test_backend.py | 9 --------- 2 files changed, 11 deletions(-) diff --git a/django_elastipymemcache/memcached.py b/django_elastipymemcache/memcached.py index cf59f77..90b084f 100644 --- a/django_elastipymemcache/memcached.py +++ b/django_elastipymemcache/memcached.py @@ -66,8 +66,6 @@ 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._cluster_timeout = self._options.get( 'cluster_timeout', socket._GLOBAL_DEFAULT_TIMEOUT) self._ignore_cluster_errors = self._options.get( diff --git a/tests/test_backend.py b/tests/test_backend.py index 455c7e1..4627cab 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -1,7 +1,6 @@ import socket import sys -from django.conf import global_settings, settings from nose.tools import eq_ if sys.version < '3': @@ -10,11 +9,6 @@ from unittest.mock import patch, Mock -settings.configure() -global_settings.configured = True - - -@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 ( @@ -39,7 +33,6 @@ 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 ( @@ -71,7 +64,6 @@ def test_node_info_cache(get_cluster_info): 'h', '0', False, socket._GLOBAL_DEFAULT_TIMEOUT) -@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 ElastiPyMemCache @@ -100,7 +92,6 @@ def test_invalidate_cache(get_cluster_info): eq_(get_cluster_info.call_count, 3) -@patch('django.conf.settings', global_settings) @patch('django_elastipymemcache.memcached.get_cluster_info') def test_client_get_many(get_cluster_info): from django_elastipymemcache.memcached import ElastiPyMemCache From f859a2283326144b383f88b0b8b0827ce82d7c65 Mon Sep 17 00:00:00 2001 From: opapy Date: Tue, 9 Oct 2018 17:31:43 +0900 Subject: [PATCH 095/149] update pymemcache=2.0.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3ebe10e..38dbf08 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ flake8==3.5.0 isort==4.3.4 mock==2.0.0 nose==1.3.7 -pymemcache==1.4.4 +pymemcache==2.0.0 readme-renderer==21.0 From 7331871795696859946e77eccbb85087c6908ca7 Mon Sep 17 00:00:00 2001 From: opapy Date: Tue, 9 Oct 2018 18:02:47 +0900 Subject: [PATCH 096/149] update to 1.2.0 --- README.rst | 2 +- django_elastipymemcache/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 5e5d2f1..c5fe4d4 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,7 @@ django-elastipymemcache :Author: UNCOVER TRUTH Inc. :Copyright: © UNCOVER TRUTH Inc. :Date: 2018-06-19 -:Version: 1.1.0 +:Version: 1.2.0 .. 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 c0ef4db..b9b86f7 100644 --- a/django_elastipymemcache/__init__.py +++ b/django_elastipymemcache/__init__.py @@ -1,2 +1,2 @@ -VERSION = (1, 1, 0) +VERSION = (1, 2, 0) __version__ = '.'.join(map(str, VERSION)) From cb207227319d0159bf918c876eafbbfdab10b1cc Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 25 Oct 2018 09:42:05 +0900 Subject: [PATCH 097/149] Update flake8 from 3.5.0 to 3.6.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 38dbf08..2474e51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ check-manifest==0.37 coverage==4.5.1 -flake8==3.5.0 +flake8==3.6.0 isort==4.3.4 mock==2.0.0 nose==1.3.7 From f9e6eda61e324da4257e84df9342b9d38b12d397 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 29 Oct 2018 05:39:18 +0900 Subject: [PATCH 098/149] Update readme-renderer from 21.0 to 24.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 38dbf08..4a6a5f2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ isort==4.3.4 mock==2.0.0 nose==1.3.7 pymemcache==2.0.0 -readme-renderer==21.0 +readme-renderer==24.0 From c5a890c98dd7b0878219233d08c0dabc5a916234 Mon Sep 17 00:00:00 2001 From: opapy Date: Tue, 6 Nov 2018 21:03:01 +0900 Subject: [PATCH 099/149] fix super method params --- django_elastipymemcache/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_elastipymemcache/client.py b/django_elastipymemcache/client.py index 6fce8c9..0f67743 100644 --- a/django_elastipymemcache/client.py +++ b/django_elastipymemcache/client.py @@ -4,7 +4,7 @@ class Client(HashClient): def get_many(self, keys, gets=False, *args, **kwargs): # pymemcache's HashClient may returns {'key': False} - end = super(Client, self).get_many(keys, gets, args, kwargs) + end = super(Client, self).get_many(keys, gets, *args, **kwargs) return {key: end.get(key) for key in end if end.get(key)} From 5fa63d5ef160ed82a49ca0db249d3eb0d988015d Mon Sep 17 00:00:00 2001 From: opapy <6140645+opapy@users.noreply.github.com> Date: Wed, 7 Nov 2018 11:03:41 +0900 Subject: [PATCH 100/149] bump version to 1.2.1 (#52) --- README.rst | 2 +- django_elastipymemcache/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index c5fe4d4..fae5f5d 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,7 @@ django-elastipymemcache :Author: UNCOVER TRUTH Inc. :Copyright: © UNCOVER TRUTH Inc. :Date: 2018-06-19 -:Version: 1.2.0 +:Version: 1.2.1 .. 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 b9b86f7..048cc88 100644 --- a/django_elastipymemcache/__init__.py +++ b/django_elastipymemcache/__init__.py @@ -1,2 +1,2 @@ -VERSION = (1, 2, 0) +VERSION = (1, 2, 1) __version__ = '.'.join(map(str, VERSION)) From c2774ece5ae7d93da87705261c087c9c42a213d2 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 29 Jan 2019 02:52:13 +0900 Subject: [PATCH 101/149] Update pymemcache from 2.0.0 to 2.1.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a59eda8..5a799a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ flake8==3.6.0 isort==4.3.4 mock==2.0.0 nose==1.3.7 -pymemcache==2.0.0 +pymemcache==2.1.1 readme-renderer==24.0 From aa4684ba89e78076f3eb5d82cffb2c92d9ca63ce Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 5 Feb 2019 23:21:31 +0900 Subject: [PATCH 102/149] Update flake8 from 3.6.0 to 3.7.5 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a59eda8..8b779ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ check-manifest==0.37 coverage==4.5.1 -flake8==3.6.0 +flake8==3.7.5 isort==4.3.4 mock==2.0.0 nose==1.3.7 From d821aa58aef09b1ddc545cc3e1a83d21d35c2d16 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 22 Jun 2020 21:03:25 +0900 Subject: [PATCH 103/149] Update supported versions --- .travis.yml | 57 +++++++++++++++++++++-------------------------------- setup.py | 12 +++++------ tox.ini | 34 ++++++++++++++++---------------- 3 files changed, 44 insertions(+), 59 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1d18bb9..a9090be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,46 +1,33 @@ sudo: false language: python +python: + - 3.6 + - 3.7 + - 3.8 cache: pip +env: + - DJANGO=2.2 + - DJANGO=3.0 + - DJANGO=dev + matrix: - fast_finish: true include: - - 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.4 - env: TOX_ENV=py34-dj20 - - python: 3.5 - env: TOX_ENV=py35-dj20 - - python: 3.6 - env: TOX_ENV=py36-dj20 - - python: 3.5 - env: TOX_ENV=py35-djdev - - python: 3.6 - env: TOX_ENV=py36-djdev - - python: 3.6 - env: TOX_ENV=flake8 - - python: 3.6 - env: TOX_ENV=isort - - python: 3.6 - env: TOX_ENV=readme - - python: 3.6 - env: TOX_ENV=check-manifest + - python: 3.5 + env: DJANGO=2.2 + - python: 3.8 + env: TOXENV=flake8 + - python: 3.8 + env: TOXENV=isort + - python: 3.8 + env: TOXENV=readme + - python: 3.8 + env: TOXENV=manifest allow_failures: - - env: TOX_ENV=py35-djdev - - env: TOX_ENV=py36-djdev + - env: DJANGO=dev install: -- pip install tox +- travis_retry python3 -m pip install tox-travis script: -- tox -e "$TOX_ENV" +- tox deploy: provider: pypi user: uncovertruth diff --git a/setup.py b/setup.py index fe3154f..1fe8063 100644 --- a/setup.py +++ b/setup.py @@ -22,23 +22,21 @@ classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', - 'Framework :: Django :: 1.11', - 'Framework :: Django :: 2.0', + 'Framework :: Django :: 2.2', + 'Framework :: Django :: 3.0', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Topic :: Software Development :: Libraries :: Python Modules', ], packages=find_packages(exclude=('tests',)), include_package_data=True, install_requires=[ 'pymemcache', - 'Django>=1.11', + 'Django>=2.2', ], ) diff --git a/tox.ini b/tox.ini index edce7e5..2040834 100644 --- a/tox.ini +++ b/tox.ini @@ -1,22 +1,22 @@ [tox] envlist = - py{py,27}-dj111,py{py3,34,35,36}-dj{111,20},py{py3,35,36}-dj{dev} + py{35,36,37,38}-{dj22}, + py{36,37,38}-{dj30,djdev}, flake8, isort, readme check-manifest +[travis:env] +DJANGO = + 2.2: dj22 + 3.0: dj30 + dev: djdev + [testenv] -basepython = - py27: python2.7 - py34: python3.4 - py35: python3.5 - py36: python3.6 - pypy: pypy - pypy3: pypy3 deps = - dj111: Django>=1.11,<2.0 - dj20: Django>=2.0,<2.1 + dj22: Django<2.3 + dj30: Django<3.1 djdev: https://github.com/django/django/archive/master.tar.gz -r{toxinidir}/requirements.txt py36-dj20: codecov @@ -25,26 +25,26 @@ setenv = CODECOV_TOKEN="8ea69bfe-d9b5-45fc-97f7-ef843bd3d9d2" commands = coverage run --source=django_elastipymemcache -m nose - py36-dj20: coverage report - py36-dj20: coverage xml - py36-dj20: codecov + py38-dj30: coverage report + py38-dj30: coverage xml + py38-dj30: codecov [testenv:flake8] -basepython = python3.6 +basepython = python3.8 commands = flake8 deps = flake8 [testenv:isort] -basepython = python3.6 +basepython = python3.8 commands = isort --verbose --check-only --diff django_elastipymemcache tests setup.py deps = isort [testenv:readme] -basepython = python3.6 +basepython = python3.8 commands = python setup.py check -r -s deps = readme_renderer [testenv:check-manifest] -basepython = python3.6 +basepython = python3.8 commands = check-manifest {toxinidir} deps = check-manifest From eab3e5a3f9f0f7ec2b1c6b197f71e7a718b3438a Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 23 Jun 2020 10:09:03 +0900 Subject: [PATCH 104/149] Change maintener --- README.rst | 18 ++++-------------- setup.py | 6 +++--- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/README.rst b/README.rst index fae5f5d..e57ea10 100644 --- a/README.rst +++ b/README.rst @@ -2,21 +2,11 @@ django-elastipymemcache ======================= -:Info: Simple Django cache backend for Amazon ElastiCache (memcached based). -:Author: UNCOVER TRUTH Inc. -:Copyright: © UNCOVER TRUTH Inc. -:Date: 2018-06-19 -:Version: 1.2.1 - .. 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 -.. 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 +.. image:: https://travis-ci.org/harikitech/django-elastipymemcache.svg?branch=master + :target: https://travis-ci.org/harikitech/django-elastipymemcache +.. image:: https://codecov.io/gh/harikitech/django-elastipymemcache/branch/master/graph/badge.svg + :target: https://codecov.io/gh/harikitech/django-elastipymemcache Purpose ------- diff --git a/setup.py b/setup.py index 1fe8063..e995dca 100644 --- a/setup.py +++ b/setup.py @@ -12,9 +12,9 @@ version=django_elastipymemcache.__version__, description='Django cache backend for Amazon ElastiCache (memcached)', keywords='elasticache amazon cache pymemcache memcached aws', - author='UNCOVER TRUTH Inc.', - author_email='develop@uncovertruth.co.jp', - url='http://github.com/uncovertruth/django-elastipymemcache', + author='HarikiTech', + author_email='harikitech+noreply@googlegroups.com', + url='http://github.com/harikitech/django-elastipymemcache', license='MIT', long_description=io.open('README.rst').read(), platforms='any', From 5c604c48d2609250b6236fe58c10d4dc1e211814 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Sat, 27 Jun 2020 20:06:55 +0900 Subject: [PATCH 105/149] Change deploy user --- .travis.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index a9090be..e9e5fea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,14 +30,15 @@ script: - tox deploy: provider: pypi - user: uncovertruth + username: __token__ 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= + secure: "HBbDdcrrzRrSXbQGz3cz24/xNIW77tVlHQ+D6DS5raOSV7en5CPaiWZ50YkMQa08nfBOR1RBT8JivrC05qz1+TqMwOz0+rinFeGPmu9gLY8f+1kx0UZnmXG7GHsm6dBGOP6nIHA40+rtK/YLuhskdd2hEBIOAuAxXuCSd1UQ/3bj/nuspDwcot9OdJiyn+Xl+cjI39Gt8WlF7ZBjgFRw1hNSIJ2f0or38zf+SSDY6QgKiJZCn+4sooy0assCms84LSKumSl7Ya50IWwBGJgJaNPxikqgTf3rDP9bT25OLtJw78zJnO4QC2PJ9Y7j2tzeYkelHJM794uAtKNaW1SanlYdBXQoE7m1CE8DszZxCtN/siuz+30whFJ885pPaSN3m5uJct/v9GxBEAHtoCrwwdfrTvzVCU43PGwPZvO768k465rRGUnBInLybO5hTq6myajecZB2WhVXMh7/KMcXlVtnD5PvIhxLkLPVIGBK+1wFfvH/4ijZl8Dhz5PXP53AcX/Ohdli7+kd9DQEvieE2bhDpBaJq2r7B4etW/bjz76GQ704uzuF5JXXEibaT1fox8nqJzghuWfcXJvoTa20iR1quuHg0Z4qBIYh76CUXafi4iIZ0fCcIDVPWM7KSp1rvt4BgCdi1csQdTasw0l0WGoKdOSu+LlNSrqzPLoXQlE=" distributions: sdist bdist_wheel skip_upload_docs: true on: - repo: uncovertruth/django-elastipymemcache + repo: harikitech/django-elastipymemcache tags: true - condition: $TOX_ENV = py36-dj20 + python: 3.8 + condition: "$DJANGO = 3.0" notifications: email: false From 4238a0b214bee12294b7c3f8f6a7186572f83142 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Sat, 27 Jun 2020 20:10:03 +0900 Subject: [PATCH 106/149] Bump version to 1.3.0 --- django_elastipymemcache/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_elastipymemcache/__init__.py b/django_elastipymemcache/__init__.py index 048cc88..619230e 100644 --- a/django_elastipymemcache/__init__.py +++ b/django_elastipymemcache/__init__.py @@ -1,2 +1,2 @@ -VERSION = (1, 2, 1) +VERSION = (1, 3, 0) __version__ = '.'.join(map(str, VERSION)) From ee06a82b7b11f824a3f3977ee5919f7252b4e152 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Sat, 27 Jun 2020 20:21:23 +0900 Subject: [PATCH 107/149] Reconfigure manifest file --- MANIFEST.in | 8 ++++++-- README.rst | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 2be18b6..89bc473 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,8 @@ +include LICENSE +include README.rst + graft django_elastipymemcache graft tests -include README.rst AUTHORS LICENSE CHANGELOG.rst setup.py setup.cfg requirements.txt tox.ini -global-exclude __pycache__ *.pyc + +global-exclude __pycache__ +global-exclude *.py[co] diff --git a/README.rst b/README.rst index e57ea10..1898db2 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ Requirements ------------ * pymemcache -* Django>=1.11 +* Django>=2.2 Installation ------------ From 24d33249d7430ddaf5cac35eca830a034aad83b2 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Sat, 27 Jun 2020 20:30:47 +0900 Subject: [PATCH 108/149] Install codecov in py38 --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 2040834..90d49cf 100644 --- a/tox.ini +++ b/tox.ini @@ -19,12 +19,12 @@ deps = dj30: Django<3.1 djdev: https://github.com/django/django/archive/master.tar.gz -r{toxinidir}/requirements.txt - py36-dj20: codecov + py38-dj30: codecov setenv = PYTHONPATH = {toxinidir} CODECOV_TOKEN="8ea69bfe-d9b5-45fc-97f7-ef843bd3d9d2" commands = - coverage run --source=django_elastipymemcache -m nose + coverage run --source=django_elastipymemcache -m nose --verbose py38-dj30: coverage report py38-dj30: coverage xml py38-dj30: codecov From e572ff30f2da2a7c76e62c1316b1cc97a829343c Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Sat, 27 Jun 2020 20:47:26 +0900 Subject: [PATCH 109/149] Install dependabot --- .github/dependabot.yml | 10 ++++++++++ requirements.txt | 14 +++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d3a512c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +--- +version: 2 +updates: + - package-ecosystem: pip + directory: "/" + schedule: + interval: daily + time: '09:00' + timezone: Asia/Tokyo + open-pull-requests-limit: 10 diff --git a/requirements.txt b/requirements.txt index dd63419..5ca792a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -check-manifest==0.37 -coverage==4.5.1 -flake8==3.7.5 -isort==4.3.4 -mock==2.0.0 +check-manifest==0.42 +coverage==5.1 +flake8==3.8.3 +isort==4.3.21 +mock==4.0.2 nose==1.3.7 -pymemcache==2.1.1 -readme-renderer==24.0 +pymemcache==3.2.0 +readme-renderer==26.0 From ea60916f0f0f3212c7bc1461bd74379c3463b1c1 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Sat, 27 Jun 2020 21:01:48 +0900 Subject: [PATCH 110/149] Good bye Python 3.5 (mock does not supported) --- .travis.yml | 2 -- setup.py | 1 - tox.ini | 5 +++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index e9e5fea..4685ab0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,8 +12,6 @@ env: matrix: include: - - python: 3.5 - env: DJANGO=2.2 - python: 3.8 env: TOXENV=flake8 - python: 3.8 diff --git a/setup.py b/setup.py index e995dca..74abe98 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,6 @@ 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', diff --git a/tox.ini b/tox.ini index 90d49cf..1a3e811 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,8 @@ [tox] envlist = - py{35,36,37,38}-{dj22}, - py{36,37,38}-{dj30,djdev}, + py{36,37,38}-dj22, + py{36,37,38}-dj30, + py{36,37,38}-djdev, flake8, isort, readme From e4ef0fe2bbc1e744dee759b0eb2842fb54d6421f Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Sat, 27 Jun 2020 21:22:11 +0900 Subject: [PATCH 111/149] Remove old token --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 1a3e811..4862074 100644 --- a/tox.ini +++ b/tox.ini @@ -23,7 +23,6 @@ deps = py38-dj30: codecov setenv = PYTHONPATH = {toxinidir} - CODECOV_TOKEN="8ea69bfe-d9b5-45fc-97f7-ef843bd3d9d2" commands = coverage run --source=django_elastipymemcache -m nose --verbose py38-dj30: coverage report From faf5a573c9415a774c544c9d4056e50ddc3ea3ce Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Sat, 27 Jun 2020 21:34:39 +0900 Subject: [PATCH 112/149] Get CODECOV_TOKEN from environment variables --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 4862074..1661a13 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ DJANGO = dev: djdev [testenv] +passenv = TOXENV CI TRAVIS TRAVIS_* CODECOV_* deps = dj22: Django<2.3 dj30: Django<3.1 From f87c15e459b32001210bb0bd5d14a2f3df6e4c59 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Sat, 27 Jun 2020 23:28:08 +0900 Subject: [PATCH 113/149] Add more tests --- django_elastipymemcache/memcached.py | 9 +- tests/test_backend.py | 95 ++++++++++++++++++-- tests/test_protocol.py | 124 ++++++++++++--------------- 3 files changed, 149 insertions(+), 79 deletions(-) diff --git a/django_elastipymemcache/memcached.py b/django_elastipymemcache/memcached.py index 90b084f..cc676b3 100644 --- a/django_elastipymemcache/memcached.py +++ b/django_elastipymemcache/memcached.py @@ -113,11 +113,6 @@ 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) @@ -137,3 +132,7 @@ def set_many(self, *args, **kwargs): @invalidate_cache_after_error def delete(self, *args, **kwargs): return super(ElastiPyMemCache, self).delete(*args, **kwargs) + + @invalidate_cache_after_error + def delete_many(self, *args, **kwargs): + return super(ElastiPyMemCache, self).delete_many(*args, **kwargs) diff --git a/tests/test_backend.py b/tests/test_backend.py index 4627cab..72edf68 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -1,12 +1,33 @@ import socket -import sys +from unittest.mock import patch, Mock -from nose.tools import eq_ +try: + import cPickle as pickle +except ImportError: + import pickle -if sys.version < '3': - from mock import patch, Mock -else: - from unittest.mock import patch, Mock + +from django.core.cache import InvalidCacheBackendError +from nose.tools import ( + eq_, + raises, +) + + +@raises(InvalidCacheBackendError) +def test_multiple_servers(): + from django_elastipymemcache.memcached import ( + ElastiPyMemCache, + ) + ElastiPyMemCache('h1:0,h2:0', {}) + + +@raises(InvalidCacheBackendError) +def test_wrong_server_format(): + from django_elastipymemcache.memcached import ( + ElastiPyMemCache, + ) + ElastiPyMemCache('h', {}) @patch('django_elastipymemcache.memcached.get_cluster_info') @@ -64,6 +85,14 @@ def test_node_info_cache(get_cluster_info): 'h', '0', False, socket._GLOBAL_DEFAULT_TIMEOUT) +@patch('django_elastipymemcache.memcached.get_cluster_info') +def test_failed_to_connect_servers(get_cluster_info): + from django_elastipymemcache.memcached import ElastiPyMemCache + backend = ElastiPyMemCache('h:0', {}) + get_cluster_info.side_effect = OSError() + eq_(backend.get_cluster_nodes(), []) + + @patch('django_elastipymemcache.memcached.get_cluster_info') def test_invalidate_cache(get_cluster_info): from django_elastipymemcache.memcached import ElastiPyMemCache @@ -149,3 +178,57 @@ def test_client_get_many(get_cluster_info): 'key2': 1509111630.048594, }, ) + + +@patch('django_elastipymemcache.memcached.get_cluster_info') +def test_client_set_many(get_cluster_info): + from django_elastipymemcache.memcached import ElastiPyMemCache + + servers = [('h1', 0), ('h2', 0)] + get_cluster_info.return_value = { + 'nodes': servers + } + + backend = ElastiPyMemCache('h:0', {}) + ret = backend.set_many({'key1': 'value1', 'key2': 'value2'}) + eq_(ret, ['key1', 'key2']) + + +@patch('django_elastipymemcache.memcached.get_cluster_info') +def test_client_delete(get_cluster_info): + from django_elastipymemcache.memcached import ElastiPyMemCache + + servers = [('h1', 0), ('h2', 0)] + get_cluster_info.return_value = { + 'nodes': servers + } + + backend = ElastiPyMemCache('h:0', {}) + ret = backend.delete('key1') + eq_(ret, None) + + +@patch('django_elastipymemcache.memcached.get_cluster_info') +def test_client_delete_many(get_cluster_info): + from django_elastipymemcache.memcached import ElastiPyMemCache + + servers = [('h1', 0), ('h2', 0)] + get_cluster_info.return_value = { + 'nodes': servers + } + + backend = ElastiPyMemCache('h:0', {}) + ret = backend.delete_many(['key1', 'key2']) + eq_(ret, None) + + +def test_serialize_pickle(): + from django_elastipymemcache.memcached import serialize_pickle + eq_(serialize_pickle('key', 'str'), ('str', 1)) + eq_(serialize_pickle('key', 0), (pickle.dumps(0), 2)) + + +def test_deserialize_pickle(): + from django_elastipymemcache.memcached import deserialize_pickle + eq_(deserialize_pickle('key', 'str', 1), 'str') + eq_(deserialize_pickle('key', pickle.dumps(0), 2), 0) diff --git a/tests/test_protocol.py b/tests/test_protocol.py index a536238..bc6a3af 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -4,7 +4,10 @@ WrongProtocolData, get_cluster_info, ) -from nose.tools import eq_, raises +from nose.tools import ( + eq_, + raises, +) if sys.version < '3': from mock import patch, call, MagicMock @@ -12,47 +15,32 @@ from unittest.mock import patch, call, MagicMock -TEST_PROTOCOL_1_READ_UNTIL = [ - b'VERSION 1.4.14', -] - -TEST_PROTOCOL_1_EXPECT = [ - (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 = [ - b'VERSION 1.4.13', -] - -TEST_PROTOCOL_2_EXPECT = [ - (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 = [ - b'VERSION 1.4.14 (Ubuntu)', -] - -TEST_PROTOCOL_3_EXPECT = [ - (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 = [ - b'VERSION 1.4.34', -] - -TEST_PROTOCOL_4_EXPECT = [ - (0, None, b'ERROR\r\n'), -] +# https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/AutoDiscovery.AddingToYourClientLibrary.html +EXAMPLE_RESPONSE = ( + b'CONFIG cluster 0 147\r\n' + b'12\n' + b'myCluster.pc4ldq.0001.use1.cache.amazonaws.com|10.82.235.120|11211 ' + b'myCluster.pc4ldq.0002.use1.cache.amazonaws.com|10.80.249.27|11211\n\r\n' + b'END\r\n' +) @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 - client.expect.side_effect = TEST_PROTOCOL_1_EXPECT + client.read_until.side_effect = [ + b'VERSION 1.4.14', + ] + client.expect.side_effect = [ + (0, None, EXAMPLE_RESPONSE), # NOQA + ] info = get_cluster_info('', 0) - eq_(info['version'], 1) - eq_(info['nodes'], [('ip', 11211), ('host', 11211)]) + eq_(info['version'], 12) + eq_(info['nodes'], [('10.82.235.120', 11211), ('10.80.249.27', 11211)]) + client.write.assert_has_calls([ + call(b'version\n'), + call(b'config get cluster\n'), + ]) @raises(WrongProtocolData) @@ -62,10 +50,14 @@ def test_bad_protocol(): @patch('django_elastipymemcache.cluster_utils.Telnet') -def test_last_versions(Telnet): +def test_ubuntu_protocol(Telnet): client = Telnet.return_value - client.read_until.side_effect = TEST_PROTOCOL_1_READ_UNTIL - client.expect.side_effect = TEST_PROTOCOL_1_EXPECT + client.read_until.side_effect = [ + b'VERSION 1.4.14 (Ubuntu)', + ] + client.expect.side_effect = [ + (0, None, b'CONFIG cluster 0 138\r\n1\nhost|ip|11211 host||11211\n\r\nEND\r\n'), # NOQA + ] get_cluster_info('', 0) client.write.assert_has_calls([ call(b'version\n'), @@ -73,44 +65,40 @@ def test_last_versions(Telnet): ]) +@raises(WrongProtocolData) @patch('django_elastipymemcache.cluster_utils.Telnet') -def test_prev_versions(Telnet): +def test_no_configuration_protocol_support_with_errors(Telnet): client = Telnet.return_value - client.read_until.side_effect = TEST_PROTOCOL_2_READ_UNTIL - client.expect.side_effect = TEST_PROTOCOL_2_EXPECT - get_cluster_info('', 0) - client.write.assert_has_calls([ - call(b'version\n'), - call(b'get AmazonElastiCache:cluster\n'), - ]) + client.read_until.side_effect = [ + b'VERSION 1.4.34', + ] + client.expect.side_effect = [ + (0, None, b'ERROR\r\n'), + ] + get_cluster_info('test', 0) +@raises(WrongProtocolData) @patch('django_elastipymemcache.cluster_utils.Telnet') -def test_ubuntu_protocol(Telnet): +def test_hoge(Telnet): client = Telnet.return_value - 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.') - get_cluster_info('', 0) - - client.write.assert_has_calls([ - call(b'version\n'), - call(b'config get cluster\n'), - ]) + client.read_until.side_effect = [ + b'VERSION 1.4.34', + ] + client.expect.side_effect = [ + (0, None, b'CONFIG cluster 0 138\r\nfail\nhost|ip|11211 host||11211\n\r\nEND\r\n'), # NOQA + ] + get_cluster_info('test', 0) @raises(WrongProtocolData) @patch('django_elastipymemcache.cluster_utils.Telnet') -def test_no_configuration_protocol_support_with_errors(Telnet): +def test_no_configuration_protocol_support_with_errors_but_ignored(Telnet): client = Telnet.return_value - client.read_until.side_effect = TEST_PROTOCOL_4_READ_UNTIL - client.expect.side_effect = TEST_PROTOCOL_4_EXPECT + client.read_until.side_effect = [ + b'VERSION 1.4.34', + ] + client.expect.side_effect = [ + (0, None, b'CONFIG cluster 0 138\r\n1\nfail\n\r\nEND\r\n'), # NOQA + ] get_cluster_info('test', 0) - client.write.assert_has_calls([ - call(b'version\n'), - call(b'config get cluster\n'), - ]) From 915e31b41dc7a6267f2cf5f736ece576b879bf23 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Sat, 27 Jun 2020 23:29:30 +0900 Subject: [PATCH 114/149] Bump version to 1.3.1 --- django_elastipymemcache/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_elastipymemcache/__init__.py b/django_elastipymemcache/__init__.py index 619230e..528a100 100644 --- a/django_elastipymemcache/__init__.py +++ b/django_elastipymemcache/__init__.py @@ -1,2 +1,2 @@ -VERSION = (1, 3, 0) +VERSION = (1, 3, 1) __version__ = '.'.join(map(str, VERSION)) From 1fefcd0917ecde8f34fdb1c6ce80d980378aebfb Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Sun, 28 Jun 2020 00:06:13 +0900 Subject: [PATCH 115/149] Remove super's args --- django_elastipymemcache/client.py | 2 +- django_elastipymemcache/cluster_utils.py | 5 +++-- django_elastipymemcache/memcached.py | 18 +++++++++--------- tests/test_backend.py | 3 +-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/django_elastipymemcache/client.py b/django_elastipymemcache/client.py index 0f67743..c7392a5 100644 --- a/django_elastipymemcache/client.py +++ b/django_elastipymemcache/client.py @@ -4,7 +4,7 @@ class Client(HashClient): def get_many(self, keys, gets=False, *args, **kwargs): # pymemcache's HashClient may returns {'key': False} - end = super(Client, self).get_many(keys, gets, *args, **kwargs) + end = super().get_many(keys, gets, *args, **kwargs) return {key: end.get(key) for key in end if end.get(key)} diff --git a/django_elastipymemcache/cluster_utils.py b/django_elastipymemcache/cluster_utils.py index 15f2d9f..aeb0473 100644 --- a/django_elastipymemcache/cluster_utils.py +++ b/django_elastipymemcache/cluster_utils.py @@ -15,8 +15,9 @@ class WrongProtocolData(ValueError): in telnet protocol """ def __init__(self, cmd, response): - super(WrongProtocolData, self).__init__( - 'Unexpected response {} for command {}'.format(response, cmd)) + super().__init__( + 'Unexpected response {} for command {}'.format(response, cmd), + ) def get_cluster_info( diff --git a/django_elastipymemcache/memcached.py b/django_elastipymemcache/memcached.py index cc676b3..66b8ff1 100644 --- a/django_elastipymemcache/memcached.py +++ b/django_elastipymemcache/memcached.py @@ -13,7 +13,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 @@ -53,10 +53,10 @@ class ElastiPyMemCache(BaseMemcachedCache): it used pyMemcache """ def __init__(self, server, params): - super(ElastiPyMemCache, self).__init__( + super().__init__( server, params, - library=pyMemcache_client, + library=pymemcache_client, value_not_found_exception=ValueError) if len(self._servers) > 1: raise InvalidCacheBackendError( @@ -115,24 +115,24 @@ def _cache(self): @invalidate_cache_after_error def get(self, *args, **kwargs): - return super(ElastiPyMemCache, self).get(*args, **kwargs) + return super().get(*args, **kwargs) @invalidate_cache_after_error def get_many(self, *args, **kwargs): - return super(ElastiPyMemCache, self).get_many(*args, **kwargs) + return super().get_many(*args, **kwargs) @invalidate_cache_after_error def set(self, *args, **kwargs): - return super(ElastiPyMemCache, self).set(*args, **kwargs) + return super().set(*args, **kwargs) @invalidate_cache_after_error def set_many(self, *args, **kwargs): - return super(ElastiPyMemCache, self).set_many(*args, **kwargs) + return super().set_many(*args, **kwargs) @invalidate_cache_after_error def delete(self, *args, **kwargs): - return super(ElastiPyMemCache, self).delete(*args, **kwargs) + return super().delete(*args, **kwargs) @invalidate_cache_after_error def delete_many(self, *args, **kwargs): - return super(ElastiPyMemCache, self).delete_many(*args, **kwargs) + return super().delete_many(*args, **kwargs) diff --git a/tests/test_backend.py b/tests/test_backend.py index 72edf68..2311a35 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -140,8 +140,7 @@ def test_client_get_many(get_cluster_info): ret = backend.get_many(['key2']) eq_(ret, {}) - with patch('django_elastipymemcache.client.Client.get_many'), \ - patch('pymemcache.client.hash.HashClient._safely_run_func') as p2: + with patch('pymemcache.client.hash.HashClient._safely_run_func') as p2: p2.return_value = { ':1:key3': 1509111630.048594 } From 7fa8d473a90190cd9fc26425c6872b1c02548047 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Sun, 28 Jun 2020 01:58:42 +0900 Subject: [PATCH 116/149] Support incr, decr and disconnect_all --- django_elastipymemcache/client.py | 17 ++++ django_elastipymemcache/cluster_utils.py | 8 +- django_elastipymemcache/memcached.py | 100 +++++++++++------------ tests/test_backend.py | 91 ++++++++++++--------- tests/test_protocol.py | 26 +++++- 5 files changed, 141 insertions(+), 101 deletions(-) diff --git a/django_elastipymemcache/client.py b/django_elastipymemcache/client.py index c7392a5..fe3750c 100644 --- a/django_elastipymemcache/client.py +++ b/django_elastipymemcache/client.py @@ -1,7 +1,20 @@ from pymemcache.client.hash import HashClient +from pymemcache.serde import ( + python_memcache_deserializer, + python_memcache_serializer, +) class Client(HashClient): + def __init__(self, *args, **kwargs): + kwargs.setdefault('serializer', python_memcache_serializer) + kwargs.setdefault('deserializer', python_memcache_deserializer) + + super(Client, self).__init__( + *args, + **kwargs + ) + def get_many(self, keys, gets=False, *args, **kwargs): # pymemcache's HashClient may returns {'key': False} end = super().get_many(keys, gets, *args, **kwargs) @@ -9,3 +22,7 @@ def get_many(self, keys, gets=False, *args, **kwargs): return {key: end.get(key) for key in end if end.get(key)} get_multi = get_many + + def disconnect_all(self): + for client in self.clients.values(): + client.close() diff --git a/django_elastipymemcache/cluster_utils.py b/django_elastipymemcache/cluster_utils.py index aeb0473..5803396 100644 --- a/django_elastipymemcache/cluster_utils.py +++ b/django_elastipymemcache/cluster_utils.py @@ -1,5 +1,5 @@ """ -utils for discovery cluster +Utils for discovery cluster """ import re from distutils.version import StrictVersion @@ -26,11 +26,11 @@ def get_cluster_info( ignore_cluster_errors=False, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): """ - return dict with info about nodes in cluster and current version + Return dict with info about nodes in cluster and current version { 'nodes': [ - 'IP:port', - 'IP:port', + 'IP:Port', + 'IP:Port', ], 'version': '1.4.4' } diff --git a/django_elastipymemcache/memcached.py b/django_elastipymemcache/memcached.py index 66b8ff1..d713659 100644 --- a/django_elastipymemcache/memcached.py +++ b/django_elastipymemcache/memcached.py @@ -5,11 +5,6 @@ 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 @@ -20,22 +15,9 @@ 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 + Catch any exception and invalidate internal cache with list of nodes """ @wraps(f) def wrapper(self, *args, **kwds): @@ -49,37 +31,47 @@ def wrapper(self, *args, **kwds): class ElastiPyMemCache(BaseMemcachedCache): """ - backend for Amazon ElastiCache (memcached) with auto discovery mode - it used pyMemcache + Backend for Amazon ElastiCache (memcached) with auto discovery mode + it used pymemcache """ def __init__(self, server, params): + params['OPTIONS'] = params.get('OPTIONS', {}) + params['OPTIONS'].setdefault('ignore_exc', True) + + self._cluster_timeout = params['OPTIONS'].pop( + 'cluster_timeout', + socket._GLOBAL_DEFAULT_TIMEOUT, + ) + self._ignore_cluster_errors = params['OPTIONS'].pop( + 'ignore_cluster_errors', + False, + ) + super().__init__( server, params, library=pymemcache_client, - value_not_found_exception=ValueError) + value_not_found_exception=ValueError, + ) + if len(self._servers) > 1: raise InvalidCacheBackendError( 'ElastiCache should be configured with only one server ' - '(Configuration Endpoint)') + '(Configuration Endpoint)', + ) + if len(self._servers[0].split(':')) != 2: raise InvalidCacheBackendError( - 'Server configuration should be in format IP:port') - - self._cluster_timeout = self._options.get( - 'cluster_timeout', socket._GLOBAL_DEFAULT_TIMEOUT) - self._ignore_cluster_errors = self._options.get( - 'ignore_cluster_errors', False) + 'Server configuration should be in format IP:Port', + ) def clear_cluster_nodes_cache(self): - """clear internal cache with list of nodes in cluster""" + """Clear internal cache with list of nodes in cluster""" if hasattr(self, '_client'): del self._client def get_cluster_nodes(self): - """ - return list with all nodes in cluster - """ + """Return list with all nodes in cluster""" server, port = self._servers[0].split(':') try: return get_cluster_info( @@ -92,47 +84,49 @@ def get_cluster_nodes(self): logger.debug( 'Cannot connect to cluster %s, err: %s', self._servers[0], - err + err, ) return [] @property 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) - options.pop('ignore_cluster_errors', None) - self._client = self._lib.Client( - self.get_cluster_nodes(), **options) - + self.get_cluster_nodes(), **self._options) return self._client @invalidate_cache_after_error - def get(self, *args, **kwargs): - return super().get(*args, **kwargs) + def add(self, *args, **kwargs): + return super().add(*args, **kwargs) @invalidate_cache_after_error - def get_many(self, *args, **kwargs): - return super().get_many(*args, **kwargs) + def get(self, *args, **kwargs): + return super().get(*args, **kwargs) @invalidate_cache_after_error def set(self, *args, **kwargs): return super().set(*args, **kwargs) - @invalidate_cache_after_error - def set_many(self, *args, **kwargs): - return super().set_many(*args, **kwargs) - @invalidate_cache_after_error def delete(self, *args, **kwargs): return super().delete(*args, **kwargs) + @invalidate_cache_after_error + def get_many(self, *args, **kwargs): + return super().get_many(*args, **kwargs) + + @invalidate_cache_after_error + def set_many(self, *args, **kwargs): + return super().set_many(*args, **kwargs) + @invalidate_cache_after_error def delete_many(self, *args, **kwargs): return super().delete_many(*args, **kwargs) + + @invalidate_cache_after_error + def incr(self, *args, **kwargs): + return super().incr(*args, **kwargs) + + @invalidate_cache_after_error + def decr(self, *args, **kwargs): + return super().decr(*args, **kwargs) diff --git a/tests/test_backend.py b/tests/test_backend.py index 2311a35..ca9e8e1 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -1,11 +1,8 @@ import socket -from unittest.mock import patch, Mock - -try: - import cPickle as pickle -except ImportError: - import pickle - +from unittest.mock import ( + patch, + Mock, +) from django.core.cache import InvalidCacheBackendError from nose.tools import ( @@ -16,27 +13,19 @@ @raises(InvalidCacheBackendError) def test_multiple_servers(): - from django_elastipymemcache.memcached import ( - ElastiPyMemCache, - ) + from django_elastipymemcache.memcached import ElastiPyMemCache ElastiPyMemCache('h1:0,h2:0', {}) @raises(InvalidCacheBackendError) def test_wrong_server_format(): - from django_elastipymemcache.memcached import ( - ElastiPyMemCache, - ) + from django_elastipymemcache.memcached import ElastiPyMemCache ElastiPyMemCache('h', {}) @patch('django_elastipymemcache.memcached.get_cluster_info') def test_split_servers(get_cluster_info): - from django_elastipymemcache.memcached import ( - ElastiPyMemCache, - deserialize_pickle, - serialize_pickle, - ) + from django_elastipymemcache.memcached import ElastiPyMemCache backend = ElastiPyMemCache('h:0', {}) servers = [('h1', 0), ('h2', 0)] get_cluster_info.return_value = { @@ -48,19 +37,13 @@ def test_split_servers(get_cluster_info): 'h', '0', False, socket._GLOBAL_DEFAULT_TIMEOUT) backend._lib.Client.assert_called_once_with( servers, - deserializer=deserialize_pickle, ignore_exc=True, - serializer=serialize_pickle ) @patch('django_elastipymemcache.memcached.get_cluster_info') def test_node_info_cache(get_cluster_info): - from django_elastipymemcache.memcached import ( - ElastiPyMemCache, - deserialize_pickle, - serialize_pickle, - ) + from django_elastipymemcache.memcached import ElastiPyMemCache servers = [('h1', 0), ('h2', 0)] get_cluster_info.return_value = { 'nodes': servers @@ -74,9 +57,7 @@ def test_node_info_cache(get_cluster_info): backend.get('key2') backend._lib.Client.assert_called_once_with( servers, - deserializer=deserialize_pickle, ignore_exc=True, - serializer=serialize_pickle ) eq_(backend._cache.get.call_count, 2) eq_(backend._cache.set.call_count, 2) @@ -121,6 +102,34 @@ def test_invalidate_cache(get_cluster_info): eq_(get_cluster_info.call_count, 3) +@patch('django_elastipymemcache.memcached.get_cluster_info') +def test_client_add(get_cluster_info): + from django_elastipymemcache.memcached import ElastiPyMemCache + + servers = [('h1', 0), ('h2', 0)] + get_cluster_info.return_value = { + 'nodes': servers + } + + backend = ElastiPyMemCache('h:0', {}) + ret = backend.add('key1', 'value1') + eq_(ret, False) + + +@patch('django_elastipymemcache.memcached.get_cluster_info') +def test_client_delete(get_cluster_info): + from django_elastipymemcache.memcached import ElastiPyMemCache + + servers = [('h1', 0), ('h2', 0)] + get_cluster_info.return_value = { + 'nodes': servers + } + + backend = ElastiPyMemCache('h:0', {}) + ret = backend.delete('key1') + eq_(ret, None) + + @patch('django_elastipymemcache.memcached.get_cluster_info') def test_client_get_many(get_cluster_info): from django_elastipymemcache.memcached import ElastiPyMemCache @@ -194,7 +203,7 @@ def test_client_set_many(get_cluster_info): @patch('django_elastipymemcache.memcached.get_cluster_info') -def test_client_delete(get_cluster_info): +def test_client_delete_many(get_cluster_info): from django_elastipymemcache.memcached import ElastiPyMemCache servers = [('h1', 0), ('h2', 0)] @@ -203,12 +212,12 @@ def test_client_delete(get_cluster_info): } backend = ElastiPyMemCache('h:0', {}) - ret = backend.delete('key1') + ret = backend.delete_many(['key1', 'key2']) eq_(ret, None) @patch('django_elastipymemcache.memcached.get_cluster_info') -def test_client_delete_many(get_cluster_info): +def test_client_incr(get_cluster_info): from django_elastipymemcache.memcached import ElastiPyMemCache servers = [('h1', 0), ('h2', 0)] @@ -217,17 +226,19 @@ def test_client_delete_many(get_cluster_info): } backend = ElastiPyMemCache('h:0', {}) - ret = backend.delete_many(['key1', 'key2']) - eq_(ret, None) + ret = backend.incr('key1', 1) + eq_(ret, False) -def test_serialize_pickle(): - from django_elastipymemcache.memcached import serialize_pickle - eq_(serialize_pickle('key', 'str'), ('str', 1)) - eq_(serialize_pickle('key', 0), (pickle.dumps(0), 2)) +@patch('django_elastipymemcache.memcached.get_cluster_info') +def test_client_decr(get_cluster_info): + from django_elastipymemcache.memcached import ElastiPyMemCache + servers = [('h1', 0), ('h2', 0)] + get_cluster_info.return_value = { + 'nodes': servers + } -def test_deserialize_pickle(): - from django_elastipymemcache.memcached import deserialize_pickle - eq_(deserialize_pickle('key', 'str', 1), 'str') - eq_(deserialize_pickle('key', pickle.dumps(0), 2), 0) + backend = ElastiPyMemCache('h:0', {}) + ret = backend.decr('key1', 1) + eq_(ret, False) diff --git a/tests/test_protocol.py b/tests/test_protocol.py index bc6a3af..64b8ace 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -26,7 +26,7 @@ @patch('django_elastipymemcache.cluster_utils.Telnet') -def test_happy_path(Telnet): +def test_get_cluster_info(Telnet): client = Telnet.return_value client.read_until.side_effect = [ b'VERSION 1.4.14', @@ -43,6 +43,24 @@ def test_happy_path(Telnet): ]) +@patch('django_elastipymemcache.cluster_utils.Telnet') +def test_get_cluster_info_before_1_4_13(Telnet): + client = Telnet.return_value + client.read_until.side_effect = [ + b'VERSION 1.4.13', + ] + client.expect.side_effect = [ + (0, None, EXAMPLE_RESPONSE), # NOQA + ] + info = get_cluster_info('', 0) + eq_(info['version'], 12) + eq_(info['nodes'], [('10.82.235.120', 11211), ('10.80.249.27', 11211)]) + client.write.assert_has_calls([ + call(b'version\n'), + call(b'get AmazonElastiCache:cluster\n'), + ]) + + @raises(WrongProtocolData) @patch('django_elastipymemcache.cluster_utils.Telnet', MagicMock()) def test_bad_protocol(): @@ -56,7 +74,7 @@ def test_ubuntu_protocol(Telnet): b'VERSION 1.4.14 (Ubuntu)', ] client.expect.side_effect = [ - (0, None, b'CONFIG cluster 0 138\r\n1\nhost|ip|11211 host||11211\n\r\nEND\r\n'), # NOQA + (0, None, EXAMPLE_RESPONSE), # NOQA ] get_cluster_info('', 0) client.write.assert_has_calls([ @@ -80,7 +98,7 @@ def test_no_configuration_protocol_support_with_errors(Telnet): @raises(WrongProtocolData) @patch('django_elastipymemcache.cluster_utils.Telnet') -def test_hoge(Telnet): +def test_cannot_parse_version(Telnet): client = Telnet.return_value client.read_until.side_effect = [ b'VERSION 1.4.34', @@ -93,7 +111,7 @@ def test_hoge(Telnet): @raises(WrongProtocolData) @patch('django_elastipymemcache.cluster_utils.Telnet') -def test_no_configuration_protocol_support_with_errors_but_ignored(Telnet): +def test_cannot_parse_nodes(Telnet): client = Telnet.return_value client.read_until.side_effect = [ b'VERSION 1.4.34', From d21d23b55a8e42243b4003e1318b2c2a141591e4 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 29 Jun 2020 13:09:59 +0900 Subject: [PATCH 117/149] Depends django-pymemcache --- README.rst | 1 + django_elastipymemcache/client.py | 28 ------------------------ django_elastipymemcache/cluster_utils.py | 12 ++++++++-- django_elastipymemcache/memcached.py | 4 ++-- tests/test_backend.py | 18 +++++++-------- tests/test_protocol.py | 4 ++-- 6 files changed, 24 insertions(+), 43 deletions(-) delete mode 100644 django_elastipymemcache/client.py diff --git a/README.rst b/README.rst index 1898db2..66fc594 100644 --- a/README.rst +++ b/README.rst @@ -22,6 +22,7 @@ Requirements * pymemcache * Django>=2.2 +* django-pymemcache>=1.0 Installation ------------ diff --git a/django_elastipymemcache/client.py b/django_elastipymemcache/client.py deleted file mode 100644 index fe3750c..0000000 --- a/django_elastipymemcache/client.py +++ /dev/null @@ -1,28 +0,0 @@ -from pymemcache.client.hash import HashClient -from pymemcache.serde import ( - python_memcache_deserializer, - python_memcache_serializer, -) - - -class Client(HashClient): - def __init__(self, *args, **kwargs): - kwargs.setdefault('serializer', python_memcache_serializer) - kwargs.setdefault('deserializer', python_memcache_deserializer) - - super(Client, self).__init__( - *args, - **kwargs - ) - - def get_many(self, keys, gets=False, *args, **kwargs): - # pymemcache's HashClient may returns {'key': False} - end = super().get_many(keys, gets, *args, **kwargs) - - return {key: end.get(key) for key in end if end.get(key)} - - get_multi = get_many - - def disconnect_all(self): - for client in self.clients.values(): - client.close() diff --git a/django_elastipymemcache/cluster_utils.py b/django_elastipymemcache/cluster_utils.py index 5803396..7d10d02 100644 --- a/django_elastipymemcache/cluster_utils.py +++ b/django_elastipymemcache/cluster_utils.py @@ -57,7 +57,10 @@ def get_cluster_info( return { 'version': version, 'nodes': [ - (smart_text(host), int(port)) + '{host}:{port:d}'.format( + host=smart_text(host), + port=int(port), + ), ] } @@ -73,7 +76,12 @@ def get_cluster_info( try: for node in ls[2].split(b' '): host, ip, port = node.split(b'|') - nodes.append((smart_text(ip or host), int(port))) + nodes.append( + '{host}:{port:d}'.format( + host=smart_text(ip or host), + port=int(port), + ), + ) except ValueError: raise WrongProtocolData(cmd, res) return { diff --git a/django_elastipymemcache/memcached.py b/django_elastipymemcache/memcached.py index d713659..1faee3c 100644 --- a/django_elastipymemcache/memcached.py +++ b/django_elastipymemcache/memcached.py @@ -8,7 +8,7 @@ from django.core.cache import InvalidCacheBackendError from django.core.cache.backends.memcached import BaseMemcachedCache -from . import client as pymemcache_client +from djpymemcache import client as djpymemcache_client from .cluster_utils import get_cluster_info @@ -50,7 +50,7 @@ def __init__(self, server, params): super().__init__( server, params, - library=pymemcache_client, + library=djpymemcache_client, value_not_found_exception=ValueError, ) diff --git a/tests/test_backend.py b/tests/test_backend.py index ca9e8e1..2fc40ce 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -44,7 +44,7 @@ def test_split_servers(get_cluster_info): @patch('django_elastipymemcache.memcached.get_cluster_info') def test_node_info_cache(get_cluster_info): from django_elastipymemcache.memcached import ElastiPyMemCache - servers = [('h1', 0), ('h2', 0)] + servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } @@ -77,7 +77,7 @@ def test_failed_to_connect_servers(get_cluster_info): @patch('django_elastipymemcache.memcached.get_cluster_info') def test_invalidate_cache(get_cluster_info): from django_elastipymemcache.memcached import ElastiPyMemCache - servers = [('h1', 0), ('h2', 0)] + servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } @@ -106,7 +106,7 @@ def test_invalidate_cache(get_cluster_info): def test_client_add(get_cluster_info): from django_elastipymemcache.memcached import ElastiPyMemCache - servers = [('h1', 0), ('h2', 0)] + servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } @@ -120,7 +120,7 @@ def test_client_add(get_cluster_info): def test_client_delete(get_cluster_info): from django_elastipymemcache.memcached import ElastiPyMemCache - servers = [('h1', 0), ('h2', 0)] + servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } @@ -134,7 +134,7 @@ def test_client_delete(get_cluster_info): def test_client_get_many(get_cluster_info): from django_elastipymemcache.memcached import ElastiPyMemCache - servers = [('h1', 0), ('h2', 0)] + servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } @@ -192,7 +192,7 @@ def test_client_get_many(get_cluster_info): def test_client_set_many(get_cluster_info): from django_elastipymemcache.memcached import ElastiPyMemCache - servers = [('h1', 0), ('h2', 0)] + servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } @@ -206,7 +206,7 @@ def test_client_set_many(get_cluster_info): def test_client_delete_many(get_cluster_info): from django_elastipymemcache.memcached import ElastiPyMemCache - servers = [('h1', 0), ('h2', 0)] + servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } @@ -220,7 +220,7 @@ def test_client_delete_many(get_cluster_info): def test_client_incr(get_cluster_info): from django_elastipymemcache.memcached import ElastiPyMemCache - servers = [('h1', 0), ('h2', 0)] + servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } @@ -234,7 +234,7 @@ def test_client_incr(get_cluster_info): def test_client_decr(get_cluster_info): from django_elastipymemcache.memcached import ElastiPyMemCache - servers = [('h1', 0), ('h2', 0)] + servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 64b8ace..7f99a91 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -36,7 +36,7 @@ def test_get_cluster_info(Telnet): ] info = get_cluster_info('', 0) eq_(info['version'], 12) - eq_(info['nodes'], [('10.82.235.120', 11211), ('10.80.249.27', 11211)]) + eq_(info['nodes'], ['10.82.235.120:11211', '10.80.249.27:11211']) client.write.assert_has_calls([ call(b'version\n'), call(b'config get cluster\n'), @@ -54,7 +54,7 @@ def test_get_cluster_info_before_1_4_13(Telnet): ] info = get_cluster_info('', 0) eq_(info['version'], 12) - eq_(info['nodes'], [('10.82.235.120', 11211), ('10.80.249.27', 11211)]) + eq_(info['nodes'], ['10.82.235.120:11211', '10.80.249.27:11211']) client.write.assert_has_calls([ call(b'version\n'), call(b'get AmazonElastiCache:cluster\n'), From 9a8e6e6389d855ecc9dcbad73d82ff42cb9adb4e Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 29 Jun 2020 13:15:40 +0900 Subject: [PATCH 118/149] Add dependencies --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 1661a13..974322c 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,7 @@ passenv = TOXENV CI TRAVIS TRAVIS_* CODECOV_* deps = dj22: Django<2.3 dj30: Django<3.1 + django-pymemcache<2.0 djdev: https://github.com/django/django/archive/master.tar.gz -r{toxinidir}/requirements.txt py38-dj30: codecov From fe3b55e35b0de335e03f823a862b59dd1027fa8e Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 29 Jun 2020 18:02:55 +0900 Subject: [PATCH 119/149] Rename class and module names (to respect pymemcache) --- README.rst | 2 +- django_elastipymemcache/__init__.py | 2 +- .../{memcached.py => backend.py} | 2 +- tests/test_backend.py | 74 +++++++++---------- 4 files changed, 40 insertions(+), 40 deletions(-) rename django_elastipymemcache/{memcached.py => backend.py} (98%) diff --git a/README.rst b/README.rst index 66fc594..fb5ef2f 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,7 @@ Your cache backend should look something like this:: CACHES = { 'default': { - 'BACKEND': 'django_elastipymemcache.memcached.ElastiPyMemCache', + 'BACKEND': 'django_elastipymemcache.backend.ElastiPymemcache', 'LOCATION': '[configuration endpoint]:11211', 'OPTIONS': { 'cluster_timeout': 1, # its used when get cluster info diff --git a/django_elastipymemcache/__init__.py b/django_elastipymemcache/__init__.py index 528a100..2c96ce3 100644 --- a/django_elastipymemcache/__init__.py +++ b/django_elastipymemcache/__init__.py @@ -1,2 +1,2 @@ -VERSION = (1, 3, 1) +VERSION = (2, 0, 0) __version__ = '.'.join(map(str, VERSION)) diff --git a/django_elastipymemcache/memcached.py b/django_elastipymemcache/backend.py similarity index 98% rename from django_elastipymemcache/memcached.py rename to django_elastipymemcache/backend.py index 1faee3c..a27619e 100644 --- a/django_elastipymemcache/memcached.py +++ b/django_elastipymemcache/backend.py @@ -29,7 +29,7 @@ def wrapper(self, *args, **kwds): return wrapper -class ElastiPyMemCache(BaseMemcachedCache): +class ElastiPymemcache(BaseMemcachedCache): """ Backend for Amazon ElastiCache (memcached) with auto discovery mode it used pymemcache diff --git a/tests/test_backend.py b/tests/test_backend.py index 2fc40ce..1097a1f 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -13,20 +13,20 @@ @raises(InvalidCacheBackendError) def test_multiple_servers(): - from django_elastipymemcache.memcached import ElastiPyMemCache - ElastiPyMemCache('h1:0,h2:0', {}) + from django_elastipymemcache.backend import ElastiPymemcache + ElastiPymemcache('h1:0,h2:0', {}) @raises(InvalidCacheBackendError) def test_wrong_server_format(): - from django_elastipymemcache.memcached import ElastiPyMemCache - ElastiPyMemCache('h', {}) + from django_elastipymemcache.backend import ElastiPymemcache + ElastiPymemcache('h', {}) -@patch('django_elastipymemcache.memcached.get_cluster_info') +@patch('django_elastipymemcache.backend.get_cluster_info') def test_split_servers(get_cluster_info): - from django_elastipymemcache.memcached import ElastiPyMemCache - backend = ElastiPyMemCache('h:0', {}) + from django_elastipymemcache.backend import ElastiPymemcache + backend = ElastiPymemcache('h:0', {}) servers = [('h1', 0), ('h2', 0)] get_cluster_info.return_value = { 'nodes': servers @@ -41,15 +41,15 @@ def test_split_servers(get_cluster_info): ) -@patch('django_elastipymemcache.memcached.get_cluster_info') +@patch('django_elastipymemcache.backend.get_cluster_info') def test_node_info_cache(get_cluster_info): - from django_elastipymemcache.memcached import ElastiPyMemCache + from django_elastipymemcache.backend import ElastiPymemcache servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } - backend = ElastiPyMemCache('h:0', {}) + backend = ElastiPymemcache('h:0', {}) backend._lib.Client = Mock() backend.set('key1', 'val') backend.get('key1') @@ -66,23 +66,23 @@ def test_node_info_cache(get_cluster_info): 'h', '0', False, socket._GLOBAL_DEFAULT_TIMEOUT) -@patch('django_elastipymemcache.memcached.get_cluster_info') +@patch('django_elastipymemcache.backend.get_cluster_info') def test_failed_to_connect_servers(get_cluster_info): - from django_elastipymemcache.memcached import ElastiPyMemCache - backend = ElastiPyMemCache('h:0', {}) + from django_elastipymemcache.backend import ElastiPymemcache + backend = ElastiPymemcache('h:0', {}) get_cluster_info.side_effect = OSError() eq_(backend.get_cluster_nodes(), []) -@patch('django_elastipymemcache.memcached.get_cluster_info') +@patch('django_elastipymemcache.backend.get_cluster_info') def test_invalidate_cache(get_cluster_info): - from django_elastipymemcache.memcached import ElastiPyMemCache + from django_elastipymemcache.backend import ElastiPymemcache servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } - backend = ElastiPyMemCache('h:0', {}) + backend = ElastiPymemcache('h:0', {}) backend._lib.Client = Mock() assert backend._cache backend._cache.get = Mock() @@ -102,44 +102,44 @@ def test_invalidate_cache(get_cluster_info): eq_(get_cluster_info.call_count, 3) -@patch('django_elastipymemcache.memcached.get_cluster_info') +@patch('django_elastipymemcache.backend.get_cluster_info') def test_client_add(get_cluster_info): - from django_elastipymemcache.memcached import ElastiPyMemCache + from django_elastipymemcache.backend import ElastiPymemcache servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } - backend = ElastiPyMemCache('h:0', {}) + backend = ElastiPymemcache('h:0', {}) ret = backend.add('key1', 'value1') eq_(ret, False) -@patch('django_elastipymemcache.memcached.get_cluster_info') +@patch('django_elastipymemcache.backend.get_cluster_info') def test_client_delete(get_cluster_info): - from django_elastipymemcache.memcached import ElastiPyMemCache + from django_elastipymemcache.backend import ElastiPymemcache servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } - backend = ElastiPyMemCache('h:0', {}) + backend = ElastiPymemcache('h:0', {}) ret = backend.delete('key1') eq_(ret, None) -@patch('django_elastipymemcache.memcached.get_cluster_info') +@patch('django_elastipymemcache.backend.get_cluster_info') def test_client_get_many(get_cluster_info): - from django_elastipymemcache.memcached import ElastiPyMemCache + from django_elastipymemcache.backend import ElastiPymemcache servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } - backend = ElastiPyMemCache('h:0', {}) + backend = ElastiPymemcache('h:0', {}) ret = backend.get_many(['key1']) eq_(ret, {}) @@ -188,57 +188,57 @@ def test_client_get_many(get_cluster_info): ) -@patch('django_elastipymemcache.memcached.get_cluster_info') +@patch('django_elastipymemcache.backend.get_cluster_info') def test_client_set_many(get_cluster_info): - from django_elastipymemcache.memcached import ElastiPyMemCache + from django_elastipymemcache.backend import ElastiPymemcache servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } - backend = ElastiPyMemCache('h:0', {}) + backend = ElastiPymemcache('h:0', {}) ret = backend.set_many({'key1': 'value1', 'key2': 'value2'}) eq_(ret, ['key1', 'key2']) -@patch('django_elastipymemcache.memcached.get_cluster_info') +@patch('django_elastipymemcache.backend.get_cluster_info') def test_client_delete_many(get_cluster_info): - from django_elastipymemcache.memcached import ElastiPyMemCache + from django_elastipymemcache.backend import ElastiPymemcache servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } - backend = ElastiPyMemCache('h:0', {}) + backend = ElastiPymemcache('h:0', {}) ret = backend.delete_many(['key1', 'key2']) eq_(ret, None) -@patch('django_elastipymemcache.memcached.get_cluster_info') +@patch('django_elastipymemcache.backend.get_cluster_info') def test_client_incr(get_cluster_info): - from django_elastipymemcache.memcached import ElastiPyMemCache + from django_elastipymemcache.backend import ElastiPymemcache servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } - backend = ElastiPyMemCache('h:0', {}) + backend = ElastiPymemcache('h:0', {}) ret = backend.incr('key1', 1) eq_(ret, False) -@patch('django_elastipymemcache.memcached.get_cluster_info') +@patch('django_elastipymemcache.backend.get_cluster_info') def test_client_decr(get_cluster_info): - from django_elastipymemcache.memcached import ElastiPyMemCache + from django_elastipymemcache.backend import ElastiPymemcache servers = ['h1:0', 'h2:0'] get_cluster_info.return_value = { 'nodes': servers } - backend = ElastiPyMemCache('h:0', {}) + backend = ElastiPymemcache('h:0', {}) ret = backend.decr('key1', 1) eq_(ret, False) From 8f2ceb623708efa5010753a3503d468f9183689b Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 29 Jun 2020 20:06:04 +0900 Subject: [PATCH 120/149] Add ConfigurationEndpointClient based on pymemcache.client.Client --- django_elastipymemcache/client.py | 84 +++++++++++++++++++++ tests/test_client.py | 117 ++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 django_elastipymemcache/client.py create mode 100644 tests/test_client.py diff --git a/django_elastipymemcache/client.py b/django_elastipymemcache/client.py new file mode 100644 index 0000000..0313ef9 --- /dev/null +++ b/django_elastipymemcache/client.py @@ -0,0 +1,84 @@ +import logging +from distutils.version import StrictVersion + +from django.utils.encoding import smart_text +from pymemcache.exceptions import ( + MemcacheUnknownError, +) +from pymemcache.client.base import ( + _readline, + Client, +) + + +logger = logging.getLogger(__name__) + + +class ConfigurationEndpointClient(Client): + # https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/AutoDiscovery.AddingToYourClientLibrary.html + + def __init__(self, *args, ignore_cluster_errors=False, **kwargs): + self.ignore_cluster_errors = ignore_cluster_errors + return super().__init__(*args, **kwargs) + + def _get_cluster_info_cmd(self): + if StrictVersion(smart_text(self.version())) < StrictVersion('1.4.14'): + return b'get AmazonElastiCache:cluster\r\n' + return b'config get cluster\r\n' + + def _extract_cluster_info(self, line): + raw_version, raw_nodes, _ = line.split(b'\n') + nodes = [] + for raw_node in raw_nodes.split(b' '): + host, ip, port = raw_node.split(b'|') + nodes.append('{host}:{port}'.format( + host=smart_text(ip or host), + port=int(port) + )) + return { + 'version': int(raw_version), + 'nodes': nodes, + } + + def _fetch_cluster_info_cmd(self, cmd, name): + if self.sock is None: + self._connect() + self.sock.sendall(cmd) + + buf = b'' + result = {} + number_of_line = 0 + + while True: + buf, line = _readline(self.sock, buf) + self._raise_errors(line, name) + if line == b'END': + if number_of_line != 2: + raise MemcacheUnknownError('Wrong response') + return result + if number_of_line == 1: + try: + result = self._extract_cluster_info(line) + except ValueError: + raise MemcacheUnknownError('Wrong format: {line}'.format( + line=line, + )) + number_of_line += 1 + + def get_cluster_info(self): + cmd = self._get_cluster_info_cmd() + try: + return self._fetch_cluster_info_cmd(cmd, 'config cluster') + except Exception as e: + if self.ignore_cluster_errors: + logger.warn('Failed to get cluster: %s', e) + return { + 'version': None, + 'nodes': [ + '{host}:{port:d}'.format( + host=self.server[0], + port=int(self.server[1]), + ), + ] + } + raise diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..274f56d --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,117 @@ +import collections +from unittest.mock import ( + call, + patch, +) + +from pymemcache.exceptions import ( + MemcacheUnknownCommandError, + MemcacheUnknownError, +) +from nose.tools import ( + eq_, + raises, +) + +from django_elastipymemcache.client import ConfigurationEndpointClient + + +EXAMPLE_RESPONSE = [ + b'CONFIG cluster 0 147\r\n', + b'12\n' + b'myCluster.pc4ldq.0001.use1.cache.amazonaws.com|10.82.235.120|11211 ' + b'myCluster.pc4ldq.0002.use1.cache.amazonaws.com|10.80.249.27|11211\n\r\n', + b'END\r\n', +] + + +@patch('socket.socket') +def test_get_cluster_info(socket): + recv_bufs = collections.deque([ + b'VERSION 1.4.14\r\n', + ] + EXAMPLE_RESPONSE) + + client = socket.return_value + client.recv.side_effect = lambda *args, **kwargs: recv_bufs.popleft() + cluster_info = ConfigurationEndpointClient(('h', 0)).get_cluster_info() + eq_(cluster_info['nodes'], ['10.82.235.120:11211', '10.80.249.27:11211']) + client.sendall.assert_has_calls([ + call(b'version\r\n'), + call(b'config get cluster\r\n'), + ]) + + +@patch('socket.socket') +def test_get_cluster_info_before_1_4_13(socket): + recv_bufs = collections.deque([ + b'VERSION 1.4.13\r\n', + ] + EXAMPLE_RESPONSE) + + client = socket.return_value + client.recv.side_effect = lambda *args, **kwargs: recv_bufs.popleft() + cluster_info = ConfigurationEndpointClient(('h', 0)).get_cluster_info() + eq_(cluster_info['nodes'], ['10.82.235.120:11211', '10.80.249.27:11211']) + client.sendall.assert_has_calls([ + call(b'version\r\n'), + call(b'get AmazonElastiCache:cluster\r\n'), + ]) + + +@raises(MemcacheUnknownCommandError) +@patch('socket.socket') +def test_no_configuration_protocol_support_with_errors(socket): + recv_bufs = collections.deque([ + b'VERSION 1.4.13\r\n', + b'ERROR\r\n', + ]) + + client = socket.return_value + client.recv.side_effect = lambda *args, **kwargs: recv_bufs.popleft() + ConfigurationEndpointClient(('h', 0)).get_cluster_info() + + +@raises(MemcacheUnknownError) +@patch('socket.socket') +def test_cannot_parse_version(socket): + recv_bufs = collections.deque([ + b'VERSION 1.4.34\r\n', + b'CONFIG cluster 0 147\r\n', + b'fail\nhost|ip|11211 host|ip|11211\n\r\n', + b'END\r\n', + ]) + + client = socket.return_value + client.recv.side_effect = lambda *args, **kwargs: recv_bufs.popleft() + ConfigurationEndpointClient(('h', 0)).get_cluster_info() + + +@raises(MemcacheUnknownError) +@patch('socket.socket') +def test_cannot_parse_nodes(socket): + recv_bufs = collections.deque([ + b'VERSION 1.4.34\r\n', + b'CONFIG cluster 0 147\r\n', + b'1\nfail\n\r\n', + b'END\r\n', + ]) + + client = socket.return_value + client.recv.side_effect = lambda *args, **kwargs: recv_bufs.popleft() + ConfigurationEndpointClient(('h', 0)).get_cluster_info() + + +@patch('socket.socket') +def test_ignore_erros(socket): + recv_bufs = collections.deque([ + b'VERSION 1.4.34\r\n', + b'fail\nfail\n\r\n', + b'END\r\n', + ]) + + client = socket.return_value + client.recv.side_effect = lambda *args, **kwargs: recv_bufs.popleft() + cluster_info = ConfigurationEndpointClient( + ('h', 0), + ignore_cluster_errors=True, + ).get_cluster_info() + eq_(cluster_info['nodes'], ['h:0']) From 530fbfbd8295cd8dd74f4ef0d69f66b63e987271 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 29 Jun 2020 20:24:49 +0900 Subject: [PATCH 121/149] Use ConfigurationEndpointClient --- django_elastipymemcache/backend.py | 40 ++++++++++++++++-------------- tests/test_backend.py | 30 +++++++++++----------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/django_elastipymemcache/backend.py b/django_elastipymemcache/backend.py index a27619e..99bb7ae 100644 --- a/django_elastipymemcache/backend.py +++ b/django_elastipymemcache/backend.py @@ -7,9 +7,9 @@ from django.core.cache import InvalidCacheBackendError from django.core.cache.backends.memcached import BaseMemcachedCache - from djpymemcache import client as djpymemcache_client -from .cluster_utils import get_cluster_info + +from .client import ConfigurationEndpointClient logger = logging.getLogger(__name__) @@ -59,40 +59,42 @@ def __init__(self, server, params): 'ElastiCache should be configured with only one server ' '(Configuration Endpoint)', ) - - if len(self._servers[0].split(':')) != 2: + try: + host, port = self._servers[0].split(':') + except ValueError: raise InvalidCacheBackendError( 'Server configuration should be in format IP:Port', ) + self.configuration_endpoint_client = ConfigurationEndpointClient( + (host, port), + ignore_cluster_errors=self._ignore_cluster_errors, + **self._options, + ) + def clear_cluster_nodes_cache(self): """Clear internal cache with list of nodes in cluster""" if hasattr(self, '_client'): del self._client def get_cluster_nodes(self): - """Return list with all nodes in cluster""" - server, port = self._servers[0].split(':') try: - return get_cluster_info( - server, - port, - self._ignore_cluster_errors, - self._cluster_timeout - )['nodes'] - except (OSError, socket.gaierror, socket.timeout) as err: - logger.debug( - 'Cannot connect to cluster %s, err: %s', - self._servers[0], - err, - ) + return self.configuration_endpoint_client \ + .get_cluster_info()['nodes'] + except ( + OSError, + socket.gaierror, + socket.timeout, + ): return [] @property def _cache(self): if getattr(self, '_client', None) is None: self._client = self._lib.Client( - self.get_cluster_nodes(), **self._options) + 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 1097a1f..c0906fa 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -10,6 +10,8 @@ raises, ) +from django_elastipymemcache.client import ConfigurationEndpointClient + @raises(InvalidCacheBackendError) def test_multiple_servers(): @@ -23,7 +25,7 @@ def test_wrong_server_format(): ElastiPymemcache('h', {}) -@patch('django_elastipymemcache.backend.get_cluster_info') +@patch.object(ConfigurationEndpointClient, 'get_cluster_info') def test_split_servers(get_cluster_info): from django_elastipymemcache.backend import ElastiPymemcache backend = ElastiPymemcache('h:0', {}) @@ -33,15 +35,14 @@ def test_split_servers(get_cluster_info): } backend._lib.Client = Mock() assert backend._cache - get_cluster_info.assert_called_once_with( - 'h', '0', False, socket._GLOBAL_DEFAULT_TIMEOUT) + get_cluster_info.assert_called() backend._lib.Client.assert_called_once_with( servers, ignore_exc=True, ) -@patch('django_elastipymemcache.backend.get_cluster_info') +@patch.object(ConfigurationEndpointClient, 'get_cluster_info') def test_node_info_cache(get_cluster_info): from django_elastipymemcache.backend import ElastiPymemcache servers = ['h1:0', 'h2:0'] @@ -62,11 +63,10 @@ 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, socket._GLOBAL_DEFAULT_TIMEOUT) + get_cluster_info.assert_called_once() -@patch('django_elastipymemcache.backend.get_cluster_info') +@patch.object(ConfigurationEndpointClient, 'get_cluster_info') def test_failed_to_connect_servers(get_cluster_info): from django_elastipymemcache.backend import ElastiPymemcache backend = ElastiPymemcache('h:0', {}) @@ -74,7 +74,7 @@ def test_failed_to_connect_servers(get_cluster_info): eq_(backend.get_cluster_nodes(), []) -@patch('django_elastipymemcache.backend.get_cluster_info') +@patch.object(ConfigurationEndpointClient, 'get_cluster_info') def test_invalidate_cache(get_cluster_info): from django_elastipymemcache.backend import ElastiPymemcache servers = ['h1:0', 'h2:0'] @@ -102,7 +102,7 @@ def test_invalidate_cache(get_cluster_info): eq_(get_cluster_info.call_count, 3) -@patch('django_elastipymemcache.backend.get_cluster_info') +@patch.object(ConfigurationEndpointClient, 'get_cluster_info') def test_client_add(get_cluster_info): from django_elastipymemcache.backend import ElastiPymemcache @@ -116,7 +116,7 @@ def test_client_add(get_cluster_info): eq_(ret, False) -@patch('django_elastipymemcache.backend.get_cluster_info') +@patch.object(ConfigurationEndpointClient, 'get_cluster_info') def test_client_delete(get_cluster_info): from django_elastipymemcache.backend import ElastiPymemcache @@ -130,7 +130,7 @@ def test_client_delete(get_cluster_info): eq_(ret, None) -@patch('django_elastipymemcache.backend.get_cluster_info') +@patch.object(ConfigurationEndpointClient, 'get_cluster_info') def test_client_get_many(get_cluster_info): from django_elastipymemcache.backend import ElastiPymemcache @@ -188,7 +188,7 @@ def test_client_get_many(get_cluster_info): ) -@patch('django_elastipymemcache.backend.get_cluster_info') +@patch.object(ConfigurationEndpointClient, 'get_cluster_info') def test_client_set_many(get_cluster_info): from django_elastipymemcache.backend import ElastiPymemcache @@ -202,7 +202,7 @@ def test_client_set_many(get_cluster_info): eq_(ret, ['key1', 'key2']) -@patch('django_elastipymemcache.backend.get_cluster_info') +@patch.object(ConfigurationEndpointClient, 'get_cluster_info') def test_client_delete_many(get_cluster_info): from django_elastipymemcache.backend import ElastiPymemcache @@ -216,7 +216,7 @@ def test_client_delete_many(get_cluster_info): eq_(ret, None) -@patch('django_elastipymemcache.backend.get_cluster_info') +@patch.object(ConfigurationEndpointClient, 'get_cluster_info') def test_client_incr(get_cluster_info): from django_elastipymemcache.backend import ElastiPymemcache @@ -230,7 +230,7 @@ def test_client_incr(get_cluster_info): eq_(ret, False) -@patch('django_elastipymemcache.backend.get_cluster_info') +@patch.object(ConfigurationEndpointClient, 'get_cluster_info') def test_client_decr(get_cluster_info): from django_elastipymemcache.backend import ElastiPymemcache From 9a5ab789e68472bc9ad0a13d69add482348658a4 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 29 Jun 2020 20:25:19 +0900 Subject: [PATCH 122/149] Remove old utils --- django_elastipymemcache/cluster_utils.py | 90 ----------------- tests/test_protocol.py | 122 ----------------------- 2 files changed, 212 deletions(-) delete mode 100644 django_elastipymemcache/cluster_utils.py delete mode 100644 tests/test_protocol.py diff --git a/django_elastipymemcache/cluster_utils.py b/django_elastipymemcache/cluster_utils.py deleted file mode 100644 index 7d10d02..0000000 --- a/django_elastipymemcache/cluster_utils.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -Utils for discovery cluster -""" -import re -from distutils.version import StrictVersion -import socket -from telnetlib import Telnet - -from django.utils.encoding import smart_text - - -class WrongProtocolData(ValueError): - """ - Exception for raising when we get something unexpected - in telnet protocol - """ - def __init__(self, cmd, response): - super().__init__( - 'Unexpected response {} for command {}'.format(response, cmd), - ) - - -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 - { - 'nodes': [ - 'IP:Port', - 'IP:Port', - ], - 'version': '1.4.4' - } - """ - 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' ') - if len(version_list) not in [2, 3] or version_list[0] != b'VERSION': - raise WrongProtocolData('version', res) - version = version_list[1] - if StrictVersion(smart_text(version)) >= StrictVersion('1.4.14'): - cmd = b'config get cluster\n' - else: - cmd = b'get AmazonElastiCache:cluster\n' - client.write(cmd) - regex_index, match_object, res = client.expect([ - re.compile(b'\n\r\nEND\r\n'), - re.compile(b'ERROR\r\n') - ]) - client.close() - - if res == b'ERROR\r\n' and ignore_cluster_errors: - return { - 'version': version, - 'nodes': [ - '{host}:{port:d}'.format( - host=smart_text(host), - port=int(port), - ), - ] - } - - ls = list(filter(None, re.compile(br'\r?\n').split(res))) - if len(ls) != 4: - raise WrongProtocolData(cmd, res) - - try: - version = int(ls[1]) - except ValueError: - raise WrongProtocolData(cmd, res) - nodes = [] - try: - for node in ls[2].split(b' '): - host, ip, port = node.split(b'|') - nodes.append( - '{host}:{port:d}'.format( - host=smart_text(ip or host), - port=int(port), - ), - ) - except ValueError: - raise WrongProtocolData(cmd, res) - return { - 'version': version, - 'nodes': nodes - } diff --git a/tests/test_protocol.py b/tests/test_protocol.py deleted file mode 100644 index 7f99a91..0000000 --- a/tests/test_protocol.py +++ /dev/null @@ -1,122 +0,0 @@ -import sys - -from django_elastipymemcache.cluster_utils import ( - WrongProtocolData, - get_cluster_info, -) -from nose.tools import ( - eq_, - raises, -) - -if sys.version < '3': - from mock import patch, call, MagicMock -else: - from unittest.mock import patch, call, MagicMock - - -# https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/AutoDiscovery.AddingToYourClientLibrary.html -EXAMPLE_RESPONSE = ( - b'CONFIG cluster 0 147\r\n' - b'12\n' - b'myCluster.pc4ldq.0001.use1.cache.amazonaws.com|10.82.235.120|11211 ' - b'myCluster.pc4ldq.0002.use1.cache.amazonaws.com|10.80.249.27|11211\n\r\n' - b'END\r\n' -) - - -@patch('django_elastipymemcache.cluster_utils.Telnet') -def test_get_cluster_info(Telnet): - client = Telnet.return_value - client.read_until.side_effect = [ - b'VERSION 1.4.14', - ] - client.expect.side_effect = [ - (0, None, EXAMPLE_RESPONSE), # NOQA - ] - info = get_cluster_info('', 0) - eq_(info['version'], 12) - eq_(info['nodes'], ['10.82.235.120:11211', '10.80.249.27:11211']) - client.write.assert_has_calls([ - call(b'version\n'), - call(b'config get cluster\n'), - ]) - - -@patch('django_elastipymemcache.cluster_utils.Telnet') -def test_get_cluster_info_before_1_4_13(Telnet): - client = Telnet.return_value - client.read_until.side_effect = [ - b'VERSION 1.4.13', - ] - client.expect.side_effect = [ - (0, None, EXAMPLE_RESPONSE), # NOQA - ] - info = get_cluster_info('', 0) - eq_(info['version'], 12) - eq_(info['nodes'], ['10.82.235.120:11211', '10.80.249.27:11211']) - client.write.assert_has_calls([ - call(b'version\n'), - call(b'get AmazonElastiCache:cluster\n'), - ]) - - -@raises(WrongProtocolData) -@patch('django_elastipymemcache.cluster_utils.Telnet', MagicMock()) -def test_bad_protocol(): - get_cluster_info('', 0) - - -@patch('django_elastipymemcache.cluster_utils.Telnet') -def test_ubuntu_protocol(Telnet): - client = Telnet.return_value - client.read_until.side_effect = [ - b'VERSION 1.4.14 (Ubuntu)', - ] - client.expect.side_effect = [ - (0, None, EXAMPLE_RESPONSE), # NOQA - ] - get_cluster_info('', 0) - client.write.assert_has_calls([ - call(b'version\n'), - call(b'config get cluster\n'), - ]) - - -@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 = [ - b'VERSION 1.4.34', - ] - client.expect.side_effect = [ - (0, None, b'ERROR\r\n'), - ] - get_cluster_info('test', 0) - - -@raises(WrongProtocolData) -@patch('django_elastipymemcache.cluster_utils.Telnet') -def test_cannot_parse_version(Telnet): - client = Telnet.return_value - client.read_until.side_effect = [ - b'VERSION 1.4.34', - ] - client.expect.side_effect = [ - (0, None, b'CONFIG cluster 0 138\r\nfail\nhost|ip|11211 host||11211\n\r\nEND\r\n'), # NOQA - ] - get_cluster_info('test', 0) - - -@raises(WrongProtocolData) -@patch('django_elastipymemcache.cluster_utils.Telnet') -def test_cannot_parse_nodes(Telnet): - client = Telnet.return_value - client.read_until.side_effect = [ - b'VERSION 1.4.34', - ] - client.expect.side_effect = [ - (0, None, b'CONFIG cluster 0 138\r\n1\nfail\n\r\nEND\r\n'), # NOQA - ] - get_cluster_info('test', 0) From 40f4d479e9ff4d6340227b932c35894a39209735 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 29 Jun 2020 20:50:09 +0900 Subject: [PATCH 123/149] Fix isort orders --- django_elastipymemcache/backend.py | 1 - django_elastipymemcache/client.py | 10 ++-------- tests/test_backend.py | 10 ++-------- tests/test_client.py | 11 ++--------- 4 files changed, 6 insertions(+), 26 deletions(-) diff --git a/django_elastipymemcache/backend.py b/django_elastipymemcache/backend.py index 99bb7ae..7211309 100644 --- a/django_elastipymemcache/backend.py +++ b/django_elastipymemcache/backend.py @@ -11,7 +11,6 @@ from .client import ConfigurationEndpointClient - logger = logging.getLogger(__name__) diff --git a/django_elastipymemcache/client.py b/django_elastipymemcache/client.py index 0313ef9..8000837 100644 --- a/django_elastipymemcache/client.py +++ b/django_elastipymemcache/client.py @@ -2,14 +2,8 @@ from distutils.version import StrictVersion from django.utils.encoding import smart_text -from pymemcache.exceptions import ( - MemcacheUnknownError, -) -from pymemcache.client.base import ( - _readline, - Client, -) - +from pymemcache.client.base import Client, _readline +from pymemcache.exceptions import MemcacheUnknownError logger = logging.getLogger(__name__) diff --git a/tests/test_backend.py b/tests/test_backend.py index c0906fa..6bcf332 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -1,14 +1,8 @@ import socket -from unittest.mock import ( - patch, - Mock, -) +from unittest.mock import Mock, patch from django.core.cache import InvalidCacheBackendError -from nose.tools import ( - eq_, - raises, -) +from nose.tools import eq_, raises from django_elastipymemcache.client import ConfigurationEndpointClient diff --git a/tests/test_client.py b/tests/test_client.py index 274f56d..724e0ad 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,21 +1,14 @@ import collections -from unittest.mock import ( - call, - patch, -) +from unittest.mock import call, patch +from nose.tools import eq_, raises from pymemcache.exceptions import ( MemcacheUnknownCommandError, MemcacheUnknownError, ) -from nose.tools import ( - eq_, - raises, -) from django_elastipymemcache.client import ConfigurationEndpointClient - EXAMPLE_RESPONSE = [ b'CONFIG cluster 0 147\r\n', b'12\n' From af8203adcc520639229203dd9bac3d95c9a81c0a Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 29 Jun 2020 20:55:17 +0900 Subject: [PATCH 124/149] Remove unused module --- tests/test_backend.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_backend.py b/tests/test_backend.py index 6bcf332..267d31d 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -1,4 +1,3 @@ -import socket from unittest.mock import Mock, patch from django.core.cache import InvalidCacheBackendError From 11c764cb0206c514d6b17ea8fc7efba7282660ab Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 29 Jun 2020 21:21:09 +0900 Subject: [PATCH 125/149] Add log --- django_elastipymemcache/backend.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/django_elastipymemcache/backend.py b/django_elastipymemcache/backend.py index 7211309..5a0bdef 100644 --- a/django_elastipymemcache/backend.py +++ b/django_elastipymemcache/backend.py @@ -84,7 +84,12 @@ def get_cluster_nodes(self): OSError, socket.gaierror, socket.timeout, - ): + ) as e: + logger.warn( + 'Cannot connect to cluster %s, err: %s', + self.configuration_endpoint_client.server, + e, + ) return [] @property From 48382bf8703c013ca02880d3f0b5c07f4b90c233 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 29 Jun 2020 22:42:43 +0900 Subject: [PATCH 126/149] Remove unused option --- README.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/README.rst b/README.rst index fb5ef2f..587d806 100644 --- a/README.rst +++ b/README.rst @@ -41,7 +41,6 @@ Your cache backend should look something like this:: 'BACKEND': 'django_elastipymemcache.backend.ElastiPymemcache', 'LOCATION': '[configuration endpoint]:11211', '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 } From 4a22a9950d7bcd93d450b382fd3982ced90b33d9 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 29 Jun 2020 22:48:18 +0900 Subject: [PATCH 127/149] Fix dependencies --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 74abe98..bd5c57e 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ packages=find_packages(exclude=('tests',)), include_package_data=True, install_requires=[ - 'pymemcache', + 'django-pymemcache>=1.0', 'Django>=2.2', ], ) From c783c381fe0ad17ea1bc7bf358a1e263c658256a Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 29 Jun 2020 22:49:48 +0900 Subject: [PATCH 128/149] Bump version to 2.0.1 --- django_elastipymemcache/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_elastipymemcache/__init__.py b/django_elastipymemcache/__init__.py index 2c96ce3..1d024ef 100644 --- a/django_elastipymemcache/__init__.py +++ b/django_elastipymemcache/__init__.py @@ -1,2 +1,2 @@ -VERSION = (2, 0, 0) +VERSION = (2, 0, 1) __version__ = '.'.join(map(str, VERSION)) From 999b5ab5d2e523b389b3bc574a5a25c7fb836ec0 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 30 Jun 2020 00:17:46 +0900 Subject: [PATCH 129/149] Fix ValueError --- django_elastipymemcache/backend.py | 1 + 1 file changed, 1 insertion(+) diff --git a/django_elastipymemcache/backend.py b/django_elastipymemcache/backend.py index 5a0bdef..70fdd1e 100644 --- a/django_elastipymemcache/backend.py +++ b/django_elastipymemcache/backend.py @@ -60,6 +60,7 @@ def __init__(self, server, params): ) try: host, port = self._servers[0].split(':') + port = int(port) except ValueError: raise InvalidCacheBackendError( 'Server configuration should be in format IP:Port', From 5c0c55a28b20db1fd05f1458a9a57c8693678b22 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 30 Jun 2020 11:06:40 +0900 Subject: [PATCH 130/149] Bump version to 2.0.2 --- django_elastipymemcache/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_elastipymemcache/__init__.py b/django_elastipymemcache/__init__.py index 1d024ef..8ff3dd0 100644 --- a/django_elastipymemcache/__init__.py +++ b/django_elastipymemcache/__init__.py @@ -1,2 +1,2 @@ -VERSION = (2, 0, 1) +VERSION = (2, 0, 2) __version__ = '.'.join(map(str, VERSION)) From 45004eda76ec5aeefbbded4397336edcbd1e876d Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 30 Jun 2020 14:13:22 +0900 Subject: [PATCH 131/149] Support functools.wrap --- django_elastipymemcache/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django_elastipymemcache/client.py b/django_elastipymemcache/client.py index 8000837..f9243b7 100644 --- a/django_elastipymemcache/client.py +++ b/django_elastipymemcache/client.py @@ -12,8 +12,9 @@ class ConfigurationEndpointClient(Client): # https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/AutoDiscovery.AddingToYourClientLibrary.html def __init__(self, *args, ignore_cluster_errors=False, **kwargs): + client = super().__init__(*args, **kwargs) self.ignore_cluster_errors = ignore_cluster_errors - return super().__init__(*args, **kwargs) + return client def _get_cluster_info_cmd(self): if StrictVersion(smart_text(self.version())) < StrictVersion('1.4.14'): From 19e74bcdf3776a7e4091775309a93eed773af09d Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 30 Jun 2020 14:16:57 +0900 Subject: [PATCH 132/149] Bump version to 2.0.3 --- django_elastipymemcache/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_elastipymemcache/__init__.py b/django_elastipymemcache/__init__.py index 8ff3dd0..7e951ca 100644 --- a/django_elastipymemcache/__init__.py +++ b/django_elastipymemcache/__init__.py @@ -1,2 +1,2 @@ -VERSION = (2, 0, 2) +VERSION = (2, 0, 3) __version__ = '.'.join(map(str, VERSION)) From 01b65aae6c64d6420e7e25312c7ca5d7ee595232 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2020 00:01:23 +0000 Subject: [PATCH 133/149] Bump isort from 4.3.21 to 5.0.6 Bumps [isort](https://github.com/timothycrosley/isort) from 4.3.21 to 5.0.6. - [Release notes](https://github.com/timothycrosley/isort/releases) - [Changelog](https://github.com/timothycrosley/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/timothycrosley/isort/compare/4.3.21...5.0.6) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5ca792a..ae068d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ check-manifest==0.42 coverage==5.1 flake8==3.8.3 -isort==4.3.21 +isort==5.0.6 mock==4.0.2 nose==1.3.7 pymemcache==3.2.0 From cc2b646d795e84e55f04901f70fa3e17b3f70305 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2020 02:21:22 +0000 Subject: [PATCH 134/149] Bump coverage from 5.1 to 5.2 Bumps [coverage](https://github.com/nedbat/coveragepy) from 5.1 to 5.2. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/coverage-5.1...coverage-5.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ae068d8..c43b407 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ check-manifest==0.42 -coverage==5.1 +coverage==5.2 flake8==3.8.3 isort==5.0.6 mock==4.0.2 From 78efb2d414457f6a2b2ff1b89f4690e50e40346a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2020 00:03:51 +0000 Subject: [PATCH 135/149] Bump isort from 5.0.6 to 5.0.9 Bumps [isort](https://github.com/timothycrosley/isort) from 5.0.6 to 5.0.9. - [Release notes](https://github.com/timothycrosley/isort/releases) - [Changelog](https://github.com/timothycrosley/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/timothycrosley/isort/compare/5.0.6...5.0.9) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c43b407..5f9566a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ check-manifest==0.42 coverage==5.2 flake8==3.8.3 -isort==5.0.6 +isort==5.0.9 mock==4.0.2 nose==1.3.7 pymemcache==3.2.0 From b00c01168f9f3fea421167905725ee39e458d2e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2020 00:04:13 +0000 Subject: [PATCH 136/149] Bump isort from 5.0.9 to 5.1.3 Bumps [isort](https://github.com/timothycrosley/isort) from 5.0.9 to 5.1.3. - [Release notes](https://github.com/timothycrosley/isort/releases) - [Changelog](https://github.com/timothycrosley/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/timothycrosley/isort/compare/5.0.9...5.1.3) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5f9566a..e734306 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ check-manifest==0.42 coverage==5.2 flake8==3.8.3 -isort==5.0.9 +isort==5.1.3 mock==4.0.2 nose==1.3.7 pymemcache==3.2.0 From ce1f9472a42ed9be1d56c104e218cdbb61903e10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2020 00:34:02 +0000 Subject: [PATCH 137/149] Bump isort from 5.1.3 to 5.1.4 Bumps [isort](https://github.com/timothycrosley/isort) from 5.1.3 to 5.1.4. - [Release notes](https://github.com/timothycrosley/isort/releases) - [Changelog](https://github.com/timothycrosley/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/timothycrosley/isort/compare/5.1.3...5.1.4) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e734306..6e64683 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ check-manifest==0.42 coverage==5.2 flake8==3.8.3 -isort==5.1.3 +isort==5.1.4 mock==4.0.2 nose==1.3.7 pymemcache==3.2.0 From 55f5df21209588abaafe5a142b4391aa655ef519 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2020 00:03:40 +0000 Subject: [PATCH 138/149] Bump coverage from 5.2 to 5.2.1 Bumps [coverage](https://github.com/nedbat/coveragepy) from 5.2 to 5.2.1. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/coverage-5.2...coverage-5.2.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6e64683..624adda 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ check-manifest==0.42 -coverage==5.2 +coverage==5.2.1 flake8==3.8.3 isort==5.1.4 mock==4.0.2 From 43dfaac66e6a7a31074a8747d7824ed7f14c4741 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2020 00:03:18 +0000 Subject: [PATCH 139/149] Bump isort from 5.1.4 to 5.2.0 Bumps [isort](https://github.com/timothycrosley/isort) from 5.1.4 to 5.2.0. - [Release notes](https://github.com/timothycrosley/isort/releases) - [Changelog](https://github.com/timothycrosley/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/timothycrosley/isort/compare/5.1.4...5.2.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6e64683..757ef03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ check-manifest==0.42 coverage==5.2 flake8==3.8.3 -isort==5.1.4 +isort==5.2.0 mock==4.0.2 nose==1.3.7 pymemcache==3.2.0 From 9c41f0e3279cd2e59416746f28231f56fb2c666f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2020 00:01:00 +0000 Subject: [PATCH 140/149] Bump isort from 5.2.0 to 5.2.1 Bumps [isort](https://github.com/timothycrosley/isort) from 5.2.0 to 5.2.1. - [Release notes](https://github.com/timothycrosley/isort/releases) - [Changelog](https://github.com/timothycrosley/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/timothycrosley/isort/compare/5.2.0...5.2.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index aa2f02a..8fecd91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ check-manifest==0.42 coverage==5.2.1 flake8==3.8.3 -isort==5.2.0 +isort==5.2.1 mock==4.0.2 nose==1.3.7 pymemcache==3.2.0 From bde60db65b6472953e5c1b899c0864afa7d92fcb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2020 00:03:21 +0000 Subject: [PATCH 141/149] Bump isort from 5.2.1 to 5.3.2 Bumps [isort](https://github.com/timothycrosley/isort) from 5.2.1 to 5.3.2. - [Release notes](https://github.com/timothycrosley/isort/releases) - [Changelog](https://github.com/timothycrosley/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/timothycrosley/isort/compare/5.2.1...5.3.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8fecd91..1b5360b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ check-manifest==0.42 coverage==5.2.1 flake8==3.8.3 -isort==5.2.1 +isort==5.3.2 mock==4.0.2 nose==1.3.7 pymemcache==3.2.0 From e0b344bca54600fedf895333efc007f30b1ad7bd Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 10 Aug 2020 13:10:49 +0900 Subject: [PATCH 142/149] Fix wrong command --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4685ab0..ae61a1e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ matrix: - python: 3.8 env: TOXENV=readme - python: 3.8 - env: TOXENV=manifest + env: TOXENV=check-manifest allow_failures: - env: DJANGO=dev install: From 6377f3c5a8f368f4f78b98e110a8f2e18daedfc0 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 10 Aug 2020 13:18:14 +0900 Subject: [PATCH 143/149] Fix check-manifest bugs --- MANIFEST.in | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 89bc473..089a35f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,13 @@ include LICENSE include README.rst -graft django_elastipymemcache -graft tests +exclude requirements.txt +exclude tox.ini global-exclude __pycache__ global-exclude *.py[co] + +prune .github +prune tests + +graft django_elastipymemcache From f8f01ccff01241034e3bd483f9c2fdf9e8532ef7 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 10 Aug 2020 13:29:35 +0900 Subject: [PATCH 144/149] Add support for Django 3.1 --- .travis.yml | 1 + setup.py | 1 + tox.ini | 11 +++++++---- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index ae61a1e..e1bae10 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ cache: pip env: - DJANGO=2.2 - DJANGO=3.0 + - DJANGO=3.1 - DJANGO=dev matrix: diff --git a/setup.py b/setup.py index bd5c57e..a914629 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ 'Environment :: Web Environment', 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.0', + 'Framework :: Django :: 3.1', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', diff --git a/tox.ini b/tox.ini index 974322c..5929d8f 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ envlist = py{36,37,38}-dj22, py{36,37,38}-dj30, + py{36,37,38}-dj31, py{36,37,38}-djdev, flake8, isort, @@ -12,6 +13,7 @@ envlist = DJANGO = 2.2: dj22 3.0: dj30 + 3.1: dj31 dev: djdev [testenv] @@ -19,17 +21,18 @@ passenv = TOXENV CI TRAVIS TRAVIS_* CODECOV_* deps = dj22: Django<2.3 dj30: Django<3.1 + dj31: Django<3.2 django-pymemcache<2.0 djdev: https://github.com/django/django/archive/master.tar.gz -r{toxinidir}/requirements.txt - py38-dj30: codecov + py38-dj31: codecov setenv = PYTHONPATH = {toxinidir} commands = coverage run --source=django_elastipymemcache -m nose --verbose - py38-dj30: coverage report - py38-dj30: coverage xml - py38-dj30: codecov + py38-dj31: coverage report + py38-dj31: coverage xml + py38-dj31: codecov [testenv:flake8] basepython = python3.8 From 6a8c5fc04b53e5f1f5520a6477759d122b56a597 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 10 Aug 2020 13:38:34 +0900 Subject: [PATCH 145/149] Fix backward imcompatible changes --- tests/test_backend.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_backend.py b/tests/test_backend.py index 267d31d..628f263 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -1,5 +1,6 @@ from unittest.mock import Mock, patch +import django from django.core.cache import InvalidCacheBackendError from nose.tools import eq_, raises @@ -120,7 +121,10 @@ def test_client_delete(get_cluster_info): backend = ElastiPymemcache('h:0', {}) ret = backend.delete('key1') - eq_(ret, None) + if django.get_version() >= '3.1': + eq_(ret, False) + else: + eq_(ret, None) @patch.object(ConfigurationEndpointClient, 'get_cluster_info') From 239ad3c63bdcf44870260ab2785f1822b5e77773 Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Tue, 11 Aug 2020 01:09:54 +0900 Subject: [PATCH 146/149] Bump version to 2.0.4 --- django_elastipymemcache/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_elastipymemcache/__init__.py b/django_elastipymemcache/__init__.py index 7e951ca..f40c6d0 100644 --- a/django_elastipymemcache/__init__.py +++ b/django_elastipymemcache/__init__.py @@ -1,2 +1,2 @@ -VERSION = (2, 0, 3) +VERSION = (2, 0, 4) __version__ = '.'.join(map(str, VERSION)) From 54dd564c695fc4244050dc9f7aa714b966029533 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2020 00:12:08 +0000 Subject: [PATCH 147/149] Bump pymemcache from 3.2.0 to 3.3.0 Bumps [pymemcache](https://github.com/pinterest/pymemcache) from 3.2.0 to 3.3.0. - [Release notes](https://github.com/pinterest/pymemcache/releases) - [Changelog](https://github.com/pinterest/pymemcache/blob/master/ChangeLog.rst) - [Commits](https://github.com/pinterest/pymemcache/compare/v3.2.0...v3.3.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1b5360b..5837e53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ flake8==3.8.3 isort==5.3.2 mock==4.0.2 nose==1.3.7 -pymemcache==3.2.0 +pymemcache==3.3.0 readme-renderer==26.0 From 31c76b90de2e9394db42c7619d5b8570fb53bf04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2020 00:01:59 +0000 Subject: [PATCH 148/149] Bump isort from 5.3.2 to 5.5.0 Bumps [isort](https://github.com/pycqa/isort) from 5.3.2 to 5.5.0. - [Release notes](https://github.com/pycqa/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/pycqa/isort/compare/5.3.2...5.5.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1b5360b..d0c9e02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ check-manifest==0.42 coverage==5.2.1 flake8==3.8.3 -isort==5.3.2 +isort==5.5.0 mock==4.0.2 nose==1.3.7 pymemcache==3.2.0 From ae40a1f5b67fce91d493601bf9893fcfd3adeb79 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Fri, 19 Mar 2021 15:13:12 -0400 Subject: [PATCH 149/149] port the change over that we care about. --- django_elastipymemcache/backend.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/django_elastipymemcache/backend.py b/django_elastipymemcache/backend.py index 70fdd1e..294f647 100644 --- a/django_elastipymemcache/backend.py +++ b/django_elastipymemcache/backend.py @@ -22,9 +22,10 @@ def invalidate_cache_after_error(f): def wrapper(self, *args, **kwds): try: return f(self, *args, **kwds) - except Exception: + except Exception as e: + logger.warning('MemcachedError: %s', e, exc_info=True) self.clear_cluster_nodes_cache() - raise + return None # Treat as a cache miss return wrapper