paisley-0.3.1/000755 000000 000024 00000000000 11536232427 013272 5ustar00rootstaff000000 000000 paisley-0.3.1/paisley/000755 000000 000024 00000000000 11536232427 014740 5ustar00rootstaff000000 000000 paisley-0.3.1/PKG-INFO000644 000000 000024 00000000536 11536232427 014373 0ustar00rootstaff000000 000000 Metadata-Version: 1.0 Name: paisley Version: 0.3.1 Summary: Paisley is a CouchDB client written in Python to be used within a Twisted application. Home-page: http://github.com/smcq/paisley Author: Paisley Developers Author-email: UNKNOWN License: MIT Download-URL: http://github.com/smcq/paisley/zipball/v0.3.1 Description: UNKNOWN Platform: UNKNOWN paisley-0.3.1/._setup.py000755 000765 000024 00000000273 11536232305 017475 0ustar00jasonjwwilliamsstaff000000 000000 Mac OS X  2‰»ATTRý<³»˜#˜#com.macromates.caret{ column = 67; line = 17; }paisley-0.3.1/setup.py000755 000765 000024 00000001177 11536232305 017264 0ustar00jasonjwwilliamsstaff000000 000000 #!/usr/bin/env python # Copyright (c) 2007-2008 # See LICENSE for details. from distutils.core import setup def main(): setup( name="paisley", version="0.3.1", description=("Paisley is a CouchDB client written in Python to be used " "within a Twisted application."), author="Paisley Developers", author_email="", license="MIT", url="http://github.com/smcq/paisley", download_url="http://github.com/smcq/paisley/zipball/v0.3.1", py_modules=["paisley", "paisley.client", "paisley.test_paisley"], ) if __name__ == "__main__": main() paisley-0.3.1/paisley/__init__.py000644 000765 000024 00000000204 11506467651 021327 0ustar00jasonjwwilliamsstaff000000 000000 # -*- test-case-name: test_paisley -*- # Copyright (c) 2007-2008 # See LICENSE for details. import client CouchDB = client.CouchDBpaisley-0.3.1/paisley/._client.py000644 000765 000024 00000000274 11533260121 021251 0ustar00jasonjwwilliamsstaff000000 000000 Mac OS X  2мATTRÖØš¼˜$˜$com.macromates.caret{ column = 28; line = 321; }paisley-0.3.1/paisley/client.py000644 000765 000024 00000027426 11533260121 021044 0ustar00jasonjwwilliamsstaff000000 000000 # -*- test-case-name: test_paisley -*- # Copyright (c) 2007-2008 # See LICENSE for details. """ CouchDB client. """ try: import json except ImportError: import simplejson as json import codecs from StringIO import StringIO from urllib import urlencode, quote from zope.interface import implements from twisted.internet import reactor from twisted.web._newclient import ResponseDone from twisted.web import error as tw_error from twisted.web.client import Agent from twisted.web.http import PotentialDataLoss from twisted.web.http_headers import Headers from twisted.web.iweb import IBodyProducer from twisted.internet.defer import Deferred, maybeDeferred from twisted.internet.protocol import Protocol try: from base64 import b64encode except ImportError: import base64 def b64encode(s): return "".join(base64.encodestring(s).split("\n")) try: from functools import partial except ImportError: class partial(object): def __init__(self, fn, *args, **kw): self.fn = fn self.args = args self.kw = kw def __call__(self, *args, **kw): if kw and self.kw: d = self.kw.copy() d.update(kw) else: d = kw or self.kw return self.fn(*(self.args + args), **d) SOCK_TIMEOUT = 300 class StringProducer(object): """ Body producer for t.w.c.Agent """ implements(IBodyProducer) def __init__(self, body): self.body = body self.length = len(body) def startProducing(self, consumer): return maybeDeferred(consumer.write, self.body) def pauseProducing(self): pass def stopProducing(self): pass class ResponseReceiver(Protocol): """ Assembles HTTP response from return stream. """ def __init__(self, deferred): self.writer = codecs.getwriter("utf_8")(StringIO()) self.deferred = deferred def dataReceived(self, bytes): self.writer.write(bytes) def connectionLost(self, reason): if reason.check(ResponseDone) or reason.check(PotentialDataLoss): self.deferred.callback(self.writer.getvalue()) else: self.deferred.errback(reason) class CouchDB(object): """ CouchDB client: hold methods for accessing a couchDB. """ def __init__(self, host, port=5984, dbName=None, username=None, password=None): """ Initialize the client for given host. @param host: address of the server. @type host: C{str} @param port: if specified, the port of the server. @type port: C{int} @param dbName: if specified, all calls needing a database name will use this one by default. @type dbName: C{str} """ self.client = Agent(reactor) self.host = host self.port = int(port) self.username = username self.password =password self.url_template = "http://%s:%s%%s" % (self.host, self.port) if dbName is not None: self.bindToDB(dbName) def parseResult(self, result): """ Parse JSON result from the DB. """ return json.loads(result) def bindToDB(self, dbName): """ Bind all operations asking for a DB name to the given DB. """ for methname in ["createDB", "deleteDB", "infoDB", "listDoc", "openDoc", "saveDoc", "deleteDoc", "openView", "tempView"]: method = getattr(self, methname) newMethod = partial(method, dbName) setattr(self, methname, newMethod) # Database operations def createDB(self, dbName): """ Creates a new database on the server. """ # Responses: {u'ok': True}, 409 Conflict, 500 Internal Server Error return self.put("/%s/" % (dbName,), "" ).addCallback(self.parseResult) def deleteDB(self, dbName): """ Deletes the database on the server. """ # Responses: {u'ok': True}, 404 Object Not Found return self.delete("/%s/" % (dbName,) ).addCallback(self.parseResult) def listDB(self): """ List the databases on the server. """ # Responses: list of db names return self.get("/_all_dbs").addCallback(self.parseResult) def infoDB(self, dbName): """ Returns info about the couchDB. """ # Responses: {u'update_seq': 0, u'db_name': u'mydb', u'doc_count': 0} # 404 Object Not Found return self.get("/%s/" % (dbName,) ).addCallback(self.parseResult) # Document operations def listDoc(self, dbName, reverse=False, startKey=0, count=-1): """ List all documents in a given database. """ # Responses: {u'rows': [{u'_rev': -1825937535, u'_id': u'mydoc'}], # u'view': u'_all_docs'}, 404 Object Not Found uri = "/%s/_all_docs" % (dbName,) args = {} if reverse: args["reverse"] = "true" if startKey > 0: args["startkey"] = int(startKey) if count >= 0: args["count"] = int(count) if args: uri += "?%s" % (urlencode(args),) return self.get(uri ).addCallback(self.parseResult) def openDoc(self, dbName, docId, revision=None, full=False, attachment=""): """ Open a document in a given database. @param revision: if specified, the revision of the document desired. @type revision: C{str} @param full: if specified, return the list of all the revisions of the document, along with the document itself. @type full: C{bool} @param attachment: if specified, return the named attachment from the document. @type attachment: C{str} """ # Responses: {u'_rev': -1825937535, u'_id': u'mydoc', ...} # 404 Object Not Found uri = "/%s/%s" % (dbName, quote(docId)) if revision is not None: uri += "?%s" % (urlencode({"rev": revision}),) elif full: uri += "?%s" % (urlencode({"full": "true"}),) elif attachment: uri += "/%s" % quote(attachment) # No parsing return self.get(uri) return self.get(uri ).addCallback(self.parseResult) def addAttachments(self, document, attachments): """ Add attachments to a document, before sending it to the DB. @param document: the document to modify. @type document: C{dict} @param attachments: the attachments to add. @type attachments: C{dict} """ document.setdefault("_attachments", {}) for name, data in attachments.iteritems(): data = b64encode(data) document["_attachments"][name] = {"type": "base64", "data": data} def saveDoc(self, dbName, body, docId=None): """ Save/create a document to/in a given database. @param dbName: identifier of the database. @type dbName: C{str} @param body: content of the document. @type body: C{str} or any structured object @param docId: if specified, the identifier to be used in the database. @type docId: C{str} """ # Responses: {u'_rev': 1175338395, u'_id': u'mydoc', u'ok': True} # 409 Conflict, 500 Internal Server Error if not isinstance(body, (str, unicode)): body = json.dumps(body) if docId is not None: d = self.put("/%s/%s" % (dbName, quote(docId)), body) else: d = self.post("/%s/" % (dbName,), body) return d.addCallback(self.parseResult) def deleteDoc(self, dbName, docId, revision): """ Delete a document on given database. """ # Responses: {u'_rev': 1469561101, u'ok': True} # 500 Internal Server Error return self.delete("/%s/%s?%s" % ( dbName, quote(docId), urlencode({'rev': revision})) ).addCallback(self.parseResult) # View operations def openView(self, dbName, docId, viewId, **kwargs): """ Open a view of a document in a given database. """ uri = "/%s/_design/%s/_view/%s" % (dbName, quote(docId), viewId) for arg in kwargs.keys(): kwargs[arg] = json.dumps(kwargs[arg]) if not kwargs: return self.get(uri).addCallback(self.parseResult) elif 'keys' in kwargs: #couchdb is crazy and requires that keys be passed in a post body options = kwargs.copy() keys = {'keys': options.pop('keys')} if options: uri += "?%s" % (urlencode(options),) return self.post(uri, body=keys) elif kwargs: # if not keys we just encode everything in the url uri += "?%s" % (urlencode(kwargs),) return self.get(uri).addCallback(self.parseResult) def addViews(self, document, views): """ Add views to a document. @param document: the document to modify. @type document: C{dict} @param views: the views to add. @type views: C{dict} """ document.setdefault("views", {}) for name, data in views.iteritems(): document["views"][name] = data def tempView(self, dbName, view): """ Make a temporary view on the server. """ d = self.post("/%s/_temp_view" % (dbName,), view) return d.addCallback(self.parseResult) # Basic http methods def _getPage(self, uri, **kwargs): """ C{getPage}-like. """ def cb_recv_resp(response): d_resp_recvd = Deferred() response.deliverBody(ResponseReceiver(d_resp_recvd)) return d_resp_recvd.addCallback(cb_process_resp, response) def cb_process_resp(body, response): # Emulate HTTPClientFactory and raise t.w.e.Error # and PageRedirect if we have errors. if response.code > 299 and response.code < 400: raise tw_error.PageRedirect(response.code, body) elif response.code > 399: raise tw_error.Error(response.code, body) return body url = str(self.url_template % (uri,)) if not kwargs.has_key("headers"): kwargs["headers"] = {} kwargs["headers"]["Accept"] = ["application/json"] kwargs["headers"]["Content-Type"] = ["application/json"] if not kwargs.has_key("method"): kwargs["method"] == "GET" if self.username: kwargs["headers"]["Authorization"] = ["Basic %s" % b64encode("%s:%s" % (self.username, self.password))] if kwargs.has_key("postdata"): body = StringProducer(kwargs["postdata"]) else: body = None d = self.client.request(kwargs["method"], url, Headers(kwargs["headers"]), body) d.addCallback(cb_recv_resp) return d def get(self, uri): """ Execute a C{GET} at C{uri}. """ return self._getPage(uri, method="GET") def post(self, uri, body): """ Execute a C{POST} of C{body} at C{uri}. """ return self._getPage(uri, method="POST", postdata=body) def put(self, uri, body): """ Execute a C{PUT} of C{body} at C{uri}. """ return self._getPage(uri, method="PUT", postdata=body) def delete(self, uri): """ Execute a C{DELETE} at C{uri}. """ return self._getPage(uri, method="DELETE") paisley-0.3.1/paisley/test_paisley.py000644 000765 000024 00000032240 11533260121 022261 0ustar00jasonjwwilliamsstaff000000 000000 # Copyright (c) 2007-2008 # See LICENSE for details. """ Test for couchdb client. """ try: import json except: import simplejson as json import cgi from twisted.trial.unittest import TestCase from twisted.internet.defer import Deferred from twisted.internet import reactor from twisted.web import resource, server import paisley class TestableCouchDB(paisley.CouchDB): """ A couchdb client that can be tested: override the getPage method. """ def __init__(self, *args, **kwargs): """ Initialize the client: forward parameters, and create attributes used in tests. """ paisley.CouchDB.__init__(self, *args, **kwargs) self.deferred = Deferred() self.uri = None self.kwargs = None self.called = False def _getPage(self, uri, *args, **kwargs): """ Fake getPage that do nothing but saving the arguments. """ if self.called: raise RuntimeError("One shot client") self.called = True self.uri = uri self.kwargs = kwargs return self.deferred class CouchDBTestCase(TestCase): """ Test methods against a couchDB. """ def setUp(self): """ Create a fake client to be used in the tests. """ self.client = TestableCouchDB("localhost") def test_auth_init(self): """ Test setting up client with authentication """ self.client_auth = paisley.CouchDB("localhost", username="test", password="testpass") self.assertEquals(self.client_auth.username, "test") self.assertEquals(self.client_auth.password, "testpass") def test_get(self): """ Test get method. """ self.client.get("foo") self.assertEquals(self.client.uri, "foo") self.assertEquals(self.client.kwargs["method"], "GET") def test_post(self): """ Test post method. """ self.client.post("bar", "egg") self.assertEquals(self.client.uri, "bar") self.assertEquals(self.client.kwargs["method"], "POST") self.assertEquals(self.client.kwargs["postdata"], "egg") def test_put(self): """ Test put method. """ self.client.put("bar", "egg") self.assertEquals(self.client.uri, "bar") self.assertEquals(self.client.kwargs["method"], "PUT") self.assertEquals(self.client.kwargs["postdata"], "egg") def test_delete(self): """ Test get method. """ self.client.delete("foo") self.assertEquals(self.client.uri, "foo") self.assertEquals(self.client.kwargs["method"], "DELETE") def _checkParseDeferred(self, d): """ Utility function to test that a Deferred is called with JSON parsing. """ d.callback('["foo"]') def cb(res): self.assertEquals(res, ["foo"]) return d.addCallback(cb) def test_createDB(self): """ Test createDB: this should C{PUT} the DB name in the uri. """ d = self.client.createDB("mydb") self.assertEquals(self.client.uri, "/mydb/") self.assertEquals(self.client.kwargs["method"], "PUT") return self._checkParseDeferred(d) def test_deleteDB(self): """ Test deleteDB: this should C{DELETE} the DB name. """ d = self.client.deleteDB("mydb") self.assertEquals(self.client.uri, "/mydb/") self.assertEquals(self.client.kwargs["method"], "DELETE") return self._checkParseDeferred(d) def test_listDB(self): """ Test listDB: this should C{GET} a specific uri. """ d = self.client.listDB() self.assertEquals(self.client.uri, "/_all_dbs") self.assertEquals(self.client.kwargs["method"], "GET") return self._checkParseDeferred(d) def test_infoDB(self): """ Test infoDB: this should C{GET} the DB name. """ d = self.client.infoDB("mydb") self.assertEquals(self.client.uri, "/mydb/") self.assertEquals(self.client.kwargs["method"], "GET") return self._checkParseDeferred(d) def test_listDoc(self): """ Test listDoc. """ d = self.client.listDoc("mydb") self.assertEquals(self.client.uri, "/mydb/_all_docs") self.assertEquals(self.client.kwargs["method"], "GET") return self._checkParseDeferred(d) def test_listDocReversed(self): """ Test listDoc reversed. """ d = self.client.listDoc("mydb", reverse=True) self.assertEquals(self.client.uri, "/mydb/_all_docs?reverse=true") self.assertEquals(self.client.kwargs["method"], "GET") return self._checkParseDeferred(d) def test_listDocStartKey(self): """ Test listDoc with a startKey. """ d = self.client.listDoc("mydb", startKey=2) self.assertEquals(self.client.uri, "/mydb/_all_docs?startkey=2") self.assertEquals(self.client.kwargs["method"], "GET") return self._checkParseDeferred(d) def test_listDocCount(self): """ Test listDoc with a count. """ d = self.client.listDoc("mydb", count=3) self.assertEquals(self.client.uri, "/mydb/_all_docs?count=3") self.assertEquals(self.client.kwargs["method"], "GET") return self._checkParseDeferred(d) def test_listDocMultipleArguments(self): """ Test listDoc with all options activated. """ d = self.client.listDoc("mydb", count=3, startKey=1, reverse=True) self.assertEquals(self.client.uri, "/mydb/_all_docs?count=3&startkey=1&reverse=true") self.assertEquals(self.client.kwargs["method"], "GET") return self._checkParseDeferred(d) def test_openDoc(self): """ Test openDoc. """ d = self.client.openDoc("mydb", "mydoc") self.assertEquals(self.client.uri, "/mydb/mydoc") self.assertEquals(self.client.kwargs["method"], "GET") return self._checkParseDeferred(d) def test_openDocAtRevision(self): """ Test openDoc with a specific revision. """ d = self.client.openDoc("mydb", "mydoc", revision="ABC") self.assertEquals(self.client.uri, "/mydb/mydoc?rev=ABC") self.assertEquals(self.client.kwargs["method"], "GET") return self._checkParseDeferred(d) def test_openDocWithRevisionHistory(self): """ Test openDoc with revision history. """ d = self.client.openDoc("mydb", "mydoc", full=True) self.assertEquals(self.client.uri, "/mydb/mydoc?full=true") self.assertEquals(self.client.kwargs["method"], "GET") return self._checkParseDeferred(d) def test_openDocAttachment(self): """ Test openDoc for an attachment. """ d = self.client.openDoc("mydb", "mydoc", attachment="bar") self.assertEquals(self.client.uri, "/mydb/mydoc/bar") self.assertEquals(self.client.kwargs["method"], "GET") # Data is transfered without parsing d.callback("test") return d.addCallback(self.assertEquals, "test") def test_saveDocWithDocId(self): """ Test saveDoc, giving an explicit document ID. """ d = self.client.saveDoc("mydb", "mybody", "mydoc") self.assertEquals(self.client.uri, "/mydb/mydoc") self.assertEquals(self.client.kwargs["method"], "PUT") return self._checkParseDeferred(d) def test_saveDocWithoutDocId(self): """ Test saveDoc without a document ID. """ d = self.client.saveDoc("mydb", "mybody") self.assertEquals(self.client.uri, "/mydb/") self.assertEquals(self.client.kwargs["method"], "POST") return self._checkParseDeferred(d) def test_saveStructuredDoc(self): """ saveDoc should automatically serialize a structured document. """ d = self.client.saveDoc("mydb", {"value": "mybody", "_id": "foo"}, "mydoc") self.assertEquals(self.client.uri, "/mydb/mydoc") self.assertEquals(self.client.kwargs["method"], "PUT") return self._checkParseDeferred(d) def test_deleteDoc(self): """ Test deleteDoc. """ d = self.client.deleteDoc("mydb", "mydoc", "1234567890") self.assertEquals(self.client.uri, "/mydb/mydoc?rev=1234567890") self.assertEquals(self.client.kwargs["method"], "DELETE") return self._checkParseDeferred(d) def test_addAttachments(self): """ Test addAttachments. """ doc = {"value": "bar"} self.client.addAttachments(doc, {"file1": "value", "file2": "second value"}) self.assertEquals(doc["_attachments"], {'file2': {'data': 'c2Vjb25kIHZhbHVl', 'type': 'base64'}, 'file1': {'data': 'dmFsdWU=', 'type': 'base64'}}) def test_openView(self): """ Test openView. """ d = self.client.openView("mydb", "viewdoc", "myview") self.assertEquals(self.client.uri, "/mydb/_design/viewdoc/_view/myview") self.assertEquals(self.client.kwargs["method"], "GET") return self._checkParseDeferred(d) def test_openViewWithQuery(self): """ Test openView with query arguments. """ d = self.client.openView("mydb", "viewdoc", "myview", startkey="foo", count=10) self.assertEquals(self.client.kwargs["method"], "GET") self.failUnless( self.client.uri.startswith("/mydb/_design/viewdoc/_view/myview")) query = cgi.parse_qs(self.client.uri.split('?', 1)[-1]) self.assertEquals(query["startkey"], ['"foo"']) self.assertEquals(query["count"], ["10"]) return self._checkParseDeferred(d) def test_openViewWithKeysQuery(self): """ Test openView handles couchdb's strange requirements for keys arguments """ d = self.client.openView("mydb2", "viewdoc2", "myview2", keys=[1,3,4, "hello, world", {1: 5}], count=5) self.assertEquals(self.client.kwargs["method"], "POST") self.failUnless( self.client.uri.startswith('/mydb2/_design/viewdoc2/_view/myview2')) query = cgi.parse_qs(self.client.uri.split('?', 1)[-1]) self.assertEquals(query, dict(count=['5'])) self.assertEquals(self.client.kwargs['postdata'], {'keys': '[1, 3, 4, "hello, world", {"1": 5}]'}) def test_tempView(self): """ Test tempView. """ d = self.client.tempView("mydb", "js code") self.assertEquals(self.client.uri, "/mydb/_temp_view") self.assertEquals(self.client.kwargs["postdata"], "js code") self.assertEquals(self.client.kwargs["method"], "POST") return self._checkParseDeferred(d) def test_addViews(self): """ Test addViews. """ doc = {"value": "bar"} self.client.addViews(doc, {"view1": "js code 1", "view2": "js code 2"}) self.assertEquals(doc["views"], {"view1": "js code 1", "view2": "js code 2"}) def test_bindToDB(self): """ Test bindToDB, calling a bind method afterwards. """ self.client.bindToDB("mydb") d = self.client.listDoc() self.assertEquals(self.client.uri, "/mydb/_all_docs") self.assertEquals(self.client.kwargs["method"], "GET") return self._checkParseDeferred(d) def test_escapeId(self): d = self.client.openDoc("mydb", "my doc with spaces") self.assertEquals(self.client.uri, "/mydb/my%20doc%20with%20spaces") self.assertEquals(self.client.kwargs["method"], "GET") return self._checkParseDeferred(d) class FakeCouchDBResource(resource.Resource): """ Fake a couchDB resource. @ivar result: value set in tests to be returned by the resource. @param result: C{str} """ result = "" def getChild(self, path, request): """ Return self as only child. """ return self def render(self, request): """ Return C{result}. """ return self.result class ConnectedCouchDBTestCase(TestCase): """ Test C{CouchDB} with a real web server. """ def setUp(self): """ Create a web server and a client bound to it. """ self.resource = FakeCouchDBResource() site = server.Site(self.resource) port = reactor.listenTCP(0, site, interface="127.0.0.1") self.addCleanup(port.stopListening) self.client = paisley.CouchDB("127.0.0.1", port.getHost().port) def test_createDB(self): """ Test listDB. """ data = [u"mydb"] self.resource.result = json.dumps(data) d = self.client.listDB() def cb(result): self.assertEquals(result, data) d.addCallback(cb) return d