Skip to content

Commit

Permalink
django-pylibmc is a cache backend using pylibmc
Browse files Browse the repository at this point in the history
  • Loading branch information
jbalogh committed May 18, 2010
0 parents commit 5cec8b9
Show file tree
Hide file tree
Showing 10 changed files with 470 additions and 0 deletions.
27 changes: 27 additions & 0 deletions LICENSE
@@ -0,0 +1,27 @@
Copyright (c) 2010, Jeff Balogh.
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

3. Neither the name of django-pylibmc nor the names of its contributors may
be used to endorse or promote products derived from this software
without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2 changes: 2 additions & 0 deletions MANIFEST.in
@@ -0,0 +1,2 @@
include LICENSE
include README.rst
35 changes: 35 additions & 0 deletions README.rst
@@ -0,0 +1,35 @@
================================
pylibmc cache backend for Django
================================

This package provides a memcached cache backend for Django using
`pylibmc http://github.com/lericson/pylibmc`_. You want to use pylibmc because
it's fast.


Requirements
------------

django-pylibmc requires Django 1.2. It was written and tested on Python 2.6.


Installation
------------


Get it from `pypi <http://pypi.python.org/pypi/django-pylibmc>`_::

pip install django-pylibmc

or `github <http://github.com/jbalogh/django-pylibmc>`_::

pip install -e git://github.com/jbalogh/django-pylibmc.git#egg=django-pylibmc


Caveats
-------

This package breaks away from the current handling of ``timeout=0`` in Django.
Django converts 0 into the default timeout, while django-pylibmc leaves it as
0. memcached takes 0 to mean "infinite timeout." You can still pass ``None``
to get the default timeout.
2 changes: 2 additions & 0 deletions django_pylibmc/__init__.py
@@ -0,0 +1,2 @@
VERSION = (0, 1)
__version__ = '.'.join(map(str, VERSION))
101 changes: 101 additions & 0 deletions django_pylibmc/memcached.py
@@ -0,0 +1,101 @@
"""
Memcached cache backend for Django using pylibmc.
If you want to use the binary protocol, specify &binary=1 in your
CACHE_BACKEND. The default is 0, using the text protocol.
pylibmc behaviors can be declared as a dict in settings.py under the name
PYLIBMC_BEHAVIORS.
Unlike the default Django caching backends, this backend lets you pass 0 as a
timeout, which translates to an infinite timeout in memcached.
"""
import time

from django.conf import settings
from django.core.cache.backends.base import BaseCache, InvalidCacheBackendError
from django.utils.encoding import smart_unicode, smart_str

try:
import pylibmc
except ImportError:
raise InvalidCacheBackendError('Could not import pylibmc.')


# It would be nice to inherit from Django's memcached backend, but that
# requires import python-memcache or cmemcache. Those probably aren't
# available since we're using pylibmc, hence the copy/paste.

This comment has been minimized.

Copy link
@robhudson

robhudson Sep 29, 2011

Contributor

FYI: It doesn't look like this is the case anymore...
https://github.com/django/django/blob/master/django/core/cache/backends/memcached.py

Also, a pylibmc backend was added. But it looks like this has some extra guards and also the preferred timeout=0 behavior.

It looks like you could import and inherit from the BaseMemcachedCache class and avoid some extra copy/paste.


class CacheClass(BaseCache):

def __init__(self, server, params):
BaseCache.__init__(self, params)
binary = int(params.get('binary', False))
self._cache = pylibmc.Client(server.split(';'), binary=binary)
self._cache.behaviors = getattr(settings, 'PYLIBMC_BEHAVIORS', {})

def _get_memcache_timeout(self, timeout):
"""
Memcached deals with long (> 30 days) timeouts in a special
way. Call this function to obtain a safe value for your timeout.
"""
timeout = self.default_timeout if timeout is None else timeout
if timeout > 2592000: # 60*60*24*30, 30 days
# See http://code.google.com/p/memcached/wiki/FAQ
# "You can set expire times up to 30 days in the future. After that
# memcached interprets it as a date, and will expire the item after
# said date. This is a simple (but obscure) mechanic."
#
# This means that we have to switch to absolute timestamps.
timeout += int(time.time())
return timeout

def add(self, key, value, timeout=None):
if isinstance(value, unicode):
value = value.encode('utf-8')
return self._cache.add(smart_str(key), value, self._get_memcache_timeout(timeout))

def get(self, key, default=None):
val = self._cache.get(smart_str(key))
if val is None:
return default
return val

def set(self, key, value, timeout=None):
self._cache.set(smart_str(key), value, self._get_memcache_timeout(timeout))

def delete(self, key):
self._cache.delete(smart_str(key))

def get_many(self, keys):
return self._cache.get_multi(map(smart_str,keys))

def close(self, **kwargs):
self._cache.disconnect_all()

def incr(self, key, delta=1):
try:
return self._cache.incr(key, delta)
except pylibmc.NotFound:
raise ValueError("Key '%s' not found" % key)

def decr(self, key, delta=1):
try:
return self._cache.decr(key, delta)
except pylibmc.NotFound:
raise ValueError("Key '%s' not found" % key)

def set_many(self, data, timeout=None):
safe_data = {}
for key, value in data.items():
if isinstance(value, unicode):
value = value.encode('utf-8')
safe_data[smart_str(key)] = value
self._cache.set_multi(safe_data, self._get_memcache_timeout(timeout))

def delete_many(self, keys):
self._cache.delete_multi(map(smart_str, keys))

def clear(self):
self._cache.flush_all()
31 changes: 31 additions & 0 deletions setup.py
@@ -0,0 +1,31 @@
from setuptools import setup

import django_pylibmc


setup(
name='django-pylibmc',
version=django_pylibmc.__version__,
description='Django cache backend using pylibmc',
long_description=open('README.rst').read(),
author='Jeff Balogh',
author_email='jbalogh@mozilla.com',
url='http://github.com/jbalogh/django-pylibmc',
license='BSD',
packages=['django_pylibmc'],
include_package_data=True,
zip_safe=False,
install_requires=['pylibmc', 'Django>=1.2'],
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
# I don't know what exactly this means, but why not?
'Environment :: Web Environment :: Mozilla',
'Framework :: Django',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Software Development :: Libraries :: Python Modules',
]
)
Empty file added tests/app/__init__.py
Empty file.
11 changes: 11 additions & 0 deletions tests/app/models.py
@@ -0,0 +1,11 @@
from django.db import models
from datetime import datetime

def expensive_calculation():
expensive_calculation.num_runs += 1
return datetime.now()

class Poll(models.Model):
question = models.CharField(max_length=200)
answer = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published', default=expensive_calculation)
11 changes: 11 additions & 0 deletions tests/settings.py
@@ -0,0 +1,11 @@
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'testing',
'SUPPORTS_TRANSACTIONS': False,
},
}

INSTALLED_APPS = (
'app',
)

0 comments on commit 5cec8b9

Please sign in to comment.