Browse Source

add couchdb support

Nathan Van Gheem 10 years ago
parent
commit
a5f121a549

+ 1 - 0
celery/backends/__init__.py

@@ -31,6 +31,7 @@ BACKEND_ALIASES = {
     'database': 'celery.backends.database:DatabaseBackend',
     'cassandra': 'celery.backends.cassandra:CassandraBackend',
     'couchbase': 'celery.backends.couchbase:CouchBaseBackend',
+    'couchdb': 'celery.backends.couchdb:CouchDBBackend',
     'riak': 'celery.backends.riak:RiakBackend',
     'disabled': 'celery.backends.base:DisabledBackend',
 }

+ 127 - 0
celery/backends/couchdb.py

@@ -0,0 +1,127 @@
+# -*- coding: utf-8 -*-
+"""
+    celery.backends.couchdb
+    ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    CouchDB result store backend.
+
+"""
+from __future__ import absolute_import
+
+import logging
+
+try:
+    import pycouchdb
+except ImportError:
+    pycouchdb = None  # noqa
+
+from kombu.utils.url import _parse_url
+
+from celery.exceptions import ImproperlyConfigured
+from celery.utils.timeutils import maybe_timedelta
+
+from .base import KeyValueStoreBackend
+
+__all__ = ['CouchDBBackend']
+
+
+class CouchDBBackend(KeyValueStoreBackend):
+    container = 'default'
+    scheme = 'http'
+    host = 'localhost'
+    port = 5984
+    username = None
+    password = None
+    quiet = False
+    conncache = None
+    unlock_gil = True
+    timeout = 2.5
+    transcoder = None
+    # supports_autoexpire = False
+
+    def __init__(self, url=None, *args, **kwargs):
+        """Initialize CouchDB backend instance.
+
+        :raises celery.exceptions.ImproperlyConfigured: if
+            module :mod:`pycouchdb` is not available.
+
+        """
+        super(CouchDBBackend, self).__init__(*args, **kwargs)
+
+        self.expires = kwargs.get('expires') or maybe_timedelta(
+            self.app.conf.CELERY_TASK_RESULT_EXPIRES)
+
+        if pycouchdb is None:
+            raise ImproperlyConfigured(
+                'You need to install the pycouchdb library to use the '
+                'CouchDB backend.',
+            )
+
+        uscheme = uhost = uport = uname = upass = ucontainer = None
+        if url:
+            _, uhost, uport, uname, upass, ucontainer , _ = _parse_url(url)  # noqa
+            ucontainer = ucontainer.strip('/') if ucontainer else None
+
+        config = self.app.conf.get('CELERY_COUCHDB_BACKEND_SETTINGS', None)
+        if config is not None:
+            if not isinstance(config, dict):
+                raise ImproperlyConfigured(
+                    'CouchDB backend settings should be grouped in a dict',
+                )
+        else:
+            config = {}
+
+        self.scheme = uscheme or config.get('scheme', self.scheme)
+        self.host = uhost or config.get('host', self.host)
+        self.port = int(uport or config.get('port', self.port))
+        self.container = ucontainer or config.get('container', self.container)
+        self.username = uname or config.get('username', self.username)
+        self.password = upass or config.get('password', self.password)
+
+        self._connection = None
+
+    def _get_connection(self):
+        """Connect to the CouchDB server."""
+        if self._connection is None:
+            if self.username and self.password:
+                conn_string = '%s://%s:%s@%s:%s' % (
+                    self.scheme, self.username, self.password,
+                    self.host, str(self.port))
+                server = pycouchdb.Server(conn_string, authmethod='basic')
+            else:
+                conn_string = '%s://%s:%s' % (
+                    self.scheme, self.host, str(self.port))
+                server = pycouchdb.Server(conn_string)
+
+            logging.debug('couchdb conn string: %s', conn_string)
+            try:
+                self._connection = server.database(self.container)
+            except pycouchdb.exceptions.NotFound:
+                self._connection = server.create(self.container)
+        return self._connection
+
+    @property
+    def connection(self):
+        return self._get_connection()
+
+    def get(self, key):
+        try:
+            return self.connection.get(key)['value']
+        except pycouchdb.exceptions.NotFound:
+            return None
+
+    def set(self, key, value):
+        data = {'_id': key, 'value': value}
+        try:
+            self.connection.save(data)
+        except pycouchdb.exceptions.Conflict:
+            # document already exists, update it
+            data = self.connection.get(key)
+            data['value'] = value
+            self.connection.save(data)
+
+    def mget(self, keys):
+        return [self.get(key) for key in keys]
+
+    def delete(self, key):
+        self.connection.delete(key)

+ 122 - 0
celery/tests/backends/test_couchdb.py

