From f2c12984c325124d72cbdf5154a33b32538da941 Mon Sep 17 00:00:00 2001 From: tretinha Date: Wed, 9 Oct 2019 10:00:24 -0300 Subject: [PATCH 01/52] Fix "DeprecationWarning" from "collections" (#451) DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working. --- src/cloudant/_common_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cloudant/_common_util.py b/src/cloudant/_common_util.py index f7885f49..d47beb56 100644 --- a/src/cloudant/_common_util.py +++ b/src/cloudant/_common_util.py @@ -19,7 +19,7 @@ import sys import platform -from collections import Sequence +from collections.abc import Sequence import json from ._2to3 import LONGTYPE, STRTYPE, NONETYPE, UNITYPE, iteritems_ From fec158e71b9d49fdae74cd1e59e2f74ad9186cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bessenyei=20Bal=C3=A1zs=20Don=C3=A1t?= Date: Tue, 15 Oct 2019 17:39:54 +0200 Subject: [PATCH 02/52] Fix broken Python 2 build (#453) f2c1298 accidentally broke the build as collections.abc is only available in Python 3.x. This commit adds a workaround that enables the import that works both on 2 and 3. --- src/cloudant/_common_util.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/cloudant/_common_util.py b/src/cloudant/_common_util.py index d47beb56..6e765c1d 100644 --- a/src/cloudant/_common_util.py +++ b/src/cloudant/_common_util.py @@ -19,12 +19,16 @@ import sys import platform -from collections.abc import Sequence import json from ._2to3 import LONGTYPE, STRTYPE, NONETYPE, UNITYPE, iteritems_ from .error import CloudantArgumentError, CloudantException, CloudantClientException +try: + from collections.abc import Sequence +except ImportError: + from collections import Sequence + # Library Constants USER_AGENT = '/'.join([ From 026f615ff389bbf99a562becd56df7c8d950189f Mon Sep 17 00:00:00 2001 From: James Mackenzie Date: Tue, 11 Feb 2020 14:13:41 +0000 Subject: [PATCH 03/52] Fixup the DesignDocumentTests::test_get_info and CloudantIndexExceptionTests::test_index_usage_via_query tests for couch3 --- tests/unit/design_document_tests.py | 5 +++-- tests/unit/index_tests.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/unit/design_document_tests.py b/tests/unit/design_document_tests.py index c986e769..35f46714 100644 --- a/tests/unit/design_document_tests.py +++ b/tests/unit/design_document_tests.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (C) 2015, 2018 IBM Corp. All rights reserved. +# Copyright (C) 2015, 2020 IBM Corp. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -776,7 +776,8 @@ def test_get_info(self): info = ddoc_remote.info() # Remove variable fields to make equality easier to check info['view_index'].pop('signature') - info['view_index'].pop('disk_size') + if 'disk_size' in info['view_index']: + info['view_index'].pop('disk_size') # Remove Cloudant/Couch 2 fields if present to allow test to pass on Couch 1.6 if 'sizes' in info['view_index']: info['view_index'].pop('sizes') diff --git a/tests/unit/index_tests.py b/tests/unit/index_tests.py index 59a952d6..4361644f 100644 --- a/tests/unit/index_tests.py +++ b/tests/unit/index_tests.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (C) 2015, 2018 IBM Corp. All rights reserved. +# Copyright (C) 2015, 2020 IBM Corp. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -390,7 +390,7 @@ def test_index_usage_via_query(self): self.populate_db_with_documents(100) result = self.db.get_query_result(fields=['name', 'age'], selector={'age': {'$eq': 6}}, raw_result=True) - self.assertTrue(str(result['warning']).startswith("no matching index found")) + self.assertTrue(str(result['warning']).lower().startswith("no matching index found")) @attr(db='cloudant') class TextIndexTests(UnitTestDbBase): From ae61f6a063f36f2b5cd1ad6341bd10e00c0ce4af Mon Sep 17 00:00:00 2001 From: Rich Ellis Date: Fri, 14 Feb 2020 09:34:39 +0000 Subject: [PATCH 04/52] Trigger search index builds before asserting metadata --- tests/unit/design_document_tests.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/unit/design_document_tests.py b/tests/unit/design_document_tests.py index c986e769..0bd27121 100644 --- a/tests/unit/design_document_tests.py +++ b/tests/unit/design_document_tests.py @@ -827,6 +827,9 @@ def test_get_search_info(self): ddoc_remote = DesignDocument(self.db, '_design/ddoc001') ddoc_remote.fetch() + # Make a request to the search index to ensure it is built + self.db.get_search_result('_design/ddoc001', 'search001', query='name:julia*') + search_info = ddoc_remote.search_info('search001') # Check the search index name self.assertEqual(search_info['name'], '_design/ddoc001/search001', 'The search index name should be correct.') @@ -856,7 +859,8 @@ def test_get_search_disk_size(self): ddoc_remote = DesignDocument(self.db, '_design/ddoc001') ddoc_remote.fetch() - ddoc_remote.search_info('search001') # trigger index build + # Make a request to the search index to ensure it is built + self.db.get_search_result('_design/ddoc001', 'search001', query='name:julia*') search_disk_size = ddoc_remote.search_disk_size('search001') From ab92f5e0b48238e549aa08b2fd3bb9a8d7e75c30 Mon Sep 17 00:00:00 2001 From: Rich Ellis Date: Fri, 14 Feb 2020 11:57:34 +0000 Subject: [PATCH 05/52] Removed data_size from view info assertion The data_size field is no longer present in CouchDB 3.x so remove it from the assertion for compatibility. --- tests/unit/design_document_tests.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/design_document_tests.py b/tests/unit/design_document_tests.py index f710e9e5..7ec3c10e 100644 --- a/tests/unit/design_document_tests.py +++ b/tests/unit/design_document_tests.py @@ -778,6 +778,8 @@ def test_get_info(self): info['view_index'].pop('signature') if 'disk_size' in info['view_index']: info['view_index'].pop('disk_size') + if 'data_size' in info['view_index']: + info['view_index'].pop('data_size') # Remove Cloudant/Couch 2 fields if present to allow test to pass on Couch 1.6 if 'sizes' in info['view_index']: info['view_index'].pop('sizes') @@ -790,8 +792,7 @@ def test_get_info(self): {'view_index': {'update_seq': 0, 'waiting_clients': 0, 'language': 'javascript', 'purge_seq': 0, 'compact_running': False, - 'waiting_commit': False, 'updater_running': False, - 'data_size': 0 + 'waiting_commit': False, 'updater_running': False }, 'name': name }) From 924c2097f2e68f7d31e553a123015ac249e524cb Mon Sep 17 00:00:00 2001 From: Rich Ellis Date: Wed, 19 Feb 2020 09:45:00 +0000 Subject: [PATCH 06/52] Extend test read timeout value --- tests/unit/replicator_tests.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/unit/replicator_tests.py b/tests/unit/replicator_tests.py index 610d3588..28d78fab 100644 --- a/tests/unit/replicator_tests.py +++ b/tests/unit/replicator_tests.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (C) 2015, 2018 IBM Corp. All rights reserved. +# Copyright (C) 2015, 2020 IBM Corp. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -220,10 +220,13 @@ def test_create_replication(self): def test_timeout_in_create_replication(self): """ Test that a read timeout exception is thrown when creating a - replicator with a timeout value of 500 ms. + replicator with a read timeout value of 5 s. """ - # Setup client with a timeout - self.set_up_client(auto_connect=True, timeout=.5) + # Setup client with a read timeout (but the standard connect timeout) + # Note that this timeout applies to all connections from this client + # setting it too short can cause intermittent failures when responses + # are not quick enough. Setting it too long makes the test take longer. + self.set_up_client(auto_connect=True, timeout=(30,5)) self.db = self.client[self.test_target_dbname] self.target_db = self.client[self.test_dbname] # Construct a replicator with the updated client From 058b96762d63f20b3e018ed35aff2070ec228436 Mon Sep 17 00:00:00 2001 From: Rich Ellis Date: Wed, 19 Feb 2020 11:12:37 +0000 Subject: [PATCH 07/52] Updated test matrix version Python 2 is EOL, removed from matrix. Updated CouchDB patch versions. --- .travis.yml | 11 +++++------ Jenkinsfile | 3 +-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8a88780e..44abb089 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,14 +3,13 @@ sudo: required language: python python: - - "2.7" - - "3.6" + - "3.8" env: - - ADMIN_PARTY=true COUCHDB_VERSION=2.1.1 - - ADMIN_PARTY=false COUCHDB_VERSION=2.1.1 - - ADMIN_PARTY=true COUCHDB_VERSION=1.7.1 - - ADMIN_PARTY=false COUCHDB_VERSION=1.7.1 + - ADMIN_PARTY=true COUCHDB_VERSION=2.3.1 + - ADMIN_PARTY=false COUCHDB_VERSION=2.3.1 + - ADMIN_PARTY=true COUCHDB_VERSION=1.7.2 + - ADMIN_PARTY=false COUCHDB_VERSION=1.7.2 services: - docker diff --git a/Jenkinsfile b/Jenkinsfile index 7501c07e..aa2f2c06 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -61,10 +61,9 @@ stage('Checkout'){ } stage('Test'){ - def py2 = '2' def py3 = '3' def axes = [:] - [py2, py3].each { version -> + [py3].each { version -> ['basic','cookie','iam'].each { auth -> axes.put("Python${version}-${auth}", {setupPythonAndTest(version, auth)}) } From 47b8a5ab4f05f036ca97e6e5019d4634b230e094 Mon Sep 17 00:00:00 2001 From: Rich Ellis Date: Wed, 19 Feb 2020 12:00:00 +0000 Subject: [PATCH 08/52] Corrected syntax to eliminate warning in 3.8 --- .travis.yml | 2 +- Jenkinsfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 44abb089..b49557a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,7 @@ before_script: # command to run tests script: - pylint ./src/cloudant - - nosetests -A 'not db or ((db is "couch" or "couch" in db) and (not couchapi or couchapi <='${COUCHDB_VERSION:0:1}'))' -w ./tests/unit + - nosetests -A 'not db or ((db == "couch" or "couch" in db) and (not couchapi or couchapi <='${COUCHDB_VERSION:0:1}'))' -w ./tests/unit notifications: email: false diff --git a/Jenkinsfile b/Jenkinsfile index aa2f2c06..ddf21dd4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -40,7 +40,7 @@ def setupPythonAndTest(pythonVersion, testSuite) { pip install -r test-requirements.txt ${'simplejson'.equals(testSuite) ? 'pip install simplejson' : ''} pylint ./src/cloudant - nosetests -A 'not db or (db is "cloudant" or "cloudant" in db)' -w ./tests/unit --with-xunit + nosetests -A 'not db or (db == "cloudant" or "cloudant" in db)' -w ./tests/unit --with-xunit """ } finally { // Load the test results From d84a0ea12d07688f0d6391616869b89792a11a1f Mon Sep 17 00:00:00 2001 From: Rich Ellis Date: Wed, 19 Feb 2020 14:43:11 +0000 Subject: [PATCH 09/52] Updated CouchDB version check in tests --- tests/unit/unit_t_db_base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/unit_t_db_base.py b/tests/unit/unit_t_db_base.py index 913d7b20..2067af0c 100644 --- a/tests/unit/unit_t_db_base.py +++ b/tests/unit/unit_t_db_base.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (C) 2015, 2019 IBM Corp. All rights reserved. +# Copyright (C) 2015, 2020 IBM Corp. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -109,7 +109,7 @@ def setUpClass(cls): if os.environ.get('DB_USER') is None: # Get couchdb docker node name - if os.environ.get('COUCHDB_VERSION') == '2.1.1': + if os.environ.get('COUCHDB_VERSION') == '2.3.1': os.environ['NODENAME'] = requests.get( '{0}/_membership'.format(os.environ['DB_URL'])).json()['all_nodes'][0] os.environ['DB_USER_CREATED'] = '1' @@ -117,7 +117,7 @@ def setUpClass(cls): unicode_(uuid.uuid4()) ) os.environ['DB_PASSWORD'] = 'password' - if os.environ.get('COUCHDB_VERSION') == '2.1.1': + if os.environ.get('COUCHDB_VERSION') == '2.3.1': resp = requests.put( '{0}/_node/{1}/_config/admins/{2}'.format( os.environ['DB_URL'], @@ -143,7 +143,7 @@ def tearDownClass(cls): """ if (os.environ.get('RUN_CLOUDANT_TESTS') is None and os.environ.get('DB_USER_CREATED') is not None): - if os.environ.get('COUCHDB_VERSION') == '2.1.1': + if os.environ.get('COUCHDB_VERSION') == '2.3.1': resp = requests.delete( '{0}://{1}:{2}@{3}/_node/{4}/_config/admins/{5}'.format( os.environ['DB_URL'].split('://', 1)[0], From 8331efae8dc7f432267c7f7b96062af8a4cd5a59 Mon Sep 17 00:00:00 2001 From: Rich Ellis Date: Wed, 19 Feb 2020 15:21:05 +0000 Subject: [PATCH 10/52] Fixed invalid DB name test The HEAD on an invalid DB name that doesn't exist returns a 400 in 1.7.2 but a 404 in 1.7.1 and 2.x --- tests/unit/client_tests.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/client_tests.py b/tests/unit/client_tests.py index f3ac4e76..2726de33 100644 --- a/tests/unit/client_tests.py +++ b/tests/unit/client_tests.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (C) 2015, 2019 IBM Corp. All rights reserved. +# Copyright (C) 2015, 2020 IBM Corp. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -405,9 +405,10 @@ def test_create_invalid_database_name(self): """ dbname = 'invalidDbName_' self.client.connect() - with self.assertRaises(CloudantDatabaseException) as cm: + with self.assertRaises((CloudantDatabaseException, HTTPError)) as cm: self.client.create_database(dbname) - self.assertEqual(cm.exception.status_code, 400) + code = cm.exception.status_code if hasattr(cm.exception, 'status_code') else cm.exception.response.status_code + self.assertEqual(code, 400) self.client.disconnect() @skip_if_not_cookie_auth From 65fcb81fadecdb27e3127b9ab2d9c9cf38cc92ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bessenyei=20Bal=C3=A1zs=20Don=C3=A1t?= Date: Tue, 14 Apr 2020 14:03:39 +0200 Subject: [PATCH 11/52] Updated CHANGES.md for 2.13.0 release --- CHANGES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 3ba7cc7f..0a09653b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ -# Unreleased +# 2.13.0 (2020-04-16) - [FIXED] Correctly raise exceptions from `create_database` calls. +- [FIXED] Fix `DeprecationWarning` from `collections`. # 2.12.0 (2019-03-28) From 28cc072bc261b48716dcee1f091944258aecdf89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bessenyei=20Bal=C3=A1zs=20Don=C3=A1t?= Date: Tue, 14 Apr 2020 14:04:02 +0200 Subject: [PATCH 12/52] Updated version to 2.13.0 --- VERSION | 2 +- docs/conf.py | 4 ++-- src/cloudant/__init__.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index 91d3a8d4..fb2c0766 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.12.1-SNAPSHOT +2.13.0 diff --git a/docs/conf.py b/docs/conf.py index 764be732..beb4f08b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '2.12.1-SNAPSHOT' +version = '2.13.0' # The full version, including alpha/beta/rc tags. -release = '2.12.1-SNAPSHOT' +release = '2.13.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/src/cloudant/__init__.py b/src/cloudant/__init__.py index 677df2ed..0b8abc5f 100644 --- a/src/cloudant/__init__.py +++ b/src/cloudant/__init__.py @@ -15,7 +15,7 @@ """ Cloudant / CouchDB Python client library API package """ -__version__ = '2.12.1-SNAPSHOT' +__version__ = '2.13.0' # pylint: disable=wrong-import-position import contextlib From a45d21949baa9d753d899ce926ca68a3d1fd5371 Mon Sep 17 00:00:00 2001 From: Rich Ellis Date: Thu, 16 Apr 2020 16:50:56 +0100 Subject: [PATCH 13/52] Updated version to 2.13.1-SNAPSHOT --- VERSION | 2 +- docs/conf.py | 4 ++-- src/cloudant/__init__.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index fb2c0766..ca3389d9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.13.0 +2.13.1-SNAPSHOT diff --git a/docs/conf.py b/docs/conf.py index beb4f08b..ada059aa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '2.13.0' +version = '2.13.1-SNAPSHOT' # The full version, including alpha/beta/rc tags. -release = '2.13.0' +release = '2.13.1-SNAPSHOT' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/src/cloudant/__init__.py b/src/cloudant/__init__.py index 0b8abc5f..0b162070 100644 --- a/src/cloudant/__init__.py +++ b/src/cloudant/__init__.py @@ -15,7 +15,7 @@ """ Cloudant / CouchDB Python client library API package """ -__version__ = '2.13.0' +__version__ = '2.13.1-SNAPSHOT' # pylint: disable=wrong-import-position import contextlib From 06735afacadf8773517cbeaa9843dfa129c3b5da Mon Sep 17 00:00:00 2001 From: Dipjyoti Bisharad Date: Fri, 8 May 2020 20:19:30 +0530 Subject: [PATCH 14/52] Fixed design document creation when partitioned parameter is false (#467) Set default value for 'partitioned' parameter to false when creating a design document. Co-authored-by: dipjyoti bisharad #466 --- CHANGES.md | 4 ++++ src/cloudant/design_document.py | 2 ++ tests/unit/database_partition_tests.py | 11 +++++++++ tests/unit/database_tests.py | 2 +- tests/unit/design_document_tests.py | 31 ++++++++++++++++---------- 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0a09653b..9e7155db 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,7 @@ +# UNRELEASED + +- [FIXED] Set default value for `partitioned` parameter to false when creating a design document. + # 2.13.0 (2020-04-16) - [FIXED] Correctly raise exceptions from `create_database` calls. diff --git a/src/cloudant/design_document.py b/src/cloudant/design_document.py index 34594b95..d3ea387a 100644 --- a/src/cloudant/design_document.py +++ b/src/cloudant/design_document.py @@ -50,6 +50,8 @@ def __init__(self, database, document_id=None, partitioned=False): if partitioned: self.setdefault('options', {'partitioned': True}) + else: + self.setdefault('options', {'partitioned': False}) self._nested_object_names = frozenset(['views', 'indexes', 'lists', 'shows']) for prop in self._nested_object_names: diff --git a/tests/unit/database_partition_tests.py b/tests/unit/database_partition_tests.py index 2c595f65..52b76e0d 100644 --- a/tests/unit/database_partition_tests.py +++ b/tests/unit/database_partition_tests.py @@ -48,6 +48,17 @@ def test_create_partitioned_design_document(self): r.raise_for_status() self.assertTrue(r.json()['options']['partitioned']) + + def test_create_non_partitioned_design_document(self): + ddoc_id = 'empty_ddoc' + + ddoc = DesignDocument(self.db, ddoc_id, partitioned=False) + ddoc.save() + + r = self.db.r_session.get(ddoc.document_url) + r.raise_for_status() + + self.assertFalse(r.json()['options']['partitioned']) def test_partitioned_all_docs(self): for partition_key in self.populate_db_with_partitioned_documents(5, 25): diff --git a/tests/unit/database_tests.py b/tests/unit/database_tests.py index d57ba654..f054dd62 100644 --- a/tests/unit/database_tests.py +++ b/tests/unit/database_tests.py @@ -364,7 +364,7 @@ def test_retrieve_design_document(self): """ # Get an empty design document object that does not exist remotely local_ddoc = self.db.get_design_document('_design/ddoc01') - self.assertEqual(local_ddoc, {'_id': '_design/ddoc01', 'indexes': {}, + self.assertEqual(local_ddoc, {'_id': '_design/ddoc01', 'indexes': {}, 'options': {'partitioned': False}, 'views': {}, 'lists': {}, 'shows': {}}) # Add the design document to the database map_func = 'function(doc) {\n emit(doc._id, 1); \n}' diff --git a/tests/unit/design_document_tests.py b/tests/unit/design_document_tests.py index 7ec3c10e..68a40505 100644 --- a/tests/unit/design_document_tests.py +++ b/tests/unit/design_document_tests.py @@ -353,6 +353,7 @@ def test_fetch_map_reduce(self): self.assertEqual(ddoc_remote, { '_id': '_design/ddoc001', '_rev': ddoc['_rev'], + 'options': {'partitioned': False}, 'lists': {}, 'shows': {}, 'indexes': {}, @@ -415,7 +416,7 @@ def test_fetch_no_views(self): ddoc_remote = DesignDocument(self.db, '_design/ddoc001') ddoc_remote.fetch() self.assertEqual(set(ddoc_remote.keys()), - {'_id', '_rev', 'indexes', 'views', 'lists', 'shows'}) + {'_id', '_rev', 'indexes', 'views', 'options', 'lists', 'shows'}) self.assertEqual(ddoc_remote['_id'], '_design/ddoc001') self.assertTrue(ddoc_remote['_rev'].startswith('1-')) self.assertEqual(ddoc_remote['_rev'], ddoc['_rev']) @@ -432,6 +433,7 @@ def test_fetch_query_views(self): data = { '_id': '_design/ddoc001', 'indexes': {}, + 'options': {'partitioned': False}, 'lists': {}, 'shows': {}, 'language': 'query', @@ -463,6 +465,7 @@ def test_fetch_text_indexes(self): data = { '_id': '_design/ddoc001', 'language': 'query', + 'options': {'partitioned': False}, 'lists': {}, 'shows': {}, 'indexes': {'index001': @@ -499,6 +502,7 @@ def test_fetch_text_indexes_and_query_views(self): 'language': 'query', 'lists': {}, 'shows': {}, + 'options': {'partitioned': False}, 'views': { 'view001': {'map': {'fields': {'name': 'asc', 'age': 'asc'}}, 'reduce': '_count', @@ -687,7 +691,7 @@ def test_save_with_no_views(self): ddoc.save() # Ensure that locally cached DesignDocument contains an # empty views dict. - self.assertEqual(set(ddoc.keys()), {'_id', '_rev', 'indexes', 'views', 'lists', 'shows'}) + self.assertEqual(set(ddoc.keys()), {'_id', '_rev', 'indexes', 'options', 'views', 'lists', 'shows'}) self.assertEqual(ddoc['_id'], '_design/ddoc001') self.assertTrue(ddoc['_rev'].startswith('1-')) self.assertEqual(ddoc.views, {}) @@ -695,7 +699,7 @@ def test_save_with_no_views(self): # include a views sub-document. resp = self.client.r_session.get(ddoc.document_url) raw_ddoc = response_to_json_dict(resp) - self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev'}) + self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev','options'}) self.assertEqual(raw_ddoc['_id'], ddoc['_id']) self.assertEqual(raw_ddoc['_rev'], ddoc['_rev']) @@ -1076,6 +1080,7 @@ def test_fetch_search_index(self): self.assertEqual(ddoc_remote, { '_id': '_design/ddoc001', '_rev': ddoc['_rev'], + 'options': {'partitioned': False}, 'indexes': { 'search001': {'index': search_index}, 'search002': {'index': search_index, 'analyzer': 'simple'}, @@ -1100,7 +1105,7 @@ def test_fetch_no_search_index(self): ddoc_remote = DesignDocument(self.db, '_design/ddoc001') ddoc_remote.fetch() self.assertEqual(set(ddoc_remote.keys()), - {'_id', '_rev', 'indexes', 'views', 'lists', 'shows'}) + {'_id', '_rev', 'indexes', 'options', 'views', 'lists', 'shows'}) self.assertEqual(ddoc_remote['_id'], '_design/ddoc001') self.assertTrue(ddoc_remote['_rev'].startswith('1-')) self.assertEqual(ddoc_remote['_rev'], ddoc['_rev']) @@ -1183,14 +1188,14 @@ def test_save_with_no_search_indexes(self): ddoc.save() # Ensure that locally cached DesignDocument contains an # empty search indexes and views dict. - self.assertEqual(set(ddoc.keys()), {'_id', '_rev', 'indexes', 'views', 'lists', 'shows'}) + self.assertEqual(set(ddoc.keys()), {'_id', '_rev', 'indexes','options', 'views', 'lists', 'shows'}) self.assertEqual(ddoc['_id'], '_design/ddoc001') self.assertTrue(ddoc['_rev'].startswith('1-')) # Ensure that remotely saved design document does not # include a search indexes sub-document. resp = self.client.r_session.get(ddoc.document_url) raw_ddoc = response_to_json_dict(resp) - self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev'}) + self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev','options'}) self.assertEqual(raw_ddoc['_id'], ddoc['_id']) self.assertEqual(raw_ddoc['_rev'], ddoc['_rev']) @@ -1413,6 +1418,7 @@ def test_fetch_list_functions(self): self.assertEqual(ddoc_remote, { '_id': '_design/ddoc001', '_rev': ddoc['_rev'], + 'options': {'partitioned': False}, 'lists': { 'list001': list_func, 'list002': list_func, @@ -1437,7 +1443,7 @@ def test_fetch_no_list_functions(self): ddoc_remote = DesignDocument(self.db, '_design/ddoc001') ddoc_remote.fetch() self.assertEqual(set(ddoc_remote.keys()), - {'_id', '_rev', 'indexes', 'views', 'lists', 'shows'}) + {'_id', '_rev', 'options', 'indexes', 'views', 'lists', 'shows'}) self.assertEqual(ddoc_remote['_id'], '_design/ddoc001') self.assertTrue(ddoc_remote['_rev'].startswith('1-')) self.assertEqual(ddoc_remote['_rev'], ddoc['_rev']) @@ -1452,14 +1458,14 @@ def test_save_with_no_list_functions(self): ddoc = DesignDocument(self.db, '_design/ddoc001') ddoc.save() # Ensure that locally cached DesignDocument contains lists dict - self.assertEqual(set(ddoc.keys()), {'_id', '_rev', 'lists', 'shows', 'indexes', 'views'}) + self.assertEqual(set(ddoc.keys()), {'_id', '_rev', 'lists', 'options', 'shows', 'indexes', 'views'}) self.assertEqual(ddoc['_id'], '_design/ddoc001') self.assertTrue(ddoc['_rev'].startswith('1-')) # Ensure that remotely saved design document does not # include a lists sub-document. resp = self.client.r_session.get(ddoc.document_url) raw_ddoc = response_to_json_dict(resp) - self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev'}) + self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev','options'}) self.assertEqual(raw_ddoc['_id'], ddoc['_id']) self.assertEqual(raw_ddoc['_rev'], ddoc['_rev']) @@ -1714,6 +1720,7 @@ def test_fetch_show_functions(self): self.assertEqual(ddoc_remote, { '_id': '_design/ddoc001', '_rev': ddoc['_rev'], + 'options': {'partitioned': False}, 'lists': {}, 'shows': { 'show001': show_func, @@ -1738,7 +1745,7 @@ def test_fetch_no_show_functions(self): ddoc_remote = DesignDocument(self.db, '_design/ddoc001') ddoc_remote.fetch() self.assertEqual(set(ddoc_remote.keys()), - {'_id', '_rev', 'indexes', 'views', 'lists', 'shows'}) + {'_id', '_rev', 'indexes', 'options', 'views', 'lists', 'shows'}) self.assertEqual(ddoc_remote['_id'], '_design/ddoc001') self.assertTrue(ddoc_remote['_rev'].startswith('1-')) self.assertEqual(ddoc_remote['_rev'], ddoc['_rev']) @@ -1753,14 +1760,14 @@ def test_save_with_no_show_functions(self): ddoc = DesignDocument(self.db, '_design/ddoc001') ddoc.save() # Ensure that locally cached DesignDocument contains shows dict - self.assertEqual(set(ddoc.keys()), {'_id', '_rev', 'lists', 'shows', 'indexes', 'views'}) + self.assertEqual(set(ddoc.keys()), {'_id', '_rev', 'lists','options', 'shows', 'indexes', 'views'}) self.assertEqual(ddoc['_id'], '_design/ddoc001') self.assertTrue(ddoc['_rev'].startswith('1-')) # Ensure that remotely saved design document does not # include a shows sub-document. resp = self.client.r_session.get(ddoc.document_url) raw_ddoc = response_to_json_dict(resp) - self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev'}) + self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev','options'}) self.assertEqual(raw_ddoc['_id'], ddoc['_id']) self.assertEqual(raw_ddoc['_rev'], ddoc['_rev']) From 37894ceb7bab61667a8c399470a1fcc8d461a27e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bessenyei=20Bal=C3=A1zs=20Don=C3=A1t?= Date: Mon, 11 May 2020 16:16:53 +0200 Subject: [PATCH 15/52] Disable false-positive linter error (#469) * Disable false-positive linter error The build has been failing recently with a false positive linter error. Pylint was complaining that a method is not callable, even though the method is defined right on the following line. As I could not find either the reason pylint was failing or a solution that resolves this issue, this commit disables the check for that specific line pylint was failing on. * Fix assertions for non-partitioned db * Pin pylint version --- src/cloudant/feed.py | 2 +- test-requirements.txt | 2 +- tests/unit/design_document_tests.py | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/cloudant/feed.py b/src/cloudant/feed.py index ef2c90fc..f038ebfe 100644 --- a/src/cloudant/feed.py +++ b/src/cloudant/feed.py @@ -144,7 +144,7 @@ def __next__(self): """ Provides Python3 compatibility. """ - return self.next() + return self.next() # pylint: disable=not-callable def next(self): """ diff --git a/test-requirements.txt b/test-requirements.txt index 88c13ffb..8d95c01d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,5 +2,5 @@ mock==1.3.0 nose sphinx sphinx_rtd_theme -pylint +pylint==2.5.2 flaky diff --git a/tests/unit/design_document_tests.py b/tests/unit/design_document_tests.py index 68a40505..52352bcc 100644 --- a/tests/unit/design_document_tests.py +++ b/tests/unit/design_document_tests.py @@ -390,7 +390,8 @@ def test_fetch_dbcopy(self): # before comparison also. Compare the removed values with # the expected content in each case. self.assertEqual(db_copy, ddoc['views']['view002'].pop('dbcopy')) - self.assertEqual({'epi': {'dbcopy': {'view002': db_copy}}}, ddoc_remote.pop('options')) + self.assertEqual({'epi': {'dbcopy': {'view002': db_copy}}, 'partitioned': False}, ddoc_remote.pop('options')) + self.assertEqual({'partitioned': False}, ddoc.pop('options')) self.assertEqual(ddoc_remote, ddoc) self.assertTrue(ddoc_remote['_rev'].startswith('1-')) self.assertEqual(ddoc_remote, { @@ -1553,7 +1554,8 @@ def test_geospatial_index(self): 'indexes': {}, 'views': {}, 'lists': {}, - 'shows': {} + 'shows': {}, + 'options': {'partitioned': False} }) # Document with geospatial point geodoc = Document(self.db, 'doc001') From dfa5a7bb11bafa44b50d4dd3edfbdf7793c8aa07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bessenyei=20Bal=C3=A1zs=20Don=C3=A1t?= Date: Wed, 13 May 2020 15:34:42 +0200 Subject: [PATCH 16/52] Remove `CHANGELOG.md` from the PR template (#471) This repo only has a `CHANGES.md` and `CHANGELOG.md` is not applicable. This PR removes the `CHANGELOG.md` from the template. --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8a9c853a..5b5697c2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,7 +5,7 @@ Thanks for your hard work, please ensure all items are complete before opening. - [ ] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](../blob/master/DCO1.1.txt) - [ ] Added tests for code changes _or_ test/build only changes -- [ ] Updated the change log file (`CHANGES.md`|`CHANGELOG.md`) _or_ test/build only changes +- [ ] Updated the change log file (`CHANGES.md`) _or_ test/build only changes - [ ] Completed the PR template below: ## Description From 14646b7c38caeceface6e2a35f94392356886b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bessenyei=20Bal=C3=A1zs=20Don=C3=A1t?= Date: Thu, 14 May 2020 13:31:03 +0200 Subject: [PATCH 17/52] Make `create_query_index` always supply `partitioned` (#470) `create_query_index` does not always supply `partitioned` which is always required by the service. This commit changes that so that `partitioned` is now always present in the `create_query_index` requests when defined. This fixes #468 --- CHANGES.md | 1 + docs/getting_started.rst | 8 ++++++-- src/cloudant/database.py | 2 +- src/cloudant/index.py | 6 +++--- tests/unit/index_tests.py | 15 +++++++++++++++ 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9e7155db..6ea317b1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ # UNRELEASED - [FIXED] Set default value for `partitioned` parameter to false when creating a design document. +- [FIXED] Corrected setting of `partitioned` flag for `create_query_index` requests. # 2.13.0 (2020-04-16) diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 9cd4b65e..c92387ca 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -284,8 +284,12 @@ when constructing the new ``DesignDocument`` class. ddoc.add_view('myview','function(doc) { emit(doc.foo, doc.bar); }') ddoc.save() -Similarly, to define a partitioned Cloudant Query index you must set the -``partitioned=True`` optional. + +To define a partitioned Cloudant Query index you may set the +``partitioned=True`` optional, but it is not required as the index will be +partitioned by default in a partitioned database. Conversely, you must +set the ``partitioned=False`` optional if you wish to create a global +(non-partitioned) index in a partitioned database. .. code-block:: python diff --git a/src/cloudant/database.py b/src/cloudant/database.py index 17978468..3586c93f 100644 --- a/src/cloudant/database.py +++ b/src/cloudant/database.py @@ -1100,7 +1100,7 @@ def create_query_index( design_document_id=None, index_name=None, index_type='json', - partitioned=False, + partitioned=None, **kwargs ): """ diff --git a/src/cloudant/index.py b/src/cloudant/index.py index 051c004b..c66c7ac2 100644 --- a/src/cloudant/index.py +++ b/src/cloudant/index.py @@ -47,7 +47,7 @@ class Index(object): :func:`~cloudant.database.CloudantDatabase.create_query_index`. """ - def __init__(self, database, design_document_id=None, name=None, partitioned=False, **kwargs): + def __init__(self, database, design_document_id=None, name=None, partitioned=None, **kwargs): self._database = database self._r_session = self._database.r_session self._ddoc_id = design_document_id @@ -154,8 +154,8 @@ def create(self): self._def_check() payload['index'] = self._def - if self._partitioned: - payload['partitioned'] = True + if self._partitioned is not None: + payload['partitioned'] = bool(self._partitioned) headers = {'Content-Type': 'application/json'} resp = self._r_session.post( diff --git a/tests/unit/index_tests.py b/tests/unit/index_tests.py index 4361644f..29a3d51c 100644 --- a/tests/unit/index_tests.py +++ b/tests/unit/index_tests.py @@ -554,6 +554,21 @@ def test_create_a_search_index_invalid_selector_value(self): '<{} \'dict\'>'.format('type' if PY2 else 'class') ) + def test_create_unpartitioned_query_index(self): + """ + Test that create_query_index works on an unpartitioned database + """ + ddoc = DesignDocument(self.db, document_id="unpartitioned_query_index_ddoc") + ddoc["language"] = "query" + ddoc.save() + index = self.db.create_query_index( + design_document_id="_design/unpartitioned_query_index_ddoc", + fields=["key"], + partitioned=False + ) + index.create() + self.assertGreater(len(self.db.get_query_indexes()), 0) + def test_search_index_via_query(self): """ Test that a created TEXT index will produce expected query results. From d080687766a3101b1e876957674983b956851a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eigel=20Ildik=C3=B3?= Date: Fri, 12 Jun 2020 14:36:04 +0200 Subject: [PATCH 18/52] Add private endpoint usage with IAM to docs (#475) --- docs/getting_started.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/getting_started.rst b/docs/getting_started.rst index c92387ca..37c5b92c 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -111,6 +111,13 @@ You can easily connect to your Cloudant account using an IAM API key: # Authenticate using an IAM API key client = Cloudant.iam(ACCOUNT_NAME, API_KEY, connect=True) +If you need to authenticate to a server outside of the `cloudant.com` domain, you can use the `url` parameter: + +.. code-block:: python + + # Authenticate using an IAM API key to an account outside of the cloudant.com domain + client = Cloudant.iam(None, API_KEY, url='https://private.endpoint.example', connect=True) + **************** Resource sharing From 4e2c377f9c955f04b2d911d4480a00a308ce581e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bessenyei=20Bal=C3=A1zs=20Don=C3=A1t?= Date: Mon, 17 Aug 2020 10:36:58 +0200 Subject: [PATCH 19/52] Make setup.py compatible with python 2 (#477) --- CHANGES.md | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 6ea317b1..0fe2e075 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ - [FIXED] Set default value for `partitioned` parameter to false when creating a design document. - [FIXED] Corrected setting of `partitioned` flag for `create_query_index` requests. +- [FIXED] Added a workaround for installation on Python 2. # 2.13.0 (2020-04-16) diff --git a/setup.py b/setup.py index e02192cd..ac2e3876 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,7 @@ """ +from io import open from os import path from setuptools import setup, find_packages From 296cf8110da40dfd2528f5431340a79d7e5fdc1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bessenyei=20Bal=C3=A1zs=20Don=C3=A1t?= Date: Mon, 17 Aug 2020 16:09:52 +0200 Subject: [PATCH 20/52] Prepare 2.14.0 release --- CHANGES.md | 2 ++ VERSION | 2 +- docs/conf.py | 4 ++-- src/cloudant/__init__.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0fe2e075..93cc5339 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,7 @@ # UNRELEASED +# 2.14.0 (2020-08-17) + - [FIXED] Set default value for `partitioned` parameter to false when creating a design document. - [FIXED] Corrected setting of `partitioned` flag for `create_query_index` requests. - [FIXED] Added a workaround for installation on Python 2. diff --git a/VERSION b/VERSION index ca3389d9..edcfe40d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.13.1-SNAPSHOT +2.14.0 diff --git a/docs/conf.py b/docs/conf.py index ada059aa..44f27314 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '2.13.1-SNAPSHOT' +version = '2.14.0' # The full version, including alpha/beta/rc tags. -release = '2.13.1-SNAPSHOT' +release = '2.14.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/src/cloudant/__init__.py b/src/cloudant/__init__.py index 0b162070..193a099c 100644 --- a/src/cloudant/__init__.py +++ b/src/cloudant/__init__.py @@ -15,7 +15,7 @@ """ Cloudant / CouchDB Python client library API package """ -__version__ = '2.13.1-SNAPSHOT' +__version__ = '2.14.0' # pylint: disable=wrong-import-position import contextlib From 3f4ae5d7c3dc79a5b85a2b481b998570c262b0fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bessenyei=20Bal=C3=A1zs=20Don=C3=A1t?= Date: Mon, 17 Aug 2020 16:42:49 +0200 Subject: [PATCH 21/52] Remove "Unreleased" from CHANGES.md --- CHANGES.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 93cc5339..9188a90d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,3 @@ -# UNRELEASED - # 2.14.0 (2020-08-17) - [FIXED] Set default value for `partitioned` parameter to false when creating a design document. From 8c703409053fc2a47d25ba9e3da43b1527370baf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bessenyei=20Bal=C3=A1zs=20Don=C3=A1t?= Date: Tue, 18 Aug 2020 17:15:37 +0200 Subject: [PATCH 22/52] Update version to 2.14.1-SNAPSHOT (#480) --- CHANGES.md | 2 ++ VERSION | 2 +- docs/conf.py | 4 ++-- src/cloudant/__init__.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9188a90d..93cc5339 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,5 @@ +# UNRELEASED + # 2.14.0 (2020-08-17) - [FIXED] Set default value for `partitioned` parameter to false when creating a design document. diff --git a/VERSION b/VERSION index edcfe40d..20738ec2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.14.0 +2.14.1-SNAPSHOT diff --git a/docs/conf.py b/docs/conf.py index 44f27314..1e338e6d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '2.14.0' +version = '2.14.1-SNAPSHOT' # The full version, including alpha/beta/rc tags. -release = '2.14.0' +release = '2.14.1-SNAPSHOT' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/src/cloudant/__init__.py b/src/cloudant/__init__.py index 193a099c..7e011096 100644 --- a/src/cloudant/__init__.py +++ b/src/cloudant/__init__.py @@ -15,7 +15,7 @@ """ Cloudant / CouchDB Python client library API package """ -__version__ = '2.14.0' +__version__ = '2.14.1-SNAPSHOT' # pylint: disable=wrong-import-position import contextlib From e80a07eb8a9723e2179872e832f0a47cd1a4fce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bessenyei=20Bal=C3=A1zs=20Don=C3=A1t?= Date: Tue, 18 Aug 2020 18:28:39 +0200 Subject: [PATCH 23/52] Remove Python 2 compatibility (#478) Python 2 was EoLed back in January. This commit removes it from the list of supported environments. --- CHANGES.md | 1 + README.md | 2 +- docs/compatibility.rst | 1 - setup.py | 2 -- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 93cc5339..b40e6fb1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,5 @@ # UNRELEASED +- [REMOVED] Removed Python 2 compatibility from the supported environments. # 2.14.0 (2020-08-17) diff --git a/README.md b/README.md index 0cc5edcf..7eaf83ba 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://travis-ci.org/cloudant/python-cloudant.svg?branch=master)](https://travis-ci.org/cloudant/python-cloudant) [![Readthedocs](https://readthedocs.org/projects/pip/badge/)](http://python-cloudant.readthedocs.io) -[![Compatibility](https://img.shields.io/badge/python-2.7%2C%203.5-blue.svg)](http://python-cloudant.readthedocs.io/en/latest/compatibility.html) +[![Compatibility](https://img.shields.io/badge/python-3.5-blue.svg)](http://python-cloudant.readthedocs.io/en/latest/compatibility.html) [![pypi](https://img.shields.io/pypi/v/cloudant.svg)](https://pypi.python.org/pypi/cloudant) This is the official Cloudant library for Python. diff --git a/docs/compatibility.rst b/docs/compatibility.rst index 1bb98cce..b756a65e 100644 --- a/docs/compatibility.rst +++ b/docs/compatibility.rst @@ -11,5 +11,4 @@ Note that some features are Cloudant specific. This library has been tested with the following versions of Python -* `Python™ 2.7 `_ * `Python™ 3.5 `_ diff --git a/setup.py b/setup.py index ac2e3876..a8ac4ef8 100644 --- a/setup.py +++ b/setup.py @@ -55,8 +55,6 @@ 'Topic :: Software Development :: Libraries', 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5' ] From 943ad117d0cb416ffb589c35e144b14eb32726f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bessenyei=20Bal=C3=A1zs=20Don=C3=A1t?= Date: Fri, 4 Sep 2020 11:10:36 +0200 Subject: [PATCH 24/52] Remove incorrect restriction from docs for bookmarks (#481) --- CHANGES.md | 1 + src/cloudant/database.py | 3 +-- src/cloudant/query.py | 9 +++------ src/cloudant/result.py | 3 +-- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b40e6fb1..e68156a0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ # UNRELEASED - [REMOVED] Removed Python 2 compatibility from the supported environments. +- [FIXED] Fixed the documentation for `bookmarks`. # 2.14.0 (2020-08-17) diff --git a/src/cloudant/database.py b/src/cloudant/database.py index 3586c93f..ccde12da 100644 --- a/src/cloudant/database.py +++ b/src/cloudant/database.py @@ -1242,8 +1242,7 @@ def get_query_result(self, selector, fields=None, raw_result=False, wrapped in a QueryResult or if the response JSON is returned. Defaults to False. :param str bookmark: A string that enables you to specify which page of - results you require. Only valid for queries using indexes of type - *text*. + results you require. :param int limit: Maximum number of results returned. Only valid if used with ``raw_result=True``. :param int page_size: Sets the page size for result iteration. Default diff --git a/src/cloudant/query.py b/src/cloudant/query.py index d6a0aebc..2362f80d 100644 --- a/src/cloudant/query.py +++ b/src/cloudant/query.py @@ -69,8 +69,7 @@ class Query(dict): :param CloudantDatabase database: A Cloudant database instance used by the Query. :param str bookmark: A string that enables you to specify which page of - results you require. Only valid for queries using indexes of type - *text*. + results you require. :param list fields: A list of fields to be returned by the query. :param int limit: Maximum number of results returned. :param int r: Read quorum needed for the result. Each document is read from @@ -141,8 +140,7 @@ def __call__(self, **kwargs): and set ``raw_result=True`` instead. :param str bookmark: A string that enables you to specify which page of - results you require. Only valid for queries using indexes of type - *text*. + results you require. :param list fields: A list of fields to be returned by the query. :param int limit: Maximum number of results returned. :param int r: Read quorum needed for the result. Each document is read @@ -204,8 +202,7 @@ def custom_result(self, **options): data = rslt[100:200] :param str bookmark: A string that enables you to specify which page of - results you require. Only valid for queries using indexes of type - *text*. + results you require. :param list fields: A list of fields to be returned by the query. :param int page_size: Sets the page size for result iteration. Default is 100. diff --git a/src/cloudant/result.py b/src/cloudant/result.py index d3dd9a09..e484cd07 100644 --- a/src/cloudant/result.py +++ b/src/cloudant/result.py @@ -488,8 +488,7 @@ class QueryResult(Result): :param query: A reference to the query callable that returns the JSON content result to be wrapped. :param str bookmark: A string that enables you to specify which page of - results you require. Only valid for queries using indexes of type - *text*. + results you require. :param list fields: A list of fields to be returned by the query. :param int page_size: Sets the page size for result iteration. Default is 100. From 1d634d69c48e23932a1ae7b0b5befefe1c274047 Mon Sep 17 00:00:00 2001 From: Rich Ellis Date: Fri, 20 Nov 2020 12:18:30 +0000 Subject: [PATCH 25/52] Correct replicator states Add the `failed` state to `follow_replication`. The replicator tests were missing some other states introduced in CouchDB 2.1 scheduler. --- CHANGES.md | 1 + src/cloudant/replicator.py | 10 ++++++++-- tests/unit/replicator_tests.py | 9 ++++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e68156a0..40299e13 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ # UNRELEASED - [REMOVED] Removed Python 2 compatibility from the supported environments. - [FIXED] Fixed the documentation for `bookmarks`. +- [FIXED] Also exit `follow_replication` for `failed` state. # 2.14.0 (2020-08-17) diff --git a/src/cloudant/replicator.py b/src/cloudant/replicator.py index e3ec3550..f1b44643 100644 --- a/src/cloudant/replicator.py +++ b/src/cloudant/replicator.py @@ -193,7 +193,12 @@ def update_state(): repl_doc, state = update_state() if repl_doc: yield repl_doc - if state is not None and state in ['error', 'completed']: + # This is a little awkward, since 2.1 the terminal states are + # "failed" and "completed", so those should be the exit states, but + # for backwards compatibility with older versions "error" is also + # needed. The code has always exited for "error" state even long + # after 2.1 was available so that behaviour is retained. + if state is not None and state in ['error', 'failed', 'completed']: return # Now listen on changes feed for the state @@ -202,7 +207,8 @@ def update_state(): repl_doc, state = update_state() if repl_doc is not None: yield repl_doc - if state is not None and state in ['error', 'completed']: + # See note about these states + if state is not None and state in ['error', 'failed', 'completed']: return def stop_replication(self, repl_id): diff --git a/tests/unit/replicator_tests.py b/tests/unit/replicator_tests.py index 28d78fab..9eb56b56 100644 --- a/tests/unit/replicator_tests.py +++ b/tests/unit/replicator_tests.py @@ -322,12 +322,14 @@ def test_retrieve_replication_state(self): ) self.replication_ids.append(repl_id) repl_state = None - valid_states = ['completed', 'error', 'triggered', 'running', None] + # note triggered is for versions prior to 2.1 + valid_states = ['completed', 'error', 'initializing', 'triggered', 'pending', 'running', 'failed', 'crashing', None] finished = False + # Wait for 5 minutes or a terminal replication state for _ in range(300): repl_state = self.replicator.replication_state(repl_id) self.assertTrue(repl_state in valid_states) - if repl_state in ('error', 'completed'): + if repl_state in ('error', 'failed', 'completed'): finished = True break time.sleep(1) @@ -407,7 +409,8 @@ def test_follow_replication(self): repl_id ) self.replication_ids.append(repl_id) - valid_states = ('completed', 'error', 'triggered', 'running', None) + # note triggered is for versions prior to 2.1 + valid_states = ['completed', 'error', 'initializing', 'triggered', 'pending', 'running', 'failed', 'crashing', None] repl_states = [] if 'scheduler' in self.client.features(): state_key = 'state' From a105ae7c191328ad8bdcc75bce12e9ba5f5ad72f Mon Sep 17 00:00:00 2001 From: Esteban Laver Date: Fri, 20 Nov 2020 13:04:15 -0500 Subject: [PATCH 26/52] Fix paging logic to check 'id' field exists for grouped view queries Added CHANGES entry Dict members aren't checked by `hasattr`. Use `get` instead and keep the value so we don't have to fetch it twice. --- CHANGES.md | 1 + src/cloudant/result.py | 5 +++-- tests/unit/database_tests.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 40299e13..ad1d3788 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ - [REMOVED] Removed Python 2 compatibility from the supported environments. - [FIXED] Fixed the documentation for `bookmarks`. - [FIXED] Also exit `follow_replication` for `failed` state. +- [FIXED] Fixed result paging for grouped view queries. # 2.14.0 (2020-08-17) diff --git a/src/cloudant/result.py b/src/cloudant/result.py index e484cd07..319d7268 100644 --- a/src/cloudant/result.py +++ b/src/cloudant/result.py @@ -397,9 +397,10 @@ def _iterator(self, response): # if we are in a view, keys could be duplicate so we # need to start from the right docid - if last['id']: + last_doc_id = last.get('id') + if last_doc_id is not None: response = self._call(startkey=last['key'], - startkey_docid=last['id']) + startkey_docid=last_doc_id) # reduce result keys are unique by definition else: response = self._call(startkey=last['key']) diff --git a/tests/unit/database_tests.py b/tests/unit/database_tests.py index f054dd62..c8c5cfe0 100644 --- a/tests/unit/database_tests.py +++ b/tests/unit/database_tests.py @@ -408,6 +408,34 @@ def test_retrieve_view_results(self): self.assertIsInstance(rslt, Result) self.assertEqual(rslt[:1], rslt['julia099']) + def test_retrieve_grouped_view_result_with_page_size(self): + """ + Test retrieving Result wrapped output from a design document grouped view + that uses a custom page size + + The view used here along with group=True will generate rows of + data where each key will be grouped into groups of 2. Such as: + {'key': 0, 'value': 2}, + {'key': 1, 'value': 2}, + ... + """ + map_func = 'function(doc) {\n emit(Math.floor(doc.age / 2), 1); \n}' + data = {'_id': '_design/ddoc01','views': {'view01': {"map": map_func, "reduce": "_count"}}} + self.db.create_document(data) + self.populate_db_with_documents(5) + + rslt = self.db.get_view_result( + '_design/ddoc01', + 'view01', + group=True, + page_size=1) + self.assertIsInstance(rslt, Result) + i = 0 + for row in rslt: + self.assertIsNotNone(row) + self.assertEqual(row['key'], i) + i += 1 + def test_retrieve_raw_view_results(self): """ Test retrieving raw output from a design document view From 43254c75cafdc19b098c5b04f19907879352a197 Mon Sep 17 00:00:00 2001 From: Esteban Laver Date: Fri, 8 Jan 2021 12:17:26 -0500 Subject: [PATCH 27/52] Add migration guide (#488) * Added migration guide to cloudant-python-sdk library * Updated README with migration guide section --- MIGRATION.md | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 5 +++ 2 files changed, 100 insertions(+) create mode 100644 MIGRATION.md diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 00000000..5503e2cc --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,95 @@ +# Migrating to the `cloudant-python-sdk` library +This document is to assist in migrating from the `python-cloudant` (module: `cloudant`) to the newly supported [`cloudant-python-sdk`](https://github.com/IBM/cloudant-python-sdk) (module: `ibmcloudant`). + +## Initializing the client connection +There are several ways to create a client connection in `cloudant-python-sdk`: +1. [Environment variables](https://github.com/IBM/cloudant-python-sdk#authentication-with-environment-variables) +2. [External configuration file](https://github.com/IBM/cloudant-python-sdk#authentication-with-external-configuration) +3. [Programmatically](https://github.com/IBM/cloudant-python-sdk#programmatic-authentication) + +[See the README](https://github.com/IBM/cloudant-python-sdk#code-examples) for code examples on using environment variables. + +## Other differences +1. The `cloudant-python-sdk` library does not support local dictionary caching of database and document objects. +1. There are no context managers in `cloudant-python-sdk`. To reproduce the behaviour of the `python-cloudant` +context managers in `cloudant-python-sdk` users need to explicitly call the specific operations against the +remote HTTP API. For example, in the case of the document context manager, this would mean doing both a `get_document` +to fetch and a `put_document` to save. +1. In `cloudant-python-sdk` View, Search, and Query (aka `_find` endpoint) operation responses contain raw JSON +content like using `raw_result=True` in `python-cloudant`. + +## Request mapping +Here's a list of the top 5 most frequently used `python-cloudant` operations and the `cloudant-python-sdk` equivalent API operation documentation link: + +| `python-cloudant` operation | `cloudant-python-sdk` API operation documentation link | +|---------------------------------------|---------------------------------| +|`Document('db_name', 'docid').fetch()` |[`getDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#getdocument)| +|`db.get_view_result()` |[`postView`](https://cloud.ibm.com/apidocs/cloudant?code=python#postview)| +|`db.get_query_result()` |[`postFind`](https://cloud.ibm.com/apidocs/cloudant?code=python#postfind)| +| `doc.exists()` |[`headDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#headdocument)| +|`Document('db_name', 'docid').save()` |[`putDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#putdocument)| + + +[A table](#reference-table) with the whole list of operations is provided at the end of this guide. + +The `cloudant-python-sdk` library is generated from a more complete API spec and provides a significant number of operations that do not exist in `python-cloudant`. See [the IBM Cloud API Documentation](https://cloud.ibm.com/apidocs/cloudant) to review request parameter and body options, code examples, and additional details for every endpoint. + +## Known Issues +There's an [outline of known issues](https://github.com/IBM/cloudant-python-sdk/blob/master/KNOWN_ISSUES.md) in the `cloudant-python-sdk` repository. + +## Reference table +The table below contains a list of `python-cloudant` functions and the `cloudant-python-sdk` equivalent API operation documentation link. The `cloudant-python-sdk` operation documentation link will contain the new function in a code sample e.g. `getServerInformation` link will contain a code example with `get_server_information()`. + +**Note:** There are many API operations included in the new `cloudant-python-sdk` that are not available in the `python-cloudant` library. The [API documentation](https://cloud.ibm.com/apidocs/cloudant?code=python) contains the full list of operations. + +| `python-cloudant` function | `cloudant-python-sdk` API operation documentation link | +|-----------------|---------------------| +|`metadata()`|[getServerInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getserverinformation)| +|`all_dbs()`|[getAllDbs](https://cloud.ibm.com/apidocs/cloudant?code=python#getalldbs)| +|`db_updates()/infinite_db_updates()`|[getDbUpdates](https://cloud.ibm.com/apidocs/cloudant?code=python#getdbupdates)| +|`Replicator.create_replication()`|[postReplicate](https://cloud.ibm.com/apidocs/cloudant?code=python#postreplicate)| +|`Scheduler.get_doc()`|[getSchedulerDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#getschedulerdocument)| +|`Scheduler.list_jobs()`|[getSchedulerJobs](https://cloud.ibm.com/apidocs/cloudant?code=python#getschedulerjobs)| +|`session()`|[getSessionInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getsessioninformation)| +|`uuids()`|[getUuids](https://cloud.ibm.com/apidocs/cloudant?code=python#getuuids)| +|`db.delete()`|[deleteDatabase](https://cloud.ibm.com/apidocs/cloudant?code=python#deletedatabase)| +|`db.metadata()`|[getDatabaseInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getdatabaseinformation)| +|`db.create_document()`|[postDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#postdocument)| +|`db.create()`|[putDatabase](https://cloud.ibm.com/apidocs/cloudant?code=python#putdatabase)| +|`db.all_docs()/db.keys()`|[postAllDocs](https://cloud.ibm.com/apidocs/cloudant?code=python#postalldocs)| +|`db.bulk_docs()`|[postBulkDocs](https://cloud.ibm.com/apidocs/cloudant?code=python#postbulkdocs)| +|`db.changes()/db.infinite_changes()`|[postChanges](https://cloud.ibm.com/apidocs/cloudant?code=python#postchanges)| +|`DesignDocument(db, '_design/doc').delete()`|[deleteDesignDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#deletedesigndocument)| +|`db.get_design_document()/DesignDocument(db, '_design/doc').fetch()`|[getDesignDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#getdesigndocument)| +|`DesignDocument(db, '_design/doc').save()`|[putDesignDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#putdesigndocument)| +|`DesignDocument(db, '_design/doc').info()`|[getDesignDocumentInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getdesigndocumentinformation)| +|`db.get_search_result()`|[postSearch](https://cloud.ibm.com/apidocs/cloudant?code=python#postsearch)| +|`db.get_view_result()`|[postView](https://cloud.ibm.com/apidocs/cloudant?code=python#postview)| +|`db.list_design_documents()`|[postDesignDocs](https://cloud.ibm.com/apidocs/cloudant?code=python#postdesigndocs)| +|`db.get_query_result()`|[postFind](https://cloud.ibm.com/apidocs/cloudant?code=python#postfind)| +|`db.get_query_indexes()`|[getIndexesInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getindexesinformation)| +|`db.create_query_index()`|[postIndex](https://cloud.ibm.com/apidocs/cloudant?code=python#postindex)| +|`db.delete_query_index()`|[deleteIndex](https://cloud.ibm.com/apidocs/cloudant?code=python#deleteindex)| +|`Document(db, 'docid').delete()`|[getLocalDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#getlocaldocument)| +|`Document(db, 'docid').create()`|[putLocalDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#putlocaldocument)| +|`db.missing_revisions()`|[postMissingRevs](https://cloud.ibm.com/apidocs/cloudant?code=python#postmissingrevs)| +|`db.partition_metadata()`|[getPartitionInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getpartitioninformation)| +|`db.partitioned_all_docs()`|[postPartitionAllDocs](https://cloud.ibm.com/apidocs/cloudant?code=python#postpartitionalldocs)| +|`db.get_partitioned_search_result()`|[postPartitionSearch](https://cloud.ibm.com/apidocs/cloudant?code=python#postpartitionsearch)| +|`db.get_partitioned_view_result()`|[postPartitionView](https://cloud.ibm.com/apidocs/cloudant?code=python#postpartitionview)| +|`db.get_partitioned_query_result()`|[postPartitionFind](https://cloud.ibm.com/apidocs/cloudant?code=python#postpartitionfind)| +|`db.revisions_diff()`|[postRevsDiff](https://cloud.ibm.com/apidocs/cloudant?code=python#postrevsdiff)| +|`db.get_security_document()/db.security_document()`|[getSecurity](https://cloud.ibm.com/apidocs/cloudant?code=python#getsecurity)| +|`db.share_database()`|[putSecurity](https://cloud.ibm.com/apidocs/cloudant?code=python#putsecurity)| +|`db.shards()`|[getShardsInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getshardsinformation)| +|`Document(db, 'docid').delete()`|[getLocalDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#getlocaldocument)| +|`Document(db, 'docid').fetch()`|[getDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#getdocument)| +|`Document(db, 'docid').exists()`|[headDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#headdocument)| +|`Document(db, 'docid').save()`|[putDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#putdocument)| +|`Document(db, 'docid').delete_attachment()`|[deleteAttachment](https://cloud.ibm.com/apidocs/cloudant?code=python#deleteattachment)| +|`Document(db, 'docid').get_attachment()`|[getAttachment](https://cloud.ibm.com/apidocs/cloudant?code=python#getattachment)| +|`Document(db, 'docid').put_attachment()`|[putAttachment](https://cloud.ibm.com/apidocs/cloudant?code=python#putattachment)| +|`generate_api_key()`|[postApiKeys](https://cloud.ibm.com/apidocs/cloudant?code=python#postapikeys)| +|`SecurityDocument().save()`|[putCloudantSecurityConfiguration](https://cloud.ibm.com/apidocs/cloudant?code=python#putcloudantsecurityconfiguration)| +|`cors_configuration()/cors_origin()`|[getCorsInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getcorsinformation)| +|`update_cors_configuration()`|[putCorsConfiguration](https://cloud.ibm.com/apidocs/cloudant?code=python#putcorsconfiguration)| diff --git a/README.md b/README.md index 7eaf83ba..f12b2814 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ This is the official Cloudant library for Python. * [Using in Other Projects](#using-in-other-projects) * [License](#license) * [Issues](#issues) +* [Migrating to `cloudant-python-sdk` library](#migrating-to-cloudant-python-sdk-library) ## Installation and Usage @@ -87,3 +88,7 @@ to see if the problem has already been reported. Note that the default search includes only open issues, but it may already have been closed. * Cloudant customers should contact Cloudant support for urgent issues. * When opening a new issue [here in github](../../issues) please complete the template fully. + +## Migrating to `cloudant-python-sdk` library +We have a newly supported Cloudant Python SDK named [cloudant-python-sdk](https://github.com/IBM/cloudant-python-sdk). +For advice on migrating from this module see [MIGRATION.md](MIGRATION.md). From 20cefd5ddd6c2975224ddfd4831de98a238f6c2f Mon Sep 17 00:00:00 2001 From: Esteban Laver Date: Thu, 28 Jan 2021 10:43:30 -0500 Subject: [PATCH 28/52] Update local document operations in migration guide (#489) --- MIGRATION.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 5503e2cc..53845e7d 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -70,8 +70,9 @@ The table below contains a list of `python-cloudant` functions and the `cloudant |`db.get_query_indexes()`|[getIndexesInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getindexesinformation)| |`db.create_query_index()`|[postIndex](https://cloud.ibm.com/apidocs/cloudant?code=python#postindex)| |`db.delete_query_index()`|[deleteIndex](https://cloud.ibm.com/apidocs/cloudant?code=python#deleteindex)| -|`Document(db, 'docid').delete()`|[getLocalDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#getlocaldocument)| -|`Document(db, 'docid').create()`|[putLocalDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#putlocaldocument)| +|`Document(db, '_local/docid').fetch()`|[getLocalDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#getlocaldocument)| +|`Document(db, '_local/docid').save()`|[putLocalDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#putlocaldocument)| +|`Document(db, '_local/docid').delete()`|[deleteLocalDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#deletelocaldocument)| |`db.missing_revisions()`|[postMissingRevs](https://cloud.ibm.com/apidocs/cloudant?code=python#postmissingrevs)| |`db.partition_metadata()`|[getPartitionInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getpartitioninformation)| |`db.partitioned_all_docs()`|[postPartitionAllDocs](https://cloud.ibm.com/apidocs/cloudant?code=python#postpartitionalldocs)| @@ -82,7 +83,7 @@ The table below contains a list of `python-cloudant` functions and the `cloudant |`db.get_security_document()/db.security_document()`|[getSecurity](https://cloud.ibm.com/apidocs/cloudant?code=python#getsecurity)| |`db.share_database()`|[putSecurity](https://cloud.ibm.com/apidocs/cloudant?code=python#putsecurity)| |`db.shards()`|[getShardsInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getshardsinformation)| -|`Document(db, 'docid').delete()`|[getLocalDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#getlocaldocument)| +|`Document(db, 'docid').delete()`|[deleteDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#deletedocument)| |`Document(db, 'docid').fetch()`|[getDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#getdocument)| |`Document(db, 'docid').exists()`|[headDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#headdocument)| |`Document(db, 'docid').save()`|[putDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#putdocument)| From 33781463ff85c8bb535bd6ede2049e4d3bec6c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eigel=20Ildik=C3=B3?= Date: Wed, 10 Feb 2021 16:47:14 +0100 Subject: [PATCH 29/52] Add replication and scheduler operations to the migration guide (#490) --- MIGRATION.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 53845e7d..29be9810 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,4 +1,4 @@ -# Migrating to the `cloudant-python-sdk` library +# Migrating to the `cloudant-python-sdk` library This document is to assist in migrating from the `python-cloudant` (module: `cloudant`) to the newly supported [`cloudant-python-sdk`](https://github.com/IBM/cloudant-python-sdk) (module: `ibmcloudant`). ## Initializing the client connection @@ -11,11 +11,11 @@ There are several ways to create a client connection in `cloudant-python-sdk`: ## Other differences 1. The `cloudant-python-sdk` library does not support local dictionary caching of database and document objects. -1. There are no context managers in `cloudant-python-sdk`. To reproduce the behaviour of the `python-cloudant` -context managers in `cloudant-python-sdk` users need to explicitly call the specific operations against the -remote HTTP API. For example, in the case of the document context manager, this would mean doing both a `get_document` +1. There are no context managers in `cloudant-python-sdk`. To reproduce the behaviour of the `python-cloudant` +context managers in `cloudant-python-sdk` users need to explicitly call the specific operations against the +remote HTTP API. For example, in the case of the document context manager, this would mean doing both a `get_document` to fetch and a `put_document` to save. -1. In `cloudant-python-sdk` View, Search, and Query (aka `_find` endpoint) operation responses contain raw JSON +1. In `cloudant-python-sdk` View, Search, and Query (aka `_find` endpoint) operation responses contain raw JSON content like using `raw_result=True` in `python-cloudant`. ## Request mapping @@ -47,8 +47,11 @@ The table below contains a list of `python-cloudant` functions and the `cloudant |`metadata()`|[getServerInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getserverinformation)| |`all_dbs()`|[getAllDbs](https://cloud.ibm.com/apidocs/cloudant?code=python#getalldbs)| |`db_updates()/infinite_db_updates()`|[getDbUpdates](https://cloud.ibm.com/apidocs/cloudant?code=python#getdbupdates)| -|`Replicator.create_replication()`|[postReplicate](https://cloud.ibm.com/apidocs/cloudant?code=python#postreplicate)| +|`Replicator.stop_replication()`|[deleteReplicationDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#deletereplicationdocument)| +|`Replicator.replication_state()`|[getReplicationDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#getreplicationdocument)| +|`Replicator.create_replication()`|[putReplicationDocument](https://cloud.ibm.com/apidocs/cloudant?code=#putreplicationdocument)| |`Scheduler.get_doc()`|[getSchedulerDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#getschedulerdocument)| +|`Scheduler.list_docs()`|[getSchedulerDocs](https://cloud.ibm.com/apidocs/cloudant?code=python#getschedulerdocs)| |`Scheduler.list_jobs()`|[getSchedulerJobs](https://cloud.ibm.com/apidocs/cloudant?code=python#getschedulerjobs)| |`session()`|[getSessionInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getsessioninformation)| |`uuids()`|[getUuids](https://cloud.ibm.com/apidocs/cloudant?code=python#getuuids)| From 18245a94a8b6551517cf5134efefc5a84b516f59 Mon Sep 17 00:00:00 2001 From: Rich Ellis Date: Fri, 14 May 2021 15:51:34 +0100 Subject: [PATCH 30/52] Make IAM token server configurable in tests --- Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile b/Jenkinsfile index ddf21dd4..d88f017c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -13,6 +13,7 @@ def getEnvForSuite(suiteName) { case 'iam': // Setting IAM_API_KEY forces tests to run using an IAM enabled client. envVars.add("IAM_API_KEY=$DB_IAM_API_KEY") + envVars.add("IAM_TOKEN_URL=$SDKS_TEST_IAM_URL") break case 'cookie': case 'simplejson': From c409805ccb148d2f0da2e3a2bd5e72bbed1ffed9 Mon Sep 17 00:00:00 2001 From: Rich Ellis Date: Fri, 14 May 2021 16:01:03 +0100 Subject: [PATCH 31/52] Add new test server env vars --- Jenkinsfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index d88f017c..e655a45c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,7 +1,7 @@ def getEnvForSuite(suiteName) { // Base environment variables def envVars = [ - "CLOUDANT_ACCOUNT=$DB_USER", + "DB_URL=${SDKS_TEST_SERVER_URL}", "RUN_CLOUDANT_TESTS=1", "SKIP_DB_UPDATES=1" // Disable pending resolution of case 71610 ] @@ -29,8 +29,8 @@ def setupPythonAndTest(pythonVersion, testSuite) { // Unstash the source on this node unstash name: 'source' // Set up the environment and test - withCredentials([usernamePassword(credentialsId: 'clientlibs-test', usernameVariable: 'DB_USER', passwordVariable: 'DB_PASSWORD'), - string(credentialsId: 'clientlibs-test-iam', variable: 'DB_IAM_API_KEY')]) { + withCredentials([usernamePassword(credentialsId: 'testServerLegacy', usernameVariable: 'DB_USER', passwordVariable: 'DB_PASSWORD'), + string(credentialsId: 'testServerIamApiKey', variable: 'DB_IAM_API_KEY')]) { withEnv(getEnvForSuite("${testSuite}")) { try { sh """ From ebcb98c544c2e24b284e0c10970ba3d72c1783d7 Mon Sep 17 00:00:00 2001 From: Rich Ellis Date: Mon, 17 May 2021 12:04:34 +0100 Subject: [PATCH 32/52] Fix client tests to not always use account --- tests/unit/client_tests.py | 40 ++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/tests/unit/client_tests.py b/tests/unit/client_tests.py index 2726de33..07c4159b 100644 --- a/tests/unit/client_tests.py +++ b/tests/unit/client_tests.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (C) 2015, 2020 IBM Corp. All rights reserved. +# Copyright (C) 2015, 2021 IBM Corp. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import sys import unittest from time import sleep +from urllib.parse import urlparse import mock import requests @@ -213,7 +214,7 @@ def test_auto_renew_enabled_with_auto_connect(self): @skip_if_not_cookie_auth def test_session(self): """ - Test getting session information. + Test getting session information. Session info is None if CouchDB Admin Party mode was selected. """ try: @@ -563,7 +564,7 @@ def test_get_cached_db_object_via_get(self): self.client.connect() # Default returns None self.assertIsNone(self.client.get('no_such_db')) - # Creates the database remotely and adds it to the + # Creates the database remotely and adds it to the # client database cache db = self.client.create_database(dbname) # Locally cached database object is returned @@ -702,7 +703,7 @@ def test_cloudant_context_helper(self): Test that the cloudant context helper works as expected. """ try: - with cloudant(self.user, self.pwd, account=self.account) as c: + with cloudant(self.user, self.pwd, url=self.url) as c: self.assertIsInstance(c, Cloudant) self.assertIsInstance(c.r_session, requests.Session) except Exception as err: @@ -718,7 +719,7 @@ def test_cloudant_bluemix_context_helper_with_legacy_creds(self): 'credentials': { 'username': self.user, 'password': self.pwd, - 'host': '{0}.cloudant.com'.format(self.account), + 'host': urlparse(self.url).hostname, 'port': 443, 'url': self.url }, @@ -744,7 +745,7 @@ def test_cloudant_bluemix_context_helper_with_iam(self): 'credentials': { 'apikey': self.iam_api_key, 'username': self.user, - 'host': '{0}.cloudant.com'.format(self.account), + 'host': urlparse(self.url).hostname, 'port': 443, 'url': self.url }, @@ -766,7 +767,7 @@ def test_cloudant_bluemix_context_helper_raise_error_for_missing_iam_and_creds(s instance_name = 'Cloudant NoSQL DB-lv' vcap_services = {'cloudantNoSQLDB': [{ 'credentials': { - 'host': '{0}.cloudant.com'.format(self.account), + 'host': urlparse(self.url).hostname, 'port': 443, 'url': self.url }, @@ -795,7 +796,7 @@ def test_cloudant_bluemix_dedicated_context_helper(self): 'credentials': { 'username': self.user, 'password': self.pwd, - 'host': '{0}.cloudant.com'.format(self.account), + 'host': urlparse(self.url).hostname, 'port': 443, 'url': self.url }, @@ -818,10 +819,10 @@ def test_constructor_with_account(self): """ # Ensure that the client is new del self.client - self.client = Cloudant(self.user, self.pwd, account=self.account) + self.client = Cloudant('user', 'pass', account='foo') self.assertEqual( self.client.server_url, - 'https://{0}.cloudant.com'.format(self.account) + 'https://foo.cloudant.com' ) @skip_if_not_cookie_auth @@ -835,7 +836,7 @@ def test_bluemix_constructor_with_legacy_creds(self): 'credentials': { 'username': self.user, 'password': self.pwd, - 'host': '{0}.cloudant.com'.format(self.account), + 'host': urlparse(self.url).hostname, 'port': 443, 'url': self.url }, @@ -870,7 +871,7 @@ def test_bluemix_constructor_with_iam(self): 'credentials': { 'apikey': self.iam_api_key, 'username': self.user, - 'host': '{0}.cloudant.com'.format(self.account), + 'host': urlparse(self.url).hostname, 'port': 443 }, 'name': instance_name @@ -901,7 +902,7 @@ def test_bluemix_constructor_specify_instance_name(self): 'credentials': { 'username': self.user, 'password': self.pwd, - 'host': '{0}.cloudant.com'.format(self.account), + 'host': urlparse(self.url).hostname, 'port': 443, 'url': self.url }, @@ -934,7 +935,7 @@ def test_bluemix_constructor_with_multiple_services(self): { 'credentials': { 'apikey': '1234api', - 'host': '{0}.cloudant.com'.format(self.account), + 'host': urlparse(self.url).hostname, 'port': 443, 'url': self.url }, @@ -973,10 +974,11 @@ def test_connect_headers(self): """ try: self.client.connect() - self.assertEqual( - self.client.r_session.headers['X-Cloudant-User'], - self.account - ) + if (self.account): + self.assertEqual( + self.client.r_session.headers['X-Cloudant-User'], + self.account + ) agent = self.client.r_session.headers.get('User-Agent') ua_parts = agent.split('/') self.assertEqual(len(ua_parts), 6) @@ -1413,4 +1415,4 @@ def test_update_cors_configuration(self): self.client.disconnect() if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() From 0f031cde8a1ea6c6b91ecfc353d9aad9242a8e50 Mon Sep 17 00:00:00 2001 From: Rich Ellis Date: Thu, 20 May 2021 14:38:45 +0100 Subject: [PATCH 33/52] Correct use of username as account name in `Cloudant.bluemix()` --- CHANGES.md | 2 ++ src/cloudant/client.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ad1d3788..8885ae26 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,8 @@ - [FIXED] Fixed the documentation for `bookmarks`. - [FIXED] Also exit `follow_replication` for `failed` state. - [FIXED] Fixed result paging for grouped view queries. +- [FIXED] Incorrect use of username as account name in `Cloudant.bluemix()`. +- [IMPROVED] Documented use of None account name and url override for `Cloudant.iam()`. # 2.14.0 (2020-08-17) diff --git a/src/cloudant/client.py b/src/cloudant/client.py index f09d3a69..47c8555c 100755 --- a/src/cloudant/client.py +++ b/src/cloudant/client.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2015, 2019 IBM Corp. All rights reserved. +# Copyright (c) 2015, 2021 IBM Corp. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -853,7 +853,7 @@ def bluemix(cls, vcap_services, instance_name=None, service_name=None, **kwargs) raise CloudantClientException(103) if hasattr(service, 'iam_api_key'): - return Cloudant.iam(service.username, + return Cloudant.iam(None, service.iam_api_key, url=service.url, **kwargs) @@ -867,7 +867,7 @@ def iam(cls, account_name, api_key, **kwargs): """ Create a Cloudant client that uses IAM authentication. - :param account_name: Cloudant account name. + :param account_name: Cloudant account name; or use None and a url kwarg. :param api_key: IAM authentication API key. """ return cls(None, From 5f3ca45c442847046f3a5dfd34f6cfcd5b5929f5 Mon Sep 17 00:00:00 2001 From: Jan Janssen Date: Sun, 30 May 2021 16:45:38 +0200 Subject: [PATCH 34/52] Include LICENSE in pip package --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 96ebb5e1..7c050d91 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -include requirements.txt VERSION +include requirements.txt VERSION LICENSE From 105fe2055562adbeb7c5ae41f97712794bf8716b Mon Sep 17 00:00:00 2001 From: Esteban Laver Date: Thu, 3 Jun 2021 09:28:45 -0400 Subject: [PATCH 35/52] Remove CouchDB 1.x from Travis CI (#502) --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b49557a3..9f770aac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,6 @@ python: env: - ADMIN_PARTY=true COUCHDB_VERSION=2.3.1 - ADMIN_PARTY=false COUCHDB_VERSION=2.3.1 - - ADMIN_PARTY=true COUCHDB_VERSION=1.7.2 - - ADMIN_PARTY=false COUCHDB_VERSION=1.7.2 services: - docker From f0c0883080a4dcf403a8f3b6e717a4de224f965b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eigel=20Ildik=C3=B3?= Date: Wed, 16 Jun 2021 15:05:39 +0200 Subject: [PATCH 36/52] Use custom encoder (if provided) for all view `key` params not just `keys` (#501) --- CHANGES.md | 1 + src/cloudant/_common_util.py | 11 +++++++---- tests/unit/database_tests.py | 9 +++++++++ tests/unit/param_translation_tests.py | 14 +++++++------- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8885ae26..522d5ede 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ - [FIXED] Fixed result paging for grouped view queries. - [FIXED] Incorrect use of username as account name in `Cloudant.bluemix()`. - [IMPROVED] Documented use of None account name and url override for `Cloudant.iam()`. +- [FIXED] Use custom encoder (if provided) for all view `key` params not just `keys`. # 2.14.0 (2020-08-17) diff --git a/src/cloudant/_common_util.py b/src/cloudant/_common_util.py index 6e765c1d..962f1dec 100644 --- a/src/cloudant/_common_util.py +++ b/src/cloudant/_common_util.py @@ -160,7 +160,7 @@ def feed_arg_types(feed_type): return _COUCH_DB_UPDATES_ARG_TYPES return _CHANGES_ARG_TYPES -def python_to_couch(options): +def python_to_couch(options, encoder=None): """ Translates query options from python style options into CouchDB/Cloudant query options. For example ``{'include_docs': True}`` will @@ -171,13 +171,14 @@ def python_to_couch(options): :func:`~cloudant.view.View.__call__` callable, both used to retrieve data. :param dict options: Python style parameters to be translated. + :param encoder: Custom encoder, defaults to None :returns: Dictionary of translated CouchDB/Cloudant query parameters """ translation = dict() for key, val in iteritems_(options): py_to_couch_validate(key, val) - translation.update(_py_to_couch_translate(key, val)) + translation.update(_py_to_couch_translate(key, val, encoder)) return translation def py_to_couch_validate(key, val): @@ -201,7 +202,7 @@ def py_to_couch_validate(key, val): if val not in ('ok', 'update_after'): raise CloudantArgumentError(135, val) -def _py_to_couch_translate(key, val): +def _py_to_couch_translate(key, val, encoder=None): """ Performs the conversion of the Python parameter value to its CouchDB equivalent. @@ -209,6 +210,8 @@ def _py_to_couch_translate(key, val): try: if key in ['keys', 'endkey_docid', 'startkey_docid', 'stale', 'update']: return {key: val} + if key in ['endkey', 'key', 'startkey']: + return {key: json.dumps(val, cls=encoder)} if val is None: return {key: None} arg_converter = TYPE_CONVERTERS.get(type(val)) @@ -249,7 +252,7 @@ def get_docs(r_session, url, encoder=None, headers=None, **params): keys = None if keys_list is not None: keys = json.dumps({'keys': keys_list}, cls=encoder) - f_params = python_to_couch(params) + f_params = python_to_couch(params, encoder) resp = None if keys is not None: # If we're using POST we are sending JSON so add the header diff --git a/tests/unit/database_tests.py b/tests/unit/database_tests.py index c8c5cfe0..a4e4bad1 100644 --- a/tests/unit/database_tests.py +++ b/tests/unit/database_tests.py @@ -512,6 +512,15 @@ def test_all_docs_get_with_long_type(self): data = self.db.all_docs(limit=1, skip=LONG_NUMBER) self.assertEqual(len(data.get('rows')), 1) + def test_all_docs_get_uses_custom_encoder(self): + """ + Test that all_docs uses the custom encoder. + """ + self.set_up_client(auto_connect=True, encoder="AEncoder") + database = self.client[self.test_dbname] + with self.assertRaises(CloudantArgumentError): + database.all_docs(endkey=['foo', 10]) + def test_custom_result_context_manager(self): """ Test using the database custom result context manager diff --git a/tests/unit/param_translation_tests.py b/tests/unit/param_translation_tests.py index 4da0d05d..6aac7210 100644 --- a/tests/unit/param_translation_tests.py +++ b/tests/unit/param_translation_tests.py @@ -36,7 +36,7 @@ def test_valid_descending(self): {'descending': 'true'} ) self.assertEqual( - python_to_couch({'descending': False}), + python_to_couch({'descending': False}), {'descending': 'false'} ) @@ -44,9 +44,9 @@ def test_valid_endkey(self): """ Test endkey translation is successful. """ - self.assertEqual(python_to_couch({'endkey': 10}), {'endkey': 10}) + self.assertEqual(python_to_couch({'endkey': 10}), {'endkey': '10'}) # Test with long type - self.assertEqual(python_to_couch({'endkey': LONG_NUMBER}), {'endkey': LONG_NUMBER}) + self.assertEqual(python_to_couch({'endkey': LONG_NUMBER}), {'endkey': str(LONG_NUMBER)}) self.assertEqual( python_to_couch({'endkey': 'foo'}), {'endkey': '"foo"'} @@ -120,9 +120,9 @@ def test_valid_key(self): """ Test key translation is successful. """ - self.assertEqual(python_to_couch({'key': 10}), {'key': 10}) + self.assertEqual(python_to_couch({'key': 10}), {'key': '10'}) # Test with long type - self.assertEqual(python_to_couch({'key': LONG_NUMBER}), {'key': LONG_NUMBER}) + self.assertEqual(python_to_couch({'key': LONG_NUMBER}), {'key': str(LONG_NUMBER)}) self.assertEqual(python_to_couch({'key': 'foo'}), {'key': '"foo"'}) self.assertEqual( python_to_couch({'key': ['foo', 10]}), @@ -194,9 +194,9 @@ def test_valid_startkey(self): """ Test startkey translation is successful. """ - self.assertEqual(python_to_couch({'startkey': 10}), {'startkey': 10}) + self.assertEqual(python_to_couch({'startkey': 10}), {'startkey': '10'}) # Test with long type - self.assertEqual(python_to_couch({'startkey': LONG_NUMBER}), {'startkey': LONG_NUMBER}) + self.assertEqual(python_to_couch({'startkey': LONG_NUMBER}), {'startkey': str(LONG_NUMBER)}) self.assertEqual( python_to_couch({'startkey': 'foo'}), {'startkey': '"foo"'} From 19b14ab9adb38d84885587b71c56a66a7789893c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eigel=20Ildik=C3=B3?= Date: Mon, 21 Jun 2021 09:48:15 +0200 Subject: [PATCH 37/52] Support boolean type for `key`, `endkey`, and `startkey` in view requests (#504) --- CHANGES.md | 1 + src/cloudant/_common_util.py | 11 ++++++----- tests/unit/param_translation_tests.py | 18 +++++++++++++++--- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 522d5ede..56700503 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ - [FIXED] Incorrect use of username as account name in `Cloudant.bluemix()`. - [IMPROVED] Documented use of None account name and url override for `Cloudant.iam()`. - [FIXED] Use custom encoder (if provided) for all view `key` params not just `keys`. +- [FIXED] Support boolean type for `key`, `endkey`, and `startkey` in view requests. # 2.14.0 (2020-08-17) diff --git a/src/cloudant/_common_util.py b/src/cloudant/_common_util.py index 962f1dec..adce56fa 100644 --- a/src/cloudant/_common_util.py +++ b/src/cloudant/_common_util.py @@ -56,20 +56,20 @@ RESULT_ARG_TYPES = { 'descending': (bool,), - 'endkey': (int, LONGTYPE, STRTYPE, Sequence,), + 'endkey': (int, LONGTYPE, STRTYPE, Sequence, bool,), 'endkey_docid': (STRTYPE,), 'group': (bool,), 'group_level': (int, LONGTYPE, NONETYPE,), 'include_docs': (bool,), 'inclusive_end': (bool,), - 'key': (int, LONGTYPE, STRTYPE, Sequence,), + 'key': (int, LONGTYPE, STRTYPE, Sequence, bool,), 'keys': (list,), 'limit': (int, LONGTYPE, NONETYPE,), 'reduce': (bool,), 'skip': (int, LONGTYPE, NONETYPE,), 'stable': (bool,), 'stale': (STRTYPE,), - 'startkey': (int, LONGTYPE, STRTYPE, Sequence,), + 'startkey': (int, LONGTYPE, STRTYPE, Sequence, bool,), 'startkey_docid': (STRTYPE,), 'update': (STRTYPE,), } @@ -191,12 +191,13 @@ def py_to_couch_validate(key, val): # Validate argument values and ensure that a boolean is not passed in # if an integer is expected if (not isinstance(val, RESULT_ARG_TYPES[key]) or - (type(val) is bool and int in RESULT_ARG_TYPES[key])): + (type(val) is bool and bool not in RESULT_ARG_TYPES[key] and + int in RESULT_ARG_TYPES[key])): raise CloudantArgumentError(117, key, RESULT_ARG_TYPES[key]) if key == 'keys': for key_list_val in val: if (not isinstance(key_list_val, RESULT_ARG_TYPES['key']) or - type(key_list_val) is bool): + isinstance(key_list_val, bool)): raise CloudantArgumentError(134, RESULT_ARG_TYPES['key']) if key == 'stale': if val not in ('ok', 'update_after'): diff --git a/tests/unit/param_translation_tests.py b/tests/unit/param_translation_tests.py index 6aac7210..fda3c002 100644 --- a/tests/unit/param_translation_tests.py +++ b/tests/unit/param_translation_tests.py @@ -55,6 +55,10 @@ def test_valid_endkey(self): python_to_couch({'endkey': ['foo', 10]}), {'endkey': '["foo", 10]'} ) + self.assertEqual( + python_to_couch({'endkey': True}), + {'endkey': 'true'} + ) def test_valid_endkey_docid(self): """ @@ -128,6 +132,10 @@ def test_valid_key(self): python_to_couch({'key': ['foo', 10]}), {'key': '["foo", 10]'} ) + self.assertEqual( + python_to_couch({'key': True}), + {'key': 'true'} + ) def test_valid_keys(self): """ @@ -205,6 +213,10 @@ def test_valid_startkey(self): python_to_couch({'startkey': ['foo', 10]}), {'startkey': '["foo", 10]'} ) + self.assertEqual( + python_to_couch({'startkey': True}), + {'startkey': 'true'} + ) def test_valid_startkey_docid(self): """ @@ -247,7 +259,7 @@ def test_invalid_endkey(self): """ msg = 'Argument endkey not instance of expected type:' with self.assertRaises(CloudantArgumentError) as cm: - python_to_couch({'endkey': True}) + python_to_couch({'endkey': {'foo': 'bar'}}) self.assertTrue(str(cm.exception).startswith(msg)) def test_invalid_endkey_docid(self): @@ -302,7 +314,7 @@ def test_invalid_key(self): """ msg = 'Argument key not instance of expected type:' with self.assertRaises(CloudantArgumentError) as cm: - python_to_couch({'key': True}) + python_to_couch({'key': {'foo': 'bar'}}) self.assertTrue(str(cm.exception).startswith(msg)) def test_invalid_keys_not_list(self): @@ -372,7 +384,7 @@ def test_invalid_startkey(self): """ msg = 'Argument startkey not instance of expected type:' with self.assertRaises(CloudantArgumentError) as cm: - python_to_couch({'startkey': True}) + python_to_couch({'startkey': {'foo': 'bar'}}) self.assertTrue(str(cm.exception).startswith(msg)) def test_invalid_startkey_docid(self): From 436eddb45093a6f8aab26f778a7ed5aabc6aba5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eigel=20Ildik=C3=B3?= Date: Mon, 21 Jun 2021 16:02:48 +0200 Subject: [PATCH 38/52] Override dict's get method (#505) Co-authored-by: dominickj-tdi <49956725+dominickj-tdi@users.noreply.github.com> --- CHANGES.md | 1 + src/cloudant/database.py | 18 ++++++++++++++ tests/unit/database_tests.py | 46 ++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 56700503..5b641708 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ - [IMPROVED] Documented use of None account name and url override for `Cloudant.iam()`. - [FIXED] Use custom encoder (if provided) for all view `key` params not just `keys`. - [FIXED] Support boolean type for `key`, `endkey`, and `startkey` in view requests. +- [NEW] Override `dict.get` method for `CouchDatabase` to add `remote` parameter allowing it to retrieve a remote document if specified. # 2.14.0 (2020-08-17) diff --git a/src/cloudant/database.py b/src/cloudant/database.py index ccde12da..4ad93f90 100644 --- a/src/cloudant/database.py +++ b/src/cloudant/database.py @@ -706,6 +706,24 @@ def __getitem__(self, key): raise KeyError(key) return doc + def get(self, key, remote=False): + """ + Overrides dict's get method. This gets an item from the database or cache + like __getitem__, but instead of throwing an exception if the item is not + found, it simply returns None. + + :param bool remote: Dictates whether a remote request is made to + retrieve the doc, if it is not present in the local cache. + Defaults to False. + """ + if remote: + try: + return self.__getitem__(key) + except KeyError: + return None + else: + return super(CouchDatabase, self).get(key) + def __contains__(self, key): """ Overrides dictionary __contains__ behavior to check if a document diff --git a/tests/unit/database_tests.py b/tests/unit/database_tests.py index a4e4bad1..3ba12584 100644 --- a/tests/unit/database_tests.py +++ b/tests/unit/database_tests.py @@ -256,6 +256,8 @@ def test_create_document_with_id(self): data = {'_id': 'julia06', 'name': 'julia', 'age': 6} doc = self.db.create_document(data) self.assertEqual(self.db['julia06'], doc) + self.assertEqual(self.db.get('julia06'), doc) + self.assertEqual(self.db.get('julia06', remote=True), doc) self.assertEqual(doc['_id'], data['_id']) self.assertTrue(doc['_rev'].startswith('1-')) self.assertEqual(doc['name'], data['name']) @@ -271,6 +273,42 @@ def test_create_document_with_id(self): 'Document with id julia06 already exists.' ) + def test_get_non_existing_document_from_remote(self): + """ + Test dict's get on non existing document from remote. + """ + doc = self.db.get('non-existing', remote=True) + self.assertIsNone(doc) + + def test_get_non_existing_document_from_cache(self): + """ + Test dict's get on non existing document from cache. + """ + doc = self.db.get('non-existing') + self.assertIsNone(doc) + + def test_get_document_from_cache(self): + """ + Test dict's get on a document from cache. + """ + doc = Document(self.db, document_id='julia06') + self.db['julia06'] = doc + self.assertEqual(self.db.get('julia06'), doc) + # doc is fetched from the local dict preferentially to remote even with remote=True + self.assertEqual(self.db.get('julia06', remote=True), doc) + self.assertEqual(self.db['julia06'], doc) + + def test_get_document_from_remote(self): + """ + Test dict's get on a document from remote. + """ + data = {'_id': 'julia06','name': 'julia06', 'age': 6} + doc = self.db.create_document(data) + self.db.clear() + self.assertIsNone(self.db.get('julia06')) + self.assertEqual(self.db.get('julia06', remote=True), doc) + self.assertEqual(self.db['julia06'], doc) + def test_create_document_that_already_exists(self): """ Test creating a document that already exists @@ -278,6 +316,8 @@ def test_create_document_that_already_exists(self): data = {'_id': 'julia'} doc = self.db.create_document(data) self.assertEqual(self.db['julia'], doc) + self.assertEqual(self.db.get('julia'), doc) + self.assertEqual(self.db.get('julia', remote=True), doc) self.assertTrue(doc['_rev'].startswith('1-')) # attempt to recreate document self.db.create_document(data, throw_on_exists=False) @@ -289,6 +329,8 @@ def test_create_document_without_id(self): data = {'name': 'julia', 'age': 6} doc = self.db.create_document(data) self.assertEqual(self.db[doc['_id']], doc) + self.assertEqual(self.db.get(doc['_id']), doc) + self.assertEqual(self.db.get(doc['_id'], remote=True), doc) self.assertTrue(doc['_rev'].startswith('1-')) self.assertEqual(doc['name'], data['name']) self.assertEqual(doc['age'], data['age']) @@ -302,6 +344,8 @@ def test_create_design_document(self): data = {'_id': '_design/julia06', 'name': 'julia', 'age': 6} doc = self.db.create_document(data) self.assertEqual(self.db['_design/julia06'], doc) + self.assertEqual(self.db.get('_design/julia06'), doc) + self.assertEqual(self.db.get('_design/julia06', remote=True), doc) self.assertEqual(doc['_id'], data['_id']) self.assertTrue(doc['_rev'].startswith('1-')) self.assertEqual(doc['name'], data['name']) @@ -316,6 +360,8 @@ def test_create_empty_document(self): """ empty_doc = self.db.new_document() self.assertEqual(self.db[empty_doc['_id']], empty_doc) + self.assertEqual(self.db.get(empty_doc['_id']), empty_doc) + self.assertEqual(self.db.get(empty_doc['_id'], remote=True), empty_doc) self.assertTrue(all(x in ['_id', '_rev'] for x in empty_doc.keys())) self.assertTrue(empty_doc['_rev'].startswith('1-')) From 66e4b188030bfa8ed4a71a0a8980f34fd8f34153 Mon Sep 17 00:00:00 2001 From: Rich Ellis Date: Thu, 3 Jun 2021 16:30:22 +0100 Subject: [PATCH 39/52] Add deprecation warnings Co-authored-by: Esteban Laver --- CHANGES.md | 10 ++++++---- README.md | 14 ++++++++++++++ src/cloudant/__init__.py | 4 ++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5b641708..21fa1a50 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,13 +1,15 @@ # UNRELEASED -- [REMOVED] Removed Python 2 compatibility from the supported environments. +- [NEW] Override `dict.get` method for `CouchDatabase` to add `remote` parameter allowing it to + retrieve a remote document if specified. - [FIXED] Fixed the documentation for `bookmarks`. - [FIXED] Also exit `follow_replication` for `failed` state. - [FIXED] Fixed result paging for grouped view queries. - [FIXED] Incorrect use of username as account name in `Cloudant.bluemix()`. -- [IMPROVED] Documented use of None account name and url override for `Cloudant.iam()`. - [FIXED] Use custom encoder (if provided) for all view `key` params not just `keys`. - [FIXED] Support boolean type for `key`, `endkey`, and `startkey` in view requests. -- [NEW] Override `dict.get` method for `CouchDatabase` to add `remote` parameter allowing it to retrieve a remote document if specified. +- [DEPRECATED] This library is now deprecated and will be EOL on Dec 31 2021. +- [REMOVED] Removed Python 2 compatibility from the supported environments. +- [IMPROVED] Documented use of `None` account name and url override for `Cloudant.iam()`. # 2.14.0 (2020-08-17) @@ -139,7 +141,7 @@ # 2.2.0 (2016-10-20) -- [NEW] Added auto connect feature to the client constructor.
 +- [NEW] Added auto connect feature to the client constructor. - [FIXED] Requests session is no longer valid after disconnect. # 2.1.1 (2016-10-03) diff --git a/README.md b/README.md index f12b2814..029edf07 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ +# DEPRECATED + +**This library is now deprecated and will be end-of-life on Dec 31 2021.** + +The library remains supported until the end-of-life date, +but will receive only _critical_ maintenance updates. + +Please see the [Migration Guide](./MIGRATION.md) for advice +about migrating to our replacement library +[cloudant-python-sdk](https://github.com/IBM/cloudant-python-sdk). + +For FAQs and additional information please refer to the +[Cloudant blog](https://blog.cloudant.com/2021/06/30/Cloudant-SDK-Transition.html). + # Cloudant Python Client [![Build Status](https://travis-ci.org/cloudant/python-cloudant.svg?branch=master)](https://travis-ci.org/cloudant/python-cloudant) diff --git a/src/cloudant/__init__.py b/src/cloudant/__init__.py index 7e011096..f3910174 100644 --- a/src/cloudant/__init__.py +++ b/src/cloudant/__init__.py @@ -19,10 +19,14 @@ # pylint: disable=wrong-import-position import contextlib +import warnings # pylint: disable=wrong-import-position from .client import Cloudant, CouchDB from ._common_util import CloudFoundryService +warnings.warn('The module cloudant is now deprecated. The replacement is ibmcloudant.', + DeprecationWarning) + @contextlib.contextmanager def cloudant(user, passwd, **kwargs): """ From a166650f795852e506adf3c758a612300fb28392 Mon Sep 17 00:00:00 2001 From: Esteban Laver Date: Wed, 28 Jul 2021 00:34:32 -0400 Subject: [PATCH 40/52] Add validation for doc ID and attachment names --- CHANGES.md | 5 + Jenkinsfile | 2 +- src/cloudant/_common_util.py | 28 +- src/cloudant/_messages.py | 6 +- src/cloudant/design_document.py | 9 +- src/cloudant/document.py | 24 +- tests/unit/document_validation_tests.py | 909 ++++++++++++++++++++++++ 7 files changed, 973 insertions(+), 10 deletions(-) create mode 100644 tests/unit/document_validation_tests.py diff --git a/CHANGES.md b/CHANGES.md index 21fa1a50..66d1f820 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,11 @@ - [DEPRECATED] This library is now deprecated and will be EOL on Dec 31 2021. - [REMOVED] Removed Python 2 compatibility from the supported environments. - [IMPROVED] Documented use of `None` account name and url override for `Cloudant.iam()`. +- [IMPROVED] - Document IDs and attachment names are now rejected if they could cause an unexpected + Cloudant request. We have seen that some applications pass unsantized document IDs to SDK functions + (e.g. direct from user requests). In response to this we have updated many functions to reject + obviously invalid paths. However, for complete safety applications must still validate that + document IDs and attachment names match expected patterns. # 2.14.0 (2020-08-17) diff --git a/Jenkinsfile b/Jenkinsfile index e655a45c..cc2d0ad4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -25,7 +25,7 @@ def getEnvForSuite(suiteName) { } def setupPythonAndTest(pythonVersion, testSuite) { - node { + node('sdks-executor') { // Unstash the source on this node unstash name: 'source' // Set up the environment and test diff --git a/src/cloudant/_common_util.py b/src/cloudant/_common_util.py index adce56fa..433e09a6 100644 --- a/src/cloudant/_common_util.py +++ b/src/cloudant/_common_util.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (C) 2015, 2019 IBM Corp. All rights reserved. +# Copyright © 2015, 2021 IBM Corp. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -31,6 +31,8 @@ # Library Constants +DESIGN_PREFIX = '_design/' +LOCAL_PREFIX = '_local/' USER_AGENT = '/'.join([ 'python-cloudant', sys.modules['cloudant'].__version__, @@ -301,6 +303,30 @@ def response_to_json_dict(response, **kwargs): response.encoding = 'utf-8' return json.loads(response.text, **kwargs) +def assert_document_type_id(docid): + """ + Validate the document ID. Raises an error if the ID is an `_` prefixed name + that isn't either `_design` or `_local`. + :return: + """ + invalid = False + if docid.startswith('_'): + if docid.startswith(DESIGN_PREFIX) and DESIGN_PREFIX != docid: + invalid = False + elif docid.startswith(LOCAL_PREFIX) and LOCAL_PREFIX != docid: + invalid = False + else: + invalid = True + if invalid: + raise CloudantArgumentError(137, docid) + +def assert_attachment_name(attname): + """ + Validate the document attachment's name. Raises an error if `_` prefixed name exists. + :return: + """ + if attname.startswith('_'): + raise CloudantArgumentError(138, attname) # Classes diff --git a/src/cloudant/_messages.py b/src/cloudant/_messages.py index 07a23d62..b428998c 100644 --- a/src/cloudant/_messages.py +++ b/src/cloudant/_messages.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2016 IBM. All rights reserved. +# Copyright © 2016, 2021 IBM Corp. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -65,7 +65,9 @@ # Common_util 134: 'Key list element not of expected type: {0}', 135: 'Invalid value for stale option {0} must be ok or update_after.', - 136: 'Error converting argument {0}: {1}' + 136: 'Error converting argument {0}: {1}', + 137: 'Invalid document ID: {0}', + 138: 'Invalid attachment name: {0}' } CLIENT = { diff --git a/src/cloudant/design_document.py b/src/cloudant/design_document.py index d3ea387a..df9fb44d 100644 --- a/src/cloudant/design_document.py +++ b/src/cloudant/design_document.py @@ -16,7 +16,8 @@ API module/class for interacting with a design document in a database. """ from ._2to3 import iteritems_, url_quote_plus, STRTYPE -from ._common_util import QUERY_LANGUAGE, codify, response_to_json_dict +from ._common_util import QUERY_LANGUAGE, codify, response_to_json_dict, \ + assert_document_type_id, DESIGN_PREFIX from .document import Document from .view import View, QueryIndexView from .error import CloudantArgumentError, CloudantDesignDocumentException @@ -44,8 +45,10 @@ class DesignDocument(Document): databases. """ def __init__(self, database, document_id=None, partitioned=False): - if document_id and not document_id.startswith('_design/'): - document_id = '_design/{0}'.format(document_id) + if document_id: + assert_document_type_id(document_id) + if document_id and not document_id.startswith(DESIGN_PREFIX): + document_id = '{0}{1}'.format(DESIGN_PREFIX, document_id) super(DesignDocument, self).__init__(database, document_id) if partitioned: diff --git a/src/cloudant/document.py b/src/cloudant/document.py index 38082054..96267bc9 100644 --- a/src/cloudant/document.py +++ b/src/cloudant/document.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (C) 2015, 2018 IBM Corp. All rights reserved. +# Copyright © 2015, 2021 IBM Corp. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ from requests.exceptions import HTTPError from ._2to3 import url_quote, url_quote_plus -from ._common_util import response_to_json_dict +from ._common_util import response_to_json_dict, assert_document_type_id, assert_attachment_name from .error import CloudantDocumentException @@ -96,6 +96,15 @@ def document_url(self): url_quote(self['_id'][8:], safe='') )) + # handle _local document url + if self['_id'].startswith('_local/'): + return '/'.join(( + self._database_host, + url_quote_plus(self._database_name), + '_local', + url_quote(self['_id'][7:], safe='') + )) + # handle document url return '/'.join(( self._database_host, @@ -113,6 +122,8 @@ def exists(self): if '_id' not in self or self['_id'] is None: return False + assert_document_type_id(self['_id']) + resp = self.r_session.head(self.document_url) if resp.status_code not in [200, 404]: resp.raise_for_status() @@ -161,6 +172,8 @@ def fetch(self): """ if self.document_url is None: raise CloudantDocumentException(101) + if '_id' in self: + assert_document_type_id(self['_id']) resp = self.r_session.get(self.document_url) resp.raise_for_status() self.clear() @@ -309,6 +322,8 @@ def delete(self): if not self.get("_rev"): raise CloudantDocumentException(103) + assert_document_type_id(self['_id']) + del_resp = self.r_session.delete( self.document_url, params={"rev": self["_rev"]}, @@ -375,7 +390,8 @@ def get_attachment( """ # need latest rev self.fetch() - attachment_url = '/'.join((self.document_url, attachment)) + assert_attachment_name(attachment) + attachment_url = '/'.join((self.document_url, url_quote(attachment, safe=''))) if headers is None: headers = {'If-Match': self['_rev']} else: @@ -418,6 +434,7 @@ def delete_attachment(self, attachment, headers=None): """ # need latest rev self.fetch() + assert_attachment_name(attachment) attachment_url = '/'.join((self.document_url, attachment)) if headers is None: headers = {'If-Match': self['_rev']} @@ -459,6 +476,7 @@ def put_attachment(self, attachment, content_type, data, headers=None): """ # need latest rev self.fetch() + assert_attachment_name(attachment) attachment_url = '/'.join((self.document_url, attachment)) if headers is None: headers = { diff --git a/tests/unit/document_validation_tests.py b/tests/unit/document_validation_tests.py new file mode 100644 index 00000000..021d8d6f --- /dev/null +++ b/tests/unit/document_validation_tests.py @@ -0,0 +1,909 @@ +#!/usr/bin/env python +# Copyright © 2021 IBM Corp. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import unittest +from enum import Enum +from unittest.mock import Mock, patch + +from mock import create_autospec + +import requests +from urllib.parse import urlparse + +from cloudant import database +from cloudant.design_document import DesignDocument +from cloudant.document import Document +from cloudant.error import CloudantArgumentError + +class ValidationExceptionMsg(Enum): + DOC = 'Invalid document ID:' + ATTACHMENT = 'Invalid attachment name:' + +class Expect(Enum): + VALIDATION_EXCEPTION_DOCID = CloudantArgumentError(137, '') + VALIDATION_EXCEPTION_ATT = CloudantArgumentError(138, '') + RESPONSE_404 = 404 + RESPONSE_200 = 200 + RESPONSE_201 = 201 + + +class ValidationTests(unittest.TestCase): + """ + Document validation unit tests + """ + def setUp(self): + self.doc_r_session_patcher = patch('cloudant.document.Document.r_session') + self.requests_get_patcher = patch('requests.get') + + self.addCleanup(patch.stopall) + + self.doc_r_session_mock = self.doc_r_session_patcher.start() + self.requests_get_mock = self.requests_get_patcher.start() + + self.db = create_autospec(database) + self.db.client = Mock() + self.db.client.server_url = 'http://mocked.url.com' + self.db.database_url = 'http://mocked.url.com/my_db' + self.db.database_name = 'mydb' + + def teardown(self): + self.addCleanup(patch.stopall) + del self.db + del self.doc_r_session_patcher + del self.requests_get_patcher + del self.doc_r_session_mock + del self.requests_get_mock + + # GET and HEAD _all_docs + # EXPECTED: validation failure + def test_get_invalid_all_docs(self): + """ + Test GET/HEAD request for invalid '_all_docs' document ID + """ + self.get_document_variants('_all_docs', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # GET and HEAD _design/foo + # EXPECTED: 200 + def test_get_valid_ddoc(self): + """ + Test GET/HEAD request for valid '_design/foo' document ID + """ + self.get_document_variants('_design/foo', Expect.RESPONSE_200.value, path_segment_count=3) + self.get_document_variants('_design/foo', Expect.RESPONSE_200.value, True, path_segment_count=3) + + # GET and HEAD _design + # EXPECTED: Validation exception + def test_get_invalid_design(self): + """ + Test GET/HEAD request for invalid '_design' document ID + """ + self.get_document_variants('_design', Expect.VALIDATION_EXCEPTION_DOCID.value) + self.get_document_variants('_design', Expect.VALIDATION_EXCEPTION_DOCID.value, True) + + # GET and HEAD /_design/foo with a slash + # EXPECTED: 404 + def test_get_missing_ddoc_with_slash(self): + """ + Test GET/HEAD request for missing '/_design/foo' document ID + """ + self.get_document_variants('/_design/foo', Expect.RESPONSE_404.value, path_segment_count=2) + + # GET and HEAD _design/foo/_view/bar + # EXPECTED: 404 + def test_get_invalid_view(self): + """ + Test GET/HEAD request for missing '_design/foo' document ID + """ + self.get_document_variants('_design/foo/_view/bar', Expect.RESPONSE_404.value, path_segment_count=3) + self.get_document_variants('_design/foo/_view/bar', Expect.RESPONSE_404.value, True, path_segment_count=3) + + # GET and HEAD _design/foo/_info + # EXPECTED: 404 + def test_get_invalid_view_info(self): + """ + Test GET/HEAD request for missing '_design/foo/_info' document ID + """ + self.get_document_variants('_design/foo/_info', Expect.RESPONSE_404.value, path_segment_count=3) + self.get_document_variants('_design/foo/_info', Expect.RESPONSE_404.value, True, path_segment_count=3) + + # GET and HEAD _design/foo/_search/bar + # EXPECTED: 404 + def test_get_invalid_search(self): + """ + Test GET/HEAD request for missing '_design/foo/_search/bar' document ID + """ + self.get_document_variants('_design/foo/_search/bar', Expect.RESPONSE_404.value, path_segment_count=3) + self.get_document_variants('_design/foo/_search/bar', Expect.RESPONSE_404.value, True, path_segment_count=3) + + # GET and HEAD _design/foo/_search_info/bar + # EXPECTED: 404 + def test_get_invalid_search_info(self): + """ + Test GET/HEAD request for missing '_design/foo/_search_info/bar' document ID + """ + self.get_document_variants('_design/foo/_search_info/bar', Expect.RESPONSE_404.value, path_segment_count=3) + self.get_document_variants('_design/foo/_search_info/bar', Expect.RESPONSE_404.value, True, path_segment_count=3) + + # GET and HEAD _design/foo/_geo/bar + # EXPECTED: 404 + def test_get_missing_geo(self): + """ + Test GET/HEAD request for missing '_design/foo/_geo/bar' document ID + """ + self.get_document_variants('_design/foo/_geo/bar', Expect.RESPONSE_404.value, path_segment_count=3) + self.get_document_variants('_design/foo/_geo/bar', Expect.RESPONSE_404.value, True, path_segment_count=3) + # with a parameter + self.get_document_variants('_design/foo/_geo/bar?bbox=-50.52,-4.46,54.59,1.45', Expect.RESPONSE_404.value, + path_segment_count=3) + self.get_document_variants('_design/foo/_geo/bar?bbox=-50.52,-4.46,54.59,1.45', Expect.RESPONSE_404.value, True, + path_segment_count=3) + + # GET and HEAD _design/foo/_geo_info/bar + # EXPECTED: 404 + def test_get_missing_geo_info(self): + """ + Test GET/HEAD request for missing '_design/foo/_geo_info/bar' document ID + """ + self.get_document_variants('_design/foo/_geo_info/bar', Expect.RESPONSE_404.value, path_segment_count=3) + self.get_document_variants('_design/foo/_geo_info/bar', Expect.RESPONSE_404.value, True, path_segment_count=3) + + # GET and HEAD _local/foo + # EXPECTED: 200 + def test_get_local_doc(self): + """ + Test GET/HEAD request for valid '_local/foo' document ID + """ + self.get_document_variants('_local/foo', Expect.RESPONSE_200.value, path_segment_count=3) + + # GET and HEAD _local + # EXPECTED: Validation exception + def test_get_invalid_local(self): + """ + Test GET/HEAD request for invalid '_local' document ID + """ + self.get_document_variants('_local', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # GET and HEAD _local_docs + # EXPECTED: Validation exception + def test_get_invalid_local_docs(self): + """ + Test GET/HEAD request for invalid '_local_docs' document ID + """ + self.get_document_variants('_local_docs', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # GET and HEAD _design_docs + # EXPECTED: Validation exception + def test_get_invalid_design_docs(self): + """ + Test GET/HEAD request for invalid '_design_docs' document ID + """ + self.get_document_variants('_design_docs', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # GET and HEAD _changes + # EXPECTED: Validation exception + def test_get_invalid_changes(self): + """ + Test GET/HEAD request for invalid '_changes' document ID + """ + self.get_document_variants('_changes', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # GET and HEAD _ensure_full_commit + # EXPECTED: Validation exception + def test_get_invalid_ensure_full_commit(self): + """ + Test GET/HEAD request for invalid '_ensure_full_commit' document ID + """ + self.get_document_variants('_ensure_full_commit', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # GET and HEAD _index + # EXPECTED: Validation exception + def test_get_invalid_index(self): + """ + Test GET/HEAD request for invalid '_index' document ID + """ + self.get_document_variants('_index', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # GET and HEAD _revs_limit + # EXPECTED: Validation exception + def test_get_invalid_revs_limit(self): + """ + Test GET/HEAD request for invalid '_revs_limit' document ID + """ + self.get_document_variants('_revs_limit', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # GET and HEAD _security + # EXPECTED: Validation exception + def test_get_invalid_security(self): + """ + Test GET/HEAD request for invalid '_security' document ID + """ + self.get_document_variants('_security', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # GET and HEAD _shards + # EXPECTED: Validation exception + def test_get_invalid_shards(self): + """ + Test GET/HEAD request for invalid '_shards' document ID + """ + self.get_document_variants('_shards', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # DELETE _index/_design/foo/json/bar + # EXPECTED: Validation exception + def test_delete_invalid_index(self): + """ + Test DELETE request for invalid '_index/_design/foo/json/bar' document ID + """ + self.delete_document_variants('_index/_design/foo/json/bar', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # DELETE _design/foo + # EXPECTED: 200 + def test_delete_valid_ddoc(self): + """ + Test DELETE request for valid '_design/foo' document ID + """ + self.delete_document_variants('_design/foo', Expect.RESPONSE_200.value, path_segment_count=3) + + # DELETE _design + # EXPECTED: Validation exception + def test_delete_invalid_ddoc(self): + """ + Test DELETE request for invalid '_design' document ID + """ + # no trailing '/' on _design prefix + self.delete_document_variants('_design', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # DELETE _local/foo + # EXPECTED: 200 + def test_delete_valid_local_doc(self): + """ + Test DELETE request for valid '_local/foo' document ID + """ + self.delete_document_variants('_local/foo', Expect.RESPONSE_200.value, path_segment_count=3) + + # DELETE _local + # EXPECTED: Validation exception + def test_delete_invalid_local(self): + """ + Test DELETE request for invalid '_local' document ID + """ + # no trailing '/' on _local prefix + self.delete_document_variants('_local', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # PUT _design/foo + # EXPECTED: 201 + def test_put_valid_ddoc(self): + """ + Test PUT request for valid '_design/foo' document ID + """ + self.put_document_variants('_design/foo', Expect.RESPONSE_201.value, path_segment_count=3) + + # PUT _design + # EXPECTED: Validation exception + def test_put_invalid_ddoc(self): + """ + Test PUT request for invalid '_design' document ID + """ + self.put_document_variants('_design', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # PUT _local/foo + # EXPECTED: 201 + def test_put_valid_local_doc(self): + """ + Test PUT request for valid '_local/foo' document ID + """ + self.put_document_variants('_local/foo', Expect.RESPONSE_201.value, path_segment_count=3) + + # PUT _local + # EXPECTED: Validation exception + def test_put_invalid_local_doc(self): + """ + Test PUT request for invalid '_local' document ID + """ + self.put_document_variants('_local', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # PUT _revs_limit + # EXPECTED: Validation exception + def test_put_invalid_revs_limit(self): + """ + Test PUT request for invalid '_revs_limit' document ID + """ + self.put_document_variants('_revs_limit', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # PUT _security + # EXPECTED: Validation exception + def test_put_invalid_security(self): + """ + Test PUT request for invalid '_security' document ID + """ + self.put_document_variants('_security', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # GET _design/foo/bar + # EXPECTED: 200 + def test_get_valid_ddoc_attachment(self): + """ + Test PUT request for valid '_design/foo/bar' document ID + """ + self.get_doc_attachment_variants('_design/foo', 'bar', Expect.RESPONSE_200.value, True, path_segment_count=4) + + # PUT _design/foo/bar + # EXPECTED: 201 + def test_put_valid_ddoc_attachment(self): + """ + Test PUT request for valid '_design/foo/bar' document ID + """ + self.put_doc_attachment_variants('_design/foo', 'bar', Expect.RESPONSE_201.value, True, path_segment_count=4) + + # DELETE _design/foo/bar + # EXPECTED: 200 + def test_delete_valid_ddoc_attachment(self): + """ + Test DELETE request for valid '_design/foo/bar' document ID + """ + self.delete_doc_attachment_variants('_design/foo', 'bar', Expect.RESPONSE_200.value, True, path_segment_count=4) + + # GET _design/foo + # EXPECTED: Validation exception + def test_get_invalid_ddoc_attachment(self): + """ + Test GET request for invalid '_design/foo' document ID + """ + # with ddoc option enabled + self.get_doc_attachment_variants('_design', 'foo', Expect.VALIDATION_EXCEPTION_DOCID.value, True) + self.get_doc_attachment_variants('_design', 'foo', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # PUT _design/foo + # EXPECTED: Validation exception + def test_put_invalid_ddoc_attachment(self): + """ + Test PUT request for invalid '_design/foo' document ID + """ + # with ddoc option enabled + self.put_doc_attachment_variants('_design', 'foo', Expect.VALIDATION_EXCEPTION_DOCID.value, True) + self.put_doc_attachment_variants('_design', 'foo', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # DELETE _design/foo + # EXPECTED: Validation exception + def test_delete_invalid_ddoc_attachment(self): + """ + Test DELETE request for invalid '_design/foo' document ID + """ + # with ddoc option enabled + self.delete_doc_attachment_variants('_design', 'foo', Expect.VALIDATION_EXCEPTION_DOCID.value, True) + self.delete_doc_attachment_variants('_design', 'foo', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # DELETE _index/_design/foo/json/bar + # EXPECTED: Validation exception + def test_delete_index_via_attachment(self): + """ + Test DELETE requests for invalid '_index/_design/foo/json/bar' + """ + self.delete_doc_attachment_variants('_index', '_design/foo/json/bar', Expect.VALIDATION_EXCEPTION_DOCID.value) + self.delete_doc_attachment_variants('_index', '_design/foo/json/bar', + Expect.VALIDATION_EXCEPTION_DOCID.value, True) + self.delete_doc_attachment_variants('_index/_design', 'foo/json/bar', Expect.VALIDATION_EXCEPTION_DOCID.value) + self.delete_doc_attachment_variants('_index/_design', 'foo/json/bar', + Expect.VALIDATION_EXCEPTION_DOCID.value, True) + self.delete_doc_attachment_variants('_index/_design/foo', 'json/bar', Expect.VALIDATION_EXCEPTION_DOCID.value) + self.delete_doc_attachment_variants('_index/_design/foo', 'json/bar', + Expect.VALIDATION_EXCEPTION_DOCID.value, True) + self.delete_doc_attachment_variants('_index/_design/foo/json', 'bar', Expect.VALIDATION_EXCEPTION_DOCID.value) + self.delete_doc_attachment_variants('_index/_design/foo/json', 'bar', + Expect.VALIDATION_EXCEPTION_DOCID.value, True) + + # GET _design/foo/_view/bar + def test_get_view_via_ddoc_attachment(self): + """ + Test GET requests for '_design/foo/_view/bar' + """ + # EXPECTED: 404 + self.get_doc_attachment_variants('_design/foo/_view', 'bar', Expect.RESPONSE_404.value, path_segment_count=4) + self.get_doc_attachment_variants('_design/foo/_view', 'bar', Expect.RESPONSE_404.value, True, path_segment_count=4) + self.get_doc_attachment_variants('_design/foo', '/_view/bar', Expect.RESPONSE_404.value, path_segment_count=4) + self.get_doc_attachment_variants('_design/foo', '/_view/bar', Expect.RESPONSE_404.value, True, path_segment_count=4) + # EXPECTED: Validation exception + self.get_doc_attachment_variants('_design/foo', '_view/bar', Expect.VALIDATION_EXCEPTION_ATT.value) + self.get_doc_attachment_variants('_design/foo', '_view/bar', Expect.VALIDATION_EXCEPTION_ATT.value, True) + self.get_doc_attachment_variants('_design', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value) + self.get_doc_attachment_variants('_design', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value, True) + self.get_doc_attachment_variants('_design/', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value) + self.get_doc_attachment_variants('_design/', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value, True) + + # PUT _design/foo/_view/bar + def test_put_view_via_ddoc_attachment(self): + """ + Test PUT requests for '_design/foo/_view/bar' + """ + # EXPECTED: Validation exception + self.put_doc_attachment_variants('_design/foo', '_view/bar', Expect.VALIDATION_EXCEPTION_ATT.value) + self.put_doc_attachment_variants('_design/foo', '_view/bar', Expect.VALIDATION_EXCEPTION_ATT.value, True) + self.put_doc_attachment_variants('_design', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value) + self.put_doc_attachment_variants('_design', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value, True) + self.put_doc_attachment_variants('_design/', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value) + self.put_doc_attachment_variants('_design/', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value, True) + + # DELETE _design/foo/_view/bar + def test_delete_view_via_ddoc_attachment(self): + """ + Test DELETE requests for '_design/foo/_view/bar' + """ + # EXPECTED: Validation exception + self.delete_doc_attachment_variants('_design/foo', '_view/bar', Expect.VALIDATION_EXCEPTION_ATT.value) + self.delete_doc_attachment_variants('_design/foo', '_view/bar', Expect.VALIDATION_EXCEPTION_ATT.value, True) + self.delete_doc_attachment_variants('_design', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value) + self.delete_doc_attachment_variants('_design', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value, True) + self.delete_doc_attachment_variants('_design/', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value) + self.delete_doc_attachment_variants('_design/', 'foo/_view/bar', Expect.VALIDATION_EXCEPTION_DOCID.value, True) + + # GET _design/foo/_info + def test_get_view_info_via_ddoc_attachment(self): + """ + Test GET requests for '_design/foo/_info' + """ + # EXPECTED: Validation exception + self.get_doc_attachment_variants('_design/foo', '_info', Expect.VALIDATION_EXCEPTION_ATT.value) + self.get_doc_attachment_variants('_design/foo', '_info', Expect.VALIDATION_EXCEPTION_ATT.value, True) + self.get_doc_attachment_variants('_design', 'foo/_info', Expect.VALIDATION_EXCEPTION_DOCID.value) + self.get_doc_attachment_variants('_design', 'foo/_info', Expect.VALIDATION_EXCEPTION_DOCID.value, True) + self.get_doc_attachment_variants('_design/', 'foo/_info', Expect.VALIDATION_EXCEPTION_DOCID.value) + self.get_doc_attachment_variants('_design/', 'foo/_info', Expect.VALIDATION_EXCEPTION_DOCID.value, True) + + # GET _design/foo/_search/bar + def test_get_search_via_ddoc_attachment(self): + """ + Test GET requests for '_design/foo/_search/bar' + """ + # EXPECTED: 404 + self.get_doc_attachment_variants('_design/foo/_search', 'bar', Expect.RESPONSE_404.value, path_segment_count=4) + self.get_doc_attachment_variants('_design/foo/_search', 'bar', Expect.RESPONSE_404.value, True, + path_segment_count=4) + self.get_doc_attachment_variants('_design/foo/_search', 'bar?q=*.*', Expect.RESPONSE_404.value, + path_segment_count=4) + self.get_doc_attachment_variants('_design/foo/_search', 'bar?q=*.*', Expect.RESPONSE_404.value, True, + path_segment_count=4) + # EXPECTED: Validation exception + self.get_doc_attachment_variants('_design/foo', '_search/bar', Expect.VALIDATION_EXCEPTION_ATT.value) + self.get_doc_attachment_variants('_design/foo', '_search/bar', Expect.VALIDATION_EXCEPTION_ATT.value, True) + self.get_doc_attachment_variants('_design', 'foo/_search/bar', Expect.VALIDATION_EXCEPTION_DOCID.value) + self.get_doc_attachment_variants('_design', 'foo/_search/bar', Expect.VALIDATION_EXCEPTION_DOCID.value, True) + self.get_doc_attachment_variants('_design/', 'foo/_search/bar', Expect.VALIDATION_EXCEPTION_DOCID.value) + self.get_doc_attachment_variants('_design/', 'foo/_search/bar', Expect.VALIDATION_EXCEPTION_DOCID.value, True) + + # GET _design/foo/_search_info/bar + def test_get_search_info_via_ddoc_attachment(self): + """ + Test GET requests for '_design/foo/_search_info/bar' + """ + # EXPECTED: 404 + self.get_doc_attachment_variants('_design/foo/_search_info', 'bar', Expect.RESPONSE_404.value, + path_segment_count=4) + self.get_doc_attachment_variants('_design/foo/_search_info', 'bar', Expect.RESPONSE_404.value, True, + path_segment_count=4) + # EXPECTED: Validation exception + self.get_doc_attachment_variants('_design/foo', '_search_info/bar', Expect.VALIDATION_EXCEPTION_ATT.value) + self.get_doc_attachment_variants('_design/foo', '_search_info/bar', Expect.VALIDATION_EXCEPTION_ATT.value, True) + + # GET _design/foo/_geo/bar + def test_get_geo_via_ddoc_attachment(self): + """ + Test GET requests for '_design/foo/_geo/bar' + """ + # EXPECTED: 404 + self.get_doc_attachment_variants('_design/foo/_geo', 'bar', Expect.RESPONSE_404.value, path_segment_count=4) + self.get_doc_attachment_variants('_design/foo/_geo', 'bar', Expect.RESPONSE_404.value, True, + path_segment_count=4) + self.get_doc_attachment_variants('_design/foo/_geo', 'bar?bbox=-50.52,-4.46,54.59,1.45', + Expect.RESPONSE_404.value, path_segment_count=4) + self.get_doc_attachment_variants('_design/foo/_geo', 'bar?bbox=-50.52,-4.46,54.59,1.45', + Expect.RESPONSE_404.value, True, path_segment_count=4) + # EXPECTED: Validation exception + self.get_doc_attachment_variants('_design/foo', '_geo/bar', Expect.VALIDATION_EXCEPTION_ATT.value) + self.get_doc_attachment_variants('_design/foo', '_geo/bar', Expect.VALIDATION_EXCEPTION_ATT.value, True) + + # GET _design/foo/_geo_info/bar + def test_get_geo_info_via_ddoc_attachment(self): + """ + Test GET requests for '_design/foo/_geo_info/bar' + """ + # EXPECTED: 404 + self.get_doc_attachment_variants('_design/foo/_geo_info', 'bar', Expect.RESPONSE_404.value, + path_segment_count=4) + self.get_doc_attachment_variants('_design/foo/_geo_info', 'bar', Expect.RESPONSE_404.value, True, + path_segment_count=4) + # EXPECTED: Validation exception + self.get_doc_attachment_variants('_design/foo', '_geo_info/bar', Expect.VALIDATION_EXCEPTION_ATT.value) + self.get_doc_attachment_variants('_design/foo', '_geo_info/bar', Expect.VALIDATION_EXCEPTION_ATT.value, True) + + # GET _partition/foo + # EXPECTED: Validation exception + def test_get_invalid_partition_info(self): + """ + Test GET requests for '_partition/foo' + """ + self.get_document_variants('_partition/foo', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # GET _partition/foo + # EXPECTED: Validation exception + def test_get_invalid_partition_info_via_attachment(self): + """ + Test GET requests for '_partition/foo' + """ + self.get_doc_attachment_variants('_partition', 'foo', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # GET _partition/foo/_all_docs + # EXPECTED: Validation exception + def test_get_partition_info(self): + """ + Test GET requests for '_partition/foo/_all_docs' + """ + self.get_document_variants('_partition/foo/_all_docs', Expect.VALIDATION_EXCEPTION_DOCID.value) + + # GET _partition/foo/_all_docs + # EXPECTED: Validation exception + def test_get_invalid_partition_all_docs_via_attachment(self): + """ + Test GET requests for '_partition/foo/_all_docs' + """ + self.get_doc_attachment_variants('_partition', 'foo/_all_docs', Expect.VALIDATION_EXCEPTION_DOCID.value) + self.get_doc_attachment_variants('_partition/foo', '_all_docs', Expect.VALIDATION_EXCEPTION_DOCID.value) + + """UTIL FUNCTIONS""" + def mocked_get_requests(self, rev=None, override_status_code=None): + """ + Create a mock GET request for documents with the expected status code + :param rev: the doc's revision (default None) + :param override_status_code: override the status code for handling + inner `fetch` request call within `get_attachment` + :return: mocked Response object + """ + resp_mock = create_autospec(requests.Response) + if override_status_code is not None: + resp_mock.status_code = override_status_code + else: + resp_mock.status_code = self.expected_enum + if (resp_mock.status_code == 200 or resp_mock.status_code == 201 + and self.doc_id is not None): + if rev is not None: + resp_mock.text = f"""{{"_id": "{self.doc_id}", "_rev": "{rev}"}}""" + else: + resp_mock.text = f"""{{"_id": "{self.doc_id}", "_rev": "1-abc"}}""" + elif resp_mock.status_code == 404: + resp_mock.raise_for_status.side_effect = requests.exceptions.HTTPError + resp_mock.encoding = None + + return resp_mock + + def mocked_get_att_requests(self): + """ + Create a mock GET request for attachments with the expected status code + """ + self.expected_att_content = f"""this is a text attachment""" + # first fetch doc call with rev + fetch_mock = self.mocked_get_requests(rev=None, override_status_code=200) + # second get to attachment + resp_mock = create_autospec(requests.Response) + resp_mock.status_code = self.expected_enum + if self.expected_enum == 200 and self.doc_id is not None and self.att_name is not None: + resp_mock.text = self.expected_att_content + if self.expected_enum == 404: + resp_mock.raise_for_status.side_effect = requests.exceptions.HTTPError + self.doc_r_session_mock.get.side_effect = [fetch_mock, resp_mock] + + def mocked_head_requests(self, override_status_code=None): + """ + Create a mock HEAD request for documents and attachments with the expected status code + """ + resp_mock = create_autospec(requests.Response) + if override_status_code is not None: + resp_mock.status_code = override_status_code + else: + resp_mock.status_code = self.expected_enum + self.doc_r_session_mock.head = Mock(return_value=resp_mock) + + def mocked_delete_requests(self): + """ + Create a mock DELETE request for documents with the expected status code + """ + resp_mock = create_autospec(requests.Response) + resp_mock.status_code = self.expected_enum + if self.expected_enum == 201 and self.doc_id is not None: + resp_mock.text = f"""{{"id": "{self.doc_id}", "rev": "2-abc", "ok": true}}""" + self.doc_r_session_mock.delete = Mock(return_value=resp_mock) + + def mocked_delete_att_requests(self): + """ + Create a mock DELETE request for attachments with the expected status code + """ + # first `fetch` document call with rev + self.doc_r_session_mock.get = Mock(return_value=self.mocked_get_requests(rev=None, override_status_code=200)) + # second delete to attachment + resp_mock = create_autospec(requests.Response) + resp_mock.status_code = self.expected_enum + resp_mock.encoding = None + if self.expected_enum == 200 and self.doc_id is not None and self.att_name is not None: + resp_mock.text = f"""{{"id": "{self.doc_id}", "rev": "2-abc", "ok": true}}""" + elif self.expected_enum == 404: + resp_mock.raise_for_status.side_effect = requests.exceptions.HTTPError + + self.doc_r_session_mock.delete = Mock(return_value=resp_mock) + + def mocked_put_doc_requests(self): + """ + Create a mock PUT request for documents with the expected status code + """ + # mock 'doc.exists' request call within 'doc.save' function + self.mocked_head_requests(200) + resp_mock = create_autospec(requests.Response) + resp_mock.status_code = self.expected_enum + resp_mock.encoding = None + if self.expected_enum == 201 and self.doc_id is not None: + resp_mock.text = f"""{{"id": "{self.doc_id}", "rev": "1-abc", "ok": true}}""" + if self.expected_enum == 404: + resp_mock.raise_for_status.side_effect = requests.exceptions.HTTPError + self.doc_r_session_mock.put = Mock(return_value=resp_mock) + + def mocked_put_att_requests(self): + """ + Create a mock PUT request for attachments with the expected status code + """ + # first `fetch` document call within `put_attachment` + fetch_mock = self.mocked_get_requests(rev=None, override_status_code=200) + # create Response object for PUT attachment + resp_mock = create_autospec(requests.Response) + resp_mock.status_code = self.expected_enum + resp_mock.encoding = None + if self.expected_enum == 201 and self.doc_id is not None: + resp_mock.text = f"""{{"id": "{self.doc_id}", "rev": "2-def", "ok": true}}""" + if self.expected_enum == 404: + resp_mock.raise_for_status.side_effect = requests.exceptions.HTTPError + # final fetch doc call + second_fetch_mock = self.mocked_get_requests(rev='2-def', override_status_code=200) + self.doc_r_session_mock.get.side_effect = [fetch_mock, second_fetch_mock] + self.doc_r_session_mock.put = Mock(return_value=resp_mock) + + def get_document_variants(self, doc_id, expected_enum, is_ddoc=False, + path_segment_count=None): + """ + Function to setup mock requests and execute GET/HEAD document requests + """ + self.doc_id = doc_id + self.expected_enum = expected_enum + self.is_ddoc = is_ddoc + self.mocked_head_requests() + self.head_document() + self.doc_r_session_mock.get.return_value = self.mocked_get_requests() + self.fetch_document() + self.assert_path_segments(self.doc_r_session_mock.get.call_args_list, path_segment_count) + + def get_doc_attachment_variants(self, doc_id, att_name, expected_enum, is_ddoc=False, + path_segment_count=None): + """ + Function to setup mock requests and execute GET attachment requests + """ + self.att_name = att_name + self.doc_id = doc_id + self.expected_enum = expected_enum + self.is_ddoc = is_ddoc + self.mocked_get_att_requests() + self.get_doc_attachment() + self.assert_path_segments(self.doc_r_session_mock.get.call_args_list, path_segment_count) + + def put_document_variants(self, doc_id, expected_enum, is_ddoc=False, + path_segment_count=None): + """ + Function to setup mock requests and execute PUT document requests + """ + self.doc_id = doc_id + self.expected_enum = expected_enum + self.is_ddoc = is_ddoc + self.mocked_put_doc_requests() + self.put_document() + self.assert_path_segments(self.doc_r_session_mock.put.call_args_list, path_segment_count) + + def put_doc_attachment_variants(self, doc_id, att_name, expected_enum, is_ddoc=False, + path_segment_count=None): + """ + Function to setup mock requests and execute PUT attachment requests + """ + self.att_name = att_name + self.doc_id = doc_id + self.expected_enum = expected_enum + self.is_ddoc = is_ddoc + self.mocked_put_att_requests() + self.put_doc_attachment() + self.assert_path_segments(self.doc_r_session_mock.put.call_args_list, path_segment_count) + + def delete_document_variants(self, doc_id, expected_enum, is_ddoc=False, + path_segment_count=None): + """ + Function to setup mock requests and execute DELETE document requests + """ + self.doc_id = doc_id + self.expected_enum = expected_enum + self.is_ddoc = is_ddoc + self.mocked_delete_requests() + self.delete_document() + self.assert_path_segments(self.doc_r_session_mock.delete.call_args_list, path_segment_count) + + def delete_doc_attachment_variants(self, doc_id, attname, expected_enum, is_ddoc=False, + path_segment_count=None): + """ + Function to setup mock requests and execute DELETE attachment requests + """ + self.doc_id = doc_id + self.att_name = attname + self.expected_enum = expected_enum + self.is_ddoc = is_ddoc + self.mocked_delete_att_requests() + self.delete_doc_attachment() + self.assert_path_segments(self.doc_r_session_mock.delete.call_args_list, path_segment_count) + + """HTTP REQUEST FUNCTIONS""" + def head_document(self): + try: + resp = self.create_doc(self.doc_id, self.is_ddoc).exists() + if self.expected_enum == 200 or self.expected_enum == 201: + self.assertTrue(resp) + elif self.expected_enum == 404: + self.assertFalse(resp) + except CloudantArgumentError as cae: + self.assert_exception_msg(cae) + + def delete_document(self): + try: + doc = self.create_doc(self.doc_id, self.is_ddoc) + doc['_rev'] = '1-abc' + doc.delete() + self.assertTrue(isinstance(self.expected_enum, int), + f"""Expected value {self.expected_enum} is not an int status code.""") + self.assertTrue(self.expected_enum < 400, + f"""Expected value {self.expected_enum} is not a successful status code.""") + self.assertEqual(self.doc_id, doc['_id']) + self.assertFalse('rev' in doc) + except CloudantArgumentError as cae: + self.assert_exception_msg(cae) + except requests.exceptions.HTTPError as err: + self.assertTrue(id(self.expected_enum), id(err)) + + def fetch_document(self): + try: + doc = self.create_doc(self.doc_id, self.is_ddoc) + doc.fetch() + self.assertTrue(isinstance(self.expected_enum, int), + f"""Expected value {self.expected_enum} is not an int status code.""") + self.assertTrue(self.expected_enum < 400, + f"""Expected value {self.expected_enum} is not a successful status code.""") + self.assertEqual(self.doc_id, doc['_id']) + self.assertIsNotNone(doc['_rev']) + except CloudantArgumentError as cae: + self.assert_exception_msg(cae) + except requests.exceptions.HTTPError as err: + self.assertTrue(id(self.expected_enum), id(err)) + + def put_document(self): + try: + doc = self.create_doc(self.doc_id, self.is_ddoc) + doc.save() + self.assertTrue(isinstance(self.expected_enum, int), + f"""Expected value {self.expected_enum} is not an int status code.""") + self.assertTrue(self.expected_enum < 400, + f"""Expected value {self.expected_enum} is not a successful status code.""") + self.assertEqual(self.doc_id, doc['_id']) + self.assertIsNotNone(doc['_rev']) + except CloudantArgumentError as cae: + self.assert_exception_msg(cae) + except requests.exceptions.HTTPError as err: + self.assertTrue(id(self.expected_enum), id(err)) + + def delete_doc_attachment(self): + try: + doc = self.create_doc(self.doc_id, self.is_ddoc) + doc['_rev'] = '1-abc' + resp = doc.delete_attachment(self.att_name) + self.assertTrue(isinstance(self.expected_enum, int), + f"""Expected value {self.expected_enum} is not an int status code.""") + self.assertTrue(self.expected_enum < 400, + f"""Expected value {self.expected_enum} is not a successful status code.""") + self.assertEqual(self.doc_id, doc['_id']) + self.assertEqual(self.doc_id, resp['id']) + self.assertEqual(doc['_rev'], resp['rev']) + except CloudantArgumentError as cae: + self.assert_exception_msg(cae) + except requests.exceptions.HTTPError as err: + self.assertTrue(id(self.expected_enum), id(err)) + + def get_doc_attachment(self): + try: + doc = self.create_doc(self.doc_id, self.is_ddoc) + resp_att = doc.get_attachment(self.att_name, attachment_type='text') + self.assertTrue(isinstance(self.expected_enum, int), + f"""Expected value {self.expected_enum} is not an int status code.""") + self.assertTrue(self.expected_enum < 400, + f"""Expected value {self.expected_enum} is not a successful status code.""") + self.assertEqual(self.doc_id, doc['_id']) + self.assertIsNotNone(resp_att) + self.assertEqual(resp_att, self.expected_att_content) + except CloudantArgumentError as cae: + self.assert_exception_msg(cae) + except requests.exceptions.HTTPError as err: + self.assertTrue(id(self.expected_enum), id(err)) + + def put_doc_attachment(self): + try: + doc = self.create_doc(self.doc_id, self.is_ddoc) + resp_att = doc.put_attachment(self.att_name, content_type='utf-8', data='test') + self.assertIsNotNone(resp_att) + self.assertTrue(isinstance(self.expected_enum, int), + f"""Expected value {self.expected_enum} is not an int status code.""") + self.assertTrue(self.expected_enum < 400, + f"""Expected value {self.expected_enum} is not a successful status code.""") + self.assertEqual(self.doc_id, resp_att['id']) + self.assertEqual(resp_att['id'], doc['_id']) + self.assertEqual(doc['_rev'], resp_att['rev']) + self.assertEqual(resp_att['ok'], True) + except CloudantArgumentError as cae: + self.assert_exception_msg(cae) + except requests.exceptions.HTTPError as err: + self.assertTrue(id(self.expected_enum), id(err)) + + """HELPER FUNCTIONS""" + def create_doc(self, doc_id=None, is_ddoc=False): + """ + Function to create and return a Document or DesignDocument object. + """ + if is_ddoc: + if doc_id is not None: + doc = DesignDocument(self.db, doc_id) + else: + doc = DesignDocument(self.db) + elif doc_id is not None: + doc = Document(self.db, doc_id) + else: + doc = Document(self.db) + self.assertIsNone(doc.get('_rev')) + return doc + + def assert_exception_msg(self, cae): + """ + Function to assert whether the exception message is for an invalid + document ID or an attachment name. + """ + self.assertTrue(id(self.expected_enum), id(cae)) + # Check that actual exception message starts with the expected msg + if str(cae).startswith(str(self.expected_enum)): + # Figure out which exception msg to assert against + if str(cae).startswith(ValidationExceptionMsg.ATTACHMENT.value): + self.assertEqual(str(cae), f"""{ValidationExceptionMsg.ATTACHMENT.value} {self.att_name}""") + elif str(cae).startswith(ValidationExceptionMsg.DOC.value): + self.assertEqual(str(cae), f"""{ValidationExceptionMsg.DOC.value} {self.doc_id}""") + else: + self.fail('Expected CloudantArgumentError message should equal actual error message.') + + def assert_path_segments(self, actual_call_args_list, exp_segment_count): + """ + Function to assert the number of path segments from a mock request + """ + # If there's no segment count, verify that the test case expects an argument error + if exp_segment_count is None: + self.assertTrue(isinstance(self.expected_enum, CloudantArgumentError), 'Path segment count should exist ' + 'when testing against valid ' + 'document or attachment names.') + else: + # get latest call in list + url, headers = actual_call_args_list[len(actual_call_args_list) - 1] + # there should only be one mocked url + self.assertEqual(len(url), 1) + # parse path of url and remove first / path segment + path = urlparse(url[0]).path[1:] + actual_segment_count = len(path.split('/')) + self.assertEqual(actual_segment_count, exp_segment_count) From f05f0921716684c4aac0c61bedbe7cb7b0823655 Mon Sep 17 00:00:00 2001 From: Rich Ellis Date: Thu, 26 Aug 2021 10:53:20 +0100 Subject: [PATCH 41/52] Update CHANGES for 2.15.0 --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 66d1f820..d5cf74e5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -# UNRELEASED +# 2.15.0 (2021-08-26) - [NEW] Override `dict.get` method for `CouchDatabase` to add `remote` parameter allowing it to retrieve a remote document if specified. - [FIXED] Fixed the documentation for `bookmarks`. From 760c8c99373b7b02dc32cc2ff994071ed4938997 Mon Sep 17 00:00:00 2001 From: Rich Ellis Date: Thu, 26 Aug 2021 10:55:33 +0100 Subject: [PATCH 42/52] Update version to 2.15.0 --- VERSION | 2 +- docs/conf.py | 4 ++-- src/cloudant/__init__.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index 20738ec2..68e69e40 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.14.1-SNAPSHOT +2.15.0 diff --git a/docs/conf.py b/docs/conf.py index 1e338e6d..403f5bd7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '2.14.1-SNAPSHOT' +version = '2.15.0' # The full version, including alpha/beta/rc tags. -release = '2.14.1-SNAPSHOT' +release = '2.15.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/src/cloudant/__init__.py b/src/cloudant/__init__.py index f3910174..720b8709 100644 --- a/src/cloudant/__init__.py +++ b/src/cloudant/__init__.py @@ -15,7 +15,7 @@ """ Cloudant / CouchDB Python client library API package """ -__version__ = '2.14.1-SNAPSHOT' +__version__ = '2.15.0' # pylint: disable=wrong-import-position import contextlib From 3bb26d75fa255802a5f308bbf9cff1ba3b34439b Mon Sep 17 00:00:00 2001 From: Rich Ellis Date: Thu, 26 Aug 2021 12:17:13 +0100 Subject: [PATCH 43/52] Update verson to 2.15.1-SNAPSHOT --- VERSION | 2 +- docs/conf.py | 4 ++-- src/cloudant/__init__.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index 68e69e40..3a7d90b0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.15.0 +2.15.1-SNAPSHOT diff --git a/docs/conf.py b/docs/conf.py index 403f5bd7..c8d4ad26 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '2.15.0' +version = '2.15.1-SNAPSHOT' # The full version, including alpha/beta/rc tags. -release = '2.15.0' +release = '2.15.1-SNAPSHOT' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/src/cloudant/__init__.py b/src/cloudant/__init__.py index 720b8709..66769dad 100644 --- a/src/cloudant/__init__.py +++ b/src/cloudant/__init__.py @@ -15,7 +15,7 @@ """ Cloudant / CouchDB Python client library API package """ -__version__ = '2.15.0' +__version__ = '2.15.1-SNAPSHOT' # pylint: disable=wrong-import-position import contextlib From c2ffcd6393e9115acc02c81135424dd9325cdfd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eigel=20Ildik=C3=B3?= Date: Tue, 21 Dec 2021 10:56:10 +0100 Subject: [PATCH 44/52] Improve migration guide (#509) --- MIGRATION.md | 109 ++++++++++++++++++++++++++------------------------- 1 file changed, 56 insertions(+), 53 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 29be9810..21c9ae5f 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -17,6 +17,10 @@ remote HTTP API. For example, in the case of the document context manager, this to fetch and a `put_document` to save. 1. In `cloudant-python-sdk` View, Search, and Query (aka `_find` endpoint) operation responses contain raw JSON content like using `raw_result=True` in `python-cloudant`. +1. Replay adapters are replaced by the [automatic retries](https://github. + com/IBM/ibm-cloud-sdk-common/#automatic-retries) feature for failed requests. +1. Error handling is not transferable from `python-cloudant` to `cloudant-python-sdk`. For more information go to the [Error handling section](https://cloud.ibm.com/apidocs/cloudant?code=python#error-handling) in our API docs. +1. Custom HTTP client configurations in `python-cloudant` are not transferable to `python-java-sdk`. For more information go to the [Configuring the HTTP client section(https://githubcom/IBM/ibm-cloud-sdk-common/#configuring-the-http-client) in the IBM Cloud SDK Common README. ## Request mapping Here's a list of the top 5 most frequently used `python-cloudant` operations and the `cloudant-python-sdk` equivalent API operation documentation link: @@ -44,56 +48,55 @@ The table below contains a list of `python-cloudant` functions and the `cloudant | `python-cloudant` function | `cloudant-python-sdk` API operation documentation link | |-----------------|---------------------| -|`metadata()`|[getServerInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getserverinformation)| -|`all_dbs()`|[getAllDbs](https://cloud.ibm.com/apidocs/cloudant?code=python#getalldbs)| -|`db_updates()/infinite_db_updates()`|[getDbUpdates](https://cloud.ibm.com/apidocs/cloudant?code=python#getdbupdates)| -|`Replicator.stop_replication()`|[deleteReplicationDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#deletereplicationdocument)| -|`Replicator.replication_state()`|[getReplicationDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#getreplicationdocument)| -|`Replicator.create_replication()`|[putReplicationDocument](https://cloud.ibm.com/apidocs/cloudant?code=#putreplicationdocument)| -|`Scheduler.get_doc()`|[getSchedulerDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#getschedulerdocument)| -|`Scheduler.list_docs()`|[getSchedulerDocs](https://cloud.ibm.com/apidocs/cloudant?code=python#getschedulerdocs)| -|`Scheduler.list_jobs()`|[getSchedulerJobs](https://cloud.ibm.com/apidocs/cloudant?code=python#getschedulerjobs)| -|`session()`|[getSessionInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getsessioninformation)| -|`uuids()`|[getUuids](https://cloud.ibm.com/apidocs/cloudant?code=python#getuuids)| -|`db.delete()`|[deleteDatabase](https://cloud.ibm.com/apidocs/cloudant?code=python#deletedatabase)| -|`db.metadata()`|[getDatabaseInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getdatabaseinformation)| -|`db.create_document()`|[postDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#postdocument)| -|`db.create()`|[putDatabase](https://cloud.ibm.com/apidocs/cloudant?code=python#putdatabase)| -|`db.all_docs()/db.keys()`|[postAllDocs](https://cloud.ibm.com/apidocs/cloudant?code=python#postalldocs)| -|`db.bulk_docs()`|[postBulkDocs](https://cloud.ibm.com/apidocs/cloudant?code=python#postbulkdocs)| -|`db.changes()/db.infinite_changes()`|[postChanges](https://cloud.ibm.com/apidocs/cloudant?code=python#postchanges)| -|`DesignDocument(db, '_design/doc').delete()`|[deleteDesignDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#deletedesigndocument)| -|`db.get_design_document()/DesignDocument(db, '_design/doc').fetch()`|[getDesignDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#getdesigndocument)| -|`DesignDocument(db, '_design/doc').save()`|[putDesignDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#putdesigndocument)| -|`DesignDocument(db, '_design/doc').info()`|[getDesignDocumentInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getdesigndocumentinformation)| -|`db.get_search_result()`|[postSearch](https://cloud.ibm.com/apidocs/cloudant?code=python#postsearch)| -|`db.get_view_result()`|[postView](https://cloud.ibm.com/apidocs/cloudant?code=python#postview)| -|`db.list_design_documents()`|[postDesignDocs](https://cloud.ibm.com/apidocs/cloudant?code=python#postdesigndocs)| -|`db.get_query_result()`|[postFind](https://cloud.ibm.com/apidocs/cloudant?code=python#postfind)| -|`db.get_query_indexes()`|[getIndexesInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getindexesinformation)| -|`db.create_query_index()`|[postIndex](https://cloud.ibm.com/apidocs/cloudant?code=python#postindex)| -|`db.delete_query_index()`|[deleteIndex](https://cloud.ibm.com/apidocs/cloudant?code=python#deleteindex)| -|`Document(db, '_local/docid').fetch()`|[getLocalDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#getlocaldocument)| -|`Document(db, '_local/docid').save()`|[putLocalDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#putlocaldocument)| -|`Document(db, '_local/docid').delete()`|[deleteLocalDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#deletelocaldocument)| -|`db.missing_revisions()`|[postMissingRevs](https://cloud.ibm.com/apidocs/cloudant?code=python#postmissingrevs)| -|`db.partition_metadata()`|[getPartitionInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getpartitioninformation)| -|`db.partitioned_all_docs()`|[postPartitionAllDocs](https://cloud.ibm.com/apidocs/cloudant?code=python#postpartitionalldocs)| -|`db.get_partitioned_search_result()`|[postPartitionSearch](https://cloud.ibm.com/apidocs/cloudant?code=python#postpartitionsearch)| -|`db.get_partitioned_view_result()`|[postPartitionView](https://cloud.ibm.com/apidocs/cloudant?code=python#postpartitionview)| -|`db.get_partitioned_query_result()`|[postPartitionFind](https://cloud.ibm.com/apidocs/cloudant?code=python#postpartitionfind)| -|`db.revisions_diff()`|[postRevsDiff](https://cloud.ibm.com/apidocs/cloudant?code=python#postrevsdiff)| -|`db.get_security_document()/db.security_document()`|[getSecurity](https://cloud.ibm.com/apidocs/cloudant?code=python#getsecurity)| -|`db.share_database()`|[putSecurity](https://cloud.ibm.com/apidocs/cloudant?code=python#putsecurity)| -|`db.shards()`|[getShardsInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getshardsinformation)| -|`Document(db, 'docid').delete()`|[deleteDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#deletedocument)| -|`Document(db, 'docid').fetch()`|[getDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#getdocument)| -|`Document(db, 'docid').exists()`|[headDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#headdocument)| -|`Document(db, 'docid').save()`|[putDocument](https://cloud.ibm.com/apidocs/cloudant?code=python#putdocument)| -|`Document(db, 'docid').delete_attachment()`|[deleteAttachment](https://cloud.ibm.com/apidocs/cloudant?code=python#deleteattachment)| -|`Document(db, 'docid').get_attachment()`|[getAttachment](https://cloud.ibm.com/apidocs/cloudant?code=python#getattachment)| -|`Document(db, 'docid').put_attachment()`|[putAttachment](https://cloud.ibm.com/apidocs/cloudant?code=python#putattachment)| -|`generate_api_key()`|[postApiKeys](https://cloud.ibm.com/apidocs/cloudant?code=python#postapikeys)| -|`SecurityDocument().save()`|[putCloudantSecurityConfiguration](https://cloud.ibm.com/apidocs/cloudant?code=python#putcloudantsecurityconfiguration)| -|`cors_configuration()/cors_origin()`|[getCorsInformation](https://cloud.ibm.com/apidocs/cloudant?code=python#getcorsinformation)| -|`update_cors_configuration()`|[putCorsConfiguration](https://cloud.ibm.com/apidocs/cloudant?code=python#putcorsconfiguration)| +|`metadata()`|[`getServerInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getserverinformation)| +|`all_dbs()`|[`getAllDbs`](https://cloud.ibm.com/apidocs/cloudant?code=python#getalldbs)| +|`db_updates()/infinite_db_updates()`|[`getDbUpdates`](https://cloud.ibm.com/apidocs/cloudant?code=python#getdbupdates)| +|`Replicator.stop_replication()`|[`deleteReplicationDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#deletereplicationdocument)| +|`Replicator.replication_state()`|[`getReplicationDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#getreplicationdocument)| +|`Replicator.create_replication()`|[`putReplicationDocument`](https://cloud.ibm.com/apidocs/cloudant?code=#putreplicationdocument)| +|`Scheduler.get_doc()`|[`getSchedulerDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#getschedulerdocument)| +|`Scheduler.list_docs()`|[`getSchedulerDocs`](https://cloud.ibm.com/apidocs/cloudant?code=python#getschedulerdocs)| +|`Scheduler.list_jobs()`|[`getSchedulerJobs`](https://cloud.ibm.com/apidocs/cloudant?code=python#getschedulerjobs)| +|`session()`|[`getSessionInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getsessioninformation)| +|`uuids()`|[`getUuids`](https://cloud.ibm.com/apidocs/cloudant?code=python#getuuids)| +|`db.delete()`|[`deleteDatabase`](https://cloud.ibm.com/apidocs/cloudant?code=python#deletedatabase)| +|`db.metadata()`|[`getDatabaseInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getdatabaseinformation)| +|`db.create_document()`|[`postDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#postdocument)| +|`db.create()`|[`putDatabase`](https://cloud.ibm.com/apidocs/cloudant?code=python#putdatabase)| +|`db.all_docs()/db.keys()`|[`postAllDocs`](https://cloud.ibm.com/apidocs/cloudant?code=python#postalldocs)| +|`db.bulk_docs()`|[`postBulkDocs`](https://cloud.ibm.com/apidocs/cloudant?code=python#postbulkdocs)| +|`db.changes()/db.infinite_changes()`|[`postChanges`](https://cloud.ibm.com/apidocs/cloudant?code=python#postchanges-databases)| +|`DesignDocument(db, '_design/doc').delete()`|[`deleteDesignDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#deletedesigndocument)| +|`db.get_design_document()/DesignDocument(db, '_design/doc').fetch()`|[`getDesignDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#getdesigndocument)| +|`DesignDocument(db, '_design/doc').save()`|[`putDesignDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#putdesigndocument)| +|`DesignDocument(db, '_design/doc').info()`|[`getDesignDocumentInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getdesigndocumentinformation)| +|`db.get_search_result()`|[`postSearch`](https://cloud.ibm.com/apidocs/cloudant?code=python#postsearch)| +|`db.get_view_result()`|[`postView`](https://cloud.ibm.com/apidocs/cloudant?code=python#postview)| +|`db.list_design_documents()`|[`postDesignDocs`](https://cloud.ibm.com/apidocs/cloudant?code=python#postdesigndocs)| +|`db.get_query_result()`|[`postFind`](https://cloud.ibm.com/apidocs/cloudant?code=python#postfind)| +|`db.get_query_indexes()`|[`getIndexesInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getindexesinformation)| +|`db.create_query_index()`|[`postIndex`](https://cloud.ibm.com/apidocs/cloudant?code=python#postindex)| +|`db.delete_query_index()`|[`deleteIndex`](https://cloud.ibm.com/apidocs/cloudant?code=python#deleteindex)| +|`Document(db, '_local/docid').fetch()`|[`getLocalDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#getlocaldocument)| +|`Document(db, '_local/docid').save()`|[`putLocalDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#putlocaldocument)| +|`Document(db, '_local/docid').delete()`|[`deleteLocalDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#deletelocaldocument)| +|`db.missing_revisions()/db.revisions_diff()`|[`postRevsDiff`](https://cloud.ibm.com/apidocs/cloudant?code=python#postrevsdiff)| +|`db.partition_metadata()`|[`getPartitionInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getpartitioninformation)| +|`db.partitioned_all_docs()`|[`postPartitionAllDocs`](https://cloud.ibm.com/apidocs/cloudant?code=python#postpartitionalldocs)| +|`db.get_partitioned_search_result()`|[`postPartitionSearch`](https://cloud.ibm.com/apidocs/cloudant?code=python#postpartitionsearch)| +|`db.get_partitioned_view_result()`|[`postPartitionView`](https://cloud.ibm.com/apidocs/cloudant?code=python#postpartitionview)| +|`db.get_partitioned_query_result()`|[`postPartitionFind`](https://cloud.ibm.com/apidocs/cloudant?code=python#postpartitionfind-partitioned-databases)| +|`db.get_security_document()/db.security_document()`|[`getSecurity`](https://cloud.ibm.com/apidocs/cloudant?code=python#getsecurity)| +|`db.share_database()`|[`putSecurity`](https://cloud.ibm.com/apidocs/cloudant?code=python#putsecurity)| +|`db.shards()`|[`getShardsInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getshardsinformation)| +|`Document(db, 'docid').delete()`|[`deleteDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#deletedocument)| +|`Document(db, 'docid').fetch()`|[`getDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#getdocument)| +|`Document(db, 'docid').exists()`|[`headDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#headdocument)| +|`Document(db, 'docid').save()`|[`putDocument`](https://cloud.ibm.com/apidocs/cloudant?code=python#putdocument)| +|`Document(db, 'docid').delete_attachment()`|[`deleteAttachment`](https://cloud.ibm.com/apidocs/cloudant?code=python#deleteattachment)| +|`Document(db, 'docid').get_attachment()`|[`getAttachment`](https://cloud.ibm.com/apidocs/cloudant?code=python#getattachment)| +|`Document(db, 'docid').put_attachment()`|[`putAttachment`](https://cloud.ibm.com/apidocs/cloudant?code=python#putattachment)| +|`generate_api_key()`|[`postApiKeys`](https://cloud.ibm.com/apidocs/cloudant?code=python#postapikeys)| +|`SecurityDocument().save()`|[`putCloudantSecurityConfiguration`](https://cloud.ibm.com/apidocs/cloudant?code=python#putcloudantsecurity)| +|`cors_configuration()/cors_origin()`|[`getCorsInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getcorsinformation)| +|`update_cors_configuration()`|[`putCorsConfiguration`](https://cloud.ibm.com/apidocs/cloudant?code=python#putcorsconfiguration)| From a92c6593ba5db69fda64b462a9847d7e06cc9cf4 Mon Sep 17 00:00:00 2001 From: Ildiko Eigel Date: Tue, 21 Dec 2021 10:58:05 +0100 Subject: [PATCH 45/52] Remove assertion from test_get_feed_descending --- tests/unit/changes_tests.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/changes_tests.py b/tests/unit/changes_tests.py index d39ab159..8d937214 100644 --- a/tests/unit/changes_tests.py +++ b/tests/unit/changes_tests.py @@ -245,7 +245,6 @@ def test_get_feed_descending(self): self.assertTrue(current < last) except AssertionError: self.assertEqual(current, last) - self.assertTrue(len(change['seq']) > len(last_seq)) seq_list.append(change['seq']) last_seq = change['seq'] self.assertEqual(len(seq_list), 50) @@ -522,7 +521,7 @@ def test_invalid_style_value(self): with self.assertRaises(CloudantArgumentError) as cm: invalid_feed = [x for x in feed] self.assertEqual( - str(cm.exception), + str(cm.exception), 'Invalid value (foo) for style option. Must be main_only, or all_docs.') if __name__ == '__main__': From e113b8f0da43d5cb9f90a84502eb5cf32d3ba1de Mon Sep 17 00:00:00 2001 From: Ildiko Eigel Date: Tue, 21 Dec 2021 10:58:54 +0100 Subject: [PATCH 46/52] Update date in copyright header --- tests/unit/changes_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/changes_tests.py b/tests/unit/changes_tests.py index 8d937214..5ec2f02e 100644 --- a/tests/unit/changes_tests.py +++ b/tests/unit/changes_tests.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (C) 2016, 2018 IBM Corp. All rights reserved. +# Copyright (C) 2016, 2021 IBM Corp. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 8d968f4c048a320c2398436333b225d792837762 Mon Sep 17 00:00:00 2001 From: Ildiko Eigel Date: Tue, 21 Dec 2021 11:34:34 +0100 Subject: [PATCH 47/52] Update comment --- tests/unit/changes_tests.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/changes_tests.py b/tests/unit/changes_tests.py index 5ec2f02e..b0e9cb3e 100644 --- a/tests/unit/changes_tests.py +++ b/tests/unit/changes_tests.py @@ -227,8 +227,7 @@ def test_get_feed_descending(self): Test getting content back for a descending feed. When testing, the sequence identifier is in the form of -. Often times the number prefix sorts as expected when using descending but sometimes the - number prefix is repeated. In these cases the check is to see if the following - random character sequence suffix is longer than its predecessor. + number prefix is repeated. """ self.populate_db_with_documents(50) feed = Feed(self.db, descending=True) From ab941812c84cfbcf90c85ae63fb034fbe136780d Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Wed, 22 Dec 2021 14:19:22 +0000 Subject: [PATCH 48/52] Use assertEqual instead of assertEquals for Python 3.11 compatibility. --- tests/unit/changes_tests.py | 6 +- tests/unit/client_tests.py | 16 +-- tests/unit/database_partition_tests.py | 22 ++--- tests/unit/database_tests.py | 130 ++++++++++++------------- tests/unit/design_document_tests.py | 6 +- tests/unit/document_tests.py | 8 +- tests/unit/iam_auth_tests.py | 4 +- tests/unit/index_tests.py | 120 +++++++++++------------ tests/unit/replicator_mock_tests.py | 12 +-- 9 files changed, 162 insertions(+), 162 deletions(-) diff --git a/tests/unit/changes_tests.py b/tests/unit/changes_tests.py index b0e9cb3e..3df0cef3 100644 --- a/tests/unit/changes_tests.py +++ b/tests/unit/changes_tests.py @@ -474,9 +474,9 @@ def test_get_feed_with_custom_filter_query_params(self): include_docs=False ) params = feed._translate(feed._options) - self.assertEquals(params['filter'], 'mailbox/new_mail') - self.assertEquals(params['foo'], 'bar') - self.assertEquals(params['include_docs'], 'false') + self.assertEqual(params['filter'], 'mailbox/new_mail') + self.assertEqual(params['foo'], 'bar') + self.assertEqual(params['include_docs'], 'false') def test_invalid_argument_type(self): """ diff --git a/tests/unit/client_tests.py b/tests/unit/client_tests.py index 07c4159b..35139168 100644 --- a/tests/unit/client_tests.py +++ b/tests/unit/client_tests.py @@ -266,7 +266,7 @@ def test_session_basic(self, m_req): timeout=None ) - self.assertEquals(all_dbs, ['animaldb']) + self.assertEqual(all_dbs, ['animaldb']) @mock.patch('cloudant._client_session.Session.request') def test_session_basic_with_no_credentials(self, m_req): @@ -333,7 +333,7 @@ def test_change_credentials_basic(self, m_req): auth=('baz', 'qux'), # uses HTTP Basic Auth timeout=None ) - self.assertEquals(all_dbs, ['animaldb']) + self.assertEqual(all_dbs, ['animaldb']) @skip_if_not_cookie_auth def test_basic_auth_str(self): @@ -442,7 +442,7 @@ def test_create_with_server_error(self, m_req): self.assertEqual(cm.exception.status_code, 500) - self.assertEquals(m_req.call_count, 3) + self.assertEqual(m_req.call_count, 3) m_req.assert_called_with( 'PUT', '/'.join([self.url, dbname]), @@ -730,7 +730,7 @@ def test_cloudant_bluemix_context_helper_with_legacy_creds(self): with cloudant_bluemix(vcap_services, instance_name=instance_name) as c: self.assertIsInstance(c, Cloudant) self.assertIsInstance(c.r_session, requests.Session) - self.assertEquals(c.session()['userCtx']['name'], self.user) + self.assertEqual(c.session()['userCtx']['name'], self.user) except Exception as err: self.fail('Exception {0} was raised.'.format(str(err))) @@ -809,7 +809,7 @@ def test_cloudant_bluemix_dedicated_context_helper(self): service_name=service_name) as c: self.assertIsInstance(c, Cloudant) self.assertIsInstance(c.r_session, requests.Session) - self.assertEquals(c.session()['userCtx']['name'], self.user) + self.assertEqual(c.session()['userCtx']['name'], self.user) except Exception as err: self.fail('Exception {0} was raised.'.format(str(err))) @@ -850,7 +850,7 @@ def test_bluemix_constructor_with_legacy_creds(self): c.connect() self.assertIsInstance(c, Cloudant) self.assertIsInstance(c.r_session, requests.Session) - self.assertEquals(c.session()['userCtx']['name'], self.user) + self.assertEqual(c.session()['userCtx']['name'], self.user) except Exception as err: self.fail('Exception {0} was raised.'.format(str(err))) @@ -916,7 +916,7 @@ def test_bluemix_constructor_specify_instance_name(self): c.connect() self.assertIsInstance(c, Cloudant) self.assertIsInstance(c.r_session, requests.Session) - self.assertEquals(c.session()['userCtx']['name'], self.user) + self.assertEqual(c.session()['userCtx']['name'], self.user) except Exception as err: self.fail('Exception {0} was raised.'.format(str(err))) @@ -960,7 +960,7 @@ def test_bluemix_constructor_with_multiple_services(self): c.connect() self.assertIsInstance(c, Cloudant) self.assertIsInstance(c.r_session, requests.Session) - self.assertEquals(c.session()['userCtx']['name'], self.user) + self.assertEqual(c.session()['userCtx']['name'], self.user) except Exception as err: self.fail('Exception {0} was raised.'.format(str(err))) diff --git a/tests/unit/database_partition_tests.py b/tests/unit/database_partition_tests.py index 52b76e0d..8b3690dd 100644 --- a/tests/unit/database_partition_tests.py +++ b/tests/unit/database_partition_tests.py @@ -63,7 +63,7 @@ def test_create_non_partitioned_design_document(self): def test_partitioned_all_docs(self): for partition_key in self.populate_db_with_partitioned_documents(5, 25): docs = self.db.partitioned_all_docs(partition_key) - self.assertEquals(len(docs['rows']), 25) + self.assertEqual(len(docs['rows']), 25) for doc in docs['rows']: self.assertTrue(doc['id'].startswith(partition_key + ':')) @@ -71,8 +71,8 @@ def test_partitioned_all_docs(self): def test_partition_metadata(self): for partition_key in self.populate_db_with_partitioned_documents(5, 25): meta = self.db.partition_metadata(partition_key) - self.assertEquals(meta['partition'], partition_key) - self.assertEquals(meta['doc_count'], 25) + self.assertEqual(meta['partition'], partition_key) + self.assertEqual(meta['doc_count'], 25) def test_partitioned_search(self): ddoc = DesignDocument(self.db, 'partitioned_search', partitioned=True) @@ -91,7 +91,7 @@ def test_partitioned_search(self): print(result) self.assertTrue(result['id'].startswith(partition_key + ':')) i += 1 - self.assertEquals(i, 10) + self.assertEqual(i, 10) def test_get_partitioned_index(self): index_name = 'test_partitioned_index' @@ -99,16 +99,16 @@ def test_get_partitioned_index(self): self.db.create_query_index(index_name=index_name, fields=['foo']) results = self.db.get_query_indexes() - self.assertEquals(len(results), 2) + self.assertEqual(len(results), 2) index_all_docs = results[0] - self.assertEquals(index_all_docs.name, '_all_docs') - self.assertEquals(type(index_all_docs), SpecialIndex) + self.assertEqual(index_all_docs.name, '_all_docs') + self.assertEqual(type(index_all_docs), SpecialIndex) self.assertFalse(index_all_docs.partitioned) index_partitioned = results[1] - self.assertEquals(index_partitioned.name, index_name) - self.assertEquals(type(index_partitioned), Index) + self.assertEqual(index_partitioned.name, index_name) + self.assertEqual(type(index_partitioned), Index) self.assertTrue(index_partitioned.partitioned) def test_partitioned_query(self): @@ -122,7 +122,7 @@ def test_partitioned_query(self): for result in results: self.assertTrue(result['_id'].startswith(partition_key + ':')) i += 1 - self.assertEquals(i, 10) + self.assertEqual(i, 10) def test_partitioned_view(self): ddoc = DesignDocument(self.db, 'partitioned_view', partitioned=True) @@ -138,4 +138,4 @@ def test_partitioned_view(self): self.assertTrue( result['id'].startswith(partition_key + ':')) i += 1 - self.assertEquals(i, 10) + self.assertEqual(i, 10) diff --git a/tests/unit/database_tests.py b/tests/unit/database_tests.py index 3ba12584..ae9898c7 100644 --- a/tests/unit/database_tests.py +++ b/tests/unit/database_tests.py @@ -885,7 +885,7 @@ def test_get_set_revision_limit(self, m_req): # Get new revisions limit. self.assertEqual(self.db.get_revision_limit(), 1234) - self.assertEquals(m_req.call_count, 3) + self.assertEqual(m_req.call_count, 3) @attr(db='couch') def test_view_clean_up(self): @@ -1084,19 +1084,19 @@ def test_create_json_index(self): ddoc = self.db[index.design_document_id] - self.assertEquals(ddoc['_id'], index.design_document_id) + self.assertEqual(ddoc['_id'], index.design_document_id) self.assertTrue(ddoc['_rev'].startswith('1-')) - self.assertEquals(ddoc['indexes'], {}) - self.assertEquals(ddoc['language'], 'query') - self.assertEquals(ddoc['lists'], {}) - self.assertEquals(ddoc['shows'], {}) + self.assertEqual(ddoc['indexes'], {}) + self.assertEqual(ddoc['language'], 'query') + self.assertEqual(ddoc['lists'], {}) + self.assertEqual(ddoc['shows'], {}) index = ddoc['views'][index.name] - self.assertEquals(index['map']['fields']['age'], 'asc') - self.assertEquals(index['map']['fields']['name'], 'asc') - self.assertEquals(index['options']['def']['fields'], ['name', 'age']) - self.assertEquals(index['reduce'], '_count') + self.assertEqual(index['map']['fields']['age'], 'asc') + self.assertEqual(index['map']['fields']['name'], 'asc') + self.assertEqual(index['options']['def']['fields'], ['name', 'age']) + self.assertEqual(index['reduce'], '_count') @attr(couchapi=2) def test_delete_json_index(self): @@ -1369,22 +1369,22 @@ def test_create_text_index(self): ddoc = self.db[index.design_document_id] - self.assertEquals(ddoc['_id'], index.design_document_id) + self.assertEqual(ddoc['_id'], index.design_document_id) self.assertTrue(ddoc['_rev'].startswith('1-')) - self.assertEquals(ddoc['language'], 'query') - self.assertEquals(ddoc['lists'], {}) - self.assertEquals(ddoc['shows'], {}) - self.assertEquals(ddoc['views'], {}) + self.assertEqual(ddoc['language'], 'query') + self.assertEqual(ddoc['lists'], {}) + self.assertEqual(ddoc['shows'], {}) + self.assertEqual(ddoc['views'], {}) text_index = ddoc['indexes'][index.name] - self.assertEquals(text_index['analyzer']['default'], 'keyword') - self.assertEquals(text_index['analyzer']['fields']['$default'], 'standard') - self.assertEquals(text_index['analyzer']['name'], 'perfield') - self.assertEquals(text_index['index']['default_analyzer'], 'keyword') - self.assertEquals(text_index['index']['default_field'], {}) - self.assertEquals(text_index['index']['fields'], [{'name': 'name', 'type': 'string'}, {'name': 'age', 'type': 'number'}]) - self.assertEquals(text_index['index']['selector'], {}) + self.assertEqual(text_index['analyzer']['default'], 'keyword') + self.assertEqual(text_index['analyzer']['fields']['$default'], 'standard') + self.assertEqual(text_index['analyzer']['name'], 'perfield') + self.assertEqual(text_index['index']['default_analyzer'], 'keyword') + self.assertEqual(text_index['index']['default_field'], {}) + self.assertEqual(text_index['index']['fields'], [{'name': 'name', 'type': 'string'}, {'name': 'age', 'type': 'number'}]) + self.assertEqual(text_index['index']['selector'], {}) self.assertTrue(text_index['index']['index_array_lengths']) def test_create_all_fields_text_index(self): @@ -1396,22 +1396,22 @@ def test_create_all_fields_text_index(self): ddoc = self.db[index.design_document_id] - self.assertEquals(ddoc['_id'], index.design_document_id) + self.assertEqual(ddoc['_id'], index.design_document_id) self.assertTrue(ddoc['_rev'].startswith('1-')) - self.assertEquals(ddoc['language'], 'query') - self.assertEquals(ddoc['lists'], {}) - self.assertEquals(ddoc['shows'], {}) - self.assertEquals(ddoc['views'], {}) + self.assertEqual(ddoc['language'], 'query') + self.assertEqual(ddoc['lists'], {}) + self.assertEqual(ddoc['shows'], {}) + self.assertEqual(ddoc['views'], {}) index = ddoc['indexes'][index.name] - self.assertEquals(index['analyzer']['default'], 'keyword') - self.assertEquals(index['analyzer']['fields'], {'$default': 'standard'}) - self.assertEquals(index['analyzer']['name'], 'perfield') - self.assertEquals(index['index']['default_analyzer'], 'keyword') - self.assertEquals(index['index']['default_field'], {}) - self.assertEquals(index['index']['fields'], 'all_fields') - self.assertEquals(index['index']['selector'], {}) + self.assertEqual(index['analyzer']['default'], 'keyword') + self.assertEqual(index['analyzer']['fields'], {'$default': 'standard'}) + self.assertEqual(index['analyzer']['name'], 'perfield') + self.assertEqual(index['index']['default_analyzer'], 'keyword') + self.assertEqual(index['index']['default_field'], {}) + self.assertEqual(index['index']['fields'], 'all_fields') + self.assertEqual(index['index']['selector'], {}) self.assertTrue(index['index']['index_array_lengths']) def test_create_multiple_indexes_one_ddoc(self): @@ -1436,27 +1436,27 @@ def test_create_multiple_indexes_one_ddoc(self): ddoc = self.db['_design/ddoc001'] - self.assertEquals(ddoc['_id'], index.design_document_id) + self.assertEqual(ddoc['_id'], index.design_document_id) self.assertTrue(ddoc['_rev'].startswith('2-')) - self.assertEquals(ddoc['language'], 'query') - self.assertEquals(ddoc['lists'], {}) - self.assertEquals(ddoc['shows'], {}) + self.assertEqual(ddoc['language'], 'query') + self.assertEqual(ddoc['lists'], {}) + self.assertEqual(ddoc['shows'], {}) json_index = ddoc['views']['json-index-001'] - self.assertEquals(json_index['map']['fields']['age'], 'asc') - self.assertEquals(json_index['map']['fields']['name'], 'asc') - self.assertEquals(json_index['options']['def']['fields'], ['name', 'age']) - self.assertEquals(json_index['reduce'], '_count') + self.assertEqual(json_index['map']['fields']['age'], 'asc') + self.assertEqual(json_index['map']['fields']['name'], 'asc') + self.assertEqual(json_index['options']['def']['fields'], ['name', 'age']) + self.assertEqual(json_index['reduce'], '_count') text_index = ddoc['indexes']['text-index-001'] - self.assertEquals(text_index['analyzer']['default'], 'keyword') - self.assertEquals(text_index['analyzer']['fields']['$default'], 'standard') - self.assertEquals(text_index['analyzer']['name'], 'perfield') - self.assertEquals(text_index['index']['default_analyzer'], 'keyword') - self.assertEquals(text_index['index']['default_field'], {}) - self.assertEquals(text_index['index']['fields'], [{'name': 'name', 'type': 'string'}, {'name': 'age', 'type': 'number'}]) - self.assertEquals(text_index['index']['selector'], {}) + self.assertEqual(text_index['analyzer']['default'], 'keyword') + self.assertEqual(text_index['analyzer']['fields']['$default'], 'standard') + self.assertEqual(text_index['analyzer']['name'], 'perfield') + self.assertEqual(text_index['index']['default_analyzer'], 'keyword') + self.assertEqual(text_index['index']['default_field'], {}) + self.assertEqual(text_index['index']['fields'], [{'name': 'name', 'type': 'string'}, {'name': 'age', 'type': 'number'}]) + self.assertEqual(text_index['index']['selector'], {}) self.assertTrue(text_index['index']['index_array_lengths']) def test_create_query_index_failure(self): @@ -1513,28 +1513,28 @@ def test_get_query_indexes_raw(self): indexes = self.db.get_query_indexes(raw_result=True) - self.assertEquals(indexes['total_rows'], 3) + self.assertEqual(indexes['total_rows'], 3) all_docs_index = indexes['indexes'][0] - self.assertEquals(all_docs_index['ddoc'], None) - self.assertEquals(all_docs_index['def']['fields'], [{'_id': 'asc'}]) - self.assertEquals(all_docs_index['name'], '_all_docs') - self.assertEquals(all_docs_index['type'], 'special') + self.assertEqual(all_docs_index['ddoc'], None) + self.assertEqual(all_docs_index['def']['fields'], [{'_id': 'asc'}]) + self.assertEqual(all_docs_index['name'], '_all_docs') + self.assertEqual(all_docs_index['type'], 'special') json_index = indexes['indexes'][1] - self.assertEquals(json_index['ddoc'], '_design/ddoc001') - self.assertEquals(json_index['def']['fields'], [{'name': 'asc'}, {'age': 'asc'}]) - self.assertEquals(json_index['name'], 'json-idx-001') - self.assertEquals(json_index['type'], 'json') + self.assertEqual(json_index['ddoc'], '_design/ddoc001') + self.assertEqual(json_index['def']['fields'], [{'name': 'asc'}, {'age': 'asc'}]) + self.assertEqual(json_index['name'], 'json-idx-001') + self.assertEqual(json_index['type'], 'json') text_index = indexes['indexes'][2] - self.assertEquals(text_index['ddoc'], '_design/ddoc001') - self.assertEquals(text_index['def']['default_analyzer'], 'keyword') - self.assertEquals(text_index['def']['default_field'], {}) - self.assertEquals(text_index['def']['fields'], []) - self.assertEquals(text_index['def']['selector'], {}) - self.assertEquals(text_index['name'], 'text-idx-001') - self.assertEquals(text_index['type'], 'text') + self.assertEqual(text_index['ddoc'], '_design/ddoc001') + self.assertEqual(text_index['def']['default_analyzer'], 'keyword') + self.assertEqual(text_index['def']['default_field'], {}) + self.assertEqual(text_index['def']['fields'], []) + self.assertEqual(text_index['def']['selector'], {}) + self.assertEqual(text_index['name'], 'text-idx-001') + self.assertEqual(text_index['type'], 'text') self.assertTrue(text_index['def']['index_array_lengths']) def test_get_query_indexes(self): diff --git a/tests/unit/design_document_tests.py b/tests/unit/design_document_tests.py index 52352bcc..86a2b5f1 100644 --- a/tests/unit/design_document_tests.py +++ b/tests/unit/design_document_tests.py @@ -843,9 +843,9 @@ def test_get_search_info(self): # Validate the metadata search_index_metadata = search_info['search_index'] self.assertIsNotNone(search_index_metadata) - self.assertEquals(search_index_metadata['doc_del_count'], 0, 'There should be no deleted docs.') + self.assertEqual(search_index_metadata['doc_del_count'], 0, 'There should be no deleted docs.') self.assertTrue(search_index_metadata['doc_count'] <= 100, 'There should be 100 or fewer docs.') - self.assertEquals(search_index_metadata['committed_seq'], 0, 'The committed_seq should be 0.') + self.assertEqual(search_index_metadata['committed_seq'], 0, 'The committed_seq should be 0.') self.assertTrue(search_index_metadata['pending_seq'] <= 101, 'The pending_seq should be 101 or fewer.') self.assertTrue(search_index_metadata['disk_size'] >0, 'The disk_size should be greater than 0.') @@ -1275,7 +1275,7 @@ def test_rewrite_rule(self): doc = Document(self.db, 'rewrite_doc') doc.save() resp = self.client.r_session.get('/'.join([ddoc.document_url, '_rewrite'])) - self.assertEquals( + self.assertEqual( response_to_json_dict(resp), { '_id': 'rewrite_doc', diff --git a/tests/unit/document_tests.py b/tests/unit/document_tests.py index 61fa9fac..09cb09c4 100644 --- a/tests/unit/document_tests.py +++ b/tests/unit/document_tests.py @@ -944,14 +944,14 @@ def object_hook(self, obj): raw_doc = self.db.all_docs(include_docs=True)['rows'][0]['doc'] - self.assertEquals(raw_doc['name'], 'julia') - self.assertEquals(raw_doc['dt']['_type'], 'datetime') - self.assertEquals(raw_doc['dt']['value'], '2018-07-09T15:11:10') + self.assertEqual(raw_doc['name'], 'julia') + self.assertEqual(raw_doc['dt']['_type'], 'datetime') + self.assertEqual(raw_doc['dt']['value'], '2018-07-09T15:11:10') doc2 = Document(self.db, doc['_id'], decoder=DTDecoder) doc2.fetch() - self.assertEquals(doc2['dt'], doc['dt']) + self.assertEqual(doc2['dt'], doc['dt']) if __name__ == '__main__': unittest.main() diff --git a/tests/unit/iam_auth_tests.py b/tests/unit/iam_auth_tests.py index f36af4e0..3e0c7cf2 100644 --- a/tests/unit/iam_auth_tests.py +++ b/tests/unit/iam_auth_tests.py @@ -88,12 +88,12 @@ def _mock_cookie(expires_secs=300): def test_iam_set_credentials(self): iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984') - self.assertEquals(iam._api_key, MOCK_API_KEY) + self.assertEqual(iam._api_key, MOCK_API_KEY) new_api_key = 'some_new_api_key' iam.set_credentials(None, new_api_key) - self.assertEquals(iam._api_key, new_api_key) + self.assertEqual(iam._api_key, new_api_key) @mock.patch('cloudant._client_session.ClientSession.request') def test_iam_get_access_token(self, m_req): diff --git a/tests/unit/index_tests.py b/tests/unit/index_tests.py index 29a3d51c..f12cf158 100644 --- a/tests/unit/index_tests.py +++ b/tests/unit/index_tests.py @@ -161,21 +161,21 @@ def test_create_an_index_using_ddoc_index_name(self): with DesignDocument(self.db, index.design_document_id) as ddoc: self.assertIsInstance(ddoc.get_view(index.name), QueryIndexView) - self.assertEquals(ddoc['_id'], index.design_document_id) + self.assertEqual(ddoc['_id'], index.design_document_id) self.assertTrue(ddoc['_rev'].startswith('1-')) - self.assertEquals(ddoc['indexes'], {}) - self.assertEquals(ddoc['language'], 'query') - self.assertEquals(ddoc['lists'], {}) - self.assertEquals(ddoc['shows'], {}) + self.assertEqual(ddoc['indexes'], {}) + self.assertEqual(ddoc['language'], 'query') + self.assertEqual(ddoc['lists'], {}) + self.assertEqual(ddoc['shows'], {}) self.assertListEqual(list(ddoc['views'].keys()), ['index001']) view = ddoc['views'][index.name] - self.assertEquals(view['map']['fields']['age'], 'asc') - self.assertEquals(view['map']['fields']['name'], 'asc') - self.assertEquals(view['options']['def']['fields'], ['name', 'age']) - self.assertEquals(view['reduce'], '_count') + self.assertEqual(view['map']['fields']['age'], 'asc') + self.assertEqual(view['map']['fields']['name'], 'asc') + self.assertEqual(view['options']['def']['fields'], ['name', 'age']) + self.assertEqual(view['reduce'], '_count') def test_create_an_index_without_ddoc_index_name(self): """ @@ -189,21 +189,21 @@ def test_create_an_index_without_ddoc_index_name(self): with DesignDocument(self.db, index.design_document_id) as ddoc: self.assertIsInstance(ddoc.get_view(index.name), QueryIndexView) - self.assertEquals(ddoc['_id'], index.design_document_id) + self.assertEqual(ddoc['_id'], index.design_document_id) self.assertTrue(ddoc['_rev'].startswith('1-')) - self.assertEquals(ddoc['indexes'], {}) - self.assertEquals(ddoc['language'], 'query') - self.assertEquals(ddoc['lists'], {}) - self.assertEquals(ddoc['shows'], {}) + self.assertEqual(ddoc['indexes'], {}) + self.assertEqual(ddoc['language'], 'query') + self.assertEqual(ddoc['lists'], {}) + self.assertEqual(ddoc['shows'], {}) self.assertListEqual(list(ddoc['views'].keys()), [index.name]) view = ddoc['views'][index.name] - self.assertEquals(view['map']['fields']['age'], 'asc') - self.assertEquals(view['map']['fields']['name'], 'asc') - self.assertEquals(view['options']['def']['fields'], ['name', 'age']) - self.assertEquals(view['reduce'], '_count') + self.assertEqual(view['map']['fields']['age'], 'asc') + self.assertEqual(view['map']['fields']['name'], 'asc') + self.assertEqual(view['options']['def']['fields'], ['name', 'age']) + self.assertEqual(view['reduce'], '_count') def test_create_an_index_with_empty_ddoc_index_name(self): """ @@ -217,21 +217,21 @@ def test_create_an_index_with_empty_ddoc_index_name(self): with DesignDocument(self.db, index.design_document_id) as ddoc: self.assertIsInstance(ddoc.get_view(index.name), QueryIndexView) - self.assertEquals(ddoc['_id'], index.design_document_id) + self.assertEqual(ddoc['_id'], index.design_document_id) self.assertTrue(ddoc['_rev'].startswith('1-')) - self.assertEquals(ddoc['indexes'], {}) - self.assertEquals(ddoc['language'], 'query') - self.assertEquals(ddoc['lists'], {}) - self.assertEquals(ddoc['shows'], {}) + self.assertEqual(ddoc['indexes'], {}) + self.assertEqual(ddoc['language'], 'query') + self.assertEqual(ddoc['lists'], {}) + self.assertEqual(ddoc['shows'], {}) self.assertListEqual(list(ddoc['views'].keys()), [index.name]) view = ddoc['views'][index.name] - self.assertEquals(view['map']['fields']['age'], 'asc') - self.assertEquals(view['map']['fields']['name'], 'asc') - self.assertEquals(view['options']['def']['fields'], ['name', 'age']) - self.assertEquals(view['reduce'], '_count') + self.assertEqual(view['map']['fields']['age'], 'asc') + self.assertEqual(view['map']['fields']['name'], 'asc') + self.assertEqual(view['options']['def']['fields'], ['name', 'age']) + self.assertEqual(view['reduce'], '_count') def test_create_an_index_using_design_prefix(self): """ @@ -245,21 +245,21 @@ def test_create_an_index_using_design_prefix(self): with DesignDocument(self.db, index.design_document_id) as ddoc: self.assertIsInstance(ddoc.get_view(index.name), QueryIndexView) - self.assertEquals(ddoc['_id'], index.design_document_id) + self.assertEqual(ddoc['_id'], index.design_document_id) self.assertTrue(ddoc['_rev'].startswith('1-')) - self.assertEquals(ddoc['indexes'], {}) - self.assertEquals(ddoc['language'], 'query') - self.assertEquals(ddoc['lists'], {}) - self.assertEquals(ddoc['shows'], {}) + self.assertEqual(ddoc['indexes'], {}) + self.assertEqual(ddoc['language'], 'query') + self.assertEqual(ddoc['lists'], {}) + self.assertEqual(ddoc['shows'], {}) self.assertListEqual(list(ddoc['views'].keys()), [index.name]) view = ddoc['views'][index.name] - self.assertEquals(view['map']['fields']['age'], 'asc') - self.assertEquals(view['map']['fields']['name'], 'asc') - self.assertEquals(view['options']['def']['fields'], ['name', 'age']) - self.assertEquals(view['reduce'], '_count') + self.assertEqual(view['map']['fields']['age'], 'asc') + self.assertEqual(view['map']['fields']['name'], 'asc') + self.assertEqual(view['options']['def']['fields'], ['name', 'age']) + self.assertEqual(view['reduce'], '_count') def test_create_uses_custom_encoder(self): """ @@ -446,22 +446,22 @@ def test_create_a_search_index_no_kwargs(self): self.assertEqual(index.design_document_id, '_design/ddoc001') self.assertEqual(index.name, 'index001') with DesignDocument(self.db, index.design_document_id) as ddoc: - self.assertEquals(ddoc['_id'], index.design_document_id) + self.assertEqual(ddoc['_id'], index.design_document_id) self.assertTrue(ddoc['_rev'].startswith('1-')) - self.assertEquals(ddoc['language'], 'query') - self.assertEquals(ddoc['lists'], {}) - self.assertEquals(ddoc['shows'], {}) - self.assertEquals(ddoc['views'], {}) + self.assertEqual(ddoc['language'], 'query') + self.assertEqual(ddoc['lists'], {}) + self.assertEqual(ddoc['shows'], {}) + self.assertEqual(ddoc['views'], {}) index = ddoc['indexes']['index001'] - self.assertEquals(index['analyzer']['default'], 'keyword') - self.assertEquals(index['analyzer']['fields']['$default'], 'standard') - self.assertEquals(index['analyzer']['name'], 'perfield') - self.assertEquals(index['index']['default_analyzer'], 'keyword') - self.assertEquals(index['index']['default_field'], {}) - self.assertEquals(index['index']['fields'], 'all_fields') - self.assertEquals(index['index']['selector'], {}) + self.assertEqual(index['analyzer']['default'], 'keyword') + self.assertEqual(index['analyzer']['fields']['$default'], 'standard') + self.assertEqual(index['analyzer']['name'], 'perfield') + self.assertEqual(index['index']['default_analyzer'], 'keyword') + self.assertEqual(index['index']['default_field'], {}) + self.assertEqual(index['index']['fields'], 'all_fields') + self.assertEqual(index['index']['selector'], {}) self.assertTrue(index['index']['index_array_lengths']) def test_create_a_search_index_with_kwargs(self): @@ -480,22 +480,22 @@ def test_create_a_search_index_with_kwargs(self): self.assertEqual(index.design_document_id, '_design/ddoc001') self.assertEqual(index.name, 'index001') with DesignDocument(self.db, index.design_document_id) as ddoc: - self.assertEquals(ddoc['_id'], index.design_document_id) + self.assertEqual(ddoc['_id'], index.design_document_id) self.assertTrue(ddoc['_rev'].startswith('1-')) - self.assertEquals(ddoc['language'], 'query') - self.assertEquals(ddoc['lists'], {}) - self.assertEquals(ddoc['shows'], {}) - self.assertEquals(ddoc['views'], {}) + self.assertEqual(ddoc['language'], 'query') + self.assertEqual(ddoc['lists'], {}) + self.assertEqual(ddoc['shows'], {}) + self.assertEqual(ddoc['views'], {}) index = ddoc['indexes']['index001'] - self.assertEquals(index['analyzer']['default'], 'keyword') - self.assertEquals(index['analyzer']['fields']['$default'], 'german') - self.assertEquals(index['analyzer']['name'], 'perfield') - self.assertEquals(index['index']['default_analyzer'], 'keyword') - self.assertEquals(index['index']['default_field']['analyzer'], 'german') - self.assertEquals(index['index']['fields'], [{'name': 'name', 'type': 'string'}, {'name': 'age', 'type': 'number'}]) - self.assertEquals(index['index']['selector'], {}) + self.assertEqual(index['analyzer']['default'], 'keyword') + self.assertEqual(index['analyzer']['fields']['$default'], 'german') + self.assertEqual(index['analyzer']['name'], 'perfield') + self.assertEqual(index['index']['default_analyzer'], 'keyword') + self.assertEqual(index['index']['default_field']['analyzer'], 'german') + self.assertEqual(index['index']['fields'], [{'name': 'name', 'type': 'string'}, {'name': 'age', 'type': 'number'}]) + self.assertEqual(index['index']['selector'], {}) self.assertTrue(index['index']['default_field']['enabled']) self.assertTrue(index['index']['index_array_lengths']) diff --git a/tests/unit/replicator_mock_tests.py b/tests/unit/replicator_mock_tests.py index 96589f9e..f49b04d4 100644 --- a/tests/unit/replicator_mock_tests.py +++ b/tests/unit/replicator_mock_tests.py @@ -86,9 +86,9 @@ def test_using_admin_party_source_and_target(self): rep.create_replication(src, tgt, repl_id=self.repl_id) kcall = m_replicator.create_document.call_args_list - self.assertEquals(len(kcall), 1) + self.assertEqual(len(kcall), 1) args, kwargs = kcall[0] - self.assertEquals(len(args), 1) + self.assertEqual(len(args), 1) expected_doc = { '_id': self.repl_id, @@ -118,9 +118,9 @@ def test_using_basic_auth_source_and_target(self): src, tgt, repl_id=self.repl_id, user_ctx=self.user_ctx) kcall = m_replicator.create_document.call_args_list - self.assertEquals(len(kcall), 1) + self.assertEqual(len(kcall), 1) args, kwargs = kcall[0] - self.assertEquals(len(args), 1) + self.assertEqual(len(args), 1) expected_doc = { '_id': self.repl_id, @@ -154,9 +154,9 @@ def test_using_iam_auth_source_and_target(self): src, tgt, repl_id=self.repl_id, user_ctx=self.user_ctx) kcall = m_replicator.create_document.call_args_list - self.assertEquals(len(kcall), 1) + self.assertEqual(len(kcall), 1) args, kwargs = kcall[0] - self.assertEquals(len(args), 1) + self.assertEqual(len(args), 1) expected_doc = { '_id': self.repl_id, From 5b1ecc215b2caea22ccc2d7310462df56be6e848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eigel=20Ildik=C3=B3?= Date: Tue, 11 Jan 2022 14:21:54 +0100 Subject: [PATCH 49/52] [MIGRATION] Add note about authentication errors and server errors (#512) --- MIGRATION.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 21c9ae5f..a122d7dd 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -20,8 +20,21 @@ content like using `raw_result=True` in `python-cloudant`. 1. Replay adapters are replaced by the [automatic retries](https://github. com/IBM/ibm-cloud-sdk-common/#automatic-retries) feature for failed requests. 1. Error handling is not transferable from `python-cloudant` to `cloudant-python-sdk`. For more information go to the [Error handling section](https://cloud.ibm.com/apidocs/cloudant?code=python#error-handling) in our API docs. -1. Custom HTTP client configurations in `python-cloudant` are not transferable to `python-java-sdk`. For more information go to the [Configuring the HTTP client section(https://githubcom/IBM/ibm-cloud-sdk-common/#configuring-the-http-client) in the IBM Cloud SDK Common README. - +1. Custom HTTP client configurations in `python-cloudant` can be set differently in + `cloudant-python-sdk`. For more information go to the + [Configuring the HTTP client section](https://github.com/IBM/ibm-cloud-sdk-common/#configuring-the-http-client) + in the IBM Cloud SDK Common README. + +### Troubleshooting +1. Authentication errors occur during service instantiation. For example, the code `service = + CloudantV1.new_instance(service_name="EXAMPLE")` will fail with `ValueError: At least one of + iam_profile_name or iam_profile_id must be specified.` if required environment variables + prefixed with `EXAMPLE` are not set. +1. Server errors occur when running a request against the service. We suggest to + check server errors with + [`getServerInformation`](https://cloud.ibm.com/apidocs/cloudant?code=python#getserverinformation) + which is the new alternative of `metadata()`. + ## Request mapping Here's a list of the top 5 most frequently used `python-cloudant` operations and the `cloudant-python-sdk` equivalent API operation documentation link: From fe141fcc3b3a3614e9b780317f3fb6efdebceb99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eigel=20Ildik=C3=B3?= Date: Thu, 27 Jan 2022 15:59:05 +0100 Subject: [PATCH 50/52] doc: note full deprecation (#513) --- CHANGES.md | 3 +++ README.md | 7 +++---- src/cloudant/__init__.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d5cf74e5..75402ba5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +# UNRELEASED +- [DEPRECATED] This library is end-of-life and no longer supported. + # 2.15.0 (2021-08-26) - [NEW] Override `dict.get` method for `CouchDatabase` to add `remote` parameter allowing it to retrieve a remote document if specified. diff --git a/README.md b/README.md index 029edf07..467c9dd3 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ -# DEPRECATED +# :warning: NO LONGER MAINTAINED :warning: -**This library is now deprecated and will be end-of-life on Dec 31 2021.** +**This library is end-of-life and no longer supported.** -The library remains supported until the end-of-life date, -but will receive only _critical_ maintenance updates. +This repository will not be updated. The repository will be kept available in read-only mode. Please see the [Migration Guide](./MIGRATION.md) for advice about migrating to our replacement library diff --git a/src/cloudant/__init__.py b/src/cloudant/__init__.py index 66769dad..8e4d7e33 100644 --- a/src/cloudant/__init__.py +++ b/src/cloudant/__init__.py @@ -24,7 +24,7 @@ from .client import Cloudant, CouchDB from ._common_util import CloudFoundryService -warnings.warn('The module cloudant is now deprecated. The replacement is ibmcloudant.', +warnings.warn('The module cloudant is now end-of-life. The replacement is ibmcloudant.', DeprecationWarning) @contextlib.contextmanager From 3636169ef2212f70668c70d169533653817a9ae7 Mon Sep 17 00:00:00 2001 From: Ivan Date: Wed, 9 Mar 2022 10:17:33 +0000 Subject: [PATCH 51/52] Update MIGRATION.md Fixing small typo that prevented a markdown link to render properly. --- MIGRATION.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index a122d7dd..3323e1d8 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -17,8 +17,7 @@ remote HTTP API. For example, in the case of the document context manager, this to fetch and a `put_document` to save. 1. In `cloudant-python-sdk` View, Search, and Query (aka `_find` endpoint) operation responses contain raw JSON content like using `raw_result=True` in `python-cloudant`. -1. Replay adapters are replaced by the [automatic retries](https://github. - com/IBM/ibm-cloud-sdk-common/#automatic-retries) feature for failed requests. +1. Replay adapters are replaced by the [automatic retries](https://github.com/IBM/ibm-cloud-sdk-common/#automatic-retries) feature for failed requests. 1. Error handling is not transferable from `python-cloudant` to `cloudant-python-sdk`. For more information go to the [Error handling section](https://cloud.ibm.com/apidocs/cloudant?code=python#error-handling) in our API docs. 1. Custom HTTP client configurations in `python-cloudant` can be set differently in `cloudant-python-sdk`. For more information go to the From 1fb0bb577e8fba6dbf95919d1e5f6326057e0b01 Mon Sep 17 00:00:00 2001 From: Robert Newson Date: Fri, 30 Aug 2024 16:36:34 +0000 Subject: [PATCH 52/52] deprecation warning --- docs/index.rst | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 0f642eed..a26ea611 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,14 +1 @@ -python-cloudant documentation -============================= - -This is the official Cloudant client library for Python. - -.. toctree:: - :maxdepth: 4 - - compatibility - getting_started - cloudant - -* :ref:`genindex` - +This library is end-of-life and no longer supported.