mirror of
https://github.com/sprockets/sprockets.clients.cassandra.git
synced 2024-11-25 03:00:20 +00:00
commit
c348faf681
16 changed files with 219 additions and 106 deletions
|
@ -1,13 +1,10 @@
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- 2.6
|
|
||||||
- 2.7
|
- 2.7
|
||||||
- pypy
|
- pypy
|
||||||
- 3.2
|
|
||||||
- 3.3
|
- 3.3
|
||||||
- 3.4
|
- 3.4
|
||||||
install:
|
install:
|
||||||
- if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi
|
|
||||||
- pip install -r requirements.txt -r test-requirements.txt
|
- pip install -r requirements.txt -r test-requirements.txt
|
||||||
script: nosetests
|
script: nosetests
|
||||||
after_success:
|
after_success:
|
||||||
|
@ -22,3 +19,5 @@ deploy:
|
||||||
python: 2.7
|
python: 2.7
|
||||||
tags: true
|
tags: true
|
||||||
all_branches: true
|
all_branches: true
|
||||||
|
services:
|
||||||
|
- cassandra
|
||||||
|
|
2
docs/api.rst
Normal file
2
docs/api.rst
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.. automodule:: sprockets.clients.cassandra
|
||||||
|
:members:
|
|
@ -1,3 +1,2 @@
|
||||||
# Add dependencies that are required to install your package. These will
|
blist==1.3.6
|
||||||
# be passed into `setup` as the `install_requires` keyword.
|
cassandra-driver==2.5.1
|
||||||
#
|
|
||||||
|
|
65
setup.py
65
setup.py
|
@ -2,51 +2,62 @@
|
||||||
import codecs
|
import codecs
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from setuptools import setup, find_packages
|
import setuptools
|
||||||
|
|
||||||
import sprockets.clients.cassandra
|
def read_requirements_file(req_name):
|
||||||
|
requirements = []
|
||||||
|
try:
|
||||||
|
with codecs.open(req_name, encoding='utf-8') as req_file:
|
||||||
|
for req_line in req_file:
|
||||||
|
if '#' in req_line:
|
||||||
|
req_line = req_line[0:req_line.find('#')].strip()
|
||||||
|
if req_line:
|
||||||
|
requirements.append(req_line.strip())
|
||||||
|
except IOError:
|
||||||
|
pass
|
||||||
|
return requirements
|
||||||
|
|
||||||
|
|
||||||
def read_requirements_file(filename):
|
install_requires = read_requirements_file('requirements.txt')
|
||||||
"""Read pip-formatted requirements from a file."""
|
setup_requires = read_requirements_file('setup-requirements.txt')
|
||||||
with open(filename, 'r') as f:
|
tests_require = read_requirements_file('test-requirements.txt')
|
||||||
return [line.strip() for line in f.readlines()
|
|
||||||
if not line.startswith('#')]
|
|
||||||
|
|
||||||
requirements = read_requirements_file('requirements.txt')
|
|
||||||
test_requirements = read_requirements_file('test-requirements.txt')
|
|
||||||
if sys.version_info < (3, ):
|
|
||||||
requirements.append('six>=1.7,<2.0')
|
|
||||||
test_requirements.append('mock>=1.0.1,<2.0')
|
|
||||||
if sys.version_info < (2, 7):
|
|
||||||
test_requirements.append('unittest2==0.5.1')
|
|
||||||
|
|
||||||
setup(
|
setuptools.setup(
|
||||||
name='sprockets.clients.cassandra',
|
name='sprockets.clients.cassandra',
|
||||||
description='Base functioanlity for accessing/modifying data in Cassandra',
|
version='0.0.0',
|
||||||
version=sprockets.clients.cassandra.__version__,
|
description='Base functionality for accessing/modifying data in Cassandra',
|
||||||
packages=find_packages(exclude=['tests', 'tests.*']),
|
|
||||||
test_suite='nose.collector',
|
|
||||||
include_package_data=True,
|
|
||||||
long_description=codecs.open('README.rst', encoding='utf-8').read(),
|
long_description=codecs.open('README.rst', encoding='utf-8').read(),
|
||||||
install_requires=requirements,
|
url='https://github.com/sprockets/sprockets.clients.cassandra.git',
|
||||||
tests_require=test_requirements,
|
author='AWeber Communications',
|
||||||
author='AWeber Communications, Inc.',
|
|
||||||
author_email='api@aweber.com',
|
author_email='api@aweber.com',
|
||||||
url='https://github.com/aweber/sprockets.clients.cassandra',
|
license=codecs.open('LICENSE', encoding='utf-8').read(),
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 1 - Planning',
|
'Development Status :: 4 - Beta',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
'License :: OSI Approved :: BSD License',
|
'License :: OSI Approved :: BSD License',
|
||||||
'Natural Language :: English',
|
'Natural Language :: English',
|
||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
'Programming Language :: Python :: 2',
|
'Programming Language :: Python :: 2',
|
||||||
'Programming Language :: Python :: 2.6',
|
|
||||||
'Programming Language :: Python :: 2.7',
|
'Programming Language :: Python :: 2.7',
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.2',
|
'Programming Language :: Python :: 3.2',
|
||||||
'Programming Language :: Python :: 3.3',
|
'Programming Language :: Python :: 3.3',
|
||||||
'Programming Language :: Python :: 3.4',
|
'Programming Language :: Python :: 3.4',
|
||||||
'Programming Language :: Python :: Implementation :: CPython',
|
'Programming Language :: Python :: Implementation :: CPython',
|
||||||
|
'Programming Language :: Python :: Implementation :: PyPy',
|
||||||
|
'Topic :: Software Development :: Libraries',
|
||||||
|
'Topic :: Software Development :: Libraries :: Python Modules'
|
||||||
],
|
],
|
||||||
)
|
packages=['sprockets',
|
||||||
|
'sprockets.clients',
|
||||||
|
'sprockets.clients.cassandra'],
|
||||||
|
package_data={'': ['LICENSE', 'README.md']},
|
||||||
|
include_package_data=True,
|
||||||
|
namespace_packages=['sprockets',
|
||||||
|
'sprockets.clients'],
|
||||||
|
install_requires=install_requires,
|
||||||
|
setup_requires=setup_requires,
|
||||||
|
tests_require=tests_require,
|
||||||
|
test_suite='nose.collector',
|
||||||
|
zip_safe=False)
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
version_info = (0, 0, 0)
|
|
||||||
__version__ = '.'.join(str(v) for v in version_info[:3])
|
|
1
sprockets/__init__.py
Normal file
1
sprockets/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
__import__('pkg_resources').declare_namespace(__name__)
|
1
sprockets/clients/__init__.py
Normal file
1
sprockets/clients/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
__import__('pkg_resources').declare_namespace(__name__)
|
101
sprockets/clients/cassandra/__init__.py
Normal file
101
sprockets/clients/cassandra/__init__.py
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
"""
|
||||||
|
clients.cassandra
|
||||||
|
=================
|
||||||
|
|
||||||
|
Base functionality for accessing/modifying data in Cassandra.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from cassandra.cluster import Cluster
|
||||||
|
from tornado.concurrent import Future
|
||||||
|
from tornado.ioloop import IOLoop
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
except:
|
||||||
|
from urlparse import urlsplit
|
||||||
|
|
||||||
|
version_info = (0, 0, 0)
|
||||||
|
__version__ = '.'.join(str(v) for v in version_info)
|
||||||
|
|
||||||
|
DEFAULT_URI = 'cassandra://localhost'
|
||||||
|
|
||||||
|
|
||||||
|
class CassandraConnection(object):
|
||||||
|
"""Maintain a connection to a Cassandra cluster.
|
||||||
|
|
||||||
|
The Sprockets Cassandra client handles provides the glue
|
||||||
|
needed to join the Tornado async I/O module with the native
|
||||||
|
python async I/O used in the Cassandra driver. The
|
||||||
|
constructor of the function will grab the current handle
|
||||||
|
to the underlying Tornado I/O loop so that a Tornado future
|
||||||
|
result can be returned to the host application.
|
||||||
|
|
||||||
|
Configuration parameters for the module are obtained from
|
||||||
|
environment variables. Currently, the only variable is
|
||||||
|
``CASSANDRA_URI``, which takes the format "cassandra://hostname".
|
||||||
|
If not located, the hostname defaults to localhost.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The hostname in the ``CASSANDRA_URI`` will be resolved
|
||||||
|
to a list of IP addresses that will be passed to the
|
||||||
|
Cassandra driver as the contact points.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ioloop=None):
|
||||||
|
self._config = self._get_cassandra_config()
|
||||||
|
self._cluster = Cluster(self._config['contact_points'])
|
||||||
|
self._session = self._cluster.connect()
|
||||||
|
self._ioloop = IOLoop.current()
|
||||||
|
|
||||||
|
def _get_cassandra_config(self):
|
||||||
|
"""Retrieve a dict containing Cassandra client config params."""
|
||||||
|
config = {}
|
||||||
|
parts = urlsplit(os.environ.get('CASSANDRA_URI', DEFAULT_URI))
|
||||||
|
if parts.scheme != 'cassandra':
|
||||||
|
raise RuntimeError(
|
||||||
|
'CASSANDRA_URI scheme is not "cassandra://"!')
|
||||||
|
|
||||||
|
_, _, ip_addresses = socket.gethostbyname_ex(parts.hostname)
|
||||||
|
if not ip_addresses:
|
||||||
|
raise RuntimeError('Unable to find Cassandra in DNS!')
|
||||||
|
|
||||||
|
config['contact_points'] = ip_addresses
|
||||||
|
return config
|
||||||
|
|
||||||
|
def set_keyspace(self, keyspace):
|
||||||
|
"""Set the keyspace used by the connection."""
|
||||||
|
self._session.set_keyspace(keyspace)
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"""Shutdown the connection to the Cassandra cluster."""
|
||||||
|
self._cluster.shutdown()
|
||||||
|
self._session = None
|
||||||
|
self._cluster = None
|
||||||
|
|
||||||
|
def execute(self, query, *args, **kwargs):
|
||||||
|
"""Asynchronously execute the specified CQL query.
|
||||||
|
|
||||||
|
The execute command also takes optional parameters and trace
|
||||||
|
keyword arguments. See cassandra-python documentation for
|
||||||
|
definition of those parameters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tornado_future = Future()
|
||||||
|
cassandra_future = self._session.execute_async(
|
||||||
|
query, *args, **kwargs)
|
||||||
|
self._ioloop.add_callback(
|
||||||
|
self._callback, cassandra_future, tornado_future)
|
||||||
|
return tornado_future
|
||||||
|
|
||||||
|
def _callback(self, cassandra_future, tornado_future):
|
||||||
|
"""Cassandra async I/O loop callback handler."""
|
||||||
|
try:
|
||||||
|
result = cassandra_future.result()
|
||||||
|
except Exception as exc:
|
||||||
|
return tornado_future.set_exception(exc)
|
||||||
|
tornado_future.set_result(result)
|
|
@ -9,4 +9,4 @@
|
||||||
# minimize breakage to our dev environment.
|
# minimize breakage to our dev environment.
|
||||||
coveralls>=0.4,<1.0
|
coveralls>=0.4,<1.0
|
||||||
nose>=1.3.1,<2.0.0
|
nose>=1.3.1,<2.0.0
|
||||||
test-helpers>=1.5.1,<2.0.0
|
tornado>=3.0.0,<4.0.0
|
||||||
|
|
68
tests.py
Normal file
68
tests.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
"""
|
||||||
|
Tests for the sprockets.clients.cassandra package
|
||||||
|
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
|
||||||
|
from cassandra.cluster import Cluster
|
||||||
|
from cassandra.protocol import SyntaxException
|
||||||
|
from tornado.testing import AsyncTestCase, gen_test
|
||||||
|
|
||||||
|
from sprockets.clients.cassandra import CassandraConnection
|
||||||
|
|
||||||
|
|
||||||
|
class TestCassandraConnectionClass(AsyncTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestCassandraConnectionClass, self).setUp()
|
||||||
|
self.cluster = Cluster(self.find_cassandra())
|
||||||
|
self.session = self.cluster.connect()
|
||||||
|
self.keyspace = 'sprocketstest{0}'.format(int(time.time()*10000))
|
||||||
|
self.create_fixtures()
|
||||||
|
self.connection = CassandraConnection()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super(TestCassandraConnectionClass, self).tearDown()
|
||||||
|
self.session.execute("DROP KEYSPACE {0}".format(self.keyspace))
|
||||||
|
self.connection.shutdown()
|
||||||
|
|
||||||
|
def find_cassandra(self):
|
||||||
|
uri = os.environ.get('CASSANDRA_URI', 'cassandra://localhost')
|
||||||
|
hostname = uri[12:]
|
||||||
|
_, _, ips = socket.gethostbyname_ex(hostname)
|
||||||
|
return ips
|
||||||
|
|
||||||
|
def create_fixtures(self):
|
||||||
|
self.session.execute(
|
||||||
|
"CREATE KEYSPACE IF NOT EXISTS {0} WITH REPLICATION = "
|
||||||
|
"{{'class': 'SimpleStrategy', "
|
||||||
|
"'replication_factor': 1}}".format(self.keyspace))
|
||||||
|
self.session.execute("USE {0}".format(self.keyspace))
|
||||||
|
self.session.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS names (name text PRIMARY KEY)")
|
||||||
|
self.session.execute(
|
||||||
|
"INSERT INTO names (name) VALUES ('Peabody')")
|
||||||
|
|
||||||
|
@gen_test
|
||||||
|
def test_several_queries(self):
|
||||||
|
futures = []
|
||||||
|
count = 100
|
||||||
|
for i in range(count):
|
||||||
|
futures.append(self.connection.execute(
|
||||||
|
"SELECT name FROM {0}.names".format(self.keyspace)))
|
||||||
|
results = 0
|
||||||
|
for future in futures:
|
||||||
|
yield future
|
||||||
|
results += 1
|
||||||
|
self.assertEqual(count, results)
|
||||||
|
|
||||||
|
@gen_test
|
||||||
|
def test_bad_query(self):
|
||||||
|
with self.assertRaises(SyntaxException):
|
||||||
|
yield self.connection.execute('goobletygook')
|
||||||
|
|
||||||
|
@gen_test
|
||||||
|
def test_set_keyspace(self):
|
||||||
|
self.connection.set_keyspace(self.keyspace)
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
from test_helpers import bases
|
|
||||||
from test_helpers.compat import mock
|
|
||||||
from test_helpers.mixins import patch_mixin
|
|
||||||
|
|
||||||
|
|
||||||
class App(object):
|
|
||||||
"""Quick example app for testing."""
|
|
||||||
|
|
||||||
def first(self, fail):
|
|
||||||
"""Example method under test."""
|
|
||||||
self.do_db_lookup('random')
|
|
||||||
if fail:
|
|
||||||
raise AttributeError
|
|
||||||
|
|
||||||
def do_db_lookup(self, name):
|
|
||||||
"""Method that reaches out to a database."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class _BaseFirstTestCase(patch_mixin.PatchMixin, bases.BaseTest):
|
|
||||||
"""Example base test showing current test style."""
|
|
||||||
|
|
||||||
patch_prefix = 'tests.unit.test_example'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def configure(cls):
|
|
||||||
cls.app = App()
|
|
||||||
|
|
||||||
cls.do_db_lookup = cls.create_patch('App.do_db_lookup')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def execute(cls):
|
|
||||||
try:
|
|
||||||
cls.app.first(cls.fail)
|
|
||||||
except AttributeError as exc:
|
|
||||||
cls.exception = exc
|
|
||||||
|
|
||||||
def should_do_db_lookup(self):
|
|
||||||
self.do_db_lookup.assert_called_once_with(mock.ANY)
|
|
||||||
|
|
||||||
|
|
||||||
class WhenFirstAppSuccessful(_BaseFirstTestCase):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def configure(cls):
|
|
||||||
cls.fail = False
|
|
||||||
super(WhenFirstAppSuccessful, cls).configure()
|
|
||||||
|
|
||||||
|
|
||||||
class WhenFirstAppFails(_BaseFirstTestCase):
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def configure(cls):
|
|
||||||
cls.fail = True
|
|
||||||
super(WhenFirstAppFails, cls).configure()
|
|
||||||
|
|
||||||
def should_raise_AttributeError(self):
|
|
||||||
self.assertIsInstance(self.exception, AttributeError)
|
|
10
tox.ini
10
tox.ini
|
@ -1,16 +1,12 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist = py26,py27,py32,py33,py34
|
envlist = py27,py33,py34
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands = nosetests []
|
commands = nosetests []
|
||||||
deps = -rtest-requirements.txt
|
deps = -rrequirements.txt
|
||||||
|
-rtest-requirements.txt
|
||||||
|
|
||||||
[testenv:py27]
|
[testenv:py27]
|
||||||
deps =
|
deps =
|
||||||
mock>=1.0.1,<2.0
|
mock>=1.0.1,<2.0
|
||||||
{[testenv]deps}
|
{[testenv]deps}
|
||||||
|
|
||||||
[testenv:py26]
|
|
||||||
deps =
|
|
||||||
unittest2==0.5.1
|
|
||||||
{[testenv:py27]deps}
|
|
||||||
|
|
Loading…
Reference in a new issue