@@ -0,0 +1,122 @@
+from __future__ import absolute_import
+
+from celery.backends import couchdb as module
+from celery.backends.couchdb import CouchDBBackend
+from celery.exceptions import ImproperlyConfigured
+from celery import backends
+from celery.tests.case import (
+    AppCase, Mock, SkipTest, patch, sentinel,
+)
+
+try:
+    import pycouchdb
+except ImportError:
+    pycouchdb = None  # noqa
+
+COUCHDB_CONTAINER = 'celery_container'
+
+
+class test_CouchDBBackend(AppCase):
+
+    def setup(self):
+        if pycouchdb is None:
+            raise SkipTest('pycouchdb is not installed.')
+        self.backend = CouchDBBackend(app=self.app)
+
+    def test_init_no_pycouchdb(self):
+        """test init no pycouchdb raises"""
+        prev, module.pycouchdb = module.pycouchdb, None
+        try:
+            with self.assertRaises(ImproperlyConfigured):
+                CouchDBBackend(app=self.app)
+        finally:
+            module.pycouchdb = prev
+
+    def test_init_no_settings(self):
+        """test init no settings"""
+        self.app.conf.CELERY_COUCHDB_BACKEND_SETTINGS = []
+        with self.assertRaises(ImproperlyConfigured):
+            CouchDBBackend(app=self.app)
+
+    def test_init_settings_is_None(self):
+        """Test init settings is None"""
+        self.app.conf.CELERY_COUCHDB_BACKEND_SETTINGS = None
+        CouchDBBackend(app=self.app)
+
+    def test_get_container_exists(self):
+        with patch('pycouchdb.client.Database') as mock_Connection:
+            self.backend._connection = sentinel._connection
+
+            connection = self.backend._get_connection()
+
+            self.assertEqual(sentinel._connection, connection)
+            self.assertFalse(mock_Connection.called)
+
+    def test_get(self):
+        """test_get
+
+        CouchDBBackend.get should return  and take two params
+        db conn to couchdb is mocked.
+        TODO Should test on key not exists
+
+        """
+        self.app.conf.CELERY_COUCHDB_BACKEND_SETTINGS = {}
+        x = CouchDBBackend(app=self.app)
+        x._connection = Mock()
+        mocked_get = x._connection.get = Mock()
+        mocked_get.return_value = sentinel.retval
+        # should return None
+        self.assertEqual(x.get('1f3fab'), sentinel.retval)
+        x._connection.get.assert_called_once_with('1f3fab')
+
+    def test_delete(self):
+        """test_delete
+
+        CouchDBBackend.delete should return and take two params
+        db conn to pycouchdb is mocked.
+        TODO Should test on key not exists
+
+        """
+        self.app.conf.CELERY_COUCHDB_BACKEND_SETTINGS = {}
+        x = CouchDBBackend(app=self.app)
+        x._connection = Mock()
+        mocked_delete = x._connection.delete = Mock()
+        mocked_delete.return_value = None
+        # should return None
+        self.assertIsNone(x.delete('1f3fab'))
+        x._connection.delete.assert_called_once_with('1f3fab')
+
+    def test_config_params(self):
+        """test_config_params
+
+        celery.conf.CELERY_COUCHDB_BACKEND_SETTINGS is properly set
+        """
+        self.app.conf.CELERY_COUCHDB_BACKEND_SETTINGS = {
+            'container': 'mycoolcontainer',
+            'host': ['here.host.com', 'there.host.com'],
+            'username': 'johndoe',
+            'password': 'mysecret',
+            'port': '1234',
+        }
+        x = CouchDBBackend(app=self.app)
+        self.assertEqual(x.container, 'mycoolcontainer')
+        self.assertEqual(x.host, ['here.host.com', 'there.host.com'],)
+        self.assertEqual(x.username, 'johndoe',)
+        self.assertEqual(x.password, 'mysecret')
+        self.assertEqual(x.port, 1234)
+
+    def test_backend_by_url(self, url='couchdb://myhost/mycoolcontainer'):
+        from celery.backends.couchdb import CouchDBBackend
+        backend, url_ = backends.get_backend_by_url(url, self.app.loader)
+        self.assertIs(backend, CouchDBBackend)
+        self.assertEqual(url_, url)
+
+    def test_backend_params_by_url(self):
+        url = 'couchdb://johndoe:mysecret@myhost:123/mycoolcontainer'
+        with self.Celery(backend=url) as app:
+            x = app.backend
+            self.assertEqual(x.container, 'mycoolcontainer')
+            self.assertEqual(x.host, 'myhost')
+            self.assertEqual(x.username, 'johndoe')
+            self.assertEqual(x.password, 'mysecret')
+            self.assertEqual(x.port, 123)

+ 52 - 0
docs/configuration.rst

@@ -221,6 +221,10 @@ Can be one of the following:
     Use `Couchbase`_ to store the results.
     See :ref:`conf-couchbase-result-backend`.
 
+* couchdb
+    Use `CouchDB`_ to store the results.
+    See :ref:`conf-couchdb-result-backend`.
+
 .. warning:
 
     While the AMQP result backend is very efficient, you must make sure
@@ -773,6 +777,54 @@ This is a dict supporting the following keys:
     Password to authenticate to the Couchbase server (optional).
 
 
+.. _conf-couchdb-result-backend:
+
+CouchDB backend settings
+------------------------
+
+.. note::
+
+    The CouchDB backend requires the :mod:`pycouchdb` library:
+    https://pypi.python.org/pypi/pycouchdb
+
+    To install the couchbase package use `pip` or `easy_install`:
+
+    .. code-block:: bash
+
+        $ pip install pycouchdb
+
+This backend can be configured via the :setting:`CELERY_RESULT_BACKEND`
+set to a couchbase URL::
+
+    CELERY_RESULT_BACKEND = 'couchdb://username:password@host:port/container'
+
+
+.. setting:: CELERY_COUCHDB_BACKEND_SETTINGS
+
+CELERY_COUCHDB_BACKEND_SETTINGS
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This is a dict supporting the following keys:
+
+* scheme
+    http or https. Defaults to ``http``.
+
+* host
+    Host name of the CouchDB server. Defaults to ``localhost``.
+
+* port
+    The port the CouchDB server is listening to. Defaults to ``8091``.
+
+* container
+    The default container the CouchDB server is writing to.
+    Defaults to ``default``.
+
+* username
+    User name to authenticate to the CouchDB server as (optional).
+
+* password
+    Password to authenticate to the CouchDB server (optional).
+
 .. _conf-messaging:
 
 Message Routing

+ 1 - 1
requirements/extras/couchdb.txt

@@ -1 +1 @@
-couchdb
+pycouchdb