From 0f4835e849272bc68d9fac4a9a4fab4e4a6f26c2 Mon Sep 17 00:00:00 2001 From: DjMorgul Date: Sat, 3 Dec 2016 14:05:07 +0800 Subject: [PATCH 01/21] Show property doc on Row string representation It was confusing to not see it even it's assigned, it led me to think that include_docs=True parameter on my request wasn't working. So after some debugging, I noticed this minor issue. --- couchdb/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchdb/client.py b/couchdb/client.py index bf1fef3d..f7422873 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -1323,7 +1323,7 @@ class Row(dict): """Representation of a row as returned by database views.""" def __repr__(self): - keys = 'id', 'key', 'error', 'value' + keys = 'id', 'key', 'doc', 'error', 'value' items = ['%s=%r' % (k, self[k]) for k in keys if k in self] return '<%s %s>' % (type(self).__name__, ', '.join(items)) From 0628d62fd4eef097a9a0ba0661d2a1cbabc73335 Mon Sep 17 00:00:00 2001 From: Rob Moore Date: Sun, 30 Oct 2016 14:21:00 +0000 Subject: [PATCH 02/21] Unit test for doc IDs with unsafe characters Specifically a slash. --- couchdb/tests/couch_tests.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/couchdb/tests/couch_tests.py b/couchdb/tests/couch_tests.py index f9e642eb..872690df 100644 --- a/couchdb/tests/couch_tests.py +++ b/couchdb/tests/couch_tests.py @@ -191,6 +191,31 @@ def test_utf8_encoding(self): for idx, row in enumerate(self.db.query(query)): self.assertEqual(texts[idx], row.key) + def test_update_with_unsafe_doc_ids(self): + doc_id = 'sanitise/the/doc/id/plz/' + design_doc = 'test_slashes_in_doc_ids' + handler_name = 'test' + func = """ + function(doc, req) { + doc.test = 'passed'; + return [doc, 'ok']; + } + """ + + # Stick an update handler in + self.db['_design/%s' % design_doc] = { + 'updates': {handler_name: func} + } + # And a test doc + self.db[doc_id] = {'test': 'failed'} + + response = self.db.update_doc( + '%s/%s' % (design_doc, handler_name), + docid=doc_id + ) + + self.assertEqual(self.db[doc_id]['test'], 'passed') + def test_design_docs(self): for i in range(50): self.db[str(i)] = {'integer': i, 'string': str(i)} From 0d29eccd0d0153c1b2c09d2729b2f0b084f81033 Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Wed, 5 Apr 2017 14:20:17 +0200 Subject: [PATCH 03/21] Enable querying the version of CouchDB server --- couchdb/client.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/couchdb/client.py b/couchdb/client.py index f7422873..b47c2caa 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -85,6 +85,7 @@ def __init__(self, url=DEFAULT_BASE_URL, full_commit=True, session=None): self.resource = url # treat as a Resource object if not full_commit: self.resource.headers['X-Couch-Full-Commit'] = 'false' + self._version_info = None def __contains__(self, name): """Return whether the server contains a database with the specified @@ -166,6 +167,18 @@ def version(self): status, headers, data = self.resource.get_json() return data['version'] + def version_info(self): + """The version of the CouchDB server as a tuple of ints. + + Note that this results in a request being made only at the first call. + Afterwards the result will be cached. + + :rtype: `tuple(int, int, int)`""" + if self._version_info is None: + version = self.version() + self._version_info = tuple(map(int, version.split('.'))) + return self._version_info + def stats(self, name=None): """Server statistics. From 48e38c6fd36f978d6101ae450875d27addfdd147 Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Wed, 5 Apr 2017 14:55:13 +0200 Subject: [PATCH 04/21] Allow `path` to also be a list for `urljoin` to do its magic --- couchdb/http.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/couchdb/http.py b/couchdb/http.py index 4a6750e9..d201f3e0 100644 --- a/couchdb/http.py +++ b/couchdb/http.py @@ -581,7 +581,10 @@ def _request(self, method, path=None, body=None, headers=None, **params): all_headers = self.headers.copy() all_headers.update(headers or {}) if path is not None: - url = urljoin(self.url, path, **params) + if isinstance(path, list): + url = urljoin(self.url, *path, **params) + else: + url = urljoin(self.url, path, **params) else: url = urljoin(self.url, **params) return self.session.request(method, url, body=body, From 6710a6329dd08222993aec0e062c024d433fc6c2 Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Tue, 25 Apr 2017 11:06:03 +0200 Subject: [PATCH 05/21] Add methods for mango queries and indexes `find` corresponds to endpoint `POST /{db}/_find` `explain` corresponds to endpoint `POST /{db}/_explain` `index`, `add_index` and `remove_index` correspond to `GET /{db}/_index`, `POST /{db}/_index` and `DELETE /{db}/_index/{designdoc}/json/{name}` These methods are only available for CouchDB server version >= 2.0.0. Added a note to `query`: temporary views are not available anymore for CouchDB server version >= 2.0.0. Also add tests for the new methods. --- couchdb/client.py | 146 ++++++++++++++++++++++++++++++++++++++++ couchdb/tests/client.py | 70 +++++++++++++++++++ 2 files changed, 216 insertions(+) diff --git a/couchdb/client.py b/couchdb/client.py index b47c2caa..9d9d0a17 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -775,6 +775,152 @@ def put_attachment(self, doc, content, filename=None, content_type=None): }, rev=doc['_rev']) doc['_rev'] = data['rev'] + def find(self, mango_query, wrapper=None): + """Execute a mango find-query against the database. + + Note: only available for CouchDB version >= 2.0.0 + + More information on the `mango_query` structure can be found here: + http://docs.couchdb.org/en/master/api/database/find.html#find-selectors + + >>> server = Server() + >>> db = server.create('python-tests') + >>> db['johndoe'] = dict(type='Person', name='John Doe') + >>> db['maryjane'] = dict(type='Person', name='Mary Jane') + >>> db['gotham'] = dict(type='City', name='Gotham City') + >>> mango = {'selector': {'type': 'Person'}, + ... 'fields': ['name'], + ... 'sort':[{'name': 'asc'}]} + >>> for row in db.find(mango): # doctest: +SKIP + ... print(row['name']) # doctest: +SKIP + John Doe + Mary Jane + >>> del server['python-tests'] + + :param mango_query: a dictionary describing criteria used to select + documents + :param wrapper: an optional callable that should be used to wrap the + resulting documents + :return: the query results as a list of `Document` (or whatever `wrapper` returns) + """ + status, headers, data = self.resource.post_json('_find', mango_query) + return map(wrapper or Document, data.get('docs', [])) + + def explain(self, mango_query): + """Explain a mango find-query. + + Note: only available for CouchDB version >= 2.0.0 + + More information on the `mango_query` structure can be found here: + http://docs.couchdb.org/en/master/api/database/find.html#db-explain + + >>> server = Server() + >>> db = server.create('python-tests') + >>> db['johndoe'] = dict(type='Person', name='John Doe') + >>> db['maryjane'] = dict(type='Person', name='Mary Jane') + >>> db['gotham'] = dict(type='City', name='Gotham City') + >>> mango = {'selector': {'type': 'Person'}, 'fields': ['name']} + >>> db.explain(mango) #doctest: +ELLIPSIS +SKIP + {...} + >>> del server['python-tests'] + + :param mango_query: a `dict` describing criteria used to select + documents + :return: the query results as a list of `Document` (or whatever + `wrapper` returns) + :rtype: `dict` + """ + _, _, data = self.resource.post_json('_explain', mango_query) + return data + + def index(self): + """Get all available indexes. + + Note: Only available for CouchDB version >= 2.0.0 . + + More information here: + http://docs.couchdb.org/en/master/api/database/find.html#get--db-_index + + >>> server = Server() + >>> db = server.create('python-tests') + >>> db.index() #doctest: +SKIP + {'indexes': [{'ddoc': None, + 'def': {'fields': [{'_id': 'asc'}]}, + 'name': '_all_docs', + 'type': 'special'}], + 'total_rows': 1} + >>> del server['python-tests'] + + :return: `dict` containing the number of indexes (`total_rows`) and + a description of each index (`indexes`) + """ + _, _, data = self.resource.get_json('_index') + return data + + def add_index(self, index, ddoc=None, name=None): + """Add an index to the database. + + Note: Only available for CouchDB version >= 2.0.0 . + + More information here: + http://docs.couchdb.org/en/master/api/database/find.html#post--db-_index + + >>> server = Server() + >>> db = server.create('python-tests') + >>> db['johndoe'] = dict(type='Person', name='John Doe') + >>> db['maryjane'] = dict(type='Person', name='Mary Jane') + >>> db['gotham'] = dict(type='City', name='Gotham City') + >>> db.add_index({'fields': [{'type': 'asc'}]}, #doctest: +SKIP + ... ddoc='foo', + ... name='bar') + {'id': '_design/foo', 'name': 'bar', 'result': 'created'} + >>> del server['python-tests'] + + :param index: `dict` describing the index to create + :param ddoc: (optional) name of the design document in which the index + will be created + :param name: (optional) name of the index + :return: `dict` containing the `id`, the `name` and the `result` of + creating the index + """ + assert isinstance(index, dict) + query = {'index': index} + if ddoc: + query['ddoc'] = ddoc + if name: + query['name'] = name + _, _, data = self.resource.post_json('_index', query) + return data + + def remove_index(self, ddoc, name): + """Remove an index from the database. + + Note: Only available for CouchDB version >= 2.0.0 . + + More information here: + http://docs.couchdb.org/en/master/api/database/find.html#delete--db-_index-designdoc-json-name + + >>> server = Server() + >>> db = server.create('python-tests') + >>> db['johndoe'] = dict(type='Person', name='John Doe') + >>> db['maryjane'] = dict(type='Person', name='Mary Jane') + >>> db['gotham'] = dict(type='City', name='Gotham City') + >>> db.add_index({'fields': [{'type': 'asc'}]}, #doctest: +SKIP + ... ddoc='foo', + ... name='bar') + {'id': '_design/foo', 'name': 'bar', 'result': 'created'} + >>> db.remove_index('foo', 'bar') #doctest: +SKIP + {'ok': True} + >>> del server['python-tests'] + + :param ddoc: name of the design document containing the index + :param name: name of the index that is to be removed + :return: `dict` containing the `id`, the `name` and the `result` of + creating the index + """ + _, _, data = self.resource.delete_json(['_index', ddoc, 'json', name]) + return data + def query(self, map_fun, reduce_fun=None, language='javascript', wrapper=None, **options): """Execute an ad-hoc query (a "temp view") against the database. diff --git a/couchdb/tests/client.py b/couchdb/tests/client.py index 5aec3939..618fccb5 100644 --- a/couchdb/tests/client.py +++ b/couchdb/tests/client.py @@ -374,6 +374,76 @@ def test_query_multi_get(self): for idx, i in enumerate(range(1, 6, 2)): self.assertEqual(i, res[idx].key) + def test_find(self): + if self.server.version_info()[0] < 2: + return + + docs = [ + dict(type='Person', name='John Doe'), + dict(type='Person', name='Mary Jane'), + dict(type='City', name='Gotham City') + ] + self.db.update(docs) + + res = list(self.db.find( + { + 'selector': { + 'type': 'Person' + }, + 'fields': ['name'], + 'sort': [{'name': 'asc'}] + } + )) + self.assertEqual(2, len(res)) + expect = ['John Doe', 'Mary Jane'] + for i, doc in enumerate(res): + self.assertEqual(set(['name']), doc.keys()) + self.assertEqual(expect[i], doc['name']) + + def test_explain(self): + if self.server.version_info()[0] < 2: + return + mango = {'selector': {'type': 'Person'}, 'fields': ['name']} + res = self.db.explain(mango) + + self.assertEqual(['name'], res['fields']) + self.assertEqual({'type': {'$eq': 'Person'}}, res['selector']) + self.assertEqual(0, res['skip']) + self.assertEqual(self.db.name, res['dbname']) + + def test_index(self): + if self.server.version_info()[0] < 2: + return + + res = self.db.index() + self.assertEqual(1, res['total_rows']) + self.assertEqual(1, len(res['indexes'])) + self.assertEqual({'ddoc': None, 'def': {'fields': [{'_id': 'asc'}]}, + 'name': '_all_docs', 'type': 'special'}, + res['indexes'][0]) + + def test_add_index(self): + if self.server.version_info()[0] < 2: + return + + res = self.db.add_index({'fields': [{'type': 'asc'}]}, ddoc='foo', name='bar') + self.assertEqual({'id': '_design/foo', + 'name': 'bar', + 'result': 'created'}, + res) + res = self.db.index() + self.assertEqual(2, res['total_rows']) + + def test_remove_index(self): + if self.server.version_info()[0] < 2: + return + + self.db.add_index({'fields': [{'type': 'asc'}]}, ddoc='foo', name='bar') + res = self.db.remove_index('foo', 'bar') + self.assertEqual({'ok': True}, res) + res = self.db.index() + self.assertEqual(1, res['total_rows']) + def test_bulk_update_conflict(self): docs = [ dict(type='Person', name='John Doe'), From 013c2b1b21ae14869bab104a7cf7cf5f11225893 Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Tue, 25 Apr 2017 11:06:45 +0200 Subject: [PATCH 06/21] Update documentation for Database.query() method --- couchdb/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/couchdb/client.py b/couchdb/client.py index 9d9d0a17..ea85fa2e 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -925,6 +925,8 @@ def query(self, map_fun, reduce_fun=None, language='javascript', wrapper=None, **options): """Execute an ad-hoc query (a "temp view") against the database. + Note: not supported for CouchDB version >= 2.0.0 + >>> server = Server() >>> db = server.create('python-tests') >>> db['johndoe'] = dict(type='Person', name='John Doe') @@ -957,7 +959,7 @@ def query(self, map_fun, reduce_fun=None, language='javascript', :param wrapper: an optional callable that should be used to wrap the result rows :param options: optional query string parameters - :return: the view reults + :return: the view results :rtype: `ViewResults` """ return TemporaryView(self.resource('_temp_view'), map_fun, From 026992fbc686acf0cbdd8ac5fb661daf8e029397 Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Tue, 25 Apr 2017 10:48:27 +0200 Subject: [PATCH 07/21] Allow `mango` filters in `_changes` API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If `filter==‘selector’` then there should be another kwarg `_selector` with a `dict` containing the selector / mango filter. --- couchdb/client.py | 14 ++++++++++++-- couchdb/tests/client.py | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index ea85fa2e..466d44e3 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -1186,7 +1186,12 @@ def update_doc(self, name, docid=None, **options): return headers, body def _changes(self, **opts): - _, _, data = self.resource.get('_changes', **opts) + # use streaming `get` and `post` methods + if opts.get('filter') == '_selector': + selector = opts.pop('_selector', None) + _, _, data = self.resource.post('_changes', selector, **opts) + else: + _, _, data = self.resource.get('_changes', **opts) lines = data.iterchunks() for ln in lines: if not ln: # skip heartbeats @@ -1205,7 +1210,12 @@ def changes(self, **opts): """ if opts.get('feed') == 'continuous': return self._changes(**opts) - _, _, data = self.resource.get_json('_changes', **opts) + + if opts.get('filter') == '_selector': + selector = opts.pop('_selector', None) + _, _, data = self.resource.post_json('_changes', selector, **opts) + else: + _, _, data = self.resource.get_json('_changes', **opts) return data diff --git a/couchdb/tests/client.py b/couchdb/tests/client.py index 618fccb5..eb022554 100644 --- a/couchdb/tests/client.py +++ b/couchdb/tests/client.py @@ -570,6 +570,28 @@ def test_changes_conn_usable(self): # in a good state from the previous request. self.assertTrue(self.db.info()['doc_count'] == 0) + def test_changes_conn_usable_selector(self): + if self.server.version_info()[0] < 2: + return + # Consume a changes feed to get a used connection in the pool. + list(self.db.changes(feed='continuous', + filter='_selector', + timeout=0, + _selector={'selector': {}})) + # Try using the connection again to make sure the connection was left + # in a good state from the previous request. + self.assertTrue(self.db.info()['doc_count'] == 0) + + def test_changes_usable_selector(self): + if self.server.version_info()[0] < 2: + return + # Consume a changes feed to get a used connection in the pool. + list(self.db.changes(filter='_selector', + _selector={'selector': {}})) + # Try using the connection again to make sure the connection was left + # in a good state from the previous request. + self.assertTrue(self.db.info()['doc_count'] == 0) + def test_changes_heartbeat(self): def wakeup(): time.sleep(.3) From 81af09da8f28ccd5718335240a8a709704b647f6 Mon Sep 17 00:00:00 2001 From: Tammo Ippen Date: Tue, 25 Apr 2017 10:49:16 +0200 Subject: [PATCH 08/21] Make the `index` endpoint a separate class --- couchdb/client.py | 194 +++++++++++++++++++++++----------------- couchdb/tests/client.py | 47 ++++++---- 2 files changed, 142 insertions(+), 99 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index 466d44e3..6ac10057 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -834,92 +834,14 @@ def explain(self, mango_query): return data def index(self): - """Get all available indexes. + """Get an object to manage the database indexes. - Note: Only available for CouchDB version >= 2.0.0 . - - More information here: - http://docs.couchdb.org/en/master/api/database/find.html#get--db-_index - - >>> server = Server() - >>> db = server.create('python-tests') - >>> db.index() #doctest: +SKIP - {'indexes': [{'ddoc': None, - 'def': {'fields': [{'_id': 'asc'}]}, - 'name': '_all_docs', - 'type': 'special'}], - 'total_rows': 1} - >>> del server['python-tests'] - - :return: `dict` containing the number of indexes (`total_rows`) and - a description of each index (`indexes`) - """ - _, _, data = self.resource.get_json('_index') - return data - - def add_index(self, index, ddoc=None, name=None): - """Add an index to the database. - - Note: Only available for CouchDB version >= 2.0.0 . - - More information here: - http://docs.couchdb.org/en/master/api/database/find.html#post--db-_index - - >>> server = Server() - >>> db = server.create('python-tests') - >>> db['johndoe'] = dict(type='Person', name='John Doe') - >>> db['maryjane'] = dict(type='Person', name='Mary Jane') - >>> db['gotham'] = dict(type='City', name='Gotham City') - >>> db.add_index({'fields': [{'type': 'asc'}]}, #doctest: +SKIP - ... ddoc='foo', - ... name='bar') - {'id': '_design/foo', 'name': 'bar', 'result': 'created'} - >>> del server['python-tests'] - - :param index: `dict` describing the index to create - :param ddoc: (optional) name of the design document in which the index - will be created - :param name: (optional) name of the index - :return: `dict` containing the `id`, the `name` and the `result` of - creating the index + :return: an `Indexes` object to manage the databes indexes + :rtype: `Indexes` """ - assert isinstance(index, dict) - query = {'index': index} - if ddoc: - query['ddoc'] = ddoc - if name: - query['name'] = name - _, _, data = self.resource.post_json('_index', query) - return data - - def remove_index(self, ddoc, name): - """Remove an index from the database. - - Note: Only available for CouchDB version >= 2.0.0 . - - More information here: - http://docs.couchdb.org/en/master/api/database/find.html#delete--db-_index-designdoc-json-name + return Indexes(self.resource('_index')) - >>> server = Server() - >>> db = server.create('python-tests') - >>> db['johndoe'] = dict(type='Person', name='John Doe') - >>> db['maryjane'] = dict(type='Person', name='Mary Jane') - >>> db['gotham'] = dict(type='City', name='Gotham City') - >>> db.add_index({'fields': [{'type': 'asc'}]}, #doctest: +SKIP - ... ddoc='foo', - ... name='bar') - {'id': '_design/foo', 'name': 'bar', 'result': 'created'} - >>> db.remove_index('foo', 'bar') #doctest: +SKIP - {'ok': True} - >>> del server['python-tests'] - :param ddoc: name of the design document containing the index - :param name: name of the index that is to be removed - :return: `dict` containing the `id`, the `name` and the `result` of - creating the index - """ - _, _, data = self.resource.delete_json(['_index', ddoc, 'json', name]) - return data def query(self, map_fun, reduce_fun=None, language='javascript', wrapper=None, **options): @@ -1526,3 +1448,111 @@ def doc(self): doc = self.get('doc') if doc: return Document(doc) + + +class Indexes(object): + """Manage indexes in CouchDB 2.0.0 and later. + + More information here: + http://docs.couchdb.org/en/2.0.0/api/database/find.html#db-index + """ + + def __init__(self, url, session=None): + if isinstance(url, util.strbase): + self.resource = http.Resource(url, session) + else: + self.resource = url + + def __setitem__(self, ddoc_name, index): + """Add an index to the database. + + >>> server = Server() + >>> db = server.create('python-tests') + >>> db['johndoe'] = dict(type='Person', name='John Doe') + >>> db['maryjane'] = dict(type='Person', name='Mary Jane') + >>> db['gotham'] = dict(type='City', name='Gotham City') + >>> idx = db.index() + >>> idx['foo', 'bar'] = [{'type': 'asc'}] #doctest: +SKIP + >>> list(idx) #doctest: +SKIP + [{'ddoc': None, + 'def': {'fields': [{'_id': 'asc'}]}, + 'name': '_all_docs', + 'type': 'special'}, + {'ddoc': '_design/foo', + 'def': {'fields': [{'type': 'asc'}]}, + 'name': 'bar', + 'type': 'json'}] + >>> idx[None, None] = [{'type': 'desc'}] #doctest: +SKIP + >>> list(idx) #doctest: +SKIP, +ELLIPSIS + [{'ddoc': None, + 'def': {'fields': [{'_id': 'asc'}]}, + 'name': '_all_docs', + 'type': 'special'}, + {'ddoc': '_design/...', + 'def': {'fields': [{'type': 'desc'}]}, + 'name': '...', + 'type': 'json'}, + {'ddoc': '_design/foo', + 'def': {'fields': [{'type': 'asc'}]}, + 'name': 'bar', + 'type': 'json'}] + >>> del server['python-tests'] + + :param index: `list` of indexes to create + :param ddoc_name: `tuple` or `list` containing first the name of the + design document, in which the index will be created, + and second name of the index. Both can be `None`. + """ + query = {'index': {'fields': index}} + ddoc, name = ddoc_name # expect ddoc / name to be a slice or list + if ddoc: + query['ddoc'] = ddoc + if name: + query['name'] = name + self.resource.post_json(body=query) + + def __delitem__(self, ddoc_name): + """Remove an index from the database. + + >>> server = Server() + >>> db = server.create('python-tests') + >>> db['johndoe'] = dict(type='Person', name='John Doe') + >>> db['maryjane'] = dict(type='Person', name='Mary Jane') + >>> db['gotham'] = dict(type='City', name='Gotham City') + >>> idx = db.index() + >>> idx['foo', 'bar'] = [{'type': 'asc'}] #doctest: +SKIP + >>> del idx['foo', 'bar'] #doctest: +SKIP + >>> list(idx) #doctest: +SKIP + [{'ddoc': None, + 'def': {'fields': [{'_id': 'asc'}]}, + 'name': '_all_docs', + 'type': 'special'}] + >>> del server['python-tests'] + + :param ddoc: name of the design document containing the index + :param name: name of the index that is to be removed + :return: `dict` containing the `id`, the `name` and the `result` of + creating the index + """ + self.resource.delete_json([ddoc_name[0], 'json', ddoc_name[1]]) + + def _list(self): + _, _, data = self.resource.get_json() + return data + + def __iter__(self): + """Iterate all indexes of the associated database. + + >>> server = Server() + >>> db = server.create('python-tests') + >>> idx = db.index() + >>> list(idx) #doctest: +SKIP + [{'ddoc': None, + 'def': {'fields': [{'_id': 'asc'}]}, + 'name': '_all_docs', + 'type': 'special'}] + >>> del server['python-tests'] + + :return: iterator yielding `dict`'s describing each index + """ + return iter(self._list()['indexes']) diff --git a/couchdb/tests/client.py b/couchdb/tests/client.py index eb022554..c21f3940 100644 --- a/couchdb/tests/client.py +++ b/couchdb/tests/client.py @@ -384,6 +384,9 @@ def test_find(self): dict(type='City', name='Gotham City') ] self.db.update(docs) + # the sort needs an index over `name`, the selector selects by `type` + idx = self.db.index() + idx['foo', 'bar'] = [{'type': 'asc'}, {'name': 'asc'}] res = list(self.db.find( { @@ -391,7 +394,8 @@ def test_find(self): 'type': 'Person' }, 'fields': ['name'], - 'sort': [{'name': 'asc'}] + # we need to specify the complete index here + 'sort': [{'type': 'asc'}, {'name': 'asc'}] } )) self.assertEqual(2, len(res)) @@ -415,34 +419,43 @@ def test_index(self): if self.server.version_info()[0] < 2: return - res = self.db.index() - self.assertEqual(1, res['total_rows']) - self.assertEqual(1, len(res['indexes'])) + res = list(self.db.index()) + self.assertEqual(1, len(res)) self.assertEqual({'ddoc': None, 'def': {'fields': [{'_id': 'asc'}]}, 'name': '_all_docs', 'type': 'special'}, - res['indexes'][0]) + res[0]) def test_add_index(self): if self.server.version_info()[0] < 2: return - res = self.db.add_index({'fields': [{'type': 'asc'}]}, ddoc='foo', name='bar') - self.assertEqual({'id': '_design/foo', - 'name': 'bar', - 'result': 'created'}, - res) - res = self.db.index() - self.assertEqual(2, res['total_rows']) + idx = self.db.index() + idx['foo', 'bar'] = [{'type': 'asc'}] + idxs = list(idx) + + self.assertEqual(2, len(idxs)) + for i in idxs: + if i['ddoc'] is not None: # special `_all_docs` index + self.assertEqual({'ddoc': '_design/foo', + 'def': {'fields': [{'type': 'asc'}]}, + 'name': 'bar', + 'type': 'json'}, + i) + return + self.failed() def test_remove_index(self): if self.server.version_info()[0] < 2: return - self.db.add_index({'fields': [{'type': 'asc'}]}, ddoc='foo', name='bar') - res = self.db.remove_index('foo', 'bar') - self.assertEqual({'ok': True}, res) - res = self.db.index() - self.assertEqual(1, res['total_rows']) + idx = self.db.index() + idx['foo', 'bar'] = [{'type': 'asc'}] + res = list(idx) + self.assertEqual(2, len(res)) + del idx['foo', 'bar'] + + res = list(idx) + self.assertEqual(1, len(res)) def test_bulk_update_conflict(self): docs = [ From 30fd1a32500ee69ccd1e770eec8bb9cf45bb0ca0 Mon Sep 17 00:00:00 2001 From: Zach Cheung Date: Fri, 28 Apr 2017 15:21:03 +0800 Subject: [PATCH 09/21] Add a blank line in docstring of some functions for sphinx --- couchdb/client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/couchdb/client.py b/couchdb/client.py index 6ac10057..6125e831 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -244,6 +244,7 @@ def replicate(self, source, target, **options): def add_user(self, name, password, roles=None): """Add regular user in authentication database. + :param name: name of regular user, normally user id :param password: password of regular user :param roles: roles of regular user @@ -261,6 +262,7 @@ def add_user(self, name, password, roles=None): def remove_user(self, name): """Remove regular user in authentication database. + :param name: name of regular user, normally user id """ user_db = self['_users'] @@ -269,6 +271,7 @@ def remove_user(self, name): def login(self, name, password): """Login regular user in couch db + :param name: name of regular user, normally user id :param password: password of regular user :return: authentication token @@ -286,6 +289,7 @@ def login(self, name, password): def logout(self, token): """Logout regular user in couch db + :param token: token of login user :return: True if successfully logout :rtype: bool @@ -299,6 +303,7 @@ def logout(self, token): def verify_token(self, token): """Verify user token + :param token: authentication token :return: True if authenticated ok :rtype: bool From 2073f3b9cc4e1bb351a0626cc5c70f531c9ea9d6 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Thu, 25 May 2017 11:55:26 +0200 Subject: [PATCH 10/21] Remove support for Python 2.6 and 3.x with x < 4 --- README.rst | 5 +++-- tox.ini | 11 +---------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index ae1e910b..41d8bdda 100644 --- a/README.rst +++ b/README.rst @@ -22,10 +22,11 @@ It also provides a couple of command-line tools: Prerequisites: -* simplejson (or Python >= 2.6, which comes with a simplejson-based JSON module in the standard library) -* Python 2.6 or later +* Python 2.7, 3.4 or later * CouchDB 0.10.x or later (0.9.x should probably work, as well) +``simplejson`` will be used if installed. + .. _Downloads: http://pypi.python.org/pypi/CouchDB .. _PyPI: http://pypi.python.org/ .. _documentation: http://packages.python.org/CouchDB/ diff --git a/tox.ini b/tox.ini index ae08bf98..53929648 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26, py27, py33, py34, py33-json, py34-json, py35-json +envlist = py27, py34, py34-json, py35-json [testenv] deps = simplejson @@ -7,15 +7,6 @@ commands = python --version {envbindir}/python -m couchdb.tests -[testenv:py26] -commands = - python --version - {envbindir}/python -m couchdb.tests.__main__ - -[testenv:py33-json] -basepython = python3.3 -deps = - [testenv:py34-json] basepython = python3.4 deps = From ce406d9dc83786bab117676c6bce97955b3d78bb Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Thu, 25 May 2017 12:32:47 +0200 Subject: [PATCH 11/21] Update Travis configuration for new Python selection --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 10cf83f6..cffb42b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,11 +3,8 @@ python: 2.7 services: - couchdb env: - - TOX_ENV=py26 - TOX_ENV=py27 - - TOX_ENV=py33 - TOX_ENV=py34 - - TOX_ENV=py33-json - TOX_ENV=py34-json install: - pip install tox From 438a39246bdcd20f93df48d81b91c6ae88338a86 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Thu, 25 May 2017 11:12:44 +0200 Subject: [PATCH 12/21] Make sure the view server output buffer accepts bytes (fixes #319) --- couchdb/view.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/couchdb/view.py b/couchdb/view.py index 0bb7e315..edc7eeb4 100755 --- a/couchdb/view.py +++ b/couchdb/view.py @@ -24,13 +24,15 @@ log = logging.getLogger('couchdb.view') -def run(input=sys.stdin, output=sys.stdout): +def run(input=sys.stdin, output=None): r"""CouchDB view function handler implementation for Python. :param input: the readable file-like object to read input from :param output: the writable file-like object to write output to """ functions = [] + if output is None: + output = sys.stdout if sys.version_info[0] < 3 else sys.stdout.buffer def _writejson(obj): obj = json.encode(obj) From e8eab1f66fdb7f82273e0780282389d512a8f519 Mon Sep 17 00:00:00 2001 From: Eric Dilmore Date: Fri, 2 Jun 2017 09:07:38 -0500 Subject: [PATCH 13/21] Fix logging response in query server (#321) Documentation can be found at http://docs.couchdb.org/en/2.0.0/query-server/protocol.html * Fix logging tests to have correct format. --- couchdb/tests/view.py | 6 +++--- couchdb/view.py | 34 +++++++++++++++++----------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/couchdb/tests/view.py b/couchdb/tests/view.py index 79a18dd9..99f7f8d1 100644 --- a/couchdb/tests/view.py +++ b/couchdb/tests/view.py @@ -53,7 +53,7 @@ def test_map_doc_with_logging(self): view.run(input=input, output=output) self.assertEqual(output.getvalue(), b'true\n' - b'{"log": "running"}\n' + b'["log", "running"]\n' b'[[[null, {"foo": "bar"}]]]\n') def test_map_doc_with_logging_json(self): @@ -64,7 +64,7 @@ def test_map_doc_with_logging_json(self): view.run(input=input, output=output) self.assertEqual(output.getvalue(), b'true\n' - b'{"log": "[1, 2, 3]"}\n' + b'["log", "[1, 2, 3]"]\n' b'[[[null, {"foo": "bar"}]]]\n') def test_reduce(self): @@ -82,7 +82,7 @@ def test_reduce_with_logging(self): output = StringIO() view.run(input=input, output=output) self.assertEqual(output.getvalue(), - b'{"log": "Summing (1, 2, 3)"}\n' + b'["log", "Summing (1, 2, 3)"]\n' b'[true, [6]]\n') def test_rereduce(self): diff --git a/couchdb/view.py b/couchdb/view.py index edc7eeb4..1412996b 100755 --- a/couchdb/view.py +++ b/couchdb/view.py @@ -45,7 +45,7 @@ def _writejson(obj): def _log(message): if not isinstance(message, util.strbase): message = json.encode(message) - _writejson({'log': message}) + _writejson(['log', message]) def reset(config=None): del functions[:] @@ -57,15 +57,15 @@ def add_fun(string): try: util.pyexec(string, {'log': _log}, globals_) except Exception as e: - return {'error': { - 'id': 'map_compilation_error', - 'reason': e.args[0] - }} - err = {'error': { - 'id': 'map_compilation_error', - 'reason': 'string must eval to a function ' + return ['error', + 'map_compilation_error', + e.args[0] + ] + err = ['error', + 'map_compilation_error', + 'string must eval to a function ' '(ex: "def(doc): return 1")' - }} + ] if len(globals_) != 1: return err function = list(globals_.values())[0] @@ -95,15 +95,15 @@ def reduce(*cmd, **kwargs): except Exception as e: log.error('runtime error in reduce function: %s', e, exc_info=True) - return {'error': { - 'id': 'reduce_compilation_error', - 'reason': e.args[0] - }} - err = {'error': { - 'id': 'reduce_compilation_error', - 'reason': 'string must eval to a function ' + return ['error', + 'reduce_compilation_error', + e.args[0] + ] + err = ['error', + 'reduce_compilation_error', + 'string must eval to a function ' '(ex: "def(keys, values): return 1")' - }} + ] if len(globals_) != 1: return err function = list(globals_.values())[0] From b41eebfb59dd7a5de2292ce6265d01e4d65ffe2b Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 9 Feb 2018 14:03:34 +0100 Subject: [PATCH 14/21] Update Python 3 support metadata --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 95af723e..5b518248 100755 --- a/setup.py +++ b/setup.py @@ -58,8 +58,8 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Topic :: Database :: Front-Ends', 'Topic :: Software Development :: Libraries :: Python Modules', ], From 16f7f8cd90e19977ad7c9a5d2e0bb9e834a7e6d4 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 9 Feb 2018 14:10:08 +0100 Subject: [PATCH 15/21] Update changelog for version 1.2 --- ChangeLog.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ChangeLog.rst b/ChangeLog.rst index 8fa6edb9..7cf2f217 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,3 +1,16 @@ +Version 1.2 (2018-02-09) +------------------------ + +* Fixed some issues relating to usage with Python 3 +* Remove support for Python 2.6 and 3.x with x < 4 +* Fix logging response in query server (fixes #321) +* Fix HTTP authentication password encoding (fixes #302) +* Add missing ``http.Forbidden`` error (fixes #305) +* Show ``doc`` property on ``Row`` string representation +* Add methods for mango queries and indexes +* Allow mango filters in ``_changes`` API + + Version 1.1 (2016-08-05) ------------------------ From 955ae2067e0525452b012beca5f4097cf6617515 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 9 Feb 2018 14:10:23 +0100 Subject: [PATCH 16/21] Bump version to 1.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5b518248..f271cb40 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ setup( name = 'CouchDB', - version = '1.1.1', + version = '1.2', description = 'Python library for working with CouchDB', long_description = \ """This is a Python library for CouchDB. It provides a convenient high level From 44ec56ced41d507fce4237d01ea467f7505f58e1 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 9 Feb 2018 14:10:48 +0100 Subject: [PATCH 17/21] Remove dev status before release --- setup.cfg | 4 ---- 1 file changed, 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index c6facbeb..22b57c7d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,3 @@ -[egg_info] -tag_build = dev -tag_svn_revision = true - [build_sphinx] source-dir = doc/ build-dir = doc/build From 1c592529c576b39112e99327ba223dc9ca269398 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 9 Feb 2018 14:19:37 +0100 Subject: [PATCH 18/21] Add back dev status --- Makefile | 2 +- setup.cfg | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index bcd4e08c..f4628527 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ doc: python setup.py build_sphinx upload-doc: doc - python setup.py upload_sphinx + python2 setup.py upload_sphinx coverage: PYTHONPATH=. coverage run couchdb/tests/__main__.py diff --git a/setup.cfg b/setup.cfg index 22b57c7d..c6facbeb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,7 @@ +[egg_info] +tag_build = dev +tag_svn_revision = true + [build_sphinx] source-dir = doc/ build-dir = doc/build From 17f5398f9299aa1e59d9991ed1d18610ef4edcd7 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 9 Feb 2018 14:19:54 +0100 Subject: [PATCH 19/21] Bump version number post release --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f271cb40..9fdcbd65 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ setup( name = 'CouchDB', - version = '1.2', + version = '1.2.1', description = 'Python library for working with CouchDB', long_description = \ """This is a Python library for CouchDB. It provides a convenient high level From 87faa0c142724e8907e7593645a20e721c03f3b5 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 9 Feb 2018 14:22:24 +0100 Subject: [PATCH 20/21] Update README to explain end of maintenance --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index 41d8bdda..3ba4e698 100644 --- a/README.rst +++ b/README.rst @@ -4,6 +4,12 @@ CouchDB-Python Library .. image:: https://travis-ci.org/djc/couchdb-python.svg :target: https://travis-ci.org/djc/couchdb-python +**Note: CouchDB-Python is no longer being maintained. After 8 years of maintaining +CouchDB-Python, I no longer have time to address open issues and new bug reports. +Consider https://github.com/cloudant/python-cloudant as an alternative. +If you're interested in taking over maintenance of CouchDB-Python, please start a +discussion on the mailing list, or open an issue or PR.** + A Python library for working with CouchDB. `Downloads`_ are available via `PyPI`_. Our `documentation`_ is also hosted there. We have a `mailing list`_. From 459bb1ef24587eef2577ad414e1c070e8b0eaff5 Mon Sep 17 00:00:00 2001 From: rockerBOO Date: Fri, 16 Feb 2018 13:44:46 -0500 Subject: [PATCH 21/21] Updating documentation link --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 3ba4e698..65e0f029 100644 --- a/README.rst +++ b/README.rst @@ -35,5 +35,5 @@ Prerequisites: .. _Downloads: http://pypi.python.org/pypi/CouchDB .. _PyPI: http://pypi.python.org/ -.. _documentation: http://packages.python.org/CouchDB/ +.. _documentation: http://couchdb-python.readthedocs.io/en/latest/ .. _mailing list: http://groups.google.com/group/couchdb-python