├── .gitignore ├── .travis.yml ├── CONTRIBUTORS.md ├── README.rst ├── cloudant ├── __init__.py ├── account.py ├── attachment.py ├── database.py ├── design.py ├── document.py ├── index.py └── resource.py ├── docs ├── __main__.py ├── site │ ├── index.html │ └── style.css ├── templates │ └── index.html └── util.py ├── readme.md ├── scripts ├── autopep └── clean ├── setup.py └── test └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg 2 | *.egg-info 3 | venv 4 | *.pyc 5 | build 6 | dist 7 | .coverage 8 | .DS_STORE -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | branches: 5 | only: 6 | - master 7 | services: 8 | - couchdb 9 | before_script: 10 | - pip install coverage coveralls 11 | script: 12 | - coverage run --source=cloudant setup.py test 13 | after_success: 14 | - coveralls -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | Contributors 2 | ============ 3 | 4 | * [Dustin Collins](https://github.com/dustinmm80) 5 | * [Max Thayer](http://github.com/garbados) 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Cloudant-Python 2 | =============== 3 | 4 | This Version is Deprecated 5 | -------------------------- 6 | 7 | As of 13 October 2015, this development branch is deprecated in favor of `the new 8 | development branch starting at version 2.0.0a1 `_. 9 | 10 | **The new version will introduce breaking changes. No attempt was made to follow the 11 | API in 0.5.10.** 12 | 13 | This is the final version of this branch -- 0.5.10. 14 | 15 | Please use the new library for your new projects and begin to migrate your old projects that have 16 | used versions 0.5.10 and prior. 17 | 18 | We will keep 0.5.10 as the latest stable version on PyPI until early 2016, at which time 19 | we plan to switch over completely to 2.0.0. 20 | 21 | Alpha and Beta versions starting with 2.0.0a1 will be uploaded to PyPI. The latest alpha 22 | or beta release may be installed by 23 | 24 | .. code-block:: python 25 | 26 | pip install --pre cloudant 27 | 28 | Note that our new development branch is still pre 2.0.0. As such, we cannot make any guarantees, though 29 | we will try, of course, not to introduce new API that will later be removed. 30 | 31 | |Build Status| |Coverage Status| |PyPi version| |PyPi downloads| 32 | 33 | An effortless Cloudant / CouchDB interface for Python 2.7 and 3.2+. 34 | 35 | Install 36 | ------- 37 | 38 | :: 39 | 40 | pip install cloudant 41 | 42 | Usage 43 | ----- 44 | 45 | Cloudant-Python is a wrapper around Python 46 | `Requests `__ for interacting 47 | with CouchDB or Cloudant instances. Check it out: 48 | 49 | .. code:: python 50 | 51 | import cloudant 52 | 53 | # connect to your account 54 | # in this case, https://garbados.cloudant.com 55 | USERNAME = 'garbados' 56 | account = cloudant.Account(USERNAME) 57 | 58 | # login, so we can make changes 59 | login = account.login(USERNAME, PASSWORD) 60 | assert login.status_code == 200 61 | 62 | # create a database object 63 | db = account.database('test') 64 | 65 | # now, create the database on the server 66 | response = db.put() 67 | print response.json() 68 | # {'ok': True} 69 | 70 | HTTP requests return 71 | `Response `__ 72 | objects, right from 73 | `Requests `__. 74 | 75 | Cloudant-Python can also make asynchronous requests by passing 76 | ``async=True`` to an object's constructor, like so: 77 | 78 | .. code:: python 79 | 80 | import cloudant 81 | 82 | # connect to your account 83 | # in this case, https://garbados.cloudant.com 84 | USERNAME = 'garbados' 85 | account = cloudant.Account(USERNAME, async=True) 86 | 87 | # login, so we can make changes 88 | future = account.login(USERNAME, PASSWORD) 89 | # block until we get the response body 90 | login = future.result() 91 | assert login.status_code == 200 92 | 93 | Asynchronous HTTP requests return 94 | `Future `__ 95 | objects, which will await the return of the HTTP response. Call 96 | ``result()`` to get the 97 | `Response `__ 98 | object. 99 | 100 | See the `API 101 | reference `__ for 102 | all the details you could ever want. 103 | 104 | Philosophy 105 | ---------- 106 | 107 | Cloudant-Python is minimal, performant, and effortless. Check it out: 108 | 109 | Pythonisms 110 | ~~~~~~~~~~ 111 | 112 | Cloudant and CouchDB expose REST APIs that map easily into native Python 113 | objects. As much as possible, Cloudant-Python uses native Python objects 114 | as shortcuts to the raw API, so that such convenience never obscures 115 | what's going on underneath. For example: 116 | 117 | .. code:: python 118 | 119 | import cloudant 120 | 121 | # connect to http://localhost:5984 122 | account = cloudant.Account() 123 | db = account.database('test') 124 | same_db = account['test'] 125 | assert db.uri == same_db.uri 126 | # True 127 | 128 | Cloudant-Python expose raw interactions -- HTTP requests, etc. -- 129 | through special methods, so we provide syntactical sugar without 130 | obscuring the underlying API. Built-ins, such as ``__getitem__``, act as 131 | Pythonic shortcuts to those methods. For example: 132 | 133 | .. code:: python 134 | 135 | import cloudant 136 | 137 | account = cloudant.Account('garbados') 138 | 139 | db_name = 'test' 140 | db = account.database(db_name) 141 | doc = db.document('test_doc') 142 | 143 | # create the document 144 | resp = doc.put(params={ 145 | '_id': 'hello_world', 146 | 'herp': 'derp' 147 | }) 148 | 149 | # delete the document 150 | rev = resp.json()['_rev'] 151 | doc.delete(rev).raise_for_status() 152 | 153 | # but this also creates a document 154 | db['hello_world'] = {'herp': 'derp'} 155 | 156 | # and this deletes the database 157 | del account[db_name] 158 | 159 | Iterate over Indexes 160 | ~~~~~~~~~~~~~~~~~~~~ 161 | 162 | Indexes, such as `views `__ 163 | and Cloudant's `search 164 | indexes `__, act as 165 | iterators. Check it out: 166 | 167 | .. code:: python 168 | 169 | import cloudant 170 | 171 | account = cloudant.Account('garbados') 172 | db = account.database('test') 173 | view = db.all_docs() # returns all docs in the database 174 | for doc in db: 175 | # iterates over every doc in the database 176 | pass 177 | for doc in view: 178 | # and so does this! 179 | pass 180 | for doc in view.iter(descending=True): 181 | # use `iter` to pass options to a view and then iterate over them 182 | pass 183 | 184 | `Behind the 185 | scenes `__, 186 | Cloudant-Python yields documents only as you consume them, so you only 187 | load into memory the documents you're using. 188 | 189 | Special Endpoints 190 | ~~~~~~~~~~~~~~~~~ 191 | 192 | If CouchDB has a special endpoint for something, it's in Cloudant-Python 193 | as a special method, so any special circumstances are taken care of 194 | automagically. As a rule, any endpoint like ``_METHOD`` is in 195 | Cloudant-Python as ``Object.METHOD``. For example: 196 | 197 | - ``https://garbados.cloudant.com/_all_dbs`` -> 198 | ``Account('garbados').all_dbs()`` 199 | - ``http://localhost:5984/DB/_all_docs`` -> 200 | ``Account().database(DB).all_docs()`` 201 | - ``http://localhost:5984/DB/_design/DOC/_view/INDEX`` -> 202 | ``Account().database(DB).design(DOC).view(INDEX)`` 203 | 204 | Asynchronous 205 | ~~~~~~~~~~~~ 206 | 207 | If you instantiate an object with the ``async=True`` option, its HTTP 208 | request methods (such as ``get`` and ``post``) will return 209 | `Future `__ 210 | objects, which represent an eventual response. This allows your code to 211 | keep executing while the request is off doing its business in 212 | cyberspace. To get the 213 | `Response `__ 214 | object (waiting until it arrives if necessary) use the ``result`` 215 | method, like so: 216 | 217 | .. code:: python 218 | 219 | import cloudant 220 | 221 | account = cloudant.Account(async=True) 222 | db = account['test'] 223 | future = db.put() 224 | response = future.result() 225 | print db.get().result().json() 226 | # {'db_name': 'test', ...} 227 | 228 | As a result, any methods which must make an HTTP request return a 229 | `Future `__ 230 | object. 231 | 232 | Option Inheritance 233 | ~~~~~~~~~~~~~~~~~~ 234 | 235 | If you use one object to create another, the child will inherit the 236 | parents' settings. So, you can create a ``Database`` object explicitly, 237 | or use ``Account.database`` to inherit cookies and other settings from 238 | the ``Account`` object. For example: 239 | 240 | .. code:: python 241 | 242 | import cloudant 243 | 244 | account = cloudant.Account('garbados') 245 | db = account.database('test') 246 | doc = db.document('test_doc') 247 | 248 | url = 'https://garbados.cloudant.com' 249 | path = '/test/test_doc' 250 | otherdoc = cloudant.Document(url + path) 251 | 252 | assert doc.uri == otherdoc.uri 253 | # True 254 | 255 | Testing 256 | ------- 257 | 258 | To run Cloudant-Python's tests, just do: 259 | 260 | :: 261 | 262 | python setup.py test 263 | 264 | Documentation 265 | ------------- 266 | 267 | The API reference is automatically generated from the docstrings of each 268 | class and its methods. To install Cloudant-Python with the necessary 269 | extensions to build the docs, do this: 270 | 271 | :: 272 | 273 | pip install -e cloudant[docs] 274 | 275 | Then, in Cloudant-Python's root directory, do this: 276 | 277 | :: 278 | 279 | python docs 280 | 281 | Note: docstrings are in 282 | `Markdown `__. 283 | 284 | License 285 | ------- 286 | 287 | `MIT `__, yo. 288 | 289 | .. |Build Status| image:: https://travis-ci.org/cloudant-labs/cloudant-python.png 290 | :target: https://travis-ci.org/cloudant-labs/cloudant-python 291 | .. |Coverage Status| image:: https://coveralls.io/repos/cloudant-labs/cloudant-python/badge.png 292 | :target: https://coveralls.io/r/cloudant-labs/cloudant-python 293 | .. |PyPi version| image:: https://pypip.in/v/cloudant/badge.png 294 | :target: https://crate.io/packages/cloudant/ 295 | .. |PyPi downloads| image:: https://pypip.in/d/cloudant/badge.png 296 | :target: https://crate.io/packages/cloudant/ 297 | -------------------------------------------------------------------------------- /cloudant/__init__.py: -------------------------------------------------------------------------------- 1 | from .resource import Resource 2 | from .account import Account 3 | from .database import Database 4 | from .document import Document 5 | from .design import Design 6 | from .attachment import Attachment 7 | from .index import Index 8 | 9 | import warnings 10 | message = """ 11 | ********************************************* 12 | `cloudant` Deprecation Warning 13 | 14 | You are using version 0.5.10 of this library, 15 | which is now deprecated. It will be replaced 16 | with version 2.0.0 in early 2016. This will 17 | introduce breaking changes. Please upgrade as 18 | soon as possible. Find out more at 19 | https://github.com/cloudant/python-cloudant 20 | ********************************************* 21 | """ 22 | 23 | #we don't use DeprecationWarning because that message is ignored by default 24 | warnings.warn(message) 25 | -------------------------------------------------------------------------------- /cloudant/account.py: -------------------------------------------------------------------------------- 1 | from .resource import Resource 2 | from .database import Database 3 | 4 | try: 5 | import urllib.parse as urlparse 6 | except ImportError: 7 | import urlparse 8 | 9 | 10 | class Account(Resource): 11 | 12 | """ 13 | An account to a Cloudant or CouchDB account. 14 | 15 | # connects to http://localhost:5984 16 | # if a string is passed, connects to %s.cloudant.com 17 | account = cloudant.Account() 18 | response = account.login(USERNAME, PASSWORD) 19 | print response.json() 20 | # { "ok": True, ... } 21 | 22 | Like all Cloudant-Python objects, pass `async=True` 23 | to make asynchronous requests, like this: 24 | 25 | account = cloudant.Account(async=True) 26 | future = account.login(USERNAME, PASSWORD) 27 | response = future.result() 28 | print response.json() 29 | # { "ok": True, ... } 30 | 31 | Although you can use `login` to request a cookie, 32 | you can also set `account._session.auth` to make Cloudant-Python 33 | use those credentials on every request, like this: 34 | 35 | account = cloudant.Account() 36 | account._session.auth = (username, password) 37 | """ 38 | 39 | def __init__(self, uri="http://localhost:5984", **kwargs): 40 | if not urlparse.urlparse(uri).scheme: 41 | uri = "https://%s.cloudant.com" % uri 42 | super(Account, self).__init__(uri, **kwargs) 43 | 44 | def database(self, name, **kwargs): 45 | """Create a `Database` object prefixed with this account's URL.""" 46 | opts = dict(self.opts, **kwargs) 47 | return Database(self._make_url(name), session=self._session, **opts) 48 | 49 | def __getitem__(self, name): 50 | """Shortcut to `Account.database`.""" 51 | return self.database(name, **self.opts) 52 | 53 | def __delitem__(self, name): 54 | """ 55 | Delete a database named `name`. 56 | Blocks until the response returns, 57 | and raises an error if the deletion failed. 58 | """ 59 | response = self.database(name, **self.opts).delete() 60 | # block until result if the object is using async 61 | if hasattr(response, 'result'): 62 | response = response.result() 63 | response.raise_for_status() 64 | 65 | def session(self, **kwargs): 66 | """Get current user's authentication and authorization status.""" 67 | return self.get(self._reset_path('_session'), **kwargs) 68 | 69 | def login(self, username, password, **kwargs): 70 | """Authenticate the connection via cookie.""" 71 | # set headers, body explicitly 72 | headers = { 73 | "Content-Type": "application/x-www-form-urlencoded" 74 | } 75 | data = "name=%s&password=%s" % (username, password) 76 | return self.post(self._reset_path('_session'), headers=headers, 77 | data=data, **kwargs) 78 | 79 | def logout(self, **kwargs): 80 | """De-authenticate the connection's cookie.""" 81 | return self.delete(self._reset_path('_session'), **kwargs) 82 | 83 | def all_dbs(self, **kwargs): 84 | """List all databases.""" 85 | return self.get('_all_dbs', **kwargs) 86 | 87 | def active_tasks(self, **kwargs): 88 | """List replication, compaction, and indexer tasks currently running.""" 89 | return self.get('_active_tasks', **kwargs) 90 | 91 | def replicate(self, source, target, opts={}, **kwargs): 92 | """ 93 | Begin a replication job. 94 | `opts` contains replication options such as whether the replication 95 | should create the target (`create_target`) or whether the replication 96 | is continuous (`continuous`). 97 | 98 | Note: unless continuous, will not return until the job is finished. 99 | """ 100 | 101 | params = { 102 | 'source': source, 103 | 'target': target 104 | } 105 | 106 | params.update(opts) 107 | if 'params' in kwargs: 108 | params.update(kwargs['params']) 109 | del kwargs['params'] 110 | 111 | return self.post('_replicate', params=params, **kwargs) 112 | 113 | def uuids(self, count=1, **kwargs): 114 | """Generate an arbitrary number of UUIDs.""" 115 | params = dict(count=count) 116 | return self.get('_uuids', params=params, **kwargs) 117 | -------------------------------------------------------------------------------- /cloudant/attachment.py: -------------------------------------------------------------------------------- 1 | from .resource import Resource 2 | 3 | 4 | class Attachment(Resource): 5 | 6 | """ 7 | Attachment methods for a single document 8 | """ 9 | 10 | pass # lawl everything is defined by the parent class :D 11 | -------------------------------------------------------------------------------- /cloudant/database.py: -------------------------------------------------------------------------------- 1 | import json 2 | from .resource import Resource 3 | from .document import Document 4 | from .design import Design 5 | from .index import Index 6 | 7 | 8 | class Database(Resource): 9 | 10 | """ 11 | Connection to a specific database. 12 | 13 | Learn more about the raw API from the [Cloudant docs](http://docs.cloudant.com/api/database.html). 14 | """ 15 | 16 | def document(self, name, **kwargs): 17 | """ 18 | Create a `Document` object from `name`. 19 | """ 20 | opts = dict(self.opts, **kwargs) 21 | return Document(self._make_url(name), session=self._session, **opts) 22 | 23 | def design(self, name, **kwargs): 24 | """ 25 | Create a `Design` object from `name`, like so: 26 | 27 | db.design('test') 28 | # refers to DB/_design/test 29 | """ 30 | opts = dict(self.opts, **kwargs) 31 | return Design(self._make_url('/'.join(['_design', name])), session=self._session, **opts) 32 | 33 | def __getitem__(self, name): 34 | """Shortcut to `Database.document`.""" 35 | return self.document(name, **self.opts) 36 | 37 | def __setitem__(self, name, doc): 38 | """Creates `doc` with an ID of `name`.""" 39 | response = self.put(name, params=doc) 40 | # block until result if the object is using async 41 | if hasattr(response, 'result'): 42 | response = response.result() 43 | response.raise_for_status() 44 | 45 | def __delitem__(self, name): 46 | """ 47 | Shortcut to synchronously deleting the document from the database. 48 | For example: 49 | 50 | del db['docKey'] 51 | """ 52 | doc = self.document(name) 53 | response = doc.get() 54 | # block until result if the object is using async/is a future 55 | if hasattr(response, 'result'): 56 | response = response.result() 57 | response.raise_for_status() 58 | rev = response.json()['_rev'] 59 | deletion = doc.delete(rev) 60 | # block until result if the object is using async/is a future 61 | if hasattr(deletion, 'result'): 62 | deletion = deletion.result() 63 | deletion.raise_for_status() 64 | 65 | def all_docs(self, **kwargs): 66 | """ 67 | Return an `Index` object referencing all documents in the database. 68 | You can treat it like an iterator: 69 | 70 | for doc in db.all_docs(): 71 | print doc 72 | """ 73 | return Index(self._make_url('_all_docs'), session=self._session, **kwargs) 74 | 75 | def __iter__(self): 76 | """Formats `Database.all_docs` for use as an iterator.""" 77 | return self.all_docs().__iter__() 78 | 79 | def bulk_docs(self, *docs, **kwargs): 80 | """ 81 | Save many docs, all at once. Each `doc` argument must be a dict, like this: 82 | 83 | db.bulk_docs({...}, {...}, {...}) 84 | # saves all documents in one HTTP request 85 | 86 | For more detail on bulk operations, see 87 | [Creating or updating multiple documents](http://docs.cloudant.com/api/database.html#creating-or-updating-multiple-documents) 88 | """ 89 | params = { 90 | 'docs': docs 91 | } 92 | return self.post('_bulk_docs', params=params, **kwargs) 93 | 94 | def changes(self, **kwargs): 95 | """ 96 | Gets a list of the changes made to the database. 97 | This can be used to monitor for update and modifications to the database 98 | for post processing or synchronization. 99 | 100 | Automatically adjusts the request to handle the different response behavior 101 | of polling, longpolling, and continuous modes. 102 | 103 | For more information about the `_changes` feed, see 104 | [the docs](http://docs.cloudant.com/api/database.html#obtaining-a-list-of-changes). 105 | """ 106 | size = 512 # requests default chunk size value 107 | if 'params' in kwargs: 108 | if 'feed' in kwargs['params']: 109 | if kwargs['params']['feed'] == 'continuous': 110 | kwargs['stream'] = True 111 | size = 1 # 1 byte because we don't want to hold the last 112 | # record in memory buffer in awaiting for new data 113 | emit_heartbeats = kwargs.pop('emit_heartbeats', False) 114 | 115 | response = self.get('_changes', **kwargs) 116 | response.raise_for_status() 117 | 118 | for line in response.iter_lines(chunk_size=size): 119 | if not line: 120 | if emit_heartbeats: 121 | yield None 122 | continue 123 | line = line.decode('utf-8') 124 | if line[-1] == ',': 125 | line = line[:-1] 126 | yield json.loads(line) 127 | 128 | def missing_revs(self, revs, **kwargs): 129 | """ 130 | Refers to [this method](http://docs.cloudant.com/api/database.html#retrieving-missing-revisions). 131 | """ 132 | return self.post('_missing_revs', params=revs, **kwargs) 133 | 134 | def revs_diff(self, revs, **kwargs): 135 | """ 136 | Refers to [this method](http://docs.cloudant.com/api/database.html#retrieving-differences-between-revisions) 137 | """ 138 | return self.post('_revs_diff', params=revs, **kwargs) 139 | 140 | def view_cleanup(self, **kwargs): 141 | """ 142 | Cleans up the cached view output on disk for a given view. For example: 143 | 144 | print db.view_cleanup().result().json() 145 | # {'ok': True} 146 | """ 147 | return self.post('_view_cleanup', **kwargs) 148 | -------------------------------------------------------------------------------- /cloudant/design.py: -------------------------------------------------------------------------------- 1 | from .document import Document 2 | from .index import Index 3 | 4 | class Design(Document): 5 | """ 6 | Connection to a design document, which stores custom indexes and other database functions. 7 | 8 | Learn more about design documents from the [Cloudant docs](http://docs.cloudant.com/api/design.html) 9 | """ 10 | 11 | 12 | def index(self, path, **kwargs): 13 | """ 14 | Create a `Index` object referencing the function at `path`. For example: 15 | 16 | index = doc.index('_view/index-name') 17 | # refers to /DB/_design/DOC/_view/index-name 18 | """ 19 | opts = dict(self.opts, **kwargs) 20 | return Index(self._make_url(path), session=self._session, **opts) 21 | 22 | def view(self, function, **kwargs): 23 | """ 24 | Create a `Index` object referencing the secondary index at `_view/{function}`. For example: 25 | 26 | index = doc.view('index-name') 27 | # refers to /DB/_design/DOC/_view/index-name 28 | 29 | For more on secondary indices, see 30 | [Querying a View](http://docs.cloudant.com/api/design-documents-querying-views.html#querying-a-view) 31 | """ 32 | return self.index('/'.join(['_view', function]), **kwargs) 33 | 34 | def search(self, function, **kwargs): 35 | """ 36 | Creates a `Index` object referencing the search index at `_search/{function}`. For example: 37 | 38 | index = doc.search('index-name') 39 | # refers to /DB/_design/DOC/_search/search-name 40 | 41 | For more details on search indexes, see 42 | [Searching for documents using Lucene queries](http://docs.cloudant.com/api/search.html#searching-for-documents-using-lucene-queries) 43 | """ 44 | return self.index('/'.join(['_search', function]), **kwargs) 45 | 46 | def list(self, function, index, **kwargs): 47 | """ 48 | Make a GET request to the list function at `_list/{function}/{index}`. For example: 49 | 50 | future = doc.list('list-name', 'index-name') 51 | # refers to /DB/_design/DOC/_list/list-name/index-name 52 | 53 | For more details on list functions, see 54 | [Querying List Functions](http://docs.cloudant.com/api/design-documents-shows-lists.html#querying-list-functions). 55 | """ 56 | return self.get(self._make_url('/'.join(['_list', function, index])), **kwargs) 57 | 58 | def show(self, function, id, **kwargs): 59 | """ 60 | Make a GET request to the show function at `_show/{function}/{id}`. For example: 61 | 62 | future = doc.show('show-name', 'document-id') 63 | # refers to /DB/_design/DOC/_show/show-name/document-id 64 | 65 | For more details on show functions, see 66 | [Querying Show Functions](http://docs.cloudant.com/api/design-documents-shows-lists.html#querying-show-functions). 67 | """ 68 | return self.get(self._make_url('/'.join(['_show', function, id])), **kwargs) 69 | -------------------------------------------------------------------------------- /cloudant/document.py: -------------------------------------------------------------------------------- 1 | from .resource import Resource 2 | from .attachment import Attachment 3 | 4 | 5 | class Document(Resource): 6 | 7 | """ 8 | Connection to a specific document. 9 | 10 | Learn more about the raw API from the [Cloudant docs](http://docs.cloudant.com/api/documents.html) 11 | """ 12 | 13 | def attachment(self, name, **kwargs): 14 | """ 15 | Create an `Attachment` object from `name` and the settings 16 | for the current database. 17 | """ 18 | opts = dict(self.opts, **kwargs) 19 | return Attachment(self._make_url(name), session=self._session, **opts) 20 | 21 | def merge(self, change, **kwargs): 22 | """ 23 | Merge `change` into the document, 24 | and then `PUT` the updated document back to the server. 25 | """ 26 | response = self.get() 27 | # block until result if the object is using async/is a future 28 | if hasattr(response, 'result'): 29 | response = response.result() 30 | 31 | # handle upserts 32 | if response.status_code == 404: 33 | doc = {} 34 | else: 35 | response.raise_for_status() 36 | doc = response.json() 37 | 38 | # merge! 39 | doc.update(change) 40 | return self.put(params=doc, **kwargs) 41 | 42 | def delete(self, rev, **kwargs): 43 | """ 44 | Delete the given revision of the current document. For example: 45 | 46 | rev = doc.get().result().json()['_rev'] 47 | doc.delete(rev) 48 | """ 49 | return super(Document, self).delete(params={'rev': rev}, **kwargs) 50 | -------------------------------------------------------------------------------- /cloudant/index.py: -------------------------------------------------------------------------------- 1 | from .resource import Resource 2 | import json 3 | 4 | 5 | class Index(Resource): 6 | 7 | """ 8 | Methods for design document indexes. 9 | Different kinds of indexes will behave differently, so here are helpful docs: 10 | 11 | * [Lucene search indexes](http://docs.cloudant.com/api/search.html#searching-for-documents-using-lucene-queries) 12 | * [Secondary indexes / "views"](http://docs.cloudant.com/api/design-documents-querying-views.html#querying-a-view) 13 | 14 | Then, you just use basic HTTP methods to perform queries, like this: 15 | 16 | index.get(params=QUERY_ARGUMENTS) 17 | 18 | Remember, before you can query an index, you have to make sure it's in your database. 19 | See [these docs](http://docs.cloudant.com/api/design-documents-get-put-delete-copy.html#creating-or-updating-a-design-document) 20 | for how to do that. 21 | """ 22 | 23 | def __iter__(self, **kwargs): 24 | response = self.get(stream=True, **kwargs) 25 | # block until result if the object is using async 26 | if hasattr(response, 'result'): 27 | response = response.result() 28 | for line in response.iter_lines(): 29 | line = line.decode('utf-8') 30 | if line: 31 | if line[-1] == ',': 32 | line = line[:-1] 33 | try: 34 | yield json.loads(line) 35 | except (TypeError, ValueError): 36 | # if we can't decode a line, ignore it 37 | pass 38 | 39 | def iter(self, **kwargs): 40 | """ 41 | Like the magic method `__iter__`, but allows you to 42 | pass query parameters, like so: 43 | 44 | view = db.view('...') 45 | options = { 46 | 'key': 'thegoodstuff', 47 | 'include_docs': True 48 | } 49 | for row in view.iter(params=options): 50 | # emits only rows with the key 'thegoodstuff' 51 | # with each row's emitting document 52 | """ 53 | return self.__iter__(**kwargs) 54 | -------------------------------------------------------------------------------- /cloudant/resource.py: -------------------------------------------------------------------------------- 1 | import json 2 | import copy 3 | 4 | import requests 5 | 6 | 7 | try: 8 | from requests_futures.sessions import FuturesSession 9 | requests_futures_available = True 10 | except ImportError: 11 | requests_futures_available = False 12 | 13 | 14 | try: 15 | import urllib.parse as urlparse 16 | except ImportError: 17 | import urlparse 18 | 19 | 20 | class RequestsFutureNotAvailable(Exception): 21 | message = "Please install request_sessions before proceeding." 22 | 23 | 24 | class Resource(object): 25 | 26 | """ 27 | REST resource class: implements GET, POST, PUT, DELETE methods 28 | for all other Divan objects, and manages settings inheritance. 29 | 30 | If you create an object, like an `Account`, then use that to 31 | create a `Database` object, the `Database` will inherit any options 32 | from the `Account` object, such as session cookies. 33 | 34 | Implements CRUD operations for all other Cloudant-Python objects. 35 | """ 36 | def __init__(self, uri, **kwargs): 37 | self.uri = uri 38 | self.uri_parts = urlparse.urlparse(self.uri) 39 | 40 | if kwargs.get('session'): 41 | self._session = kwargs['session'] 42 | del kwargs['session'] 43 | elif 'async' in kwargs: 44 | if kwargs['async']: 45 | if not requests_futures_available: 46 | raise RequestsFutureNotAvailable() 47 | self._session = FuturesSession() 48 | else: 49 | self._session = requests.Session() 50 | del kwargs['async'] 51 | else: 52 | self._session = requests.Session() 53 | 54 | self._set_options(**kwargs) 55 | 56 | def _set_options(self, **kwargs): 57 | if not hasattr(self, 'opts'): 58 | self.opts = { 59 | 'headers': { 60 | 'Content-Type': 'application/json', 61 | 'Accept': 'application/json' 62 | } 63 | } 64 | if kwargs: 65 | self.opts.update(kwargs) 66 | 67 | def _make_url(self, path=''): 68 | """Joins the uri, and optional path""" 69 | if path: 70 | # if given a full URL, use that instead 71 | if urlparse.urlparse(path).scheme: 72 | return path 73 | else: 74 | return '/'.join([self.uri, path]) 75 | else: 76 | return self.uri 77 | 78 | def _reset_path(self, path): 79 | parts = list(self.uri_parts) 80 | parts[2] = path 81 | url = urlparse.urlunparse(parts) 82 | return url 83 | 84 | def _make_request(self, method, path='', **kwargs): 85 | # kwargs supercede self.opts 86 | opts = copy.copy(self.opts) 87 | opts.update(kwargs) 88 | 89 | # normalize `params` kwarg according to method 90 | if 'params' in opts: 91 | if method in ['post', 'put'] and 'data' not in opts: 92 | opts['data'] = json.dumps(opts['params']) 93 | del opts['params'] 94 | else: 95 | # cloudant breaks on True and False, so lowercase it 96 | params = opts['params'] 97 | for key, value in params.items(): 98 | if value in [True, False]: 99 | params[key] = str(value).lower() 100 | elif type(value) in [list, dict, tuple]: 101 | params[key] = json.dumps(value) 102 | opts['params'] = params 103 | 104 | # make the request 105 | future = getattr(self._session, method)(self._make_url(path), **opts) 106 | return future 107 | 108 | def head(self, path='', **kwargs): 109 | """ 110 | Make a HEAD request against the object's URI joined 111 | with `path`. `kwargs` are passed directly to Requests. 112 | """ 113 | return self._make_request('head', path, **kwargs) 114 | 115 | def get(self, path='', **kwargs): 116 | """ 117 | Make a GET request against the object's URI joined 118 | with `path`. `kwargs` are passed directly to Requests. 119 | """ 120 | return self._make_request('get', path, **kwargs) 121 | 122 | def put(self, path='', **kwargs): 123 | """ 124 | Make a PUT request against the object's URI joined 125 | with `path`. 126 | 127 | `kwargs['params']` are turned into JSON before being 128 | passed to Requests. If you want to indicate the message 129 | body without it being modified, use `kwargs['data']`. 130 | """ 131 | return self._make_request('put', path, **kwargs) 132 | 133 | def post(self, path='', **kwargs): 134 | """ 135 | Make a POST request against the object's URI joined 136 | with `path`. 137 | 138 | `kwargs['params']` are turned into JSON before being 139 | passed to Requests. If you want to indicate the message 140 | body without it being modified, use `kwargs['data']`. 141 | """ 142 | return self._make_request('post', path, **kwargs) 143 | 144 | def delete(self, path='', **kwargs): 145 | """ 146 | Make a DELETE request against the object's URI joined 147 | with `path`. `kwargs` are passed directly to Requests. 148 | """ 149 | return self._make_request('delete', path, **kwargs) 150 | -------------------------------------------------------------------------------- /docs/__main__.py: -------------------------------------------------------------------------------- 1 | import util 2 | import os 3 | 4 | ORDER = [ 5 | 'Account', 6 | 'Database', 7 | 'Document', 8 | 'Design', 9 | 'Index', 10 | 'Attachment' 11 | ] 12 | 13 | dirname, filename = os.path.split(os.path.abspath(__file__)) 14 | maindir = os.path.normpath(os.path.join(dirname, '..')) 15 | 16 | README = os.path.normpath(os.path.join(dirname, '..', 'readme.md')) 17 | OUTPUT = os.path.join(dirname, 'site', 'index.html') 18 | TEMPLATE = os.path.join(dirname, 'templates', 'index.html') 19 | 20 | util.generate_docs(ORDER, TEMPLATE, OUTPUT, README) -------------------------------------------------------------------------------- /docs/site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 | Jump To … 13 |
14 |
15 | Home 16 | API Reference 17 | 18 | Account 19 | 20 | Database 21 | 22 | Document 23 | 24 | Design 25 | 26 | Index 27 | 28 | Attachment 29 | 30 |
31 |
32 |
33 |
34 |
35 |

Cloudant-Python

36 |

Build Status 37 | Coverage Status 38 | PyPi version 39 | PyPi downloads

40 |

An effortless Cloudant / CouchDB interface for Python.

41 |

Install

42 |
pip install cloudant
  43 | 
44 |

Usage

45 |

Cloudant-Python is a wrapper around Python Requests for interacting with CouchDB or Cloudant instances. It runs on Python 2.7 and 3.2+. Check it out:

46 |
import cloudant
  47 | 
  48 | # connect to your account
  49 | # in this case, https://garbados.cloudant.com
  50 | USERNAME = 'garbados'
  51 | account = cloudant.Account(USERNAME)
  52 | 
  53 | # login, so we can make changes
  54 | login = account.login(USERNAME, PASSWORD)
  55 | assert login.status_code == 200
  56 | 
  57 | # create a database object
  58 | db = account.database('test')
  59 | 
  60 | # now, create the database on the server
  61 | response = db.put()
  62 | print response.json()
  63 | # {'ok': True}
  64 | 
65 | 66 |

HTTP requests return Response objects, right from Requests.

67 |

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

68 |
import cloudant
  69 | 
  70 | # connect to your account
  71 | # in this case, https://garbados.cloudant.com
  72 | USERNAME = 'garbados'
  73 | account = cloudant.Account(USERNAME, async=True)
  74 | 
  75 | # login, so we can make changes
  76 | future = account.login(USERNAME, PASSWORD)
  77 | # block until we get the response body
  78 | login = future.result()
  79 | assert login.status_code == 200
  80 | 
81 | 82 |

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

83 |

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

84 |

Philosophy

85 |

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

86 |

Pythonisms

87 |

Cloudant and CouchDB expose REST APIs that map easily into native Python objects. As much as possible, Cloudant-Python uses native Python objects as shortcuts to the raw API, so that such convenience never obscures what's going on underneath. For example:

88 |
import cloudant
  89 | 
  90 | # connect to http://localhost:5984
  91 | account = cloudant.Account()
  92 | db = account.database('test')
  93 | same_db = account['test']
  94 | assert db.uri == same_db.uri
  95 | # True
  96 | 
97 | 98 |

Cloudant-Python expose raw interactions -- HTTP requests, etc. -- through special methods, so we provide syntactical sugar without obscuring the underlying API. Built-ins, such as __getitem__, act as Pythonic shortcuts to those methods. For example:

99 |
import cloudant
 100 | 
 101 | account = cloudant.Account('garbados')
 102 | 
 103 | db_name = 'test'
 104 | db = account.database(db_name)
 105 | doc = db.document('test_doc')
 106 | 
 107 | # create the document
 108 | resp = doc.put(params={
 109 |   '_id': 'hello_world',
 110 |   'herp': 'derp'
 111 |   })
 112 | 
 113 | # delete the document
 114 | rev = resp.json()['_rev']
 115 | doc.delete(rev).raise_for_status()
 116 | 
 117 | # but this also creates a document
 118 | db['hello_world'] = {'herp': 'derp'}
 119 | 
 120 | # and this deletes the database
 121 | del account[db_name]
 122 | 
123 | 124 |

Iterate over Indexes

125 |

Indexes, such as views and Cloudant's search indexes, act as iterators. Check it out:

126 |
import cloudant
 127 | 
 128 | account = cloudant.Account('garbados')
 129 | db = account.database('test')
 130 | view = db.all_docs() # returns all docs in the database
 131 | for doc in db:
 132 |   # iterates over every doc in the database
 133 |   pass
 134 | for doc in view:
 135 |   # and so does this!
 136 |   pass
 137 | for doc in view.iter(descending=True):
 138 |   # use `iter` to pass options to a view and then iterate over them
 139 |   pass
 140 | 
141 | 142 |

Behind the scenes, Cloudant-Python yields documents only as you consume them, so you only load into memory the documents you're using.

143 |

Special Endpoints

144 |

If CouchDB has a special endpoint for something, it's in Cloudant-Python as a special method, so any special circumstances are taken care of automagically. As a rule, any endpoint like _METHOD is in Cloudant-Python as Object.METHOD. For example:

145 |
    146 |
  • https://garbados.cloudant.com/_all_dbs -> Account('garbados').all_dbs()
  • 147 |
  • http://localhost:5984/DB/_all_docs -> Account().database(DB).all_docs()
  • 148 |
  • http://localhost:5984/DB/_design/DOC/_view/INDEX -> Account().database(DB).design(DOC).view(INDEX)
  • 149 |
150 |

Asynchronous

151 |

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:

152 |
import cloudant
 153 | 
 154 | account = cloudant.Account(async=True)
 155 | db = account['test']
 156 | future = db.put()
 157 | response = future.result()
 158 | print db.get().result().json()
 159 | # {'db_name': 'test', ...}
 160 | 
161 | 162 |

As a result, any methods which must make an HTTP request return a Future object.

163 |

Option Inheritance

164 |

If you use one object to create another, the child will inherit the parents' settings. So, you can create a Database object explicitly, or use Account.database to inherit cookies and other settings from the Account object. For example:

165 |
import cloudant
 166 | 
 167 | account = cloudant.Account('garbados')
 168 | db = account.database('test')
 169 | doc = db.document('test_doc')
 170 | 
 171 | url = 'https://garbados.cloudant.com'
 172 | path = '/test/test_doc'
 173 | otherdoc = cloudant.Document(url + path)
 174 | 
 175 | assert doc.uri == otherdoc.uri
 176 | # True
 177 | 
178 | 179 |

Testing

180 |

To run Cloudant-Python's tests, just do:

181 |
python setup.py test
 182 | 
183 |

Documentation

184 |

The API reference is automatically generated from the docstrings of each class and its methods. To install Cloudant-Python with the necessary extensions to build the docs, do this:

185 |
pip install -e cloudant[docs]
 186 | 
187 |

Then, in Cloudant-Python's root directory, do this:

188 |
python docs
 189 | 
190 |

Note: docstrings are in Markdown.

191 |

License

192 |

MIT, yo.

193 |
194 |
195 |
196 |
197 |
198 |

API Reference

199 |
200 |
201 |
202 | 203 |
204 |
205 |

Account

206 |

An account to a Cloudant or CouchDB account.

207 |
account = cloudant.Account()
 208 | response = account.login(USERNAME, PASSWORD)
 209 | print response.json()
 210 | # { "ok": True, ... }
 211 | 
212 |

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

214 |
account = cloudant.Account(async=True)
 215 | future = account.login(USERNAME, PASSWORD)
 216 | response = future.result()
 217 | print response.json()
 218 | # { "ok": True, ... }
 219 | 
220 |

Although you can use login to request a cookie, 221 | you can also set account._session.auth to make Cloudant-Python 222 | use those credentials on every request, like this:

223 |
account = cloudant.Account()
 224 | account._session.auth = (username, password)
 225 | 
226 |
227 |
228 |
229 | 230 |
231 |
232 |
233 | # 234 |
235 |

active_tasks

236 |

List replication, compaction, and indexer tasks currently running.

237 |
238 |
239 |
    def active_tasks(self, **kwargs):
 240 |         
 241 |         return self.get('_active_tasks', **kwargs)
 242 | 
243 |
244 |
245 |
246 | 247 |
248 |
249 |
250 | # 251 |
252 |

all_dbs

253 |

List all databases.

254 |
255 |
256 |
    def all_dbs(self, **kwargs):
 257 |         
 258 |         return self.get('_all_dbs', **kwargs)
 259 | 
260 |
261 |
262 |
263 | 264 |
265 |
266 |
267 | # 268 |
269 |

database

270 |

Create a Database object prefixed with this account's URL.

271 |
272 |
273 |
    def database(self, name, **kwargs):
 274 |         
 275 |         opts = dict(self.opts.items() + kwargs.items())
 276 |         return Database(self._make_url(name), session=self._session, **opts)
 277 | 
278 |
279 |
280 |
281 | 282 |
283 |
284 |
285 | # 286 |
287 |

delete

288 |

Make a DELETE request against the object's URI joined 289 | with path. kwargs are passed directly to Requests.

290 |
291 |
292 |
    def delete(self, path='', **kwargs):
 293 |         
 294 |         return self._make_request('delete', path, **kwargs)
 295 | 
296 |
297 |
298 |
299 | 300 |
301 |
302 |
303 | # 304 |
305 |

get

306 |

Make a GET request against the object's URI joined 307 | with path. kwargs are passed directly to Requests.

308 |
309 |
310 |
    def get(self, path='', **kwargs):
 311 |         
 312 |         return self._make_request('get', path, **kwargs)
 313 | 
314 |
315 |
316 |
317 | 318 |
319 |
320 |
321 | # 322 |
323 |

login

324 |

Authenticate the connection via cookie.

325 |
326 |
327 |
    def login(self, username, password, **kwargs):
 328 |         
 329 |         # set headers, body explicitly
 330 |         headers = {
 331 |             "Content-Type": "application/x-www-form-urlencoded"
 332 |         }
 333 |         data = "name=%s&password=%s" % (username, password)
 334 |         return self.post(self._reset_path('_session'), headers=headers, data=data, **kwargs)
 335 | 
336 |
337 |
338 |
339 | 340 |
341 |
342 |
343 | # 344 |
345 |

logout

346 |

De-authenticate the connection's cookie.

347 |
348 |
349 |
    def logout(self, **kwargs):
 350 |         
 351 |         return self.delete(self._reset_path('_session'), **kwargs)
 352 | 
353 |
354 |
355 |
356 | 357 |
358 |
359 |
360 | # 361 |
362 |

post

363 |

Make a POST request against the object's URI joined 364 | with path.

365 |

kwargs['params'] are turned into JSON before being 366 | passed to Requests. If you want to indicate the message 367 | body without it being modified, use kwargs['data'].

368 |
369 |
370 |
    def post(self, path='', **kwargs):
 371 |         
 372 |         return self._make_request('post', path, **kwargs)
 373 | 
374 |
375 |
376 |
377 | 378 |
379 |
380 |
381 | # 382 |
383 |

put

384 |

Make a PUT request against the object's URI joined 385 | with path.

386 |

kwargs['params'] are turned into JSON before being 387 | passed to Requests. If you want to indicate the message 388 | body without it being modified, use kwargs['data'].

389 |
390 |
391 |
    def put(self, path='', **kwargs):
 392 |         
 393 |         return self._make_request('put', path, **kwargs)
 394 | 
395 |
396 |
397 |
398 | 399 |
400 |
401 |
402 | # 403 |
404 |

replicate

405 |

Begin a replication job. 406 | opts contains replication options such as whether the replication 407 | should create the target (create_target) or whether the replication 408 | is continuous (continuous).

409 |

Note: unless continuous, will not return until the job is finished.

410 |
411 |
412 |
    def replicate(self, source, target, opts={}, **kwargs):
 413 |         
 414 | 
 415 |         params = {
 416 |             'source': source,
 417 |             'target': target
 418 |         }
 419 | 
 420 |         params.update(opts)
 421 |         if 'params' in kwargs:
 422 |             params.update(kwargs['params'])
 423 |             del kwargs['params']
 424 | 
 425 |         return self.post('_replicate', params=params, **kwargs)
 426 | 
427 |
428 |
429 |
430 | 431 |
432 |
433 |
434 | # 435 |
436 |

session

437 |

Get current user's authentication and authorization status.

438 |
439 |
440 |
    def session(self, **kwargs):
 441 |         
 442 |         return self.get(self._reset_path('_session'), **kwargs)
 443 | 
444 |
445 |
446 |
447 | 448 |
449 |
450 |
451 | # 452 |
453 |

uuids

454 |

Generate an arbitrary number of UUIDs.

455 |
456 |
457 |
    def uuids(self, count=1, **kwargs):
 458 |         
 459 |         params = dict(count=count)
 460 |         return self.get('_uuids', params=params, **kwargs)
 461 | 
462 |
463 |
464 |
465 | 466 |
467 | 468 |
469 |
470 |

Database

471 |

Connection to a specific database.

472 |

Learn more about the raw API from the Cloudant docs.

473 |
474 |
475 |
476 | 477 |
478 |
479 |
480 | # 481 |
482 |

all_docs

483 |

Return an Index object referencing all documents in the database. 484 | You can treat it like an iterator:

485 |
for doc in db.all_docs():
 486 |     print doc
 487 | 
488 |
489 |
490 |
    def all_docs(self, **kwargs):
 491 |         
 492 |         return Index(self._make_url('_all_docs'), session=self._session, **kwargs)
 493 | 
494 |
495 |
496 |
497 | 498 |
499 |
500 |
501 | # 502 |
503 |

bulk_docs

504 |

Save many docs, all at once. Each doc argument must be a dict, like this:

505 |
    db.bulk_docs({...}, {...}, {...})
 506 |     # saves all documents in one HTTP request
 507 | 
508 |

For more detail on bulk operations, see 509 | Creating or updating multiple documents

510 |
511 |
512 |
    def bulk_docs(self, *docs, **kwargs):
 513 |         
 514 |         params = {
 515 |             'docs': docs
 516 |         }
 517 |         return self.post('_bulk_docs', params=params, **kwargs)
 518 | 
519 |
520 |
521 |
522 | 523 |
524 |
525 |
526 | # 527 |
528 |

changes

529 |

Gets a list of the changes made to the database. 530 | This can be used to monitor for update and modifications to the database 531 | for post processing or synchronization.

532 |

Automatically adjusts the request to handle the different response behavior 533 | of polling, longpolling, and continuous modes.

534 |

For more information about the _changes feed, see 535 | the docs.

536 |
537 |
538 |
    def changes(self, **kwargs):
 539 |         
 540 |         if 'params' in kwargs:
 541 |             if 'feed' in kwargs['params']:
 542 |                 if kwargs['params']['feed'] == 'continuous':
 543 |                     kwargs['stream'] = True
 544 | 
 545 |         return self.get('_changes', **kwargs)
 546 | 
547 |
548 |
549 |
550 | 551 |
552 |
553 |
554 | # 555 |
556 |

delete

557 |

Make a DELETE request against the object's URI joined 558 | with path. kwargs are passed directly to Requests.

559 |
560 |
561 |
    def delete(self, path='', **kwargs):
 562 |         
 563 |         return self._make_request('delete', path, **kwargs)
 564 | 
565 |
566 |
567 |
568 | 569 |
570 |
571 |
572 | # 573 |
574 |

design

575 |

Create a Design object from name, like so:

576 |
db.design('test')
 577 | # refers to DB/_design/test
 578 | 
579 |
580 |
581 |
    def design(self, name, **kwargs):
 582 |         
 583 |         opts = dict(self.opts.items() + kwargs.items())
 584 |         return Design(self._make_url('/'.join(['_design', name])), session=self._session, **opts)
 585 | 
586 |
587 |
588 |
589 | 590 |
591 |
592 |
593 | # 594 |
595 |

document

596 |

Create a Document object from name.

597 |
598 |
599 |
    def document(self, name, **kwargs):
 600 |         
 601 |         opts = dict(self.opts.items() + kwargs.items())
 602 |         return Document(self._make_url(name), session=self._session, **opts)
 603 | 
604 |
605 |
606 |
607 | 608 |
609 |
610 |
611 | # 612 |
613 |

get

614 |

Make a GET request against the object's URI joined 615 | with path. kwargs are passed directly to Requests.

616 |
617 |
618 |
    def get(self, path='', **kwargs):
 619 |         
 620 |         return self._make_request('get', path, **kwargs)
 621 | 
622 |
623 |
624 |
625 | 626 |
627 |
628 |
629 | # 630 |
631 |

login

632 |

Authenticate the connection via cookie.

633 |
634 |
635 |
    def login(self, username, password, **kwargs):
 636 |         
 637 |         # set headers, body explicitly
 638 |         headers = {
 639 |             "Content-Type": "application/x-www-form-urlencoded"
 640 |         }
 641 |         data = "name=%s&password=%s" % (username, password)
 642 |         return self.post(self._reset_path('_session'), headers=headers, data=data, **kwargs)
 643 | 
644 |
645 |
646 |
647 | 648 |
649 |
650 |
651 | # 652 |
653 |

logout

654 |

De-authenticate the connection's cookie.

655 |
656 |
657 |
    def logout(self, **kwargs):
 658 |         
 659 |         return self.delete(self._reset_path('_session'), **kwargs)
 660 | 
661 |
662 |
663 |
664 | 665 |
666 |
667 |
668 | # 669 |
670 |

missing_revs

671 |

Refers to this method.

672 |
673 |
674 |
    def missing_revs(self, revs, **kwargs):
 675 |         
 676 |         return self.post('_missing_revs', params=revs, **kwargs)
 677 | 
678 |
679 |
680 |
681 | 682 |
683 |
684 |
685 | # 686 |
687 |

post

688 |

Make a POST request against the object's URI joined 689 | with path.

690 |

kwargs['params'] are turned into JSON before being 691 | passed to Requests. If you want to indicate the message 692 | body without it being modified, use kwargs['data'].

693 |
694 |
695 |
    def post(self, path='', **kwargs):
 696 |         
 697 |         return self._make_request('post', path, **kwargs)
 698 | 
699 |
700 |
701 |
702 | 703 |
704 |
705 |
706 | # 707 |
708 |

put

709 |

Make a PUT request against the object's URI joined 710 | with path.

711 |

kwargs['params'] are turned into JSON before being 712 | passed to Requests. If you want to indicate the message 713 | body without it being modified, use kwargs['data'].

714 |
715 |
716 |
    def put(self, path='', **kwargs):
 717 |         
 718 |         return self._make_request('put', path, **kwargs)
 719 | 
720 |
721 |
722 |
723 | 724 |
725 |
726 |
727 | # 728 |
729 |

revs_diff

730 |

Refers to this method

731 |
732 |
733 |
    def revs_diff(self, revs, **kwargs):
 734 |         
 735 |         return self.post('_revs_diff', params=revs, **kwargs)
 736 | 
737 |
738 |
739 |
740 | 741 |
742 |
743 |
744 | # 745 |
746 |

session

747 |

Get current user's authentication and authorization status.

748 |
749 |
750 |
    def session(self, **kwargs):
 751 |         
 752 |         return self.get(self._reset_path('_session'), **kwargs)
 753 | 
754 |
755 |
756 |
757 | 758 |
759 |
760 |
761 | # 762 |
763 |

view_cleanup

764 |

Cleans up the cached view output on disk for a given view. For example:

765 |
print db.view_cleanup().result().json()
 766 | # {'ok': True}
 767 | 
768 |
769 |
770 |
    def view_cleanup(self, **kwargs):
 771 |         
 772 |         return self.post('_view_cleanup', **kwargs)
 773 | 
774 |
775 |
776 |
777 | 778 |
779 | 780 |
781 |
782 |

Document

783 |

Connection to a specific document.

784 |

Learn more about the raw API from the Cloudant docs

785 |
786 |
787 |
788 | 789 |
790 |
791 |
792 | # 793 |
794 |

attachment

795 |

Create an Attachment object from name and the settings 796 | for the current database.

797 |
798 |
799 |
    def attachment(self, name, **kwargs):
 800 |         
 801 |         opts = dict(self.opts.items() + kwargs.items())
 802 |         return Attachment(self._make_url(name), session=self._session, **opts)
 803 | 
804 |
805 |
806 |
807 | 808 |
809 |
810 |
811 | # 812 |
813 |

delete

814 |

Delete the given revision of the current document. For example:

815 |
rev = doc.get().result().json()['_rev']
 816 | doc.delete(rev)
 817 | 
818 |
819 |
820 |
    def delete(self, rev, **kwargs):
 821 |         
 822 |         return super(Document, self).delete(params={'rev': rev}, **kwargs)
 823 | 
824 |
825 |
826 |
827 | 828 |
829 |
830 |
831 | # 832 |
833 |

get

834 |

Make a GET request against the object's URI joined 835 | with path. kwargs are passed directly to Requests.

836 |
837 |
838 |
    def get(self, path='', **kwargs):
 839 |         
 840 |         return self._make_request('get', path, **kwargs)
 841 | 
842 |
843 |
844 |
845 | 846 |
847 |
848 |
849 | # 850 |
851 |

login

852 |

Authenticate the connection via cookie.

853 |
854 |
855 |
    def login(self, username, password, **kwargs):
 856 |         
 857 |         # set headers, body explicitly
 858 |         headers = {
 859 |             "Content-Type": "application/x-www-form-urlencoded"
 860 |         }
 861 |         data = "name=%s&password=%s" % (username, password)
 862 |         return self.post(self._reset_path('_session'), headers=headers, data=data, **kwargs)
 863 | 
864 |
865 |
866 |
867 | 868 |
869 |
870 |
871 | # 872 |
873 |

logout

874 |

De-authenticate the connection's cookie.

875 |
876 |
877 |
    def logout(self, **kwargs):
 878 |         
 879 |         return self.delete(self._reset_path('_session'), **kwargs)
 880 | 
881 |
882 |
883 |
884 | 885 |
886 |
887 |
888 | # 889 |
890 |

merge

891 |

Merge change into the document, 892 | and then PUT the updated document back to the server.

893 |
894 |
895 |
    def merge(self, change, **kwargs):
 896 |         
 897 |         response = self.get()
 898 |         # block until result if the object is using async
 899 |         if hasattr(response, 'result'):
 900 |             response = response.result()
 901 |         # handle upserts
 902 |         if response.status_code == 404:
 903 |             doc = {}
 904 |         else:
 905 |             doc = response.json()
 906 |         # merge!
 907 |         doc.update(change)
 908 |         return self.put(params=doc, **kwargs)
 909 | 
910 |
911 |
912 |
913 | 914 |
915 |
916 |
917 | # 918 |
919 |

post

920 |

Make a POST request against the object's URI joined 921 | with path.

922 |

kwargs['params'] are turned into JSON before being 923 | passed to Requests. If you want to indicate the message 924 | body without it being modified, use kwargs['data'].

925 |
926 |
927 |
    def post(self, path='', **kwargs):
 928 |         
 929 |         return self._make_request('post', path, **kwargs)
 930 | 
931 |
932 |
933 |
934 | 935 |
936 |
937 |
938 | # 939 |
940 |

put

941 |

Make a PUT request against the object's URI joined 942 | with path.

943 |

kwargs['params'] are turned into JSON before being 944 | passed to Requests. If you want to indicate the message 945 | body without it being modified, use kwargs['data'].

946 |
947 |
948 |
    def put(self, path='', **kwargs):
 949 |         
 950 |         return self._make_request('put', path, **kwargs)
 951 | 
952 |
953 |
954 |
955 | 956 |
957 |
958 |
959 | # 960 |
961 |

session

962 |

Get current user's authentication and authorization status.

963 |
964 |
965 |
    def session(self, **kwargs):
 966 |         
 967 |         return self.get(self._reset_path('_session'), **kwargs)
 968 | 
969 |
970 |
971 |
972 | 973 |
974 | 975 |
976 |
977 |

Design

978 |

Connection to a design document, which stores custom indexes and other database functions.

979 |

Learn more about design documents from the Cloudant docs

980 |
981 |
982 |
983 | 984 |
985 |
986 |
987 | # 988 |
989 |

attachment

990 |

Create an Attachment object from name and the settings 991 | for the current database.

992 |
993 |
994 |
    def attachment(self, name, **kwargs):
 995 |         
 996 |         opts = dict(self.opts.items() + kwargs.items())
 997 |         return Attachment(self._make_url(name), session=self._session, **opts)
 998 | 
999 |
1000 |
1001 |
1002 | 1003 |
1004 |
1005 |
1006 | # 1007 |
1008 |

delete

1009 |

Delete the given revision of the current document. For example:

1010 |
rev = doc.get().result().json()['_rev']
1011 | doc.delete(rev)
1012 | 
1013 |
1014 |
1015 |
    def delete(self, rev, **kwargs):
1016 |         
1017 |         return super(Document, self).delete(params={'rev': rev}, **kwargs)
1018 | 
1019 |
1020 |
1021 |
1022 | 1023 |
1024 |
1025 |
1026 | # 1027 |
1028 |

get

1029 |

Make a GET request against the object's URI joined 1030 | with path. kwargs are passed directly to Requests.

1031 |
1032 |
1033 |
    def get(self, path='', **kwargs):
1034 |         
1035 |         return self._make_request('get', path, **kwargs)
1036 | 
1037 |
1038 |
1039 |
1040 | 1041 |
1042 |
1043 |
1044 | # 1045 |
1046 |

index

1047 |

Create a Index object referencing the function at path. For example:

1048 |
index = doc.index('_view/index-name')
1049 | # refers to /DB/_design/DOC/_view/index-name
1050 | 
1051 |
1052 |
1053 |
    def index(self, path, **kwargs):
1054 |         
1055 |         opts = dict(self.opts.items() + kwargs.items())
1056 |         return Index(self._make_url(path), session=self._session, **opts)
1057 | 
1058 |
1059 |
1060 |
1061 | 1062 |
1063 |
1064 |
1065 | # 1066 |
1067 |

list

1068 |

Make a GET request to the list function at _list/{function}/{index}. For example:

1069 |
future = doc.list('list-name', 'index-name')
1070 | # refers to /DB/_design/DOC/_list/list-name/index-name
1071 | 
1072 |

For more details on list functions, see 1073 | Querying List Functions.

1074 |
1075 |
1076 |
    def list(self, function, index, **kwargs):
1077 |         
1078 |         return self.get(self._make_url('/'.join(['_list', function, index])), **kwargs)
1079 | 
1080 |
1081 |
1082 |
1083 | 1084 |
1085 |
1086 |
1087 | # 1088 |
1089 |

login

1090 |

Authenticate the connection via cookie.

1091 |
1092 |
1093 |
    def login(self, username, password, **kwargs):
1094 |         
1095 |         # set headers, body explicitly
1096 |         headers = {
1097 |             "Content-Type": "application/x-www-form-urlencoded"
1098 |         }
1099 |         data = "name=%s&password=%s" % (username, password)
1100 |         return self.post(self._reset_path('_session'), headers=headers, data=data, **kwargs)
1101 | 
1102 |
1103 |
1104 |
1105 | 1106 |
1107 |
1108 |
1109 | # 1110 |
1111 |

logout

1112 |

De-authenticate the connection's cookie.

1113 |
1114 |
1115 |
    def logout(self, **kwargs):
1116 |         
1117 |         return self.delete(self._reset_path('_session'), **kwargs)
1118 | 
1119 |
1120 |
1121 |
1122 | 1123 |
1124 |
1125 |
1126 | # 1127 |
1128 |

merge

1129 |

Merge change into the document, 1130 | and then PUT the updated document back to the server.

1131 |
1132 |
1133 |
    def merge(self, change, **kwargs):
1134 |         
1135 |         response = self.get()
1136 |         # block until result if the object is using async
1137 |         if hasattr(response, 'result'):
1138 |             response = response.result()
1139 |         # handle upserts
1140 |         if response.status_code == 404:
1141 |             doc = {}
1142 |         else:
1143 |             doc = response.json()
1144 |         # merge!
1145 |         doc.update(change)
1146 |         return self.put(params=doc, **kwargs)
1147 | 
1148 |
1149 |
1150 |
1151 | 1152 |
1153 |
1154 |
1155 | # 1156 |
1157 |

post

1158 |

Make a POST request against the object's URI joined 1159 | with path.

1160 |

kwargs['params'] are turned into JSON before being 1161 | passed to Requests. If you want to indicate the message 1162 | body without it being modified, use kwargs['data'].

1163 |
1164 |
1165 |
    def post(self, path='', **kwargs):
1166 |         
1167 |         return self._make_request('post', path, **kwargs)
1168 | 
1169 |
1170 |
1171 |
1172 | 1173 |
1174 |
1175 |
1176 | # 1177 |
1178 |

put

1179 |

Make a PUT request against the object's URI joined 1180 | with path.

1181 |

kwargs['params'] are turned into JSON before being 1182 | passed to Requests. If you want to indicate the message 1183 | body without it being modified, use kwargs['data'].

1184 |
1185 |
1186 |
    def put(self, path='', **kwargs):
1187 |         
1188 |         return self._make_request('put', path, **kwargs)
1189 | 
1190 |
1191 |
1192 |
1193 | 1194 | 1214 |
1215 | 1216 |
1217 |
1218 |
1219 | # 1220 |
1221 |

session

1222 |

Get current user's authentication and authorization status.

1223 |
1224 |
1225 |
    def session(self, **kwargs):
1226 |         
1227 |         return self.get(self._reset_path('_session'), **kwargs)
1228 | 
1229 |
1230 |
1231 |
1232 | 1233 |
1234 |
1235 |
1236 | # 1237 |
1238 |

show

1239 |

Make a GET request to the show function at _show/{function}/{id}. For example:

1240 |
future = doc.show('show-name', 'document-id')
1241 | # refers to /DB/_design/DOC/_show/show-name/document-id
1242 | 
1243 |

For more details on show functions, see 1244 | Querying Show Functions.

1245 |
1246 |
1247 |
    def show(self, function, id, **kwargs):
1248 |         
1249 |         return self.get(self._make_url('/'.join(['_show', function, id])), **kwargs)
1250 | 
1251 |
1252 |
1253 |
1254 | 1255 |
1256 |
1257 |
1258 | # 1259 |
1260 |

view

1261 |

Create a Index object referencing the secondary index at _view/{function}. For example:

1262 |
index = doc.view('index-name')
1263 | # refers to /DB/_design/DOC/_view/index-name
1264 | 
1265 |

For more on secondary indices, see 1266 | Querying a View

1267 |
1268 |
1269 |
    def view(self, function, **kwargs):
1270 |         
1271 |         return self.index('/'.join(['_view', function]), **kwargs)
1272 | 
1273 |
1274 |
1275 |
1276 | 1277 |
1278 | 1279 |
1280 |
1281 |

Index

1282 |

Methods for design document indexes. 1283 | Different kinds of indexes will behave differently, so here are helpful docs:

1284 | 1288 |

Then, you just use basic HTTP methods to perform queries, like this:

1289 |
index.get(params=QUERY_ARGUMENTS)
1290 | 
1291 |

Remember, before you can query an index, you have to make sure it's in your database. 1292 | See these docs 1293 | for how to do that.

1294 |
1295 |
1296 |
1297 | 1298 |
1299 |
1300 |
1301 | # 1302 |
1303 |

delete

1304 |

Make a DELETE request against the object's URI joined 1305 | with path. kwargs are passed directly to Requests.

1306 |
1307 |
1308 |
    def delete(self, path='', **kwargs):
1309 |         
1310 |         return self._make_request('delete', path, **kwargs)
1311 | 
1312 |
1313 |
1314 |
1315 | 1316 |
1317 |
1318 |
1319 | # 1320 |
1321 |

get

1322 |

Make a GET request against the object's URI joined 1323 | with path. kwargs are passed directly to Requests.

1324 |
1325 |
1326 |
    def get(self, path='', **kwargs):
1327 |         
1328 |         return self._make_request('get', path, **kwargs)
1329 | 
1330 |
1331 |
1332 |
1333 | 1334 |
1335 |
1336 |
1337 | # 1338 |
1339 |

iter

1340 |

Like the magic method __iter__, but allows you to 1341 | pass query parameters, like so:

1342 |
view = db.view('...')
1343 | options = {
1344 |     'key': 'thegoodstuff',
1345 |     'include_docs': True
1346 | }
1347 | for row in view.iter(params=options):
1348 |     # emits only rows with the key 'thegoodstuff'
1349 |     # with each row's emitting document
1350 | 
1351 |
1352 |
1353 |
    def iter(self, **kwargs):
1354 |         
1355 |         return self.__iter__(**kwargs)
1356 | 
1357 |
1358 |
1359 |
1360 | 1361 |
1362 |
1363 |
1364 | # 1365 |
1366 |

login

1367 |

Authenticate the connection via cookie.

1368 |
1369 |
1370 |
    def login(self, username, password, **kwargs):
1371 |         
1372 |         # set headers, body explicitly
1373 |         headers = {
1374 |             "Content-Type": "application/x-www-form-urlencoded"
1375 |         }
1376 |         data = "name=%s&password=%s" % (username, password)
1377 |         return self.post(self._reset_path('_session'), headers=headers, data=data, **kwargs)
1378 | 
1379 |
1380 |
1381 |
1382 | 1383 |
1384 |
1385 |
1386 | # 1387 |
1388 |

logout

1389 |

De-authenticate the connection's cookie.

1390 |
1391 |
1392 |
    def logout(self, **kwargs):
1393 |         
1394 |         return self.delete(self._reset_path('_session'), **kwargs)
1395 | 
1396 |
1397 |
1398 |
1399 | 1400 |
1401 |
1402 |
1403 | # 1404 |
1405 |

post

1406 |

Make a POST request against the object's URI joined 1407 | with path.

1408 |

kwargs['params'] are turned into JSON before being 1409 | passed to Requests. If you want to indicate the message 1410 | body without it being modified, use kwargs['data'].

1411 |
1412 |
1413 |
    def post(self, path='', **kwargs):
1414 |         
1415 |         return self._make_request('post', path, **kwargs)
1416 | 
1417 |
1418 |
1419 |
1420 | 1421 |
1422 |
1423 |
1424 | # 1425 |
1426 |

put

1427 |

Make a PUT request against the object's URI joined 1428 | with path.

1429 |

kwargs['params'] are turned into JSON before being 1430 | passed to Requests. If you want to indicate the message 1431 | body without it being modified, use kwargs['data'].

1432 |
1433 |
1434 |
    def put(self, path='', **kwargs):
1435 |         
1436 |         return self._make_request('put', path, **kwargs)
1437 | 
1438 |
1439 |
1440 |
1441 | 1442 |
1443 |
1444 |
1445 | # 1446 |
1447 |

session

1448 |

Get current user's authentication and authorization status.

1449 |
1450 |
1451 |
    def session(self, **kwargs):
1452 |         
1453 |         return self.get(self._reset_path('_session'), **kwargs)
1454 | 
1455 |
1456 |
1457 |
1458 | 1459 |
1460 | 1461 |
1462 |
1463 |

Attachment

1464 |

Attachment methods for a single document

1465 |
1466 |
1467 |
1468 | 1469 |
1470 |
1471 |
1472 | # 1473 |
1474 |

delete

1475 |

Make a DELETE request against the object's URI joined 1476 | with path. kwargs are passed directly to Requests.

1477 |
1478 |
1479 |
    def delete(self, path='', **kwargs):
1480 |         
1481 |         return self._make_request('delete', path, **kwargs)
1482 | 
1483 |
1484 |
1485 |
1486 | 1487 |
1488 |
1489 |
1490 | # 1491 |
1492 |

get

1493 |

Make a GET request against the object's URI joined 1494 | with path. kwargs are passed directly to Requests.

1495 |
1496 |
1497 |
    def get(self, path='', **kwargs):
1498 |         
1499 |         return self._make_request('get', path, **kwargs)
1500 | 
1501 |
1502 |
1503 |
1504 | 1505 |
1506 |
1507 |
1508 | # 1509 |
1510 |

login

1511 |

Authenticate the connection via cookie.

1512 |
1513 |
1514 |
    def login(self, username, password, **kwargs):
1515 |         
1516 |         # set headers, body explicitly
1517 |         headers = {
1518 |             "Content-Type": "application/x-www-form-urlencoded"
1519 |         }
1520 |         data = "name=%s&password=%s" % (username, password)
1521 |         return self.post(self._reset_path('_session'), headers=headers, data=data, **kwargs)
1522 | 
1523 |
1524 |
1525 |
1526 | 1527 |
1528 |
1529 |
1530 | # 1531 |
1532 |

logout

1533 |

De-authenticate the connection's cookie.

1534 |
1535 |
1536 |
    def logout(self, **kwargs):
1537 |         
1538 |         return self.delete(self._reset_path('_session'), **kwargs)
1539 | 
1540 |
1541 |
1542 |
1543 | 1544 |
1545 |
1546 |
1547 | # 1548 |
1549 |

post

1550 |

Make a POST request against the object's URI joined 1551 | with path.

1552 |

kwargs['params'] are turned into JSON before being 1553 | passed to Requests. If you want to indicate the message 1554 | body without it being modified, use kwargs['data'].

1555 |
1556 |
1557 |
    def post(self, path='', **kwargs):
1558 |         
1559 |         return self._make_request('post', path, **kwargs)
1560 | 
1561 |
1562 |
1563 |
1564 | 1565 |
1566 |
1567 |
1568 | # 1569 |
1570 |

put

1571 |

Make a PUT request against the object's URI joined 1572 | with path.

1573 |

kwargs['params'] are turned into JSON before being 1574 | passed to Requests. If you want to indicate the message 1575 | body without it being modified, use kwargs['data'].

1576 |
1577 |
1578 |
    def put(self, path='', **kwargs):
1579 |         
1580 |         return self._make_request('put', path, **kwargs)
1581 | 
1582 |
1583 |
1584 |
1585 | 1586 |
1587 |
1588 |
1589 | # 1590 |
1591 |

session

1592 |

Get current user's authentication and authorization status.

1593 |
1594 |
1595 |
    def session(self, **kwargs):
1596 |         
1597 |         return self.get(self._reset_path('_session'), **kwargs)
1598 | 
1599 |
1600 |
1601 |
1602 | 1603 |
1604 | 1605 | -------------------------------------------------------------------------------- /docs/site/style.css: -------------------------------------------------------------------------------- 1 | /*--------------------- Layout and Typography ----------------------------*/ 2 | body { 3 | font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; 4 | font-size: 16px; 5 | line-height: 24px; 6 | color: #252519; 7 | margin: 0; padding: 0; 8 | background: #f5f5ff; 9 | } 10 | a { 11 | color: #261a3b; 12 | } 13 | a:visited { 14 | color: #261a3b; 15 | } 16 | p { 17 | margin: 0 0 15px 0; 18 | } 19 | h1, h2, h3, h4, h5, h6 { 20 | margin: 40px 0 15px 0; 21 | } 22 | h2, h3, h4, h5, h6 { 23 | margin-top: 0; 24 | } 25 | #container { 26 | background: white; 27 | } 28 | #container, div.section { 29 | position: relative; 30 | } 31 | #background { 32 | position: absolute; 33 | top: 0; left: 580px; right: 0; bottom: 0; 34 | background: #f5f5ff; 35 | border-left: 1px solid #e5e5ee; 36 | z-index: 0; 37 | } 38 | #jump_to, #jump_page { 39 | background: white; 40 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; 41 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; 42 | font: 10px Arial; 43 | text-transform: uppercase; 44 | cursor: pointer; 45 | text-align: right; 46 | } 47 | #jump_to, #jump_wrapper { 48 | position: fixed; 49 | right: 0; top: 0; 50 | padding: 5px 10px; 51 | z-index: 1000; 52 | } 53 | #jump_wrapper { 54 | padding: 0; 55 | display: none; 56 | } 57 | #jump_to:hover #jump_wrapper { 58 | display: block; 59 | } 60 | #jump_page { 61 | padding: 5px 0 3px; 62 | margin: 0 0 25px 25px; 63 | } 64 | #jump_page .source { 65 | display: block; 66 | padding: 5px 10px; 67 | text-decoration: none; 68 | border-top: 1px solid #eee; 69 | } 70 | #jump_page .source:hover { 71 | background: #f5f5ff; 72 | } 73 | #jump_page .source:first-child { 74 | } 75 | div.docs { 76 | float: left; 77 | max-width: 500px; 78 | min-width: 500px; 79 | min-height: 5px; 80 | padding: 10px 25px 1px 50px; 81 | vertical-align: top; 82 | text-align: left; 83 | } 84 | .docs pre { 85 | margin: 15px 0 15px; 86 | padding-left: 15px; 87 | } 88 | .docs p tt, .docs p code { 89 | background: #f8f8ff; 90 | border: 1px solid #dedede; 91 | font-size: 12px; 92 | padding: 0 0.2em; 93 | } 94 | .octowrap { 95 | position: relative; 96 | } 97 | .octothorpe { 98 | font: 12px Arial; 99 | text-decoration: none; 100 | color: #454545; 101 | position: absolute; 102 | top: 3px; left: -20px; 103 | padding: 1px 2px; 104 | opacity: 0; 105 | -webkit-transition: opacity 0.2s linear; 106 | } 107 | div.docs:hover .octothorpe { 108 | opacity: 1; 109 | } 110 | div.code { 111 | margin-left: 580px; 112 | padding: 14px 15px 16px 50px; 113 | vertical-align: top; 114 | } 115 | .code pre, .docs p code { 116 | font-size: 12px; 117 | } 118 | pre, tt, code { 119 | line-height: 18px; 120 | font-family: Monaco, Consolas, "Lucida Console", monospace; 121 | margin: 0; padding: 0; 122 | } 123 | div.clearall { 124 | clear: both; 125 | } 126 | 127 | 128 | /*---------------------- Syntax Highlighting -----------------------------*/ 129 | td.linenos { background-color: #f0f0f0; padding-right: 10px; } 130 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } 131 | body .hll { background-color: #ffffcc } 132 | body .c { color: #408080; font-style: italic } /* Comment */ 133 | body .err { border: 1px solid #FF0000 } /* Error */ 134 | body .k { color: #954121 } /* Keyword */ 135 | body .o { color: #666666 } /* Operator */ 136 | body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ 137 | body .cp { color: #BC7A00 } /* Comment.Preproc */ 138 | body .c1 { color: #408080; font-style: italic } /* Comment.Single */ 139 | body .cs { color: #408080; font-style: italic } /* Comment.Special */ 140 | body .gd { color: #A00000 } /* Generic.Deleted */ 141 | body .ge { font-style: italic } /* Generic.Emph */ 142 | body .gr { color: #FF0000 } /* Generic.Error */ 143 | body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 144 | body .gi { color: #00A000 } /* Generic.Inserted */ 145 | body .go { color: #808080 } /* Generic.Output */ 146 | body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 147 | body .gs { font-weight: bold } /* Generic.Strong */ 148 | body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 149 | body .gt { color: #0040D0 } /* Generic.Traceback */ 150 | body .kc { color: #954121 } /* Keyword.Constant */ 151 | body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ 152 | body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ 153 | body .kp { color: #954121 } /* Keyword.Pseudo */ 154 | body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ 155 | body .kt { color: #B00040 } /* Keyword.Type */ 156 | body .m { color: #666666 } /* Literal.Number */ 157 | body .s { color: #219161 } /* Literal.String */ 158 | body .na { color: #7D9029 } /* Name.Attribute */ 159 | body .nb { color: #954121 } /* Name.Builtin */ 160 | body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 161 | body .no { color: #880000 } /* Name.Constant */ 162 | body .nd { color: #AA22FF } /* Name.Decorator */ 163 | body .ni { color: #999999; font-weight: bold } /* Name.Entity */ 164 | body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ 165 | body .nf { color: #0000FF } /* Name.Function */ 166 | body .nl { color: #A0A000 } /* Name.Label */ 167 | body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 168 | body .nt { color: #954121; font-weight: bold } /* Name.Tag */ 169 | body .nv { color: #19469D } /* Name.Variable */ 170 | body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 171 | body .w { color: #bbbbbb } /* Text.Whitespace */ 172 | body .mf { color: #666666 } /* Literal.Number.Float */ 173 | body .mh { color: #666666 } /* Literal.Number.Hex */ 174 | body .mi { color: #666666 } /* Literal.Number.Integer */ 175 | body .mo { color: #666666 } /* Literal.Number.Oct */ 176 | body .sb { color: #219161 } /* Literal.String.Backtick */ 177 | body .sc { color: #219161 } /* Literal.String.Char */ 178 | body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ 179 | body .s2 { color: #219161 } /* Literal.String.Double */ 180 | body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ 181 | body .sh { color: #219161 } /* Literal.String.Heredoc */ 182 | body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ 183 | body .sx { color: #954121 } /* Literal.String.Other */ 184 | body .sr { color: #BB6688 } /* Literal.String.Regex */ 185 | body .s1 { color: #219161 } /* Literal.String.Single */ 186 | body .ss { color: #19469D } /* Literal.String.Symbol */ 187 | body .bp { color: #954121 } /* Name.Builtin.Pseudo */ 188 | body .vc { color: #19469D } /* Name.Variable.Class */ 189 | body .vg { color: #19469D } /* Name.Variable.Global */ 190 | body .vi { color: #19469D } /* Name.Variable.Instance */ 191 | body .il { color: #666666 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /docs/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | 8 | 9 |
10 |
11 |
12 | Jump To … 13 |
14 |
15 | Home 16 | API Reference 17 | {% for section in sections %} 18 | {{ section.name }} 19 | {% endfor %} 20 |
21 |
22 |
23 |
24 |
25 | {{ readme }} 26 |
27 |
28 |
29 |
30 |
31 |

API Reference

32 |
33 |
34 |
35 | {% for section in sections %} 36 |
37 |
38 |

{{ section.name }}

39 | {{ section.head.html }} 40 |
41 |
42 |
43 | {% for method in section.methods %} 44 |
45 |
46 |
47 | # 48 |
49 |

{{ method.name }}

50 | {{ method.html }} 51 |
52 |
53 |
{{ method.code }}
54 |
55 |
56 |
57 | {% endfor %} 58 |
59 | {% endfor %} 60 | -------------------------------------------------------------------------------- /docs/util.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import cloudant 3 | import jinja2 4 | import markdown 5 | import re 6 | 7 | def is_module(item): 8 | return inspect.ismodule(item) 9 | 10 | def is_private(method_name): 11 | return bool(method_name[0] == "_") 12 | 13 | def is_function(method): 14 | return hasattr(method, '__call__') 15 | 16 | def get_first_indent(string): 17 | match = re.match(r'(\s{2,})\w', string) 18 | if match: 19 | return match.groups()[0] 20 | else: 21 | return match 22 | 23 | def get_docs(method): 24 | # get the source code 25 | source = inspect.getsource(method) 26 | if method.__doc__: 27 | source = source.replace(method.__doc__, '').replace('""""""', '') 28 | 29 | # get the docstring 30 | if method.__doc__: 31 | first_indent = get_first_indent(method.__doc__) 32 | if first_indent: 33 | docstring = method.__doc__.replace(first_indent, '\n') 34 | else: 35 | docstring = '\n' + method.__doc__ + '\n' 36 | else: 37 | docstring = '' 38 | 39 | # get arguments 40 | try: 41 | arg_spec = inspect.getargspec(method) 42 | except TypeError: 43 | arg_spec = inspect.getargspec(method.__init__) 44 | args = ', '.join(arg_spec.args[1:]) 45 | kwargs = arg_spec.keywords 46 | 47 | # mush it together 48 | return { 49 | 'args': ', '.join([args, kwargs]), 50 | 'docs': docstring, 51 | 'html': markdown.markdown(docstring), 52 | 'code': source 53 | } 54 | 55 | def get_sections(order): 56 | docs = [] 57 | for item_name in order: 58 | item = getattr(cloudant, item_name) 59 | if not (is_module(item) or is_private(item_name)): 60 | section = dict( 61 | head = get_docs(item), 62 | name = item_name, 63 | methods = [] 64 | ) 65 | if not item.__doc__: 66 | print "WARNING: %s has no documentation!" % (item_name) 67 | for method_name in sorted(dir(item)): 68 | method = getattr(item, method_name) 69 | if is_function(method) and not is_private(method_name): 70 | if not method.__doc__: 71 | print "WARNING: %s.%s has no documentation!" % (item_name, method_name) 72 | method_docs = get_docs(method) 73 | method_docs['name'] = method_name 74 | section['methods'].append(method_docs) 75 | docs.append(section) 76 | return docs 77 | 78 | def generate_docs(order, template, output, readme): 79 | sections = get_sections(order) 80 | 81 | with open(template, 'r') as f: 82 | template = jinja2.Template(f.read()) 83 | 84 | with open(readme, 'r') as f: 85 | readme = markdown.markdown(f.read(), ['fenced_code']) 86 | 87 | with open(output, 'w') as f: 88 | f.write(template.render(sections=sections, readme=readme)) -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Cloudant-Python 2 | 3 | [![Build Status](https://travis-ci.org/cloudant-labs/cloudant-python.png)](https://travis-ci.org/cloudant-labs/cloudant-python) 4 | [![Coverage Status](https://coveralls.io/repos/cloudant-labs/cloudant-python/badge.png)](https://coveralls.io/r/cloudant-labs/cloudant-python) 5 | [![PyPi version](https://pypip.in/v/cloudant/badge.png)](https://crate.io/packages/cloudant/) 6 | [![PyPi downloads](https://pypip.in/d/cloudant/badge.png)](https://crate.io/packages/cloudant/) 7 | 8 | [futures]: http://docs.python.org/dev/library/concurrent.futures.html#future-objects 9 | [requests]: http://www.python-requests.org/en/latest/ 10 | [responses]: http://www.python-requests.org/en/latest/api/#requests.Response 11 | 12 | ## This Version is Deprecated 13 | 14 | As of 13 October 2015, this development repository is deprecated in favor of [the new 15 | repository and development branch starting at version 2.0.0a1](https://github.com/cloudant/python-cloudant). 16 | 17 | **The new version will introduce breaking changes. No attempt was made to follow the 18 | API in 0.5.10.** 19 | 20 | This is the final version of this repository and branch -- 0.5.10. 21 | 22 | Please use the new library for your new projects and begin to migrate your old projects that have 23 | used versions 0.5.10 and prior. 24 | 25 | We will keep 0.5.10 as the latest stable version on PyPI until early 2016, at which time 26 | we plan to switch over completely to 2.0.0. Also at that time, this repository will 27 | be taken down. 28 | 29 | Alpha and Beta versions starting with 2.0.0a1 will be uploaded to PyPI. The latest alpha 30 | or beta release may be installed by 31 | 32 | pip install --pre cloudant 33 | 34 | Note that our new development repository is still pre 2.0.0. As such, we cannot make any guarantees, though 35 | we will try, of course, not to introduce new API that will later be removed. 36 | 37 | 38 | ## Install 39 | 40 | pip install cloudant 41 | 42 | ## Usage 43 | 44 | Cloudant-Python is a wrapper around Python [Requests][requests] for interacting with CouchDB or Cloudant instances. Check it out: 45 | 46 | ```python 47 | import cloudant 48 | 49 | # connect to your account 50 | # in this case, https://garbados.cloudant.com 51 | USERNAME = 'garbados' 52 | account = cloudant.Account(USERNAME) 53 | 54 | # login, so we can make changes 55 | login = account.login(USERNAME, PASSWORD) 56 | assert login.status_code == 200 57 | 58 | # create a database object 59 | db = account.database('test') 60 | 61 | # now, create the database on the server 62 | response = db.put() 63 | print response.json() 64 | # {'ok': True} 65 | ``` 66 | 67 | HTTP requests return [Response][responses] objects, right from [Requests][requests]. 68 | 69 | Cloudant-Python can also make asynchronous requests by passing `async=True` to an object's constructor, like so: 70 | 71 | ```python 72 | import cloudant 73 | 74 | # connect to your account 75 | # in this case, https://garbados.cloudant.com 76 | USERNAME = 'garbados' 77 | account = cloudant.Account(USERNAME, async=True) 78 | 79 | # login, so we can make changes 80 | future = account.login(USERNAME, PASSWORD) 81 | # block until we get the response body 82 | login = future.result() 83 | assert login.status_code == 200 84 | ``` 85 | 86 | Asynchronous HTTP requests return [Future][futures] objects, which will await the return of the HTTP response. Call `result()` to get the [Response][responses] object. 87 | 88 | See the [API reference](http://cloudant-labs.github.io/cloudant-python/#api) for all the details you could ever want. 89 | 90 | ## Philosophy 91 | 92 | Cloudant-Python is minimal, performant, and effortless. Check it out: 93 | 94 | ### Pythonisms 95 | 96 | Cloudant and CouchDB expose REST APIs that map easily into native Python objects. As much as possible, Cloudant-Python uses native Python objects as shortcuts to the raw API, so that such convenience never obscures what's going on underneath. For example: 97 | 98 | ```python 99 | import cloudant 100 | 101 | # connect to http://localhost:5984 102 | account = cloudant.Account() 103 | db = account.database('test') 104 | same_db = account['test'] 105 | assert db.uri == same_db.uri 106 | # True 107 | ``` 108 | 109 | Cloudant-Python expose raw interactions -- HTTP requests, etc. -- through special methods, so we provide syntactical sugar without obscuring the underlying API. Built-ins, such as `__getitem__`, act as Pythonic shortcuts to those methods. For example: 110 | 111 | ```python 112 | import cloudant 113 | 114 | account = cloudant.Account('garbados') 115 | 116 | db_name = 'test' 117 | db = account.database(db_name) 118 | doc = db.document('test_doc') 119 | 120 | # create the document 121 | resp = doc.put(params={ 122 | '_id': 'hello_world', 123 | 'herp': 'derp' 124 | }) 125 | 126 | # delete the document 127 | rev = resp.json()['_rev'] 128 | doc.delete(rev).raise_for_status() 129 | 130 | # but this also creates a document 131 | db['hello_world'] = {'herp': 'derp'} 132 | 133 | # and this deletes the database 134 | del account[db_name] 135 | ``` 136 | 137 | ### Iterate over Indexes 138 | 139 | Indexes, such as [views](https://cloudant.com/for-developers/views/) and Cloudant's [search indexes](https://cloudant.com/for-developers/search/), act as iterators. Check it out: 140 | 141 | ```python 142 | import cloudant 143 | 144 | account = cloudant.Account('garbados') 145 | db = account.database('test') 146 | view = db.all_docs() # returns all docs in the database 147 | for doc in db: 148 | # iterates over every doc in the database 149 | pass 150 | for doc in view: 151 | # and so does this! 152 | pass 153 | for doc in view.iter(descending=True): 154 | # use `iter` to pass options to a view and then iterate over them 155 | pass 156 | ``` 157 | 158 | [Behind the scenes](https://github.com/cloudant-labs/cloudant-python/blob/master/cloudant/index.py#L23-L33), Cloudant-Python yields documents only as you consume them, so you only load into memory the documents you're using. 159 | 160 | ### Special Endpoints 161 | 162 | If CouchDB has a special endpoint for something, it's in Cloudant-Python as a special method, so any special circumstances are taken care of automagically. As a rule, any endpoint like `_METHOD` is in Cloudant-Python as `Object.METHOD`. For example: 163 | 164 | * `https://garbados.cloudant.com/_all_dbs` -> `Account('garbados').all_dbs()` 165 | * `http://localhost:5984/DB/_all_docs` -> `Account().database(DB).all_docs()` 166 | * `http://localhost:5984/DB/_design/DOC/_view/INDEX` -> `Account().database(DB).design(DOC).view(INDEX)` 167 | 168 | ### Asynchronous 169 | 170 | 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: 171 | 172 | ```python 173 | import cloudant 174 | 175 | account = cloudant.Account(async=True) 176 | db = account['test'] 177 | future = db.put() 178 | response = future.result() 179 | print db.get().result().json() 180 | # {'db_name': 'test', ...} 181 | ``` 182 | 183 | As a result, any methods which must make an HTTP request return a [Future][futures] object. 184 | 185 | ### Option Inheritance 186 | 187 | If you use one object to create another, the child will inherit the parents' settings. So, you can create a `Database` object explicitly, or use `Account.database` to inherit cookies and other settings from the `Account` object. For example: 188 | 189 | ```python 190 | import cloudant 191 | 192 | account = cloudant.Account('garbados') 193 | db = account.database('test') 194 | doc = db.document('test_doc') 195 | 196 | url = 'https://garbados.cloudant.com' 197 | path = '/test/test_doc' 198 | otherdoc = cloudant.Document(url + path) 199 | 200 | assert doc.uri == otherdoc.uri 201 | # True 202 | ``` 203 | 204 | ## Testing 205 | 206 | To run Cloudant-Python's tests, just do: 207 | 208 | python setup.py test 209 | 210 | ## Documentation 211 | 212 | The API reference is automatically generated from the docstrings of each class and its methods. To install Cloudant-Python with the necessary extensions to build the docs, do this: 213 | 214 | pip install -e cloudant[docs] 215 | 216 | Then, in Cloudant-Python's root directory, do this: 217 | 218 | python docs 219 | 220 | Note: docstrings are in [Markdown](http://daringfireball.net/projects/markdown/). 221 | 222 | ## License 223 | 224 | [MIT](http://opensource.org/licenses/MIT), yo. 225 | -------------------------------------------------------------------------------- /scripts/autopep: -------------------------------------------------------------------------------- 1 | # /usr/local/bin/bash 2 | 3 | autopep8 -irv cloudant test setup.py 4 | -------------------------------------------------------------------------------- /scripts/clean: -------------------------------------------------------------------------------- 1 | # /usr/local/bin/bash 2 | 3 | rm *.pyc divan/*.pyc test/*.pyc docs/*.pyc 4 | rm -rf build dist Divan.egg-info -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | 5 | from setuptools import setup 6 | 7 | def read(fname): 8 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 9 | 10 | setup(name='cloudant', 11 | version='0.5.10', 12 | description='Asynchronous Cloudant / CouchDB Interface', 13 | author='IBM', 14 | author_email='alfinkel@us.ibm.com', 15 | url='https://github.com/cloudant-labs/cloudant', 16 | packages=['cloudant'], 17 | license='MIT', 18 | install_requires=[ 19 | 'requests-futures==0.9.4', 20 | ], 21 | test_suite="test", 22 | # install with `pip install -e cloudant[doc]` 23 | extras_require={ 24 | 'docs': [ 25 | 'jinja2>=2.7', 26 | 'markdown>=2.3.1' 27 | ] 28 | }, 29 | classifiers=[ 30 | 'Intended Audience :: Developers', 31 | 'Natural Language :: English', 32 | 'License :: OSI Approved :: MIT License', 33 | 'Programming Language :: Python', 34 | 'Programming Language :: Python :: 2', 35 | 'Programming Language :: Python :: 2.7', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.2', 38 | 'Programming Language :: Python :: 3.3', 39 | 'Development Status :: 7 - Inactive' 40 | ], 41 | ) 42 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | import cloudant 2 | from collections import defaultdict 3 | from types import GeneratorType 4 | import signal 5 | import time 6 | import json 7 | import unittest 8 | 9 | 10 | 11 | class ResourceTest(unittest.TestCase): 12 | 13 | def setUp(self): 14 | self.uri = 'http://localhost:5984' 15 | 16 | names = cloudant.Account(self.uri).uuids(4).json()['uuids'] 17 | # database names must start with a letter 18 | names = ['a' + name for name in names] 19 | self.db_name = names[0] 20 | self.otherdb_name = names[1] 21 | self.doc_name = names[2] 22 | self.otherdoc_name = names[3] 23 | 24 | self.test_doc = { 25 | 'herp': 'derp', 26 | 'name': 'Luke Skywalker' 27 | } 28 | self.test_otherdoc = { 29 | 'derp': 'herp', 30 | 'name': 'Larry, the Incorrigible Miscreant' 31 | } 32 | 33 | class AsyncTest(ResourceTest): 34 | 35 | def setUp(self): 36 | super(AsyncTest, self).setUp() 37 | self.account = cloudant.Account(self.uri, async=True) 38 | self.database = self.account.database(self.db_name) 39 | self.document = self.database.document(self.doc_name) 40 | 41 | def testSync(self): 42 | account = cloudant.Account(async=False) 43 | database = account[self.db_name] 44 | database.put().raise_for_status() 45 | database.delete().raise_for_status() 46 | 47 | def testAccount(self): 48 | future = self.account.get() 49 | response = future.result() 50 | response.raise_for_status() 51 | 52 | def testDatabase(self): 53 | future = self.database.put() 54 | response = future.result() 55 | response.raise_for_status() 56 | del self.account[self.db_name] 57 | 58 | def testDocument(self): 59 | future = self.database.put() 60 | response = future.result() 61 | response.raise_for_status() 62 | self.database[self.doc_name] = self.test_doc 63 | future = self.document.merge(self.test_otherdoc) 64 | response = future.result() 65 | response.raise_for_status() 66 | del self.account[self.db_name] 67 | 68 | def testIndex(self): 69 | future = self.database.put() 70 | response = future.result() 71 | response.raise_for_status() 72 | 73 | future = self.database.bulk_docs(self.test_doc, self.test_otherdoc) 74 | response = future.result() 75 | response.raise_for_status() 76 | 77 | total = [] 78 | for doc in self.database: 79 | total.append(doc) 80 | assert len(total) == 2 81 | 82 | del self.account[self.db_name] 83 | 84 | class AccountTest(ResourceTest): 85 | 86 | def setUp(self): 87 | super(AccountTest, self).setUp() 88 | self.account = cloudant.Account(self.uri) 89 | 90 | def testCloudant(self): 91 | account = cloudant.Account('garbados') 92 | assert account.uri == "https://garbados.cloudant.com" 93 | 94 | def testAllDbs(self): 95 | assert self.account.all_dbs().status_code == 200 96 | 97 | def testSession(self): 98 | assert self.account.session().status_code == 200 99 | 100 | def testActiveTasks(self): 101 | assert self.account.active_tasks().status_code == 200 102 | 103 | def testSecurity(self): 104 | username = 'user' 105 | password = 'password' 106 | # try auth login when admin party is on 107 | assert self.account.login(username, password).status_code == 401 108 | # disable admin party 109 | path = '_config/admins/%s' % username 110 | assert self.account.put(path, data="\"%s\"" % 111 | password).status_code == 200 112 | # login, logout 113 | assert self.account.login(username, password).status_code == 200 114 | assert self.account.logout().status_code == 200 115 | # re-enable admin party 116 | assert self.account.login(username, password).status_code == 200 117 | assert self.account.delete(path).status_code == 200 118 | 119 | def testReplicate(self): 120 | self.db = self.account.database(self.db_name) 121 | assert self.db.put().status_code == 201 122 | 123 | resp = self.account.replicate( 124 | self.db_name, 125 | self.otherdb_name, 126 | params=dict(create_target=True)) 127 | assert resp.status_code == 200 128 | 129 | assert self.db.delete().status_code == 200 130 | del self.account[self.otherdb_name] 131 | 132 | def testCreateDb(self): 133 | self.account.database(self.db_name) 134 | self.account[self.db_name] 135 | 136 | def testUuids(self): 137 | assert self.account.uuids().status_code == 200 138 | 139 | 140 | class DatabaseTest(ResourceTest): 141 | 142 | def setUp(self): 143 | super(DatabaseTest, self).setUp() 144 | 145 | db_name = '/'.join([self.uri, self.db_name]) 146 | self.db = cloudant.Database(db_name) 147 | 148 | response = self.db.put() 149 | response.raise_for_status() 150 | 151 | def testHead(self): 152 | assert self.db.head().status_code == 200 153 | 154 | def testGet(self): 155 | assert self.db.get().status_code == 200 156 | 157 | def testBulk(self): 158 | assert self.db.bulk_docs( 159 | self.test_doc, self.test_otherdoc).status_code == 201 160 | 161 | def testIter(self): 162 | assert self.db.bulk_docs( 163 | self.test_doc, self.test_otherdoc).status_code == 201 164 | for derp in self.db: 165 | pass 166 | 167 | def testAllDocs(self): 168 | resp = self.db.all_docs().get() 169 | assert resp.status_code == 200 170 | 171 | def testAllDocsWithKeys(self): 172 | resp = self.db.all_docs().get(params={'keys':['hello', 'goodbye']}) 173 | assert resp.status_code == 200 174 | 175 | def testChanges(self): 176 | assert isinstance(self.db.changes(), GeneratorType) 177 | 178 | def testChangesContinuous(self): 179 | def iterator(iterable): 180 | _iterable = iter(iterable) 181 | def wrapper(): 182 | for item in _iterable: 183 | return item 184 | return wrapper 185 | 186 | self.db.bulk_docs(self.test_doc, self.test_otherdoc) 187 | 188 | resp = self.db.changes(params={ 189 | 'feed': 'continuous', 190 | 'timeout': 2000 191 | }) 192 | start = time.time() 193 | _next = iterator(resp) 194 | _next() 195 | _next() 196 | assert time.time() - start < 2, \ 197 | 'should return changes event for both documents before block' 198 | 199 | def testChangesFeedEmitsHeartbeats(self): 200 | def signal_handler(signum, frame): 201 | raise Exception("Timed out!") 202 | 203 | def loop(): 204 | feed = self.db.changes(params={ 205 | 'feed': 'continuous', 206 | 'timeout': 2000, 207 | 'heartbeat': 1000 208 | }, emit_heartbeats=True) 209 | for item in feed: 210 | stack.append(item) 211 | 212 | stack = [] 213 | self.db.bulk_docs(self.test_doc, self.test_otherdoc) 214 | 215 | signal.signal(signal.SIGALRM, signal_handler) 216 | signal.alarm(3) 217 | 218 | try: 219 | loop() 220 | except Exception: 221 | assert stack[-1] is None 222 | 223 | def testViewCleanup(self): 224 | assert self.db.view_cleanup().status_code == 202 225 | 226 | def testRevs(self): 227 | # put some docs 228 | assert self.db.bulk_docs( 229 | self.test_doc, self.test_otherdoc).status_code == 201 230 | # get their revisions 231 | revs = defaultdict(list) 232 | for doc in self.db: 233 | revs[doc['id']].append(doc['value']['rev']) 234 | assert self.db.missing_revs(revs).status_code == 200 235 | assert self.db.revs_diff(revs).status_code == 200 236 | 237 | def tearDown(self): 238 | assert self.db.delete().status_code == 200 239 | 240 | 241 | class DocumentTest(ResourceTest): 242 | 243 | def setUp(self): 244 | super(DocumentTest, self).setUp() 245 | self.db = cloudant.Database('/'.join([self.uri, self.db_name])) 246 | assert self.db.put().status_code == 201 247 | self.doc = self.db.document(self.doc_name) 248 | 249 | def testCrud(self): 250 | assert self.doc.put(params=self.test_doc).status_code == 201 251 | resp = self.doc.get() 252 | assert resp.status_code == 200 253 | del self.db[self.doc_name] 254 | assert self.doc.get().status_code == 404 255 | 256 | def testDict(self): 257 | self.db[self.doc_name] = self.test_doc 258 | self.db[self.doc_name] 259 | 260 | def testMerge(self): 261 | # test upsert 262 | assert self.doc.merge(self.test_doc).status_code == 201 263 | # test merge 264 | assert self.doc.merge(self.test_otherdoc).status_code == 201 265 | 266 | def testAttachment(self): 267 | self.doc.attachment('file') 268 | 269 | def testNoLoginLogout(self): 270 | assert not hasattr(self.doc, 'login') 271 | assert not hasattr(self.doc, 'logout') 272 | 273 | def tearDown(self): 274 | assert self.db.delete().status_code == 200 275 | 276 | 277 | class DesignTest(ResourceTest): 278 | 279 | def setUp(self): 280 | super(DesignTest, self).setUp() 281 | self.db = cloudant.Database('/'.join([self.uri, self.db_name])) 282 | assert self.db.put().status_code == 201 283 | self.doc = self.db.design('ddoc') 284 | assert self.doc.put(params=self.test_doc).status_code == 201 285 | 286 | def testView(self): 287 | self.doc.index('_view/derp') 288 | self.doc.view('derp') 289 | self.doc.search('derp') 290 | 291 | def testList(self): 292 | # todo: test on actual list and show functions 293 | assert self.doc.list('herp', 'derp').status_code == 404 294 | assert self.doc.show('herp', 'derp').status_code == 500 295 | 296 | def tearDown(self): 297 | assert self.db.delete().status_code == 200 298 | 299 | 300 | class AttachmentTest(ResourceTest): 301 | pass 302 | 303 | 304 | class IndexTest(ResourceTest): 305 | 306 | def setUp(self): 307 | super(IndexTest, self).setUp() 308 | self.db = cloudant.Database('/'.join([self.uri, self.db_name])) 309 | assert self.db.put().status_code == 201 310 | self.doc = self.db.document(self.doc_name) 311 | 312 | def testPrimaryIndex(self): 313 | """ 314 | Show that views can be used as iterators 315 | """ 316 | for doc in [self.test_doc, self.test_otherdoc]: 317 | assert self.db.post(params=doc).status_code == 201 318 | 319 | docs = [] 320 | for derp in self.db.all_docs(): 321 | docs.append(derp['id']) 322 | 323 | for derp in self.db.all_docs().iter(params=dict(descending=True, reduce=False)): 324 | assert docs.pop() == derp['id'] 325 | 326 | def testQueryParams(self): 327 | view = self.db.all_docs() 328 | response = view.get(params=dict(reduce=False)) 329 | assert 'reduce=False' not in response.url 330 | 331 | def testMultiKeyViewQuery(self): 332 | """ 333 | Test using multi-key view query (post to view) 334 | """ 335 | view = self.db.all_docs() 336 | d = [self.doc_name, self.otherdoc_name] 337 | response = view.post(params=dict(reduce=False), 338 | data=json.dumps(dict(keys=d))) 339 | assert 'reduce=false' in response.url 340 | assert response.status_code == 200 341 | key_return = response.json()['rows'] 342 | assert len(key_return) == len(d) 343 | 344 | # The documents weren't in there, so make sure they are not found 345 | for adoc in key_return: 346 | assert adoc["key"] in d 347 | assert "error" in adoc 348 | 349 | def tearDown(self): 350 | assert self.db.delete().status_code == 200 351 | 352 | 353 | class ErrorTest(ResourceTest): 354 | 355 | def setUp(self): 356 | super(ErrorTest, self).setUp() 357 | self.db = cloudant.Database('/'.join([self.uri, self.db_name])) 358 | 359 | def testMissing(self): 360 | response = self.db.get() 361 | assert response.status_code == 404 362 | 363 | if __name__ == "__main__": 364 | unittest.main() 365 | --------------------------------------------------------------------------------