diff --git a/cloudant/account.py b/cloudant/account.py index 5ad212e..18f5e93 100644 --- a/cloudant/account.py +++ b/cloudant/account.py @@ -6,12 +6,21 @@ class Account(Resource): """ - A account to a Cloudant or CouchDB account. + An account to a Cloudant or CouchDB account. account = cloudant.Account() - account.login(USERNAME, PASSWORD).result() - print account.get().result().json() - # {"couchdb": "Welcome", ...} + response = account.login(USERNAME, PASSWORD) + print response.json() + # { "ok": True, ... } + + Like all Cloudant-Python objects, pass `async=True` + to make asynchronous requests, like this: + + account = cloudant.Account(async=True) + future = account.login(USERNAME, PASSWORD) + response = future.result() + print response.json() + # { "ok": True, ... } """ def __init__(self, uri="http://localhost:5984", **kwargs): @@ -34,7 +43,11 @@ def __delitem__(self, name): Blocks until the response returns, and raises an error if the deletion failed. """ - return self.database(name, **self.opts).delete().result().raise_for_status() + response = self.database(name, **self.opts).delete() + # block until result if the object is using async + if hasattr(response, 'result'): + response = response.result() + response.raise_for_status() def all_dbs(self, **kwargs): """List all databases.""" diff --git a/cloudant/database.py b/cloudant/database.py index 2398c7a..e599631 100644 --- a/cloudant/database.py +++ b/cloudant/database.py @@ -35,7 +35,11 @@ def __getitem__(self, name): def __setitem__(self, name, doc): """Creates `doc` with an ID of `name`.""" - self.put(name, params=doc).result() + response = self.put(name, params=doc) + # block until result if the object is using async + if hasattr(response, 'result'): + response = response.result() + response.raise_for_status() def all_docs(self, **kwargs): """ diff --git a/cloudant/document.py b/cloudant/document.py index 309f45b..aabc9bc 100644 --- a/cloudant/document.py +++ b/cloudant/document.py @@ -23,7 +23,11 @@ def merge(self, change, **kwargs): Merge `change` into the document, and then `PUT` the updated document back to the server. """ - doc = self.get().result().json() + response = self.get() + # block until result if the object is using async + if hasattr(response, 'result'): + response = response.result() + doc = response.json() doc.update(change) return self.put(params=doc, **kwargs) diff --git a/cloudant/index.py b/cloudant/index.py index 152b93d..8c89186 100644 --- a/cloudant/index.py +++ b/cloudant/index.py @@ -21,7 +21,10 @@ class Index(Resource): """ def __iter__(self): - response = self.get(stream=True).result() + response = self.get(stream=True) + # block until result if the object is using async + if hasattr(response, 'result'): + response = response.result() for line in response.iter_lines(): if line: if line[-1] == ',': diff --git a/cloudant/resource.py b/cloudant/resource.py index f9e260b..7bb938e 100644 --- a/cloudant/resource.py +++ b/cloudant/resource.py @@ -1,4 +1,5 @@ from requests_futures.sessions import FuturesSession +import requests import urlparse import json import copy @@ -20,11 +21,14 @@ def __init__(self, uri, **kwargs): self.uri = uri self.uri_parts = urlparse.urlparse(self.uri) - if 'session' in kwargs.keys(): + if kwargs.get('session'): self._session = kwargs['session'] del kwargs['session'] - else: + elif kwargs.get('async'): self._session = FuturesSession() + del kwargs['async'] + else: + self._session = requests.Session() self._set_options(**kwargs) diff --git a/docs/site/index.html b/docs/site/index.html index baa7848..468731b 100644 --- a/docs/site/index.html +++ b/docs/site/index.html @@ -42,7 +42,7 @@

Install

pip install cloudant
 

Usage

-

Cloudant-Python is an asynchronous wrapper around Python Requests for interacting with CouchDB or Cloudant instances. Check it out:

+

Cloudant-Python is a wrapper around Python Requests for interacting with CouchDB or Cloudant instances. Check it out:

