From 0d6d22e0e586b4ffea41d805b04377350d8e444c Mon Sep 17 00:00:00 2001 From: brettlangdon Date: Sat, 7 Sep 2013 10:32:38 -0400 Subject: [PATCH] initial commit --- .gitignore | 5 + LICENSE.txt | 21 ++++ MANIFEST.ini | 4 + README.md | 4 + requirements.txt | 1 + riakcached/__init__.py | 1 + riakcached/client.py | 197 ++++++++++++++++++++++++++++++++++++ riakcached/exceptions.py | 23 +++++ riakcached/test/__init__.py | 0 setup.py | 27 +++++ 10 files changed, 283 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 MANIFEST.ini create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 riakcached/__init__.py create mode 100644 riakcached/client.py create mode 100644 riakcached/exceptions.py create mode 100644 riakcached/test/__init__.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9daddca --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.ropeproject +*.py[co] +*.log +build +dist diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..0a1aaa2 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Brett Langdon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS 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. diff --git a/MANIFEST.ini b/MANIFEST.ini new file mode 100644 index 0000000..773cda4 --- /dev/null +++ b/MANIFEST.ini @@ -0,0 +1,4 @@ +include README.* setup.py setup.cfg +recursive-include riakcached *.py +global-exclude *.pyc +global-exclude *.pyo diff --git a/README.md b/README.md new file mode 100644 index 0000000..caf2bf3 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +Riakcached +========== + +A Memcached like interface to the Riak HTTP Client. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b625c3a --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +urllib3==1.7 diff --git a/riakcached/__init__.py b/riakcached/__init__.py new file mode 100644 index 0000000..3dc1f76 --- /dev/null +++ b/riakcached/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/riakcached/client.py b/riakcached/client.py new file mode 100644 index 0000000..01eca2f --- /dev/null +++ b/riakcached/client.py @@ -0,0 +1,197 @@ +__all__ = ["RiakClient"] + +import json +import Queue +import threading + +import urllib3 + +from riakcached import exceptions + + +class RiakClient(object): + """ + """ + __slots__ = [ + "_serializers", + "_deserializers", + "_pool", + "_timeout", + "bucket", + "url", + ] + _serializers = { + "application/json": json.dumps, + } + _deserializers = { + "application/json": json.loads, + } + + def __init__(self, bucket, url="http://127.0.0.1:8098", timeout=2): + """ + """ + self.bucket = bucket + self.url = url.strip("/") + self._timeout = timeout + self._connect() + + def set_serializer(self, content_type, serializer, deserializer): + """ + """ + content_type = content_type.lower() + self._serializers[content_type] = serializer + self._deserializers[content_type] = deserializer + + def close(self): + """ + """ + if self._pool: + self._pool.close() + + def get(self, key): + """ + """ + response = self._request( + method="GET", + url="%s/riak/%s/%s" % (self.url, self.bucket, key), + ) + if response.status == 400: + raise exceptions.RiakcachedBadRequest(response.data) + elif response.status == 503: + raise exceptions.RiakcachedServiceUnavailable(response.data) + + if response.status not in (200, 300, 304): + return None + + deserializer = self._deserializers.get(response.getheader("content-type"), str) + return deserializer(response.data) + + def get_many(self, keys): + """ + """ + def worker(key, results): + results.put((key, self.get(key))) + + args = [[key] for key in keys] + results = self._many(worker, args) + results = dict((key, value) for key, value in results.iteritems() if value is not None) + return results or None + + def set(self, key, value, content_type="text/plain"): + """ + """ + serializer = self._serializers.get(content_type.lower(), str) + value = serializer(value) + + response = self._request( + method="POST", + url="%s/riak/%s/%s" % (self.url, self.bucket, key), + body=value, + headers={ + "Content-Type": content_type, + }, + ) + if response.status == 400: + raise exceptions.RiakcachedBadRequest(response.data) + elif response.status == 412: + raise exceptions.RiakcachedPreconditionFailed(response.data) + return response.status in (200, 201, 204, 300) + + def set_many(self, values): + """ + """ + def worker(key, value, results): + results.put((key, self.set(key, value))) + + args = [list(data) for data in values.items()] + return self._many(worker, args) + + def delete(self, key): + """ + """ + response = self._request( + method="DELETE", + url="%s/riak/%s/%s" % (self.url, self.bucket, key), + ) + if response.status == 400: + raise exceptions.RiakcachedBadRequest(response.data) + return response.status in (204, 404) + + def delete_many(self, keys): + """ + """ + def worker(key, results): + results.put((key, self.delete(key))) + + args = [[key] for key in keys] + + return self._many(worker, args) + + def stats(self): + """ + """ + response = self._request( + method="GET", + url="%s/stats" % self.url, + ) + if response.status == 200: + return json.loads(response.data) + return None + + def ping(self): + """ + """ + response = self._request( + method="GET", + url="%s/ping" % self.url, + ) + return response.status == 200 + + def incr(self, key, value=1): + """ + """ + response = self._request( + method="POST", + url="%s/riak/%s/counters/%s" % (self.url, self.bucket, key), + body=str(value), + ) + if response.status == 409: + raise exceptions.RiakcachedConflict(response.data) + return True + + def _connect(self): + self._pool = urllib3.connection_from_url(self.url) + + def _request(self, method, url, body=None, headers=None): + try: + return self._pool.urlopen( + method=method, + url=url, + body=body, + headers=headers, + timeout=self._timeout, + redirect=False, + ) + except urllib3.exceptions.TimeoutError, e: + raise exceptions.RiakcachedTimeout(e.message) + except urllib3.exceptions.HTTPError, e: + raise exceptions.RiakcachedConnectionError(e.message) + + def _many(self, target, args_list): + workers = [] + worker_results = Queue.Queue() + for args in args_list: + args.append(worker_results) + worker = threading.Thread(target=target, args=args) + worker.daemon = True + worker.start() + workers.append(worker) + + for worker in workers: + worker.join() + + results = {} + while not worker_results.empty(): + key, value = worker_results.get() + results[key] = value + return results diff --git a/riakcached/exceptions.py b/riakcached/exceptions.py new file mode 100644 index 0000000..31c4edb --- /dev/null +++ b/riakcached/exceptions.py @@ -0,0 +1,23 @@ +class RiakcachedException(Exception): + pass + +class RiakcachedBadRequest(RiakcachedException): + pass + +class RiakcachedNotFound(RiakcachedException): + pass + +class RiakcachedServiceUnavailable(RiakcachedException): + pass + +class RiakcachedPreconditionFailed(RiakcachedException): + pass + +class RiakcachedConflict(RiakcachedException): + pass + +class RiakcachedTimeout(RiakcachedException): + pass + +class RiakcachedConnectionError(RiakcachedException): + pass diff --git a/riakcached/test/__init__.py b/riakcached/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d7ec54a --- /dev/null +++ b/setup.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +from setuptools import setup, find_packages + +from riakcached import __version__ + +setup( + name="riakcached", + version=__version__, + author="Brett Langdon", + author_email="brett@blangdon.com", + packages=find_packages(), + install_requires=["urllib3==1.7"], + setup_requires=["nose>=1.0"], + description="A Memcached like interface to Riak", + long_description=open("README.md").read(), + license="MIT", + url='https://github.com/brettlangdon/riakcached', + classifiers=[ + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "License :: OSI Approved :: MIT License", + "Topic :: Database", + ], +)