From 9aad7615750db40865afcd619c5dc85a270dad39 Mon Sep 17 00:00:00 2001 From: Jan Korte Date: Mon, 22 Jun 2015 18:58:49 +0200 Subject: [PATCH 01/78] authentication doc --- couchdb/client.py | 4 +++- doc/getting-started.rst | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/couchdb/client.py b/couchdb/client.py index 6f571e2e..5d9b0468 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -43,7 +43,9 @@ class Server(object): """Representation of a CouchDB server. - >>> server = Server() + >>> server = Server() # connects to the local_server + >>> remote_server = Server('http://example.com:5984/') + >>> secure_remote_server = Server('https://username:password@example.com:5984/') This class behaves like a dictionary of databases. For example, to get a list of database names on the server, you can simply iterate over the diff --git a/doc/getting-started.rst b/doc/getting-started.rst index bac3e518..94770e74 100644 --- a/doc/getting-started.rst +++ b/doc/getting-started.rst @@ -14,6 +14,8 @@ running elsewhere, set it up like this: >>> couch = couchdb.Server('http://example.com:5984/') +You can also pass authentication credentials or use SSL: https://username:password@host:port/ + You can create a new database from Python, or use an existing database: >>> db = couch.create('test') # newly created From a0031210ea5198d2975c93906501ffd15e9d1a60 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Sun, 12 Jul 2015 15:39:08 +0200 Subject: [PATCH 02/78] Tighten getting started doc a little bit --- doc/getting-started.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/getting-started.rst b/doc/getting-started.rst index 94770e74..e08b704e 100644 --- a/doc/getting-started.rst +++ b/doc/getting-started.rst @@ -14,7 +14,9 @@ running elsewhere, set it up like this: >>> couch = couchdb.Server('http://example.com:5984/') -You can also pass authentication credentials or use SSL: https://username:password@host:port/ +You can also pass authentication credentials and/or use SSL: + + >>> couch = couchdb.Server('https://username:password@host:port/') You can create a new database from Python, or use an existing database: From 03187e11a8f39c2de86985e6fc8bf0dc1e88334a Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Sun, 17 Jan 2016 13:05:37 +0100 Subject: [PATCH 03/78] Correctly join path parts in replicate script (fixes #269) --- couchdb/tools/replicate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchdb/tools/replicate.py b/couchdb/tools/replicate.py index f527ffa9..a60fcd67 100755 --- a/couchdb/tools/replicate.py +++ b/couchdb/tools/replicate.py @@ -40,7 +40,7 @@ def findpath(parser, s): cut = None for i in range(0, len(parts) + 1): try: - data = res.get_json(parts[:i])[2] + data = res.get_json('/'.join(parts[:i]))[2] except Exception: data = None if data and 'couchdb' in data: From 0da158ce7cff979836dfa5e2057616e8ad877fdd Mon Sep 17 00:00:00 2001 From: Jelte Fennema Date: Mon, 18 Jan 2016 12:05:55 +0100 Subject: [PATCH 04/78] Fix id and rev for some special documents Some special documents (e.g. _security) have no id or rev, this results in an error when trying to get them. This is done in the __repr__ so these documents can not be printed. --- couchdb/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index 5d9b0468..7c697260 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -1011,7 +1011,8 @@ def id(self): :rtype: basestring """ - return self['_id'] + return self.get('_id') + @property def rev(self): @@ -1019,7 +1020,7 @@ def rev(self): :rtype: basestring """ - return self['_rev'] + return self.get('_rev') class View(object): From 458207d90bb3a5b3a83ed8bd6d3473ab9121c6b8 Mon Sep 17 00:00:00 2001 From: Philippe Vignau Date: Fri, 11 Mar 2016 15:09:35 +0100 Subject: [PATCH 05/78] Fix #224 for AppEngine httplib resp::isclosed --- couchdb/http.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/couchdb/http.py b/couchdb/http.py index dd22319b..26a704fd 100644 --- a/couchdb/http.py +++ b/couchdb/http.py @@ -160,7 +160,9 @@ def _release_conn(self): def close(self): while not self.resp.isclosed(): - self.resp.read(CHUNK_SIZE) + chunk = self.resp.read(CHUNK_SIZE) + if not chunk: + self.resp.close() if self.conn: self._release_conn() From 19c8fa6863ff429e9d75a4be7c11223a90c86d86 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Sat, 12 Mar 2016 12:35:32 +0100 Subject: [PATCH 06/78] Fix style nit --- couchdb/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchdb/http.py b/couchdb/http.py index 26a704fd..51929036 100644 --- a/couchdb/http.py +++ b/couchdb/http.py @@ -160,7 +160,7 @@ def _release_conn(self): def close(self): while not self.resp.isclosed(): - chunk = self.resp.read(CHUNK_SIZE) + chunk = self.resp.read(CHUNK_SIZE) if not chunk: self.resp.close() if self.conn: From 0a6730280de979c0eec42fec6a2a06d2ab8c00e9 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Sat, 12 Mar 2016 12:38:36 +0100 Subject: [PATCH 07/78] Update ChangeLog for 1.0.1 release --- ChangeLog.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ChangeLog.rst b/ChangeLog.rst index a1ab9a53..9d97e5b5 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,3 +1,12 @@ +Version 1.0.1 (2016-03-12) +-------------------------- + +* Make sure connections are correctly closed on GAE (fixes #224) +* Correctly join path parts in replicate script (fixes #269) +* Fix id and rev for some special documents +* Make it possible to disable SSL verification + + Version 1.0 (2014-11-16) ------------------------ From f330318b823bf1e112e9445500836f11b8075ffd Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Sat, 12 Mar 2016 12:40:11 +0100 Subject: [PATCH 08/78] Upgrade development status to Mature --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5bc3b8cd..6e3deb3e 100755 --- a/setup.py +++ b/setup.py @@ -55,7 +55,7 @@ license = 'BSD', url = 'https://github.com/djc/couchdb-python/', classifiers = [ - 'Development Status :: 4 - Beta', + 'Development Status :: 6 - Mature', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', From 04e0eeff07f83ba3c379bf8fc39feeb04414ced1 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Sat, 12 Mar 2016 12:41:06 +0100 Subject: [PATCH 09/78] Remove bits of support for Python < 2.6 --- setup.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 6e3deb3e..d7db0e92 100755 --- a/setup.py +++ b/setup.py @@ -16,11 +16,6 @@ has_setuptools = False -requirements = [] -if sys.version_info < (2, 6): - requirements += ['simplejson'] - - # Build setuptools-specific options (if installed). if not has_setuptools: print("WARNING: setuptools/distribute not available. Console scripts will not be installed.") @@ -35,7 +30,7 @@ 'couchdb-replicate = couchdb.tools.replicate:main', ], }, - 'install_requires': requirements, + 'install_requires': [], 'test_suite': 'couchdb.tests.__main__.suite', 'zip_safe': True, } From e71419795a2e175edf57a481bb789f277482d624 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Sat, 12 Mar 2016 12:43:18 +0100 Subject: [PATCH 10/78] Prepare setup.cfg for 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 4f9edb665ee309e5251f7d33ccf093cbfc5891ff Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Sat, 12 Mar 2016 12:45:37 +0100 Subject: [PATCH 11/78] Revert change to setup.cfg --- setup.cfg | 4 ++++ 1 file changed, 4 insertions(+) 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 62e3934d5ab131840878230baff91589439ba3fa Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Sat, 12 Mar 2016 12:46:03 +0100 Subject: [PATCH 12/78] Bump development version number to 1.0.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d7db0e92..ad0bca55 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ setup( name = 'CouchDB', - version = '1.0.1', + version = '1.0.2', description = 'Python library for working with CouchDB', long_description = \ """This is a Python library for CouchDB. It provides a convenient high level From d77cdc0488f77a8951d6ba43cd15c89647bc031a Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Sat, 12 Mar 2016 12:48:13 +0100 Subject: [PATCH 13/78] Update release procedure checklist a bit --- RELEASING.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/RELEASING.rst b/RELEASING.rst index 87ba8337..e6f01c19 100644 --- a/RELEASING.rst +++ b/RELEASING.rst @@ -4,11 +4,10 @@ Release procedure A list of steps to perform when releasing. * Run tests against latest CouchDB release (ideally also trunk) -* Run tests on different Python versions +* Make sure the version number in setup.py is correct * Update ChangeLog and add a release date, then commit -* Merge changes from default to stable -* Edit setup.cfg (in the tag), remove the egg_info section and commit -* Tag the just-committed changeset, then update to the tag +* Edit setup.cfg, remove the egg_info section and commit +* Tag the just-committed changeset * python setup.py bdist_wheel sdist --formats=gztar upload -* Revert the setup.cfg change. -* Update the version number on the branch to 0.10.1 +* Revert the setup.cfg change +* Update the version number in setup.py From 382b86c3c2ce70460a31f62d99468e43a48df00c Mon Sep 17 00:00:00 2001 From: leesper Date: Thu, 10 Mar 2016 11:05:49 +0800 Subject: [PATCH 14/78] add regular user register --- couchdb/client.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/couchdb/client.py b/couchdb/client.py index 7c697260..e39a8793 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -227,6 +227,23 @@ def replicate(self, source, target, **options): status, headers, data = self.resource.post_json('_replicate', data) return data + 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 + :return: (id, rev) tuple of the registered user + :rtype: `tuple` + """ + user_db = self['_users'] + return user_db.save({ + '_id': 'org.couchdb.user:' + name, + 'name': name, + 'password': password, + 'roles': roles or [], + 'type': 'user', + }) + class Database(object): """Representation of a database on a CouchDB server. From 4e2abf94eef08c5ec4547da002c6af9c7c7b05bf Mon Sep 17 00:00:00 2001 From: leesper Date: Wed, 23 Mar 2016 21:21:31 +0100 Subject: [PATCH 15/78] add login method to Server class --- couchdb/client.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/couchdb/client.py b/couchdb/client.py index e39a8793..e627bbca 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -30,6 +30,7 @@ from inspect import getsource from textwrap import dedent import warnings +import sys from couchdb import http, json, util @@ -244,6 +245,28 @@ def add_user(self, name, password, roles=None): 'type': 'user', }) + def login_user(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: (status, token) tuple of the login user + :rtype: `tuple` + """ + data = { + 'name': name, + 'password': password, + } + try: + status, headers, _ = self.resource.post_json('_session', data) + if sys.version_info > (3, ): + cookie = headers._headers[0][1] + else: + cookie = headers.headers[0].split(';')[0] + pos = cookie.find('=') + return status, cookie[pos + 1:] + except http.Unauthorized: + return 401, None + class Database(object): """Representation of a database on a CouchDB server. From 0acaf2c83d15ea2c3fff3f0770722501df9e9f43 Mon Sep 17 00:00:00 2001 From: leesper Date: Wed, 23 Mar 2016 21:22:09 +0100 Subject: [PATCH 16/78] add logout method to Server class --- couchdb/client.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/couchdb/client.py b/couchdb/client.py index e627bbca..a44ee6e0 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -267,6 +267,20 @@ def login_user(self, name, password): except http.Unauthorized: return 401, None + def logout_user(self, token): + """Logout regular user in couch db + :param token: token of login user + :return: True if successfully logout + :rtype: bool + """ + header = { + 'Accept': 'application/json', + 'Cookie': 'AuthSession=' + token, + } + status, _, _ = self.resource.delete_json('_session', headers=header) + return status == 200 + + class Database(object): """Representation of a database on a CouchDB server. From efd89185b68f63db7f3ee60bf9797f6cb694b839 Mon Sep 17 00:00:00 2001 From: leesper Date: Wed, 23 Mar 2016 21:23:02 +0100 Subject: [PATCH 17/78] add verify method to Server class --- couchdb/client.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/couchdb/client.py b/couchdb/client.py index a44ee6e0..23aa299e 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -280,6 +280,40 @@ def logout_user(self, token): status, _, _ = self.resource.delete_json('_session', headers=header) return status == 200 + def verify_user(self, token_or_name, password=None): + """Verify user by token or username/password pairs + :param token_or_name: token or username of login user + :param password: password of login user if given + :return: True if authenticated ok + :rtype: bool + """ + def generate_headers(token=None): + if token is None: + headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + } + else: + headers = { + 'Accept': 'application/json', + 'Cookie': 'AuthSession=' + token, + } + return headers + + try: + if password is None: + header = generate_headers(token_or_name) + status, _, _ = self.resource.get_json('_session', header) + else: + header = generate_headers() + body = { + 'name': token_or_name, + 'password': password, + } + status, _, _ = self.resource.post_json('_session', body, header) + except http.Unauthorized: + return False + return status == 200 class Database(object): From dd7c50593bdf0ae098a55cccc03213f91854652d Mon Sep 17 00:00:00 2001 From: leesper Date: Wed, 23 Mar 2016 21:34:34 +0100 Subject: [PATCH 18/78] add couchdb.Server.remove_user() --- couchdb/client.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/couchdb/client.py b/couchdb/client.py index 23aa299e..23945ad4 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -245,6 +245,19 @@ def add_user(self, name, password, roles=None): 'type': 'user', }) + def remove_user(self, name): + """Remove regular user in authentication database. + :param name: name of regular user, normally user id + :return: True if successfully removed + :rtype: bool + """ + user_db = self['_users'] + doc_id = 'org.couchdb.user:' + name + if doc_id not in user_db: + return False + del user_db[doc_id] + return True + def login_user(self, name, password): """Login regular user in couch db :param name: name of regular user, normally user id From 44dc1f584a3000a21713dcdc506a28074d76e80c Mon Sep 17 00:00:00 2001 From: leesper Date: Wed, 23 Mar 2016 21:35:08 +0100 Subject: [PATCH 19/78] add unit tests for user management things --- couchdb/tests/client.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/couchdb/tests/client.py b/couchdb/tests/client.py index 3792d941..e80f57b5 100644 --- a/couchdb/tests/client.py +++ b/couchdb/tests/client.py @@ -136,6 +136,21 @@ def test_basic_auth(self): dbname = 'couchdb-python/test_basic_auth' self.assertRaises(http.Unauthorized, server.create, dbname) + def test_user_management(self): + url = client.DEFAULT_BASE_URL + if not isinstance(url, util.utype): + url = url.decode('utf-8') + + server = client.Server(url) + try: + server.add_user('foo', 'secret', roles=['hero']) + status, token = server.login_user('foo', 'secret') + self.assertEqual(status, 200) + self.assertTrue(server.verify_user(token)) + self.assertTrue(server.logout_user(token)) + finally: + server.remove_user('foo') + class DatabaseTestCase(testutil.TempDatabaseMixin, unittest.TestCase): From ed75ebdb667f81e57fb07c448149664cfb37625d Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Wed, 23 Mar 2016 22:07:25 +0100 Subject: [PATCH 20/78] Simplify Server.remove_user() method --- couchdb/client.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index 23945ad4..01fcd4c6 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -248,15 +248,10 @@ 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 - :return: True if successfully removed - :rtype: bool """ user_db = self['_users'] doc_id = 'org.couchdb.user:' + name - if doc_id not in user_db: - return False del user_db[doc_id] - return True def login_user(self, name, password): """Login regular user in couch db From 0052ad858f591bfe69e225e6e968f557e02c5086 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Wed, 23 Mar 2016 22:11:34 +0100 Subject: [PATCH 21/78] Rename login_user() and logout_user() to login() and logout() --- couchdb/client.py | 4 ++-- couchdb/tests/client.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index 01fcd4c6..fef95cfc 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -253,7 +253,7 @@ def remove_user(self, name): doc_id = 'org.couchdb.user:' + name del user_db[doc_id] - def login_user(self, name, password): + 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 @@ -275,7 +275,7 @@ def login_user(self, name, password): except http.Unauthorized: return 401, None - def logout_user(self, token): + def logout(self, token): """Logout regular user in couch db :param token: token of login user :return: True if successfully logout diff --git a/couchdb/tests/client.py b/couchdb/tests/client.py index e80f57b5..dc0070f2 100644 --- a/couchdb/tests/client.py +++ b/couchdb/tests/client.py @@ -144,10 +144,10 @@ def test_user_management(self): server = client.Server(url) try: server.add_user('foo', 'secret', roles=['hero']) - status, token = server.login_user('foo', 'secret') + status, token = server.login('foo', 'secret') self.assertEqual(status, 200) self.assertTrue(server.verify_user(token)) - self.assertTrue(server.logout_user(token)) + self.assertTrue(server.logout(token)) finally: server.remove_user('foo') From 2409ad0d2133100368c9a42520457859a89cc063 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Wed, 23 Mar 2016 22:15:58 +0100 Subject: [PATCH 22/78] Only verify tokens, rename verify_user() and simplify accordingly --- couchdb/client.py | 34 +++++++--------------------------- couchdb/tests/client.py | 2 +- 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index fef95cfc..227427de 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -288,37 +288,17 @@ def logout(self, token): status, _, _ = self.resource.delete_json('_session', headers=header) return status == 200 - def verify_user(self, token_or_name, password=None): - """Verify user by token or username/password pairs - :param token_or_name: token or username of login user - :param password: password of login user if given + def verify_token(self, token): + """Verify user token + :param token: authentication token :return: True if authenticated ok :rtype: bool """ - def generate_headers(token=None): - if token is None: - headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - } - else: - headers = { - 'Accept': 'application/json', - 'Cookie': 'AuthSession=' + token, - } - return headers - try: - if password is None: - header = generate_headers(token_or_name) - status, _, _ = self.resource.get_json('_session', header) - else: - header = generate_headers() - body = { - 'name': token_or_name, - 'password': password, - } - status, _, _ = self.resource.post_json('_session', body, header) + status, _, _ = self.resource.get_json('_session', { + 'Accept': 'application/json', + 'Cookie': 'AuthSession=' + token, + }) except http.Unauthorized: return False return status == 200 diff --git a/couchdb/tests/client.py b/couchdb/tests/client.py index dc0070f2..12bd57f3 100644 --- a/couchdb/tests/client.py +++ b/couchdb/tests/client.py @@ -146,7 +146,7 @@ def test_user_management(self): server.add_user('foo', 'secret', roles=['hero']) status, token = server.login('foo', 'secret') self.assertEqual(status, 200) - self.assertTrue(server.verify_user(token)) + self.assertTrue(server.verify_token(token)) self.assertTrue(server.logout(token)) finally: server.remove_user('foo') From d4a52343f515f80c3b286f388ec99b729e81b652 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Wed, 23 Mar 2016 22:18:28 +0100 Subject: [PATCH 23/78] Simplify login() method, don't catch Exceptions --- couchdb/client.py | 23 +++++++++-------------- couchdb/tests/client.py | 3 +-- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index 227427de..9e8f4f06 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -257,23 +257,18 @@ 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: (status, token) tuple of the login user - :rtype: `tuple` + :return: authentication token """ - data = { + status, headers, _ = self.resource.post_json('_session', { 'name': name, 'password': password, - } - try: - status, headers, _ = self.resource.post_json('_session', data) - if sys.version_info > (3, ): - cookie = headers._headers[0][1] - else: - cookie = headers.headers[0].split(';')[0] - pos = cookie.find('=') - return status, cookie[pos + 1:] - except http.Unauthorized: - return 401, None + }) + if sys.version_info[0] > 2: + cookie = headers._headers[0][1] + else: + cookie = headers.headers[0].split(';')[0] + pos = cookie.find('=') + return cookie[pos + 1:] def logout(self, token): """Logout regular user in couch db diff --git a/couchdb/tests/client.py b/couchdb/tests/client.py index 12bd57f3..5aec3939 100644 --- a/couchdb/tests/client.py +++ b/couchdb/tests/client.py @@ -144,8 +144,7 @@ def test_user_management(self): server = client.Server(url) try: server.add_user('foo', 'secret', roles=['hero']) - status, token = server.login('foo', 'secret') - self.assertEqual(status, 200) + token = server.login('foo', 'secret') self.assertTrue(server.verify_token(token)) self.assertTrue(server.logout(token)) finally: From 687cf12ac29bbb015ad1aae72ed569aaf8ad81c3 Mon Sep 17 00:00:00 2001 From: Rob Moore Date: Fri, 8 Apr 2016 14:03:03 +0100 Subject: [PATCH 24/78] Make Client.update_doc documentation clearer I was a little thrown off by the documentation here, as there didn't seem to be a way to provide my own request body to the request when calling an update handler; the documentation only indicates that query string params can be given as extra arguments. A little digging into the code shows that options are just passed to the post/put methods intact, so params of those methods can indeed be provided, including path (probably not recommended!), body, and headers. I've updated the documentation accordingly. --- couchdb/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/couchdb/client.py b/couchdb/client.py index 9e8f4f06..e81df012 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -1006,7 +1006,10 @@ def update_doc(self, name, docid=None, **options): :param name: the name of the update handler function in the format ``designdoc/updatename``. :param docid: optional ID of a document to pass to the update handler. - :param options: optional query string parameters. + :param options: additional (optional) params to pass to the underlying + http resource handler, including ``headers``, ``body``, + and ```path```. Other arguments will be treated as + query string params. See :class:`couchdb.http.Resource` :return: (headers, body) tuple, where headers is a dict of headers returned from the list function and body is a readable file-like instance From 9141667c4d6a11df4bc4aad58bbbaf57d66d3bc7 Mon Sep 17 00:00:00 2001 From: Ruben Di Battista Date: Wed, 27 Apr 2016 00:57:50 +0200 Subject: [PATCH 25/78] Add python3.5 environment to tox.ini Refers to issue #280. The test with python35 worked ok. --- tox.ini | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 33406f6b..ae08bf98 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26, py27, py33, py34, py33-json, py34-json +envlist = py26, py27, py33, py34, py33-json, py34-json, py35-json [testenv] deps = simplejson @@ -19,3 +19,7 @@ deps = [testenv:py34-json] basepython = python3.4 deps = + +[testenv:py35-json] +basepython = python3.5 +deps = From f0230e6cb4a0b82b047d104e9d042ba4ce659ddc Mon Sep 17 00:00:00 2001 From: Ruben Di Battista Date: Wed, 27 Apr 2016 00:27:42 +0200 Subject: [PATCH 26/78] Fix to issue #278 Added an if statement in the _wrap_row method of couchdb.mapping.Document to ensure the correct handling of the revision '_rev' key inside the _data dictionary the class uses to wrap data from the database Added the test_wrapped_view unittest related to this change --- couchdb/mapping.py | 2 ++ couchdb/tests/mapping.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/couchdb/mapping.py b/couchdb/mapping.py index bedddc13..ef90d2db 100644 --- a/couchdb/mapping.py +++ b/couchdb/mapping.py @@ -406,6 +406,8 @@ def _wrap_row(cls, row): return cls.wrap(doc) data = row['value'] data['_id'] = row['id'] + if 'rev' in data: # When data is client.Document + data['_rev'] = data['rev'] return cls.wrap(data) diff --git a/couchdb/tests/mapping.py b/couchdb/tests/mapping.py index 94387cb3..b42ab097 100644 --- a/couchdb/tests/mapping.py +++ b/couchdb/tests/mapping.py @@ -242,6 +242,12 @@ def test_view(self): include_docs=True) self.assertEqual(type(results.rows[0]), self.Item) + def test_wrapped_view(self): + self.Item().store(self.db) + results = self.db.view('_all_docs', wrapper=self.Item._wrap_row) + doc = results.rows[0] + self.db.delete(doc) + def test_query(self): self.Item().store(self.db) results = self.Item.query(self.db, all_map_func, None) From 06bec55085168935dfac1055b5d09bd1b794a049 Mon Sep 17 00:00:00 2001 From: Daniel Holth Date: Wed, 4 May 2016 14:46:04 -0400 Subject: [PATCH 27/78] add design doc from filesystem loader --- couchdb/loader.py | 115 ++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 1 + 2 files changed, 116 insertions(+) create mode 100755 couchdb/loader.py diff --git a/couchdb/loader.py b/couchdb/loader.py new file mode 100755 index 00000000..ae3a12d2 --- /dev/null +++ b/couchdb/loader.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +""" +Load design documents from the filesystem into a dict. +Subset of couchdbkit/couchapp functionality. + +Description +----------- + +Convert a target directory into an object (dict). + +Each filename (without extension) or subdirectory name is a key in this object. + +For files, the utf-8-decoded contents are the value, except for .json files +which are first decoded as json. + +Subdirectories are converted into objects using the same procedure and then +added to the parent object. + +Typically used for design documents. This directory tree:: + + . + ├── filters + │   └── forms_only.js + ├── _id + ├── language + ├── lib + │   └── validate.js + └── views + ├── view_a + │   └── map.js + ├── view_b + │   └── map.js + └── view_c + └── map.js + +Becomes this object:: + + { + "views": { + "view_a": { + "map": "function(doc) { ... }" + }, + "view_b": { + "map": "function(doc) { ... }" + }, + "view_c": { + "map": "function(doc) { ... }" + } + }, + "_id": "_design/name_of_design_document", + "filters": { + "forms_only": "function(doc, req) { ... }" + }, + "language": "javascript", + "lib": { + "validate": "// A library for validations ..." + } + } + +""" + +from __future__ import unicode_literals, absolute_import + +import os.path +import pprint +import codecs +import json + +def load_design_doc(directory, strip_files=False): + """ + Load a design document from the filesystem. + + strip_files: remove leading and trailing whitespace from file contents, + like couchdbkit. + """ + objects = {} + + for (dirpath, dirnames, filenames) in os.walk(directory, topdown=False): + key = os.path.split(dirpath)[-1] + ob = {} + objects[dirpath] = (key, ob) + + for name in filenames: + fkey = os.path.splitext(name)[0] + fullname = os.path.join(dirpath, name) + with codecs.open(fullname, 'r', 'utf-8') as f: + contents = f.read() + if name.endswith('.json'): + contents = json.loads(contents) + elif strip_files: + contents = contents.strip() + ob[fkey] = contents + + for name in dirnames: + if name == '_attachments': + raise NotImplementedError() + subkey, subthing = objects[os.path.join(dirpath, name)] + ob[subkey] = subthing + + return ob + + +def main(): + import sys + try: + directory = sys.argv[1] + except IndexError: + sys.stderr.write("Usage:\n\t{} [directory]\n".format(sys.argv[0])) + sys.exit(1) + obj = load_design_doc(directory) + sys.stdout.write(json.dumps(obj, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index ad0bca55..7eaf0f0c 100755 --- a/setup.py +++ b/setup.py @@ -28,6 +28,7 @@ 'couchdb-dump = couchdb.tools.dump:main', 'couchdb-load = couchdb.tools.load:main', 'couchdb-replicate = couchdb.tools.replicate:main', + 'couchdb-load-design-doc = couchdb.loader:main', ], }, 'install_requires': [], From 3ac9366d0e80acc7330d6ba40fe9a99588b6c630 Mon Sep 17 00:00:00 2001 From: Daniel Holth Date: Wed, 4 May 2016 20:05:26 +0000 Subject: [PATCH 28/78] add rudimentary test for loader --- couchdb/loader.py | 1 + couchdb/tests/_loader/_id | 1 + couchdb/tests/_loader/filters/filter.js | 1 + couchdb/tests/_loader/language | 1 + couchdb/tests/_loader/views/a/map.js | 3 +++ couchdb/tests/loader.py | 36 +++++++++++++++++++++++++ 6 files changed, 43 insertions(+) create mode 100644 couchdb/tests/_loader/_id create mode 100644 couchdb/tests/_loader/filters/filter.js create mode 100644 couchdb/tests/_loader/language create mode 100644 couchdb/tests/_loader/views/a/map.js create mode 100644 couchdb/tests/loader.py diff --git a/couchdb/loader.py b/couchdb/loader.py index ae3a12d2..4ab98296 100755 --- a/couchdb/loader.py +++ b/couchdb/loader.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- """ Load design documents from the filesystem into a dict. Subset of couchdbkit/couchapp functionality. diff --git a/couchdb/tests/_loader/_id b/couchdb/tests/_loader/_id new file mode 100644 index 00000000..4e85db7d --- /dev/null +++ b/couchdb/tests/_loader/_id @@ -0,0 +1 @@ +_design/loader diff --git a/couchdb/tests/_loader/filters/filter.js b/couchdb/tests/_loader/filters/filter.js new file mode 100644 index 00000000..d10461f1 --- /dev/null +++ b/couchdb/tests/_loader/filters/filter.js @@ -0,0 +1 @@ +function(doc, req) { return true; } diff --git a/couchdb/tests/_loader/language b/couchdb/tests/_loader/language new file mode 100644 index 00000000..a059bb79 --- /dev/null +++ b/couchdb/tests/_loader/language @@ -0,0 +1 @@ +javascript diff --git a/couchdb/tests/_loader/views/a/map.js b/couchdb/tests/_loader/views/a/map.js new file mode 100644 index 00000000..2c7a5409 --- /dev/null +++ b/couchdb/tests/_loader/views/a/map.js @@ -0,0 +1,3 @@ +function(doc) { + emit(doc.property_to_index); +} diff --git a/couchdb/tests/loader.py b/couchdb/tests/loader.py new file mode 100644 index 00000000..c33e966c --- /dev/null +++ b/couchdb/tests/loader.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2016 Daniel Holth +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. + +import unittest +import os.path + +from couchdb import loader +from couchdb.tests import testutil + +expected = { + '_id': u'_design/loader', + 'filters': {'filter': u'function(doc, req) { return true; }'}, + 'language': u'javascript', + 'views': {'a': {'map': u'function(doc) {\n emit(doc.property_to_index);\n}'}}} + +class LoaderTestCase(unittest.TestCase): + + def test_loader(self): + directory = os.path.join(os.path.dirname(__file__), '_loader') + doc = loader.load_design_doc(directory, strip_files=True) + self.assertEqual(doc, expected) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(LoaderTestCase)) + return suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') From 9324ae415c4967aa534f82fd066cc10764a9ecae Mon Sep 17 00:00:00 2001 From: Daniel Holth Date: Thu, 5 May 2016 22:19:41 +0200 Subject: [PATCH 29/78] Fix formatting for Python 2.6 --- couchdb/loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchdb/loader.py b/couchdb/loader.py index 4ab98296..88cfa8d1 100755 --- a/couchdb/loader.py +++ b/couchdb/loader.py @@ -106,7 +106,7 @@ def main(): try: directory = sys.argv[1] except IndexError: - sys.stderr.write("Usage:\n\t{} [directory]\n".format(sys.argv[0])) + sys.stderr.write("Usage:\n\t{0} [directory]\n".format(sys.argv[0])) sys.exit(1) obj = load_design_doc(directory) sys.stdout.write(json.dumps(obj, indent=2)) From 1d3bd1fe50806180c8fb6889b1bed28f602608d6 Mon Sep 17 00:00:00 2001 From: Daniel Holth Date: Thu, 5 May 2016 21:58:25 +0200 Subject: [PATCH 30/78] Include loader tests in test suite --- couchdb/tests/__main__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/couchdb/tests/__main__.py b/couchdb/tests/__main__.py index c3d9d9a0..ebc8d257 100644 --- a/couchdb/tests/__main__.py +++ b/couchdb/tests/__main__.py @@ -9,7 +9,8 @@ import unittest from couchdb.tests import client, couch_tests, design, couchhttp, \ - multipart, mapping, view, package, tools + multipart, mapping, view, package, tools, \ + loader def suite(): @@ -23,6 +24,7 @@ def suite(): suite.addTest(couch_tests.suite()) suite.addTest(package.suite()) suite.addTest(tools.suite()) + suite.addTest(loader.suite()) return suite From 282b8e333715b3678b5e2c59d0ece91bdae78cad Mon Sep 17 00:00:00 2001 From: Daniel Holth Date: Thu, 5 May 2016 21:58:50 +0200 Subject: [PATCH 31/78] Rename loader strip_files argument to strip --- couchdb/loader.py | 6 +++--- couchdb/tests/loader.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/couchdb/loader.py b/couchdb/loader.py index 88cfa8d1..735912dd 100755 --- a/couchdb/loader.py +++ b/couchdb/loader.py @@ -67,11 +67,11 @@ import codecs import json -def load_design_doc(directory, strip_files=False): +def load_design_doc(directory, strip=False): """ Load a design document from the filesystem. - strip_files: remove leading and trailing whitespace from file contents, + strip: remove leading and trailing whitespace from file contents, like couchdbkit. """ objects = {} @@ -88,7 +88,7 @@ def load_design_doc(directory, strip_files=False): contents = f.read() if name.endswith('.json'): contents = json.loads(contents) - elif strip_files: + elif strip: contents = contents.strip() ob[fkey] = contents diff --git a/couchdb/tests/loader.py b/couchdb/tests/loader.py index c33e966c..ab7e6e3e 100644 --- a/couchdb/tests/loader.py +++ b/couchdb/tests/loader.py @@ -22,7 +22,7 @@ class LoaderTestCase(unittest.TestCase): def test_loader(self): directory = os.path.join(os.path.dirname(__file__), '_loader') - doc = loader.load_design_doc(directory, strip_files=True) + doc = loader.load_design_doc(directory, strip=True) self.assertEqual(doc, expected) From 6912102dd9729aebf2fbab4dc57ada08d84abe33 Mon Sep 17 00:00:00 2001 From: Daniel Holth Date: Thu, 5 May 2016 21:59:45 +0200 Subject: [PATCH 32/78] Raise error from loader if argument is not a directory --- couchdb/loader.py | 3 +++ couchdb/tests/loader.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/couchdb/loader.py b/couchdb/loader.py index 735912dd..ea8ecaa2 100755 --- a/couchdb/loader.py +++ b/couchdb/loader.py @@ -76,6 +76,9 @@ def load_design_doc(directory, strip=False): """ objects = {} + if not os.path.isdir(directory): + raise OSError("No directory: '{0}'".format(directory)) + for (dirpath, dirnames, filenames) in os.walk(directory, topdown=False): key = os.path.split(dirpath)[-1] ob = {} diff --git a/couchdb/tests/loader.py b/couchdb/tests/loader.py index ab7e6e3e..42e22c52 100644 --- a/couchdb/tests/loader.py +++ b/couchdb/tests/loader.py @@ -25,6 +25,12 @@ def test_loader(self): doc = loader.load_design_doc(directory, strip=True) self.assertEqual(doc, expected) + def test_bad_directory(self): + def bad_directory(): + doc = loader.load_design_doc('directory_does_not_exist') + + self.assertRaises(OSError, bad_directory) + def suite(): suite = unittest.TestSuite() From 5f3b201e48ec71356fc1d433676df13291553442 Mon Sep 17 00:00:00 2001 From: Daniel Holth Date: Thu, 5 May 2016 22:00:53 +0200 Subject: [PATCH 33/78] Clarify error about lack of support for _attachments --- couchdb/loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchdb/loader.py b/couchdb/loader.py index ea8ecaa2..67ed9b41 100755 --- a/couchdb/loader.py +++ b/couchdb/loader.py @@ -97,7 +97,7 @@ def load_design_doc(directory, strip=False): for name in dirnames: if name == '_attachments': - raise NotImplementedError() + raise NotImplementedError("_attachments are not supported") subkey, subthing = objects[os.path.join(dirpath, name)] ob[subkey] = subthing From 835c5f1872e45f744807290886b5fb5464225a10 Mon Sep 17 00:00:00 2001 From: Daniel Holth Date: Thu, 5 May 2016 21:59:14 +0200 Subject: [PATCH 34/78] Add predicate argument to loader function --- couchdb/loader.py | 11 +++++++++-- couchdb/tests/loader.py | 5 ++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/couchdb/loader.py b/couchdb/loader.py index 67ed9b41..4d5a8670 100755 --- a/couchdb/loader.py +++ b/couchdb/loader.py @@ -67,12 +67,16 @@ import codecs import json -def load_design_doc(directory, strip=False): +def load_design_doc(directory, strip=False, predicate=lambda x: True): """ Load a design document from the filesystem. strip: remove leading and trailing whitespace from file contents, like couchdbkit. + + predicate: function that is passed the full path to each file or directory. + Each entry is only added to the document if predicate returns True. + Can be used to ignore backup files etc. """ objects = {} @@ -87,6 +91,7 @@ def load_design_doc(directory, strip=False): for name in filenames: fkey = os.path.splitext(name)[0] fullname = os.path.join(dirpath, name) + if not predicate(fullname): continue with codecs.open(fullname, 'r', 'utf-8') as f: contents = f.read() if name.endswith('.json'): @@ -98,7 +103,9 @@ def load_design_doc(directory, strip=False): for name in dirnames: if name == '_attachments': raise NotImplementedError("_attachments are not supported") - subkey, subthing = objects[os.path.join(dirpath, name)] + fullpath = os.path.join(dirpath, name) + if not predicate(fullpath): continue + subkey, subthing = objects[fullpath] ob[subkey] = subthing return ob diff --git a/couchdb/tests/loader.py b/couchdb/tests/loader.py index 42e22c52..ae5d3904 100644 --- a/couchdb/tests/loader.py +++ b/couchdb/tests/loader.py @@ -22,7 +22,10 @@ class LoaderTestCase(unittest.TestCase): def test_loader(self): directory = os.path.join(os.path.dirname(__file__), '_loader') - doc = loader.load_design_doc(directory, strip=True) + doc = loader.load_design_doc(directory, + strip=True, + predicate=lambda x: \ + not x.endswith('.xml')) self.assertEqual(doc, expected) def test_bad_directory(self): From eaeaa890dc10c53185541a15818e0a634e196f70 Mon Sep 17 00:00:00 2001 From: Daniel Holth Date: Thu, 5 May 2016 22:09:08 +0200 Subject: [PATCH 35/78] Raise error from loader in case of duplicate keys --- couchdb/loader.py | 9 +++++++++ couchdb/tests/loader.py | 23 +++++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/couchdb/loader.py b/couchdb/loader.py index 4d5a8670..e593da3c 100755 --- a/couchdb/loader.py +++ b/couchdb/loader.py @@ -67,6 +67,9 @@ import codecs import json +class DuplicateKeyError(ValueError): + pass + def load_design_doc(directory, strip=False, predicate=lambda x: True): """ Load a design document from the filesystem. @@ -92,6 +95,9 @@ def load_design_doc(directory, strip=False, predicate=lambda x: True): fkey = os.path.splitext(name)[0] fullname = os.path.join(dirpath, name) if not predicate(fullname): continue + if fkey in ob: + raise DuplicateKeyError("file '{0}' clobbers key '{1}'" + .format(fullname, fkey)) with codecs.open(fullname, 'r', 'utf-8') as f: contents = f.read() if name.endswith('.json'): @@ -106,6 +112,9 @@ def load_design_doc(directory, strip=False, predicate=lambda x: True): fullpath = os.path.join(dirpath, name) if not predicate(fullpath): continue subkey, subthing = objects[fullpath] + if subkey in ob: + raise DuplicateKeyError("directory '{0}' clobbers key '{1}'" + .format(fullpath,subkey)) ob[subkey] = subthing return ob diff --git a/couchdb/tests/loader.py b/couchdb/tests/loader.py index ae5d3904..f6eb7f16 100644 --- a/couchdb/tests/loader.py +++ b/couchdb/tests/loader.py @@ -20,9 +20,10 @@ class LoaderTestCase(unittest.TestCase): + directory = os.path.join(os.path.dirname(__file__), '_loader') + def test_loader(self): - directory = os.path.join(os.path.dirname(__file__), '_loader') - doc = loader.load_design_doc(directory, + doc = loader.load_design_doc(self.directory, strip=True, predicate=lambda x: \ not x.endswith('.xml')) @@ -34,6 +35,24 @@ def bad_directory(): self.assertRaises(OSError, bad_directory) + def test_clobber_1(self): + def clobber(): + doc = loader.load_design_doc(self.directory, + strip=True, + predicate=lambda x: \ + not x.endswith('filters.xml')) + + self.assertRaises(loader.DuplicateKeyError, clobber) + + def test_clobber_2(self): + def clobber(): + doc = loader.load_design_doc(self.directory, + strip=True, + predicate=lambda x: \ + not x.endswith('language.xml')) + + self.assertRaises(loader.DuplicateKeyError, clobber) + def suite(): suite = unittest.TestSuite() From 30b9890d0ccd747adfbe8efbeed2b903e82a6902 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Thu, 5 May 2016 22:33:39 +0200 Subject: [PATCH 36/78] Add test data files --- couchdb/tests/_loader/filters.xml | 1 + couchdb/tests/_loader/language.xml | 1 + 2 files changed, 2 insertions(+) create mode 100644 couchdb/tests/_loader/filters.xml create mode 100644 couchdb/tests/_loader/language.xml diff --git a/couchdb/tests/_loader/filters.xml b/couchdb/tests/_loader/filters.xml new file mode 100644 index 00000000..22694cbd --- /dev/null +++ b/couchdb/tests/_loader/filters.xml @@ -0,0 +1 @@ +

Assert clobber of 'filters' directory

\ No newline at end of file diff --git a/couchdb/tests/_loader/language.xml b/couchdb/tests/_loader/language.xml new file mode 100644 index 00000000..737b39dd --- /dev/null +++ b/couchdb/tests/_loader/language.xml @@ -0,0 +1 @@ +

Assert clobber of 'language' (without an extension)

From b4296412f2c21e93f5c0c97f3a7f68e81cee0d0f Mon Sep 17 00:00:00 2001 From: hermitdemschoenenleben Date: Sun, 15 May 2016 19:19:14 +0200 Subject: [PATCH 37/78] Expand relative URLs from Location headers (fixes #287) --- couchdb/http.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/couchdb/http.py b/couchdb/http.py index 51929036..44e1a31b 100644 --- a/couchdb/http.py +++ b/couchdb/http.py @@ -349,6 +349,14 @@ def _try_request(): if num_redirects > self.max_redirects: raise RedirectLimit('Redirection limit exceeded') location = resp.getheader('location') + + # in case of relative location: add scheme and host to the location + location_split = util.urlsplit(location) + + if not location_split[0]: + orig_url_split = util.urlsplit(url) + location = util.urlunsplit(orig_url_split[:2] + location_split[2:]) + if status == 301: self.perm_redirects[url] = location elif status == 303: From 38642032a5f5c78968cbd46ce8214d9acecb9055 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Sun, 5 Jun 2016 19:23:19 +0200 Subject: [PATCH 38/78] Generate docs before uploading them --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index eba2386f..bcd4e08c 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ test3: doc: python setup.py build_sphinx -upload-doc: +upload-doc: doc python setup.py upload_sphinx coverage: From 411ef7b28fa6490190df20dd5c8b50e18d230841 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Sun, 5 Jun 2016 19:23:33 +0200 Subject: [PATCH 39/78] Add note about uploading docs to RELEASING.rst --- RELEASING.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASING.rst b/RELEASING.rst index e6f01c19..318b7a50 100644 --- a/RELEASING.rst +++ b/RELEASING.rst @@ -11,3 +11,4 @@ A list of steps to perform when releasing. * python setup.py bdist_wheel sdist --formats=gztar upload * Revert the setup.cfg change * Update the version number in setup.py +* Upload docs to PyPI with ``make upload-doc`` From 65da2106dafed1831ee6a57c259b994543b99dcc Mon Sep 17 00:00:00 2001 From: Ruben Di Battista Date: Thu, 9 Jun 2016 22:09:07 +0200 Subject: [PATCH 40/78] Add microseconds support for DateTimeField (#293) * Add microseconds support for DateTimeField A check is introduced in the DateTimeField to understand if microseconds are provided in the datetime string or not. The return value is then handled in accordance to that check. Tests related to the feature implemented are also introduced. Doctest examples are updated too. * Add microseconds support for DateTimeField A check is introduced in the DateTimeField to understand if microseconds are provided in the datetime string or not. The return value is then handled in accordance to that check. Tests related to the feature implemented are also introduced. Doctest examples are updated too. --- couchdb/mapping.py | 24 +++++++++++++++++------- couchdb/tests/mapping.py | 10 ++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/couchdb/mapping.py b/couchdb/mapping.py index ef90d2db..626aaedc 100644 --- a/couchdb/mapping.py +++ b/couchdb/mapping.py @@ -474,12 +474,16 @@ def _to_json(self, value): class DateTimeField(Field): """Mapping field for storing date/time values. - + >>> field = DateTimeField() >>> field._to_python('2007-04-01T15:30:00Z') datetime.datetime(2007, 4, 1, 15, 30) - >>> field._to_json(datetime(2007, 4, 1, 15, 30, 0, 9876)) + >>> field._to_python('2007-04-01T15:30:00.009876Z') + datetime.datetime(2007, 4, 1, 15, 30, 0, 9876) + >>> field._to_json(datetime(2007, 4, 1, 15, 30, 0)) '2007-04-01T15:30:00Z' + >>> field._to_json(datetime(2007, 4, 1, 15, 30, 0, 9876)) + '2007-04-01T15:30:00.009876Z' >>> field._to_json(date(2007, 4, 1)) '2007-04-01T00:00:00Z' """ @@ -487,9 +491,15 @@ class DateTimeField(Field): def _to_python(self, value): if isinstance(value, util.strbase): try: - value = value.split('.', 1)[0] # strip out microseconds - value = value.rstrip('Z') # remove timezone separator - value = datetime(*strptime(value, '%Y-%m-%dT%H:%M:%S')[:6]) + split_value = value.split('.') # strip out microseconds + if len(split_value) == 1: # No microseconds provided + value = split_value[0] + value = value.rstrip('Z') #remove timezone separator + value = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S') + else: + value = value.rstrip('Z') + value = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f') + except ValueError: raise ValueError('Invalid ISO date/time %r' % value) return value @@ -499,12 +509,12 @@ def _to_json(self, value): value = datetime.utcfromtimestamp(timegm(value)) elif not isinstance(value, datetime): value = datetime.combine(value, time(0)) - return value.replace(microsecond=0).isoformat() + 'Z' + return value.isoformat() + 'Z' class TimeField(Field): """Mapping field for storing times. - + >>> field = TimeField() >>> field._to_python('15:30:00') datetime.time(15, 30) diff --git a/couchdb/tests/mapping.py b/couchdb/tests/mapping.py index b42ab097..7f49b690 100644 --- a/couchdb/tests/mapping.py +++ b/couchdb/tests/mapping.py @@ -11,6 +11,7 @@ from couchdb import design, mapping from couchdb.tests import testutil +from datetime import datetime class DocumentTestCase(testutil.TempDatabaseMixin, unittest.TestCase): @@ -83,6 +84,15 @@ def test_old_datetime(self): dt = mapping.DateTimeField() assert dt._to_python('1880-01-01T00:00:00Z') + def test_datetime_with_microseconds(self): + dt = mapping.DateTimeField() + assert dt._to_python('2016-06-09T21:21:49.739248Z') + + def test_datetime_to_json(self): + dt = mapping.DateTimeField() + d = datetime.now() + assert dt._to_json(d) + def test_get_has_default(self): doc = mapping.Document() doc.get('foo') From 026d0d1dd2476582020dca87b37fe34af4e0c410 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Sun, 31 Jul 2016 12:07:33 +0200 Subject: [PATCH 41/78] Support Python 3 in couchdb-dump script (fixes #296) --- couchdb/tools/dump.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/couchdb/tools/dump.py b/couchdb/tools/dump.py index c69a30d0..9ed78d2e 100755 --- a/couchdb/tools/dump.py +++ b/couchdb/tools/dump.py @@ -11,6 +11,7 @@ file. """ +from __future__ import print_function from base64 import b64decode from optparse import OptionParser import sys @@ -25,7 +26,7 @@ def dump_docs(envelope, db, docs): for doc in docs: - print >> sys.stderr, 'Dumping document %r' % doc.id + print('Dumping document %r' % doc.id, file=sys.stderr) attachments = doc.pop('_attachments', {}) jsondoc = json.encode(doc) @@ -57,7 +58,10 @@ def dump_docs(envelope, db, docs): }) def dump_db(dburl, username=None, password=None, boundary=None, - output=sys.stdout, bulk_size=BULK_SIZE): + output=None, bulk_size=BULK_SIZE): + + if output is None: + output = sys.stdout if sys.version_info[0] < 3 else sys.stdout.buffer db = Database(dburl) if username is not None and password is not None: From 4971ccc501917b62290383a8f3fe7faaaf62b9d3 Mon Sep 17 00:00:00 2001 From: Steve Phillips Date: Wed, 3 Aug 2016 23:09:04 -0700 Subject: [PATCH 42/78] client.py docs typo: identity -> identify (#298) --- couchdb/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchdb/client.py b/couchdb/client.py index e81df012..3c8f6e17 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -481,7 +481,7 @@ def save(self, doc, **options): If doc has no _id then the server will allocate a random ID and a new document will be created. Otherwise the doc's _id will be used to - identity the document to create or update. Trying to update an existing + identify the document to create or update. Trying to update an existing document with an incorrect _rev will raise a ResourceConflict exception. Note that it is generally better to avoid saving documents with no _id From ae055cec3c126188144e70f9e55374890af3f3f7 Mon Sep 17 00:00:00 2001 From: Benjamin Wiegand Date: Fri, 5 Aug 2016 14:10:03 +0200 Subject: [PATCH 43/78] fix changes feed for couchbase sync gateway --- couchdb/http.py | 17 +++++++++++++++-- couchdb/tests/couchhttp.py | 4 ++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/couchdb/http.py b/couchdb/http.py index 44e1a31b..5984ddde 100644 --- a/couchdb/http.py +++ b/couchdb/http.py @@ -168,6 +168,7 @@ def close(self): def iterchunks(self): assert self.chunked + buffer = b'' while True: if self.resp.isclosed(): break @@ -178,8 +179,20 @@ def iterchunks(self): self._release_conn() break chunk = self.resp.fp.read(chunksz) - for ln in chunk.splitlines(): - yield ln + + for ln in chunk.splitlines(True): + end = (ln == b'\n') and not buffer # end of response + + if ln and not end: + if ln.endswith(b'\n'): + # end of a document + yield buffer + ln + buffer = b'' + else: + # a break inside a document --> add to buffer and reuse + # later + buffer += ln + self.resp.fp.read(2) #crlf diff --git a/couchdb/tests/couchhttp.py b/couchdb/tests/couchhttp.py index 41c42f4e..8646cf58 100644 --- a/couchdb/tests/couchhttp.py +++ b/couchdb/tests/couchhttp.py @@ -64,11 +64,11 @@ def close(self): def isclosed(self): return len(self.fp.getvalue()) == self.fp.tell() - data = b'foobarbaz' + data = b'foobarbaz\n' data = b'\n'.join([hex(len(data))[2:].encode('utf-8'), data]) response = http.ResponseBody(TestHttpResp(util.StringIO(data)), None, None, None) - self.assertEqual(list(response.iterchunks()), [b'foobarbaz']) + self.assertEqual(list(response.iterchunks()), [b'foobarbaz\n']) self.assertEqual(list(response.iterchunks()), []) From c88ba9e1aa3c35ccf925de0de30a3fffd765c0ff Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 5 Aug 2016 14:24:50 +0200 Subject: [PATCH 44/78] Simplify code to reduce indentation levels --- couchdb/http.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/couchdb/http.py b/couchdb/http.py index 5984ddde..a70ef0c5 100644 --- a/couchdb/http.py +++ b/couchdb/http.py @@ -179,19 +179,15 @@ def iterchunks(self): self._release_conn() break chunk = self.resp.fp.read(chunksz) - for ln in chunk.splitlines(True): end = (ln == b'\n') and not buffer # end of response - if ln and not end: - if ln.endswith(b'\n'): - # end of a document - yield buffer + ln - buffer = b'' - else: - # a break inside a document --> add to buffer and reuse - # later - buffer += ln + if not ln or end: + break + buffer += ln + if ln.endswith(b'\n'): + yield buffer + buffer = b'' self.resp.fp.read(2) #crlf From a5cd8a21df3368faee39465c05822fd821c2ce88 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 5 Aug 2016 14:27:21 +0200 Subject: [PATCH 45/78] Replace string buffer with list --- couchdb/http.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/couchdb/http.py b/couchdb/http.py index a70ef0c5..60c6b17d 100644 --- a/couchdb/http.py +++ b/couchdb/http.py @@ -168,7 +168,7 @@ def close(self): def iterchunks(self): assert self.chunked - buffer = b'' + buffer = [] while True: if self.resp.isclosed(): break @@ -184,10 +184,10 @@ def iterchunks(self): if not ln or end: break - buffer += ln + buffer.append(ln) if ln.endswith(b'\n'): - yield buffer - buffer = b'' + yield b''.join(buffer) + buffer = [] self.resp.fp.read(2) #crlf From 88fcad9a77c732a86b1e35d66af32953744d555e Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 5 Aug 2016 14:28:11 +0200 Subject: [PATCH 46/78] Reformat iterchunks() method for easier readability --- couchdb/http.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/couchdb/http.py b/couchdb/http.py index 60c6b17d..1f6806c0 100644 --- a/couchdb/http.py +++ b/couchdb/http.py @@ -170,20 +170,24 @@ def iterchunks(self): assert self.chunked buffer = [] while True: + if self.resp.isclosed(): break + chunksz = int(self.resp.fp.readline().strip(), 16) if not chunksz: self.resp.fp.read(2) #crlf self.resp.close() self._release_conn() break + chunk = self.resp.fp.read(chunksz) for ln in chunk.splitlines(True): - end = (ln == b'\n') and not buffer # end of response + end = ln == b'\n' and not buffer # end of response if not ln or end: break + buffer.append(ln) if ln.endswith(b'\n'): yield b''.join(buffer) From 87808bcab899d26ddd1573ba57b8da6a8e379283 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 5 Aug 2016 14:41:19 +0200 Subject: [PATCH 47/78] Bumping version to 1.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7eaf0f0c..61464382 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ setup( name = 'CouchDB', - version = '1.0.2', + version = '1.1', description = 'Python library for working with CouchDB', long_description = \ """This is a Python library for CouchDB. It provides a convenient high level From 1a4e4b777caf97e690192293993aa74c9e141993 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 5 Aug 2016 14:41:40 +0200 Subject: [PATCH 48/78] Add changelog for version 1.1 --- ChangeLog.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ChangeLog.rst b/ChangeLog.rst index 9d97e5b5..8fa6edb9 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,3 +1,15 @@ +Version 1.1 (2016-08-05) +------------------------ + +* Add script to load design documents from disk +* Add methods on ``Server`` for user/session management +* Add microseconds support for DateTimeFields +* Handle changes feed as emitted by CouchBase (fixes #289) +* Support Python 3 in ``couchdb-dump`` script (fixes #296) +* Expand relative URLs from Location headers (fixes #287) +* Correctly handle ``_rev`` fields in mapped documents (fixes #278) + + Version 1.0.1 (2016-03-12) -------------------------- From 38f48c6d13cce15112f93fa97b4b8b67fb857632 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 5 Aug 2016 14:42:06 +0200 Subject: [PATCH 49/78] Temporarily remove dev build handling --- 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 34a2644194833101fc2934350a208632c05c7641 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 5 Aug 2016 14:44:13 +0200 Subject: [PATCH 50/78] Enable dev build tagging again --- setup.cfg | 4 ++++ 1 file changed, 4 insertions(+) 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 01f7a4ed18423857486b44cd659a8a6508aa8406 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 5 Aug 2016 14:44:31 +0200 Subject: [PATCH 51/78] Bump version for next release --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 61464382..95af723e 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ setup( name = 'CouchDB', - version = '1.1', + version = '1.1.1', description = 'Python library for working with CouchDB', long_description = \ """This is a Python library for CouchDB. It provides a convenient high level From 53b0bb7f7dda0d9a91ae793dd7c24a8575b50c7a Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 5 Aug 2016 14:47:28 +0200 Subject: [PATCH 52/78] Update default theme for the docs --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index dd548cc3..fc57dd40 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -93,7 +93,7 @@ # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'default' +html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the From 370a5b9fc09dfb212a4b38c6cb7cd557a655047a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Wed, 7 Sep 2016 21:18:22 +0200 Subject: [PATCH 53/78] Fix HTTP auth password encoding (#302) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Username/password encoding in HTTP basic auth is currently broken for non-ASCII password. Example with user `user` and password `unusual-char-é`. With curl it works as expected: ```shell curl -v http://user:unusual-char-%C3%A9@localhost:5984/ ``` ``` > GET / HTTP/1.1 > Authorization: Basic YWxpY2U6YWRyaWVuPTohw6k= > < HTTP/1.1 200 OK ``` But with couchdb-python the string is decoded from `utf-8` then re-encoded into `latin1`, causing an incorrect Authorization header: ```python url = 'http://user:unusual-char-%C3%A9@localhost:5984/' couchdb.Server(url).version() ``` ``` > GET / HTTP/1.1 > Authorization: Basic dXNlcjp1bnVzdWFsLWNoYXIt6Q== > < HTTP/1.1 401 Unauthorized ``` This patch fixes this wrong encoding charset by using `utf-8` (used by default by CouchDB) instead of `latin1`. Closes: #301 --- couchdb/http.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/couchdb/http.py b/couchdb/http.py index 1f6806c0..7e6e5095 100644 --- a/couchdb/http.py +++ b/couchdb/http.py @@ -362,7 +362,7 @@ def _try_request(): if num_redirects > self.max_redirects: raise RedirectLimit('Redirection limit exceeded') location = resp.getheader('location') - + # in case of relative location: add scheme and host to the location location_split = util.urlsplit(location) @@ -592,7 +592,7 @@ def _request_json(self, method, path=None, body=None, headers=None, **params): def extract_credentials(url): """Extract authentication (user name and password) credentials from the given URL. - + >>> extract_credentials('http://localhost:5984/_config/') ('http://localhost:5984/_config/', None) >>> extract_credentials('http://joe:secret@localhost:5984/_config/') @@ -620,8 +620,8 @@ def basic_auth(credentials): >>> basic_auth(()) """ if credentials: - token = b64encode(('%s:%s' % credentials).encode('latin1')) - return ('Basic %s' % token.strip().decode('latin1')).encode('ascii') + token = b64encode(('%s:%s' % credentials).encode('utf-8')) + return ('Basic %s' % token.strip().decode('utf-8')).encode('ascii') def quote(string, safe=''): From 61b440947df8fc6bacfd32824ee2686fe96a1aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Tue, 20 Sep 2016 21:38:30 +0200 Subject: [PATCH 54/78] Add missing http.Forbidden error (#305) "401 Unauthorized" and "403 Forbidden" are two different HTTP errors. CouchDB uses the first one when the server requires authentication credentials, and the second one when the request requires an authorisation that the current user does not have. For instance, using a `validate_doc_update` design document can result in "403 Forbidden" being returned. See the official documentation [1] for more details and examples: // user is not authorized to make the change but may re-authenticate throw({ unauthorized: 'Error message here.' }); // change is not allowed throw({ forbidden: 'Error message here.' }); This patch adds Forbidden to the list of HTTP errors raised by couchdb-python. [1]: http://docs.couchdb.org/en/stable/couchapp/ddocs.html#validate-document-update-functions --- couchdb/__init__.py | 3 ++- couchdb/http.py | 12 ++++++++++-- couchdb/tests/package.py | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/couchdb/__init__.py b/couchdb/__init__.py index 06edd826..e7f6bbf0 100644 --- a/couchdb/__init__.py +++ b/couchdb/__init__.py @@ -8,7 +8,8 @@ from .client import Database, Document, Server from .http import HTTPError, PreconditionFailed, Resource, \ - ResourceConflict, ResourceNotFound, ServerError, Session, Unauthorized + ResourceConflict, ResourceNotFound, ServerError, Session, \ + Unauthorized, Forbidden try: __version__ = __import__('pkg_resources').get_distribution('CouchDB').version diff --git a/couchdb/http.py b/couchdb/http.py index 7e6e5095..4a6750e9 100644 --- a/couchdb/http.py +++ b/couchdb/http.py @@ -38,8 +38,8 @@ from couchdb import util __all__ = ['HTTPError', 'PreconditionFailed', 'ResourceNotFound', - 'ResourceConflict', 'ServerError', 'Unauthorized', 'RedirectLimit', - 'Session', 'Resource'] + 'ResourceConflict', 'ServerError', 'Unauthorized', 'Forbidden', + 'RedirectLimit', 'Session', 'Resource'] __docformat__ = 'restructuredtext en' @@ -114,6 +114,12 @@ class Unauthorized(HTTPError): """ +class Forbidden(HTTPError): + """Exception raised when the request requires an authorisation that the + current user does not have. + """ + + class RedirectLimit(Exception): """Exception raised when a request is redirected more often than allowed by the maximum number of redirections. @@ -411,6 +417,8 @@ def _try_request(): error = '' if status == 401: raise Unauthorized(error) + elif status == 403: + raise Forbidden(error) elif status == 404: raise ResourceNotFound(error) elif status == 409: diff --git a/couchdb/tests/package.py b/couchdb/tests/package.py index 862bdca8..33f12f81 100644 --- a/couchdb/tests/package.py +++ b/couchdb/tests/package.py @@ -11,7 +11,7 @@ def test_exports(self): 'Server', 'Database', 'Document', # couchdb.http 'HTTPError', 'PreconditionFailed', 'ResourceNotFound', - 'ResourceConflict', 'ServerError', 'Unauthorized', + 'ResourceConflict', 'ServerError', 'Unauthorized', 'Forbidden', 'Resource', 'Session' ]) exported = set(e for e in dir(couchdb) if not e.startswith('_')) From 8561f559445c2d6b839215d3d81be56b0e785b4f Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Wed, 21 Sep 2016 09:01:53 +0200 Subject: [PATCH 55/78] Don't catch all exceptions in Server methods --- couchdb/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index 3c8f6e17..0e6dc787 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -113,7 +113,7 @@ def __nonzero__(self): try: self.resource.head() return True - except: + except http.ResourceNotFound: return False def __bool__(self): @@ -391,7 +391,7 @@ def __nonzero__(self): try: self.resource.head() return True - except: + except http.ResourceNotFound: return False def __bool__(self): From 3af398f9fb4c41eadd6f8387e248da36a7f677f4 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Sat, 24 Sep 2016 10:17:21 +0200 Subject: [PATCH 56/78] Catch socket errors when testing for Database/Server existence --- couchdb/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/couchdb/client.py b/couchdb/client.py index 0e6dc787..bf1fef3d 100644 --- a/couchdb/client.py +++ b/couchdb/client.py @@ -31,6 +31,7 @@ from textwrap import dedent import warnings import sys +import socket from couchdb import http, json, util @@ -95,7 +96,7 @@ def __contains__(self, name): try: self.resource.head(name) return True - except http.ResourceNotFound: + except (socket.error, http.ResourceNotFound): return False def __iter__(self): @@ -113,7 +114,7 @@ def __nonzero__(self): try: self.resource.head() return True - except http.ResourceNotFound: + except (socket.error, http.ResourceNotFound): return False def __bool__(self): From 31c64f3fe450c40c4c36c95011d6cb702d55e285 Mon Sep 17 00:00:00 2001 From: Marcelo Duarte Date: Wed, 5 Oct 2016 13:22:13 -0300 Subject: [PATCH 57/78] py3 fixes --- couchdb/tools/load.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/couchdb/tools/load.py b/couchdb/tools/load.py index 95f04196..0d5b7866 100755 --- a/couchdb/tools/load.py +++ b/couchdb/tools/load.py @@ -11,6 +11,7 @@ file. """ +from __future__ import print_function from base64 import b64encode from optparse import OptionParser import sys @@ -36,7 +37,7 @@ def load_db(fileobj, dburl, username=None, password=None, ignore_errors=False): doc['_attachments'] = {} else: doc['_attachments'][headers['content-id']] = { - 'data': b64encode(payload), + 'data': b64encode(payload).decode('ascii'), 'content_type': headers['content-type'], 'length': len(payload) } @@ -45,13 +46,13 @@ def load_db(fileobj, dburl, username=None, password=None, ignore_errors=False): doc = json.decode(payload) del doc['_rev'] - print>>sys.stderr, 'Loading document %r' % docid + print('Loading document %r' % docid, file=sys.stderr) try: db[docid] = doc except Exception as e: if not ignore_errors: raise - print>>sys.stderr, 'Error: %s' % e + print('Error: %s' % e, file=sys.stderr) def main(): From 0f4835e849272bc68d9fac4a9a4fab4e4a6f26c2 Mon Sep 17 00:00:00 2001 From: DjMorgul Date: Sat, 3 Dec 2016 14:05:07 +0800 Subject: [PATCH 58/78] 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 59/78] 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 60/78] 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 61/78] 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 62/78] 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 63/78] 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 64/78] 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 65/78] 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 66/78] 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 67/78] 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 68/78] 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 69/78] 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 70/78] 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 71/78] 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 72/78] 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 73/78] 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 74/78] 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 75/78] 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 76/78] 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 77/78] 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 78/78] 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