import cloudant
 
 # connect to your account
@@ -52,19 +52,34 @@ 

Usage

# login, so we can make changes login = account.login(USERNAME, PASSWORD) -assert login.result().status_code == 200 +assert login.status_code == 200 # create a database object db = account.database('test') # now, create the database on the server -future = db.put() -response = future.result() +response = db.put() print response.json() # {'ok': True}
-

HTTP requests return Future objects, which will await the return of the HTTP response. Call result() to get the Response object.

+

HTTP requests return [Response][response] objects, right from Requests.

+

Cloudant-Python can also make asynchronous requests by passing async=True to an object's constructor, like so:

+
import cloudant
+
+# connect to your account
+# in this case, https://garbados.cloudant.com
+USERNAME = 'garbados'
+account = cloudant.Account(USERNAME, async=True)
+
+# login, so we can make changes
+future = account.login(USERNAME, PASSWORD)
+# block until we get the response body
+login = future.result()
+assert login.status_code == 200
+
+ +

Asynchronous HTTP requests return Future objects, which will await the return of the HTTP response. Call result() to get the Response object.

See the API reference for all the details you could ever want.

Philosophy

Cloudant-Python is minimal, performant, and effortless. Check it out:

@@ -92,11 +107,11 @@

Pythonisms

resp = doc.put({ '_id': 'hello_world', 'herp': 'derp' - }).result() + }) # delete the document rev = resp.json()['_rev'] -doc.delete(rev).result() +doc.delete(rev).raise_for_status() # but this also creates a document db['hello_world'] = {'herp': 'derp'} @@ -129,7 +144,7 @@

