├── requirements_test.txt ├── .travis-runs-tests.sh ├── django_bmemcached ├── __init__.py └── memcached.py ├── tests ├── settings.py └── __init__.py ├── .travis.yml ├── tox.ini ├── LICENSE ├── setup.py └── README.md /requirements_test.txt: -------------------------------------------------------------------------------- 1 | nose 2 | coverage 3 | python-binary-memcached 4 | tox -------------------------------------------------------------------------------- /.travis-runs-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo service memcached start 3 | tox 4 | exit $? 5 | -------------------------------------------------------------------------------- /django_bmemcached/__init__.py: -------------------------------------------------------------------------------- 1 | from .memcached import BMemcached 2 | assert BMemcached 3 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | SECRET_KEY = "some value so that django>1.5 won't fail when this is empty" 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | - "3.7" 9 | - "3.8" 10 | 11 | install: pip install -r requirements_test.txt tox-travis 12 | script: ./.travis-runs-tests.sh 13 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skip_install = true 3 | envlist = 4 | py{27}-django{19,110,111} 5 | py{34,35}-django{19,110,111,20} 6 | py{36,37}-django{111,20,21,22,30} 7 | py{38}-django{30} 8 | 9 | [testenv] 10 | deps = -rrequirements_test.txt 11 | django18: Django>=1.8,<1.9 12 | django19: Django>=1.9,<1.10 13 | django110: Django>=1.10,<1.11 14 | django111: Django>=1.11,<2.0 15 | django20: Django>=2.0,<2.1 16 | django21: Django>=2.1,<2.2 17 | django22: Django>=2.2,<2.3 18 | django30: Django>=3.0,<3.1 19 | setenv = 20 | DJANGO_SETTINGS_MODULE = tests.settings 21 | 22 | commands = nosetests --with-coverage --cover-package=django_bmemcached 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | Copyright (c) 2011 Jayson Reis 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='django-bmemcached', 5 | version='0.3.0', 6 | author='Jayson Reis', 7 | author_email='santosdosreis@gmail.com', 8 | description='A Django cache backend to use bmemcached module which ' + 9 | 'supports memcached binary protocol with authentication.', 10 | url='https://github.com/jaysonsantos/django-bmemcached', 11 | packages=['django_bmemcached'], 12 | install_requires=['python-binary-memcached'], 13 | classifiers=[ 14 | 'Development Status :: 4 - Beta', 15 | 'Framework :: Django', 16 | 'Framework :: Django :: 1.8', 17 | 'Framework :: Django :: 1.9', 18 | 'Framework :: Django :: 1.10', 19 | 'Framework :: Django :: 1.11', 20 | 'Framework :: Django :: 2.0', 21 | 'Framework :: Django :: 2.1', 22 | 'Framework :: Django :: 2.2', 23 | 'Framework :: Django :: 3.0', 24 | 'License :: OSI Approved :: MIT License', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python', 27 | 'Programming Language :: Python :: 2', 28 | 'Programming Language :: Python :: 2.7', 29 | 'Programming Language :: Python :: 3', 30 | 'Programming Language :: Python :: 3.4', 31 | 'Programming Language :: Python :: 3.5', 32 | 'Programming Language :: Python :: 3.6', 33 | 'Programming Language :: Python :: 3.7', 34 | 'Programming Language :: Python :: 3.8', 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/jaysonsantos/django-bmemcached.png?branch=master)](http://travis-ci.org/jaysonsantos/django-bmemcached) 2 | 3 | # Django-BMemcached 4 | 5 | A django cache backend to use [bmemcached](https://github.com/jaysonsantos/python-binary-memcached) 6 | module which supports memcached binary protocol with authentication. 7 | 8 | ## Installing 9 | 10 | Use pip: 11 | 12 | ```bash 13 | pip install django-bmemcached 14 | ``` 15 | 16 | ## Using 17 | 18 | In your settings.py add bmemcached as backend: 19 | 20 | ```python 21 | CACHES = { 22 | 'default': { 23 | 'BACKEND': 'django_bmemcached.memcached.BMemcached', 24 | 'LOCATION': 'your_server:port', 25 | } 26 | } 27 | ``` 28 | 29 | If you are using Django 1.11 or above, you can also add `OPTIONS` which will be 30 | passed through to the client. The options available for `BMemcached` are as follows: 31 | 32 | ```python 33 | CACHES = { 34 | 'default': { 35 | 'BACKEND': 'django_bmemcached.memcached.BMemcached', 36 | 'LOCATION': 'your_server:port', 37 | 'OPTIONS': { 38 | 'username': 'user', 39 | 'password': 'password', 40 | 'compression': None, 41 | 'socket_timeout': bmemcached.client.constants.SOCKET_TIMEOUT, 42 | 'pickler': pickle.Pickler, 43 | 'unpickler': pickle.Unpickler, 44 | 'pickle_protocol': 0 45 | } 46 | } 47 | } 48 | ``` 49 | 50 | NB If you have options in your configuration that are not supported (see the sample 51 | above, or `django_bmemcached.memcached.VALID_CACHE_OPTIONS`) 52 | the cache will raise an `InvalidCacheOptions` error the first time it is accessed: 53 | 54 | ```python 55 | >>> from django_bmemcached import BMemcached 56 | >>> client = BMemcached(None, {'OPTIONS': {'bad_option': True}}) 57 | >>> client.get("foo") 58 | Traceback (most recent call last): 59 | ... 60 | django_bmemcached.memcached.InvalidCacheOptions: Error initialising BMemcached - invalid options detected: {'bad_option'} 61 | Please check your CACHES config contains only valid OPTIONS: {'username', 'unpickler', 'compression', 'pickle_protocol', 'password', 'pickler', 'socket_timeout'} 62 | >>> 63 | ``` 64 | 65 | ### Using in Heroku 66 | 67 | Just add bmemcached as backend. It will work automagically if you have added memcached as Heroku addon. 68 | 69 | ```python 70 | CACHES = { 71 | 'default': { 72 | 'BACKEND': 'django_bmemcached.memcached.BMemcached' 73 | } 74 | } 75 | ``` 76 | 77 | ## Testing 78 | 79 | ```bash 80 | DJANGO_SETTINGS_MODULE=tests.settings nosetests 81 | ``` 82 | -------------------------------------------------------------------------------- /django_bmemcached/memcached.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import bmemcached 4 | from django.core.cache.backends import memcached 5 | from django.core.exceptions import ImproperlyConfigured 6 | 7 | # keys that are acceptable in the Django 8 | # settings.CACHES[...]["OPTIONS"] dictionary. 9 | VALID_CACHE_OPTIONS = { 10 | "username", 11 | "password", 12 | "compression", 13 | "socket_timeout", 14 | "pickler", 15 | "unpickler", 16 | "pickle_protocol", 17 | } 18 | 19 | 20 | class InvalidCacheOptions(ImproperlyConfigured): 21 | """Custom error raised when Client initialisation fails.""" 22 | 23 | def __init__(self, options): 24 | # in Python 3 you can compare a list and set, but in Python 2 25 | # you cannot, so need to cast the options.keys() explicitly to a set. 26 | # TODO: remove set() once Python 2 support is dropped. 27 | invalid_options = set(options.keys()) - VALID_CACHE_OPTIONS 28 | msg = ( 29 | "Error initialising BMemcached - invalid options detected: %s\n" 30 | "Please check your CACHES config contains only valid OPTIONS: %s" 31 | % (invalid_options, VALID_CACHE_OPTIONS) 32 | ) 33 | super(InvalidCacheOptions, self).__init__(msg) 34 | 35 | 36 | class BMemcached(memcached.BaseMemcachedCache): 37 | """ 38 | An implementation of a cache binding using python-binary-memcached 39 | A.K.A BMemcached. 40 | """ 41 | def __init__(self, server, params): 42 | params.setdefault('OPTIONS', {}) 43 | 44 | username = params['OPTIONS'].get('username', params.get('USERNAME', os.environ.get('MEMCACHE_USERNAME'))) 45 | 46 | if username: 47 | params['OPTIONS']['username'] = username 48 | 49 | password = params['OPTIONS'].get('password', params.get('PASSWORD', os.environ.get('MEMCACHE_PASSWORD'))) 50 | 51 | if password: 52 | params['OPTIONS']['password'] = password 53 | 54 | if not server: 55 | server = tuple(os.environ.get('MEMCACHE_SERVERS', '').split(',')) 56 | 57 | super(BMemcached, self).__init__(server, params, library=bmemcached, value_not_found_exception=ValueError) 58 | 59 | def close(self, **kwargs): 60 | # Override base behavior of disconnecting from memcache on every HTTP request. 61 | # This method is, in practice, only called by Django on the request_finished signal 62 | pass 63 | 64 | @property 65 | def _cache(self): 66 | client = getattr(self, '_client', None) 67 | if client: 68 | return client 69 | 70 | if self._options: 71 | try: 72 | client = self._lib.Client(self._servers, **self._options) 73 | except TypeError: 74 | raise InvalidCacheOptions(self._options) 75 | else: 76 | client = self._lib.Client(self._servers) 77 | 78 | self._client = client 79 | 80 | return client 81 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import bz2 2 | import os 3 | import pickle 4 | import unittest 5 | 6 | import django.conf 7 | import django.core.cache 8 | import django_bmemcached 9 | from django.test import override_settings 10 | from django_bmemcached import BMemcached 11 | from django_bmemcached.memcached import InvalidCacheOptions 12 | from pickle import Pickler 13 | 14 | 15 | class TestWithExplicitAuth(unittest.TestCase): 16 | def setUp(self): 17 | self.client = BMemcached( 18 | ('127.0.0.1:11211',), 19 | {'OPTIONS': {'username': 'user', 'password': 'password'}} 20 | ) 21 | 22 | def tearDown(self): 23 | self.client.delete('key') 24 | 25 | def testGet(self): 26 | self.client.set('key', 'value') 27 | self.assertEqual('value', self.client.get('key')) 28 | 29 | def testDelete(self): 30 | self.client.set('key', 'value') 31 | self.assertEqual('value', self.client.get('key')) 32 | self.client.delete('key') 33 | self.assertEqual(None, self.client.get('key')) 34 | 35 | def testPropertyCacheRetunsAwaysSameServer(self): 36 | self.client.set('key', 'value') 37 | self.assertEqual(self.client._client, self.client._cache) 38 | 39 | 40 | class TestWithTopLevelAuthParams(TestWithExplicitAuth): 41 | def setUp(self): 42 | self.client = BMemcached( 43 | ('127.0.0.1:11211',), 44 | {'USERNAME': 'user', 'PASSWORD': 'password'} 45 | ) 46 | 47 | 48 | class TestWithEnvironmentAuth(TestWithExplicitAuth): 49 | def setUp(self): 50 | os.environ['MEMCACHE_SERVERS'] = '127.0.0.1' 51 | os.environ['MEMCACHE_USERNAME'] = 'user' 52 | os.environ['MEMCACHE_PASSWORD'] = 'password' 53 | self.client = BMemcached(None, {}) 54 | 55 | def tearDown(self): 56 | del os.environ['MEMCACHE_SERVERS'] 57 | del os.environ['MEMCACHE_USERNAME'] 58 | del os.environ['MEMCACHE_PASSWORD'] 59 | 60 | 61 | class TestWithoutAuth(TestWithExplicitAuth): 62 | def setUp(self): 63 | self.client = BMemcached(('127.0.0.1:11211',), {}) 64 | 65 | 66 | class TestPickler(Pickler): 67 | pass 68 | 69 | class TestUnpickler(pickle.Unpickler): 70 | pass 71 | 72 | 73 | class TestOptions(unittest.TestCase): 74 | 75 | # these options are all non-default - so that we can confirm that 76 | # they are passed through to the client. 77 | TEST_OPTIONS = { 78 | "compression": bz2, 79 | "pickle_protocol": 2, 80 | "socket_timeout": 1.0, 81 | "pickler": TestPickler, 82 | "unpickler": TestUnpickler 83 | } 84 | TEST_CACHE_CONFIG = { 85 | "default": { 86 | "BACKEND": "django_bmemcached.memcached.BMemcached", 87 | "BINARY": True, 88 | "OPTIONS": TEST_OPTIONS 89 | } 90 | } 91 | 92 | @unittest.skipIf(django.get_version() < '1.11', "OPTIONS did not exist pre-1.11") 93 | @override_settings(CACHES=TEST_CACHE_CONFIG) 94 | def testWithOptions(self): 95 | client = django.core.cache.caches["default"] 96 | options = self.TEST_OPTIONS 97 | self.assertIsInstance(client, BMemcached) 98 | self.assertEqual(client._cache.pickle_protocol, options["pickle_protocol"]) 99 | self.assertEqual(client._cache.compression, options["compression"]) 100 | self.assertEqual(client._cache.socket_timeout, options["socket_timeout"]) 101 | self.assertEqual(client._cache.pickler, options["pickler"]) 102 | self.assertEqual(client._cache.unpickler, options["unpickler"]) 103 | 104 | def testInvalidOptions(self): 105 | """Check that InvalidCacheOptions is raised.""" 106 | client = BMemcached(None, {'OPTIONS': {'bad_option': True}}) 107 | with self.assertRaises(InvalidCacheOptions): 108 | client._cache 109 | --------------------------------------------------------------------------------