Special Endpoints

  • http://localhost:5984/DB/_design/DOC/_view/INDEX -> Account().database(DB).design(DOC).view(INDEX)
  • Asynchronous

    -

    HTTP request methods like get and post return Future objects, which represent an eventual response. This allows your code to keep executing while the request is off doing its business in cyberspace. To get the Response object (waiting until it arrives if necessary) use the result method, like so:

    +

    If you instantiate an object with the async=True option, its HTTP request methods (such as get and post) will return Future objects, which represent an eventual response. This allows your code to keep executing while the request is off doing its business in cyberspace. To get the Response object (waiting until it arrives if necessary) use the result method, like so:

    import cloudant
     
     account = cloudant.Account()
    @@ -186,9 +201,17 @@ 

    API Reference

    Account

    A account to a Cloudant or CouchDB account.

    account = cloudant.Account()
    -account.login(USERNAME, PASSWORD).result()
    -print account.get().result().json()
    -# {"couchdb": "Welcome", ...}
    +response = account.login(USERNAME, PASSWORD)
    +print response.json()
    +# { "ok": True, ... }
    +
    +

    Like all Cloudant-Python objects, pass async=True +to make asynchronous requests, like this:

    +
    account = cloudant.Account(async=True)
    +future = account.login(USERNAME, PASSWORD)
    +response = future.result()
    +print response.json()
    +# { "ok": True, ... }
     
    @@ -861,7 +884,11 @@

    merge

        def merge(self, change, **kwargs):
             
    -        doc = self.get().result().json()
    +        response = self.get()
    +        # block until result if the object is using async
    +        if hasattr(response, 'result'):
    +            response = response.result()
    +        doc = response.json()
             doc.update(change)
             return self.put(params=doc, **kwargs)
     
    @@ -1090,7 +1117,11 @@

    merge

        def merge(self, change, **kwargs):
             
    -        doc = self.get().result().json()
    +        response = self.get()
    +        # block until result if the object is using async
    +        if hasattr(response, 'result'):
    +            response = response.result()
    +        doc = response.json()
             doc.update(change)
             return self.put(params=doc, **kwargs)
     
    diff --git a/readme.md b/readme.md index bd8d24c..ee9bbf0 100644 --- a/readme.md +++ b/readme.md @@ -6,6 +6,7 @@ [![PyPi downloads](https://pypip.in/d/cloudant/badge.png)](https://crate.io/packages/cloudant/) [futures]: http://docs.python.org/dev/library/concurrent.futures.html#future-objects +[requests]: http://www.python-requests.org/en/latest/ [responses]: http://www.python-requests.org/en/latest/api/#requests.Response An effortless Cloudant / CouchDB interface for Python. @@ -16,7 +17,7 @@ An effortless Cloudant / CouchDB interface for Python. ## Usage -Cloudant-Python is an asynchronous wrapper around Python [Requests](http://www.python-requests.org/en/latest/) for interacting with CouchDB or Cloudant instances. Check it out: +Cloudant-Python is a wrapper around Python [Requests][requests] for interacting with CouchDB or Cloudant instances. Check it out: ```python import cloudant @@ -28,19 +29,37 @@ account = cloudant.Account(USERNAME) # login, so we can make changes login = account.login(USERNAME, PASSWORD) -assert login.result().status_code == 200 +assert login.status_code == 200 # create a database object db = account.database('test') # now, create the database on the server -future = db.put() -response = future.result() +response = db.put() print response.json() # {'ok': True} ``` -HTTP requests return [Future][futures] objects, which will await the return of the HTTP response. Call `result()` to get the [Response][responses] object. +HTTP requests return [Response][responses] objects, right from [Requests][requests]. + +Cloudant-Python can also make asynchronous requests by passing `async=True` to an object's constructor, like so: + +```python +import cloudant + +# connect to your account +# in this case, https://garbados.cloudant.com +USERNAME = 'garbados' +account = cloudant.Account(USERNAME, async=True) + +# login, so we can make changes +future = account.login(USERNAME, PASSWORD) +# block until we get the response body +login = future.result() +assert login.status_code == 200 +``` + +Asynchronous HTTP requests return [Future][futures] objects, which will await the return of the HTTP response. Call `result()` to get the [Response][responses] object. See the [API reference](http://cloudant-labs.github.io/cloudant-python/#api) for all the details you could ever want. @@ -77,11 +96,11 @@ doc = db.document('test_doc') resp = doc.put({ '_id': 'hello_world', 'herp': 'derp' - }).result() + }) # delete the document rev = resp.json()['_rev'] -doc.delete(rev).result() +doc.delete(rev).raise_for_status() # but this also creates a document db['hello_world'] = {'herp': 'derp'} @@ -120,12 +139,12 @@ If CouchDB has a special endpoint for something, it's in Cloudant-Python as a sp ### Asynchronous -HTTP request methods like `get` and `post` return [Future][futures] objects, which represent an eventual response. This allows your code to keep executing while the request is off doing its business in cyberspace. To get the [Response][responses] object (waiting until it arrives if necessary) use the `result` method, like so: +If you instantiate an object with the `async=True` option, its HTTP request methods (such as `get` and `post`) will return [Future][futures] objects, which represent an eventual response. This allows your code to keep executing while the request is off doing its business in cyberspace. To get the [Response][responses] object (waiting until it arrives if necessary) use the `result` method, like so: ```python import cloudant -account = cloudant.Account() +account = cloudant.Account(async=True) db = account['test'] future = db.put() response = future.result() diff --git a/test/__init__.py b/test/__init__.py index 0bd64cd..1795753 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -9,7 +9,7 @@ class ResourceTest(unittest.TestCase): def setUp(self): self.uri = 'http://localhost:5984' - names = cloudant.Account(self.uri).uuids(4).result().json()['uuids'] + names = cloudant.Account(self.uri).uuids(4).json()['uuids'] # database names must start with a letter names = map(lambda name: 'a' + name, names) self.db_name = names[0] @@ -26,6 +26,50 @@ def setUp(self): 'name': 'Larry, the Incorrigible Miscreant' } +class AsyncTest(ResourceTest): + + def setUp(self): + super(AsyncTest, self).setUp() + self.account = cloudant.Account(self.uri, async=True) + self.database = self.account.database(self.db_name) + self.document = self.database.document(self.doc_name) + + def testAccount(self): + future = self.account.get() + response = future.result() + response.raise_for_status() + + def testDatabase(self): + future = self.database.put() + response = future.result() + response.raise_for_status() + del self.account[self.db_name] + + def testDocument(self): + future = self.database.put() + response = future.result() + response.raise_for_status() + self.database[self.doc_name] = self.test_doc + future = self.document.merge(self.test_otherdoc) + response = future.result() + response.raise_for_status() + del self.account[self.db_name] + + def testIndex(self): + future = self.database.put() + response = future.result() + response.raise_for_status() + + future = self.database.bulk_docs(self.test_doc, self.test_otherdoc) + response = future.result() + response.raise_for_status() + + total = [] + for doc in self.database: + total.append(doc) + assert len(total) == 2 + + del self.account[self.db_name] class AccountTest(ResourceTest): @@ -38,39 +82,39 @@ def testCloudant(self): assert account.uri == "https://garbados.cloudant.com" def testAllDbs(self): - assert self.account.all_dbs().result().status_code == 200 + assert self.account.all_dbs().status_code == 200 def testSession(self): - assert self.account.session().result().status_code == 200 + assert self.account.session().status_code == 200 def testActiveTasks(self): - assert self.account.active_tasks().result().status_code == 200 + assert self.account.active_tasks().status_code == 200 def testSecurity(self): username = 'user' password = 'password' # try auth login when admin party is on - assert self.account.login(username, password).result().status_code == 401 + assert self.account.login(username, password).status_code == 401 # disable admin party path = '_config/admins/%s' % username assert self.account.put(path, data="\"%s\"" % - password).result().status_code == 200 + password).status_code == 200 # login, logout - assert self.account.login(username, password).result().status_code == 200 - assert self.account.logout().result().status_code == 200 + assert self.account.login(username, password).status_code == 200 + assert self.account.logout().status_code == 200 # re-enable admin party - assert self.account.login(username, password).result().status_code == 200 - assert self.account.delete(path).result().status_code == 200 + assert self.account.login(username, password).status_code == 200 + assert self.account.delete(path).status_code == 200 def testReplicate(self): self.db = self.account.database(self.db_name) - assert self.db.put().result().status_code == 201 + assert self.db.put().status_code == 201 params = dict(create_target=True) assert self.account.replicate( - self.db_name, self.otherdb_name, params=params).result().status_code == 200 + self.db_name, self.otherdb_name, params=params).status_code == 200 - assert self.db.delete().result().status_code == 200 + assert self.db.delete().status_code == 200 del self.account[self.otherdb_name] def testCreateDb(self): @@ -78,7 +122,7 @@ def testCreateDb(self): self.account[self.db_name] def testUuids(self): - assert self.account.uuids().result().status_code == 200 + assert self.account.uuids().status_code == 200 class DatabaseTest(ResourceTest): @@ -89,19 +133,19 @@ def setUp(self): db_name = '/'.join([self.uri, self.db_name]) self.db = cloudant.Database(db_name) - response = self.db.put().result() + response = self.db.put() response.raise_for_status() def testGet(self): - assert self.db.get().result().status_code == 200 + assert self.db.get().status_code == 200 def testBulk(self): assert self.db.bulk_docs( - self.test_doc, self.test_otherdoc).result().status_code == 201 + self.test_doc, self.test_otherdoc).status_code == 201 def testIter(self): assert self.db.bulk_docs( - self.test_doc, self.test_otherdoc).result().status_code == 201 + self.test_doc, self.test_otherdoc).status_code == 201 for derp in self.db: pass @@ -109,27 +153,27 @@ def testAllDocs(self): self.db.all_docs() def testChanges(self): - assert self.db.changes().result().status_code == 200 + assert self.db.changes().status_code == 200 assert self.db.changes(params={ 'feed': 'continuous' - }).result().status_code == 200 + }).status_code == 200 def testViewCleanup(self): - assert self.db.view_cleanup().result().status_code == 202 + assert self.db.view_cleanup().status_code == 202 def testRevs(self): # put some docs assert self.db.bulk_docs( - self.test_doc, self.test_otherdoc).result().status_code == 201 + self.test_doc, self.test_otherdoc).status_code == 201 # get their revisions revs = defaultdict(list) for doc in self.db: revs[doc['id']].append(doc['value']['rev']) - assert self.db.missing_revs(revs).result().status_code == 200 - assert self.db.revs_diff(revs).result().status_code == 200 + assert self.db.missing_revs(revs).status_code == 200 + assert self.db.revs_diff(revs).status_code == 200 def tearDown(self): - assert self.db.delete().result().status_code == 200 + assert self.db.delete().status_code == 200 class DocumentTest(ResourceTest): @@ -137,29 +181,29 @@ class DocumentTest(ResourceTest): def setUp(self): super(DocumentTest, self).setUp() self.db = cloudant.Database('/'.join([self.uri, self.db_name])) - assert self.db.put().result().status_code == 201 + assert self.db.put().status_code == 201 self.doc = self.db.document(self.doc_name) def testCrud(self): - assert self.doc.put(params=self.test_doc).result().status_code == 201 - resp = self.doc.get().result() + assert self.doc.put(params=self.test_doc).status_code == 201 + resp = self.doc.get() assert resp.status_code == 200 rev = resp.json()['_rev'] - assert self.doc.delete(rev).result().status_code == 200 + assert self.doc.delete(rev).status_code == 200 def testDict(self): self.db[self.doc_name] = self.test_doc self.db[self.doc_name] def testMerge(self): - assert self.doc.put(params=self.test_doc).result().status_code == 201 - assert self.doc.merge(self.test_otherdoc).result().status_code == 201 + assert self.doc.put(params=self.test_doc).status_code == 201 + assert self.doc.merge(self.test_otherdoc).status_code == 201 def testAttachment(self): self.doc.attachment('file') def tearDown(self): - assert self.db.delete().result().status_code == 200 + assert self.db.delete().status_code == 200 class DesignTest(ResourceTest): @@ -167,9 +211,9 @@ class DesignTest(ResourceTest): def setUp(self): super(DesignTest, self).setUp() self.db = cloudant.Database('/'.join([self.uri, self.db_name])) - assert self.db.put().result().status_code == 201 + assert self.db.put().status_code == 201 self.doc = self.db.design('ddoc') - assert self.doc.put(params=self.test_doc).result().status_code == 201 + assert self.doc.put(params=self.test_doc).status_code == 201 def testView(self): self.doc.index('_view/derp') @@ -178,11 +222,11 @@ def testView(self): def testList(self): # todo: test on actual list and show functions - assert self.doc.list('herp', 'derp').result().status_code == 404 - assert self.doc.show('herp', 'derp').result().status_code == 500 + assert self.doc.list('herp', 'derp').status_code == 404 + assert self.doc.show('herp', 'derp').status_code == 500 def tearDown(self): - assert self.db.delete().result().status_code == 200 + assert self.db.delete().status_code == 200 class AttachmentTest(ResourceTest): @@ -194,7 +238,7 @@ class IndexTest(ResourceTest): def setUp(self): super(IndexTest, self).setUp() self.db = cloudant.Database('/'.join([self.uri, self.db_name])) - assert self.db.put().result().status_code == 201 + assert self.db.put().status_code == 201 self.doc = self.db.document(self.doc_name) def testPrimaryIndex(self): @@ -202,12 +246,12 @@ def testPrimaryIndex(self): Show that views can be used as iterators """ for doc in [self.test_doc, self.test_otherdoc]: - assert self.db.post(params=doc).result().status_code == 201 + assert self.db.post(params=doc).status_code == 201 for derp in self.db.all_docs(): pass def tearDown(self): - assert self.db.delete().result().status_code == 200 + assert self.db.delete().status_code == 200 class ErrorTest(ResourceTest): @@ -217,7 +261,7 @@ def setUp(self): self.db = cloudant.Database('/'.join([self.uri, self.db_name])) def testMissing(self): - response = self.db.get().result() + response = self.db.get() assert response.status_code == 404 if __name__ == "__main__":