diff --git a/docs/getting_started.rst b/docs/getting_started.rst
index 17fe3e6..f4ce130 100644
--- a/docs/getting_started.rst
+++ b/docs/getting_started.rst
@@ -88,6 +88,30 @@ following statements hold true:
                      connect=True,
                      auto_renew=True)
 
+
+************************************
+Identity and Access Management (IAM)
+************************************
+
+IBM Cloud Identity & Access Management enables you to securely authenticate
+users and control access to all cloud resources consistently in the IBM Bluemix
+Cloud Platform.
+
+See `IBM Cloud Identity and Access Management <https://console.bluemix.net/docs/services/Cloudant/guides/iam.html#ibm-cloud-identity-and-access-management>`_
+for more information.
+
+The production IAM token service at *https://iam.bluemix.net/oidc/token* is used
+by default. You can set an ``IAM_TOKEN_URL`` environment variable to override
+this.
+
+You can easily connect to your Cloudant account using an IAM API key:
+
+.. code-block:: python
+
+    # Authenticate using an IAM API key
+    client = Cloudant.iam(ACCOUNT_NAME, API_KEY, connect=True)
+
+
 ****************
 Resource sharing
 ****************
diff --git a/src/cloudant/_2to3.py b/src/cloudant/_2to3.py
index 2e52af9..5c7d412 100644
--- a/src/cloudant/_2to3.py
+++ b/src/cloudant/_2to3.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2016 IBM. All rights reserved.
+# Copyright (c) 2016, 2017 IBM. 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.
@@ -39,7 +39,9 @@ if PY2:
     # pylint: disable=wrong-import-position,no-name-in-module,import-error,unused-import
     from urllib import quote as url_quote, quote_plus as url_quote_plus
     from urlparse import urlparse as url_parse
+    from urlparse import urljoin as url_join
     from ConfigParser import RawConfigParser
+    from cookielib import Cookie
 
     def iteritems_(adict):
         """
@@ -60,9 +62,11 @@ if PY2:
         return itr.next()
 else:
     from urllib.parse import urlparse as url_parse  # pylint: disable=wrong-import-position,no-name-in-module,import-error,ungrouped-imports
+    from urllib.parse import urljoin as url_join  # pylint: disable=wrong-import-position,no-name-in-module,import-error,ungrouped-imports
     from urllib.parse import quote as url_quote  # pylint: disable=wrong-import-position,no-name-in-module,import-error,ungrouped-imports
     from urllib.parse import quote_plus as url_quote_plus  # pylint: disable=wrong-import-position,no-name-in-module,import-error,ungrouped-imports
     from configparser import RawConfigParser  # pylint: disable=wrong-import-position,no-name-in-module,import-error
+    from http.cookiejar import Cookie # pylint: disable=wrong-import-position,no-name-in-module,import-error
 
     def iteritems_(adict):
         """
diff --git a/src/cloudant/__init__.py b/src/cloudant/__init__.py
index 8cb9f75..04131db 100644
--- a/src/cloudant/__init__.py
+++ b/src/cloudant/__init__.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright (c) 2015 IBM. All rights reserved.
+# Copyright (c) 2015, 2017 IBM. 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.
@@ -62,6 +62,35 @@ def cloudant(user, passwd, **kwargs):
     yield cloudant_session
     cloudant_session.disconnect()
 
+@contextlib.contextmanager
+def cloudant_iam(account_name, api_key, **kwargs):
+    """
+    Provides a context manager to create a Cloudant session using IAM
+    authentication and provide access to databases, docs etc.
+
+    :param account_name: Cloudant account name.
+    :param api_key: IAM authentication API key.
+
+    For example:
+
+    .. code-block:: python
+
+        # cloudant context manager
+        from cloudant import cloudant_iam
+
+        with cloudant_iam(ACCOUNT_NAME, API_KEY) as client:
+            # Context handles connect() and disconnect() for you.
+            # Perform library operations within this context.  Such as:
+            print client.all_dbs()
+            # ...
+
+    """
+    cloudant_session = Cloudant.iam(account_name, api_key, **kwargs)
+
+    cloudant_session.connect()
+    yield cloudant_session
+    cloudant_session.disconnect()
+
 @contextlib.contextmanager
 def cloudant_bluemix(vcap_services, instance_name=None, **kwargs):
     """
diff --git a/src/cloudant/_common_util.py b/src/cloudant/_common_util.py
index 6dcf331..fe2e068 100644
--- a/src/cloudant/_common_util.py
+++ b/src/cloudant/_common_util.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright (c) 2015, 2016, 2017 IBM Corp. All rights reserved.
+# Copyright (c) 2015, 2017 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.
@@ -17,13 +17,14 @@ Module containing miscellaneous classes, functions, and constants used
 throughout the library.
 """
 
+import os
 import sys
 import platform
 from collections import Sequence
 import json
-from requests import Session
+from requests import RequestException, Session
 
-from ._2to3 import LONGTYPE, STRTYPE, NONETYPE, UNITYPE, iteritems_, url_parse
+from ._2to3 import LONGTYPE, STRTYPE, NONETYPE, UNITYPE, iteritems_, url_join
 from .error import CloudantArgumentError, CloudantException
 
 # Library Constants
@@ -276,6 +277,7 @@ def append_response_error_content(response, **kwargs):
 
 # Classes
 
+
 class _Code(str):
     """
     Wraps a ``str`` object as a _Code object providing the means to handle
@@ -287,66 +289,212 @@ class _Code(str):
             return str.__new__(cls, code.encode('utf8'))
         return str.__new__(cls, code)
 
-class InfiniteSession(Session):
+
+class ClientSession(Session):
+    """
+    This class extends Session and provides a default timeout.
+    """
+
+    def __init__(self, **kwargs):
+        super(ClientSession, self).__init__()
+        self._timeout = kwargs.get('timeout', None)
+
+    def request(self, method, url, **kwargs):  # pylint: disable=W0221
+        """
+        Overrides ``requests.Session.request`` to set the timeout.
+        """
+        resp = super(ClientSession, self).request(
+            method, url, timeout=self._timeout, **kwargs)
+
+        return resp
+
+
+class CookieSession(ClientSession):
     """
-    This class provides for the ability to automatically renew session login
-    information in the event of expired session authentication.
+    This class extends ClientSession and provides cookie authentication.
     """
 
     def __init__(self, username, password, server_url, **kwargs):
-        super(InfiniteSession, self).__init__()
+        super(CookieSession, self).__init__(**kwargs)
         self._username = username
         self._password = password
-        self._server_url = server_url
-        self._timeout = kwargs.get('timeout', None)
+        self._auto_renew = kwargs.get('auto_renew', False)
+        self._session_url = url_join(server_url, '_session')
+
+    def info(self):
+        """
+        Get cookie based login user information.
+        """
+        resp = self.get(self._session_url)
+        resp.raise_for_status()
+
+        return resp.json()
+
+    def login(self):
+        """
+        Perform cookie based user login.
+        """
+        resp = super(CookieSession, self).request(
+            'POST',
+            self._session_url,
+            data={'name': self._username, 'password': self._password},
+        )
+        resp.raise_for_status()
+
+    def logout(self):
+        """
+        Logout cookie based user.
+        """
+        resp = super(CookieSession, self).request('DELETE', self._session_url)
+        resp.raise_for_status()
 
     def request(self, method, url, **kwargs):  # pylint: disable=W0221
         """
-        Overrides ``requests.Session.request`` to perform a POST to the
-        _session endpoint to renew Session cookie authentication settings and
-        then retry the original request, if necessary.
+        Overrides ``requests.Session.request`` to renew the cookie and then
+        retry the original request (if required).
         """
-        resp = super(InfiniteSession, self).request(
-            method, url, timeout=self._timeout, **kwargs)
-        path = url_parse(url).path.lower()
-        post_to_session = method.upper() == 'POST' and path == '/_session'
+        resp = super(CookieSession, self).request(method, url, **kwargs)
+
+        if not self._auto_renew:
+            return resp
+
         is_expired = any((
             resp.status_code == 403 and
             resp.json().get('error') == 'credentials_expired',
             resp.status_code == 401
         ))
-        if not post_to_session and is_expired:
-            super(InfiniteSession, self).request(
-                'POST',
-                '/'.join([self._server_url, '_session']),
-                data={'name': self._username, 'password': self._password},
-                headers={'Content-Type': 'application/x-www-form-urlencoded'}
-            )
-            resp = super(InfiniteSession, self).request(
-                method, url, timeout=self._timeout, **kwargs)
+
+        if is_expired:
+            self.login()
+            resp = super(CookieSession, self).request(method, url, **kwargs)
 
         return resp
 
-class ClientSession(Session):
+    def set_credentials(self, username, password):
+        """
+        Set a new username and password.
+
+        :param str username: New username.
+        :param str password: New password.
+        """
+        if username is not None:
+            self._username = username
+
+        if password is not None:
+            self._password = password
+
+
+class IAMSession(ClientSession):
     """
-    This class extends Session and provides a default timeout.
+    This class extends ClientSession and provides IAM authentication.
     """
 
-    def __init__(self, username, password, server_url, **kwargs):
-        super(ClientSession, self).__init__()
-        self._username = username
-        self._password = password
-        self._server_url = server_url
-        self._timeout = kwargs.get('timeout', None)
+    def __init__(self, api_key, server_url, **kwargs):
+        super(IAMSession, self).__init__(**kwargs)
+        self._api_key = api_key
+        self._auto_renew = kwargs.get('auto_renew', False)
+        self._session_url = url_join(server_url, '_iam_session')
+        self._token_url = os.environ.get(
+            'IAM_TOKEN_URL', 'https://iam.bluemix.net/oidc/token')
+
+    def info(self):
+        """
+        Get IAM cookie based login user information.
+        """
+        resp = self.get(self._session_url)
+        resp.raise_for_status()
+
+        return resp.json()
+
+    def login(self):
+        """
+        Perform IAM cookie based user login.
+        """
+        access_token = self._get_access_token()
+        try:
+            super(IAMSession, self).request(
+                'POST',
+                self._session_url,
+                headers={'Content-Type': 'application/json'},
+                data=json.dumps({'access_token': access_token})
+            ).raise_for_status()
+
+        except RequestException:
+            raise CloudantException(
+                'Failed to exchange IAM token with Cloudant')
+
+    def logout(self):
+        """
+        Logout IAM cookie based user.
+        """
+        self.cookies.clear()
 
     def request(self, method, url, **kwargs):  # pylint: disable=W0221
         """
-        Overrides ``requests.Session.request`` to set the timeout.
+        Overrides ``requests.Session.request`` to renew the IAM cookie
+        and then retry the original request (if required).
         """
-        resp = super(ClientSession, self).request(
-            method, url, timeout=self._timeout, **kwargs)
+        # The CookieJar API prevents callers from getting an individual Cookie
+        # object by name.
+        # We are forced to use the only exposed method of discarding expired
+        # cookies from the CookieJar. Internally this involves iterating over
+        # the entire CookieJar and calling `.is_expired()` on each Cookie
+        # object.
+        self.cookies.clear_expired_cookies()
+
+        if self._auto_renew and 'IAMSession' not in self.cookies.keys():
+            self.login()
+
+        resp = super(IAMSession, self).request(method, url, **kwargs)
+
+        if not self._auto_renew:
+            return resp
+
+        if resp.status_code == 401:
+            self.login()
+            resp = super(IAMSession, self).request(method, url, **kwargs)
+
         return resp
 
+    def set_credentials(self, username, api_key):
+        """
+        Set a new IAM API key.
+
+        :param str username: Username parameter is unused.
+        :param str api_key: New IAM API key.
+        """
+        if api_key is not None:
+            self._api_key = api_key
+
+    def _get_access_token(self):
+        """
+        Get IAM access token using API key.
+        """
+        err = 'Failed to contact IAM token service'
+        try:
+            resp = super(IAMSession, self).request(
+                'POST',
+                self._token_url,
+                auth=('bx', 'bx'),  # required for user API keys
+                headers={'Accepts': 'application/json'},
+                data={
+                    'grant_type': 'urn:ibm:params:oauth:grant-type:apikey',
+                    'response_type': 'cloud_iam',
+                    'apikey': self._api_key
+                }
+            )
+            err = resp.json().get('errorMessage', err)
+            resp.raise_for_status()
+
+            return resp.json()['access_token']
+
+        except KeyError:
+            raise CloudantException('Invalid response from IAM token service')
+
+        except RequestException:
+            raise CloudantException(err)
+
+
 class CloudFoundryService(object):
     """ Manages Cloud Foundry service configuration. """
 
diff --git a/src/cloudant/client.py b/src/cloudant/client.py
index 7c69c46..3a1360c 100755
--- a/src/cloudant/client.py
+++ b/src/cloudant/client.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright (C) 2015, 2016, 2017 IBM Corp. All rights reserved.
+# Copyright (C) 2015, 2017 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.
@@ -29,10 +29,10 @@ from .error import (
 from ._common_util import (
     USER_AGENT,
     append_response_error_content,
-    InfiniteSession,
     ClientSession,
-    CloudFoundryService)
-
+    CloudFoundryService,
+    CookieSession,
+    IAMSession)
 
 class CouchDB(dict):
     """
@@ -67,6 +67,10 @@ class CouchDB(dict):
         `Requests library timeout argument
         <http://docs.python-requests.org/en/master/user/quickstart/#timeouts>`_.
         but will apply to every request made using this client.
+    :param bool use_iam: Keyword argument, if set to True performs
+        IAM authentication with server. Default is False.
+        Use :func:`~cloudant.client.CouchDB.iam` to construct an IAM
+        authenticated client.
     """
     _DATABASE_CLASS = CouchDatabase
 
@@ -74,7 +78,6 @@ class CouchDB(dict):
         super(CouchDB, self).__init__()
         self._user = user
         self._auth_token = auth_token
-        self._client_session = None
         self.server_url = kwargs.get('url')
         self._client_user_header = None
         self.admin_party = admin_party
@@ -83,6 +86,7 @@ class CouchDB(dict):
         self._timeout = kwargs.get('timeout', None)
         self.r_session = None
         self._auto_renew = kwargs.get('auto_renew', False)
+        self._use_iam = kwargs.get('use_iam', False)
         connect_to_couch = kwargs.get('connect', False)
         if connect_to_couch and self._DATABASE_CLASS == CouchDatabase:
             self.connect()
@@ -93,29 +97,34 @@ class CouchDB(dict):
         authentication if necessary.
         """
         if self.r_session:
-            return
+            self.session_logout()
 
-        if self._auto_renew and not self.admin_party:
-            self.r_session = InfiniteSession(
-                self._user,
+        if self.admin_party:
+            self.r_session = ClientSession(timeout=self._timeout)
+        elif self._use_iam:
+            self.r_session = IAMSession(
                 self._auth_token,
                 self.server_url,
+                auto_renew=self._auto_renew,
                 timeout=self._timeout
             )
         else:
-            self.r_session = ClientSession(
+            self.r_session = CookieSession(
                 self._user,
                 self._auth_token,
                 self.server_url,
+                auto_renew=self._auto_renew,
                 timeout=self._timeout
             )
+
         # If a Transport Adapter was supplied add it to the session
         if self.adapter is not None:
             self.r_session.mount(self.server_url, self.adapter)
         if self._client_user_header is not None:
             self.r_session.headers.update(self._client_user_header)
-        self.session_login(self._user, self._auth_token)
-        self._client_session = self.session()
+
+        self.session_login()
+
         # Utilize an event hook to append to the response message
         # using :func:`~cloudant.common_util.append_response_error_content`
         self.r_session.hooks['response'].append(append_response_error_content)
@@ -124,7 +133,9 @@ class CouchDB(dict):
         """
         Ends a client authentication session, performs a logout and a clean up.
         """
-        self.session_logout()
+        if self.r_session:
+            self.session_logout()
+
         self.r_session = None
         self.clear()
 
@@ -137,11 +148,8 @@ class CouchDB(dict):
         """
         if self.admin_party:
             return None
-        sess_url = '/'.join((self.server_url, '_session'))
-        resp = self.r_session.get(sess_url)
-        resp.raise_for_status()
-        sess_data = resp.json()
-        return sess_data
+
+        return self.r_session.info()
 
     def session_cookie(self):
         """
@@ -153,26 +161,16 @@ class CouchDB(dict):
             return None
         return self.r_session.cookies.get('AuthSession')
 
-    def session_login(self, user, passwd):
+    def session_login(self, user=None, passwd=None):
         """
         Performs a session login by posting the auth information
         to the _session endpoint.
-
-        :param str user: Username used to connect.
-        :param str passwd: Passcode used to connect.
         """
         if self.admin_party:
             return
-        sess_url = '/'.join((self.server_url, '_session'))
-        resp = self.r_session.post(
-            sess_url,
-            data={
-                'name': user,
-                'password': passwd
-            },
-            headers={'Content-Type': 'application/x-www-form-urlencoded'}
-        )
-        resp.raise_for_status()
+
+        self.r_session.set_credentials(user, passwd)
+        self.r_session.login()
 
     def session_logout(self):
         """
@@ -181,9 +179,8 @@ class CouchDB(dict):
         """
         if self.admin_party:
             return
-        sess_url = '/'.join((self.server_url, '_session'))
-        resp = self.r_session.delete(sess_url)
-        resp.raise_for_status()
+
+        self.r_session.logout()
 
     def basic_auth_str(self):
         """
@@ -783,3 +780,18 @@ class Cloudant(CouchDB):
                         service.password,
                         url=service.url,
                         **kwargs)
+
+    @classmethod
+    def iam(cls, account_name, api_key, **kwargs):
+        """
+        Create a Cloudant client that uses IAM authentication.
+
+        :param account_name: Cloudant account name.
+        :param api_key: IAM authentication API key.
+        """
+        return cls(None,
+                   api_key,
+                   account=account_name,
+                   auto_renew=kwargs.get('auto_renew', True),
+                   use_iam=True,
+                   **kwargs)

Support IAM integration
Cloudant client libraries need to be able to exchange an IAM API key for an IAM access token. The user of the client library provides the API key. The access token is obtained by calling an IAM endpoint. The access token is time limited, like a cookie, so refresh logic also needs writing.
diff --git a/tests/unit/auth_renewal_tests.py b/tests/unit/auth_renewal_tests.py
index 799783d..3d9b7cc 100644
--- a/tests/unit/auth_renewal_tests.py
+++ b/tests/unit/auth_renewal_tests.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright (c) 2016 IBM. All rights reserved.
+# Copyright (c) 2016, 2017 IBM. 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.
@@ -23,14 +23,14 @@ import os
 import requests
 import time
 
-from cloudant._common_util import InfiniteSession
+from cloudant._common_util import CookieSession
 
 from .unit_t_db_base import UnitTestDbBase
 
 @unittest.skipIf(os.environ.get('ADMIN_PARTY') == 'true', 'Skipping - Admin Party mode')
 class AuthRenewalTests(UnitTestDbBase):
     """
-    Auto renewal tests primarily testing the InfiniteSession functionality
+    Auto renewal tests primarily testing the CookieSession functionality
     """
 
     def setUp(self):
@@ -62,10 +62,10 @@ class AuthRenewalTests(UnitTestDbBase):
             db_2_auth_session = db_2.r_session.cookies.get('AuthSession')
             doc_auth_session = doc.r_session.cookies.get('AuthSession')
             
-            self.assertIsInstance(self.client.r_session, InfiniteSession)
-            self.assertIsInstance(db.r_session, InfiniteSession)
-            self.assertIsInstance(db_2.r_session, InfiniteSession)
-            self.assertIsInstance(doc.r_session, InfiniteSession)
+            self.assertIsInstance(self.client.r_session, CookieSession)
+            self.assertIsInstance(db.r_session, CookieSession)
+            self.assertIsInstance(db_2.r_session, CookieSession)
+            self.assertIsInstance(doc.r_session, CookieSession)
             self.assertIsNotNone(auth_session)
             self.assertTrue(
                 auth_session ==
diff --git a/tests/unit/client_tests.py b/tests/unit/client_tests.py
index b2dbcbf..796e5fc 100644
--- a/tests/unit/client_tests.py
+++ b/tests/unit/client_tests.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright (c) 2015, 2016, 2017 IBM Corp. All rights reserved.
+# Copyright (c) 2015, 2017 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.
@@ -28,13 +28,14 @@ import sys
 import os
 import datetime
 
-from requests import ConnectTimeout
+from requests import ConnectTimeout, HTTPError
+from time import sleep
 
 from cloudant import cloudant, cloudant_bluemix, couchdb, couchdb_admin_party
 from cloudant.client import Cloudant, CouchDB
 from cloudant.error import CloudantArgumentError, CloudantClientException
 from cloudant.feed import Feed, InfiniteFeed
-from cloudant._common_util import InfiniteSession
+from cloudant._common_util import CookieSession
 
 from .unit_t_db_base import UnitTestDbBase
 from .. import bytes_, str_
@@ -163,7 +164,7 @@ class ClientTests(UnitTestDbBase):
 
     def test_auto_renew_enabled(self):
         """
-        Test that InfiniteSession is used when auto_renew is enabled.
+        Test that CookieSession is used when auto_renew is enabled.
         """
         try:
             self.set_up_client(auto_renew=True)
@@ -171,13 +172,13 @@ class ClientTests(UnitTestDbBase):
             if os.environ.get('ADMIN_PARTY') == 'true':
                 self.assertIsInstance(self.client.r_session, requests.Session)
             else:
-                self.assertIsInstance(self.client.r_session, InfiniteSession)
+                self.assertIsInstance(self.client.r_session, CookieSession)
         finally:
             self.client.disconnect()
 
     def test_auto_renew_enabled_with_auto_connect(self):
         """
-        Test that InfiniteSession is used when auto_renew is enabled along with
+        Test that CookieSession is used when auto_renew is enabled along with
         an auto_connect.
         """
         try:
@@ -185,7 +186,7 @@ class ClientTests(UnitTestDbBase):
             if os.environ.get('ADMIN_PARTY') == 'true':
                 self.assertIsInstance(self.client.r_session, requests.Session)
             else:
-                self.assertIsInstance(self.client.r_session, InfiniteSession)
+                self.assertIsInstance(self.client.r_session, CookieSession)
         finally:
             self.client.disconnect()
 
@@ -492,6 +493,30 @@ class CloudantClientTests(UnitTestDbBase):
     Cloudant specific client unit tests
     """
 
+    def test_cloudant_session_login(self):
+        """
+        Test that the Cloudant client session successfully authenticates.
+        """
+        self.client.connect()
+        old_cookie = self.client.session_cookie()
+
+        sleep(5)  # ensure we get a different cookie back
+
+        self.client.session_login()
+        self.assertNotEqual(self.client.session_cookie(), old_cookie)
+
+    def test_cloudant_session_login_with_new_credentials(self):
+        """
+        Test that the Cloudant client session fails to authenticate when
+        passed incorrect credentials.
+        """
+        self.client.connect()
+
+        with self.assertRaises(HTTPError) as cm:
+            self.client.session_login('invalid-user-123', 'pa$$w0rd01')
+
+        self.assertTrue(str(cm.exception).find('Name or password is incorrect'))
+
     def test_cloudant_context_helper(self):
         """
         Test that the cloudant context helper works as expected.
diff --git a/tests/unit/iam_auth_tests.py b/tests/unit/iam_auth_tests.py
new file mode 100644
index 0000000..a65d3f0
--- /dev/null
+++ b/tests/unit/iam_auth_tests.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python
+# Copyright (c) 2017 IBM. 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.
+""" Unit tests for IAM authentication. """
+import time
+import unittest
+import json
+import mock
+
+from cloudant._2to3 import Cookie
+from cloudant._common_util import IAMSession
+from cloudant.client import Cloudant
+
+MOCK_API_KEY = 'CqbrIYzdO3btWV-5t4teJLY_etfT_dkccq-vO-5vCXSo'
+
+MOCK_ACCESS_TOKEN = ('eyJraWQiOiIyMDE3MDQwMi0wMDowMDowMCIsImFsZyI6IlJTMjU2In0.e'
+                     'yJpYW1faWQiOiJJQk1pZC0yNzAwMDdHRjBEIiwiaWQiOiJJQk1pZC0yNz'
+                     'AwMDdHRjBEIiwicmVhbG1pZCI6IklCTWlkIiwiaWRlbnRpZmllciI6IjI'
+                     '3MDAwN0dGMEQiLCJnaXZlbl9uYW1lIjoiVG9tIiwiZmFtaWx5X25hbWUi'
+                     'OiJCbGVuY2giLCJuYW1lIjoiVG9tIEJsZW5jaCIsImVtYWlsIjoidGJsZ'
+                     'W5jaEB1ay5pYm0uY29tIiwic3ViIjoidGJsZW5jaEB1ay5pYm0uY29tIi'
+                     'wiYWNjb3VudCI6eyJic3MiOiI1ZTM1ZTZhMjlmYjJlZWNhNDAwYWU0YzN'
+                     'lMWZhY2Y2MSJ9LCJpYXQiOjE1MDA0NjcxMDIsImV4cCI6MTUwMDQ3MDcw'
+                     'MiwiaXNzIjoiaHR0cHM6Ly9pYW0ubmcuYmx1ZW1peC5uZXQvb2lkYy90b'
+                     '2tlbiIsImdyYW50X3R5cGUiOiJ1cm46aWJtOnBhcmFtczpvYXV0aDpncm'
+                     'FudC10eXBlOmFwaWtleSIsInNjb3BlIjoib3BlbmlkIiwiY2xpZW50X2l'
+                     'kIjoiZGVmYXVsdCJ9.XAPdb5K4n2nYih-JWTWBGoKkxTXM31c1BB1g-Ci'
+                     'auc2LxuoNXVTyz_mNqf1zQL07FUde1Cb_dwrbotjickNcxVPost6byQzt'
+                     'fc0mRF1x2S6VR8tn7SGiRmXBjLofkTh1JQq-jutp2MS315XbTG6K6m16u'
+                     'YzL9qfMnRvQHxsZWErzfPiJx-Trg_j7OX-qNFjdNUGnRpU7FmULy0r7Rx'
+                     'Ld8mhG-M1yxVzRBAZzvM63s0XXfMnk1oLi-BuUUTqVOdrM0KyYMWfD0Q7'
+                     '2PTo4Exa17V-R_73Nq8VPCwpOvZcwKRA2sPTVgTMzU34max8b5kpTzVGJ'
+                     '6SXSItTVOUdAygZBng')
+
+MOCK_OIDC_TOKEN_RESPONSE = {
+    'access_token':  MOCK_ACCESS_TOKEN,
+    'refresh_token': ('MO61FKNvVRWkSa4vmBZqYv_Jt1kkGMUc-XzTcNnR-GnIhVKXHUWxJVV3'
+                      'RddE8Kqh3X_TZRmyK8UySIWKxoJ2t6obUSUalPm90SBpTdoXtaljpNyo'
+                      'rmqCCYPROnk6JBym72ikSJqKHHEZVQkT0B5ggZCwPMnKagFj0ufs-VIh'
+                      'CF97xhDxDKcIPMWG02xxPuESaSTJJug7e_dUDoak_ZXm9xxBmOTRKwOx'
+                      'n5sTKthNyvVpEYPE7jIHeiRdVDOWhN5LomgCn3TqFCLpMErnqwgNYbyC'
+                      'Bd9rNm-alYKDb6Jle4njuIBpXxQPb4euDwLd1osApaSME3nEarFWqRBz'
+                      'hjoqCe1Kv564s_rY7qzD1nHGvKOdpSa0ZkMcfJ0LbXSQPs7gBTSVrBFZ'
+                      'qwlg-2F-U3Cto62-9qRR_cEu_K9ZyVwL4jWgOlngKmxV6Ku4L5mHp4Kg'
+                      'EJSnY_78_V2nm64E--i2ZA1FhiKwIVHDOivVNhggE9oabxg54vd63glp'
+                      '4GfpNnmZsMOUYG9blJJpH4fDX4Ifjbw-iNBD7S2LRpP8b8vG9pb4WioG'
+                      'zN43lE5CysveKYWrQEZpThznxXlw1snDu_A48JiL3Lrvo1LobLhF3zFV'
+                      '-kQ='),
+    'token_type': 'Bearer',
+    'expires_in': 3600,  # 60mins
+    'expiration': 1500470702  # Wed Jul 19 14:25:02 2017
+}
+
+
+class IAMAuthTests(unittest.TestCase):
+    """ Unit tests for IAM authentication. """
+
+    @staticmethod
+    def _mock_cookie(expires_secs=300):
+        return Cookie(
+            version=0,
+            name='IAMSession',
+            value=('SQJCaUQxMqEfMEAyRKU6UopLVXceS0c9RPuQgDArCEYoN3l_TEY4gdf-DJ7'
+                   '4sHfjcNEUVjfdOvA'),
+            port=None,
+            port_specified=False,
+            domain='localhost',
+            domain_specified=False,
+            domain_initial_dot=False,
+            path="/",
+            path_specified=True,
+            secure=True,
+            expires=int(time.time() + expires_secs),
+            discard=False,
+            comment=None,
+            comment_url=None,
+            rest={'HttpOnly': None},
+            rfc2109=True)
+
+    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)
+
+        new_api_key = 'some_new_api_key'
+        iam.set_credentials(None, new_api_key)
+
+        self.assertEquals(iam._api_key, new_api_key)
+
+    @mock.patch('cloudant._common_util.ClientSession.request')
+    def test_iam_get_access_token(self, m_req):
+        m_response = mock.MagicMock()
+        m_response.json.return_value = MOCK_OIDC_TOKEN_RESPONSE
+        m_req.return_value = m_response
+
+        iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984')
+        access_token = iam._get_access_token()
+
+        m_req.assert_called_once_with(
+            'POST',
+            iam._token_url,
+            auth=('bx', 'bx'),
+            headers={'Accepts': 'application/json'},
+            data={
+                'grant_type': 'urn:ibm:params:oauth:grant-type:apikey',
+                'response_type': 'cloud_iam',
+                'apikey': MOCK_API_KEY
+            }
+        )
+
+        self.assertEqual(access_token, MOCK_ACCESS_TOKEN)
+        self.assertTrue(m_response.raise_for_status.called)
+        self.assertTrue(m_response.json.called)
+
+    @mock.patch('cloudant._common_util.ClientSession.request')
+    @mock.patch('cloudant._common_util.IAMSession._get_access_token')
+    def test_iam_login(self, m_token, m_req):
+        m_token.return_value = MOCK_ACCESS_TOKEN
+        m_response = mock.MagicMock()
+        m_req.return_value = m_response
+
+        iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984')
+        iam.login()
+
+        m_req.assert_called_once_with(
+            'POST',
+            iam._session_url,
+            headers={'Content-Type': 'application/json'},
+            data=json.dumps({'access_token': MOCK_ACCESS_TOKEN})
+        )
+
+        self.assertEqual(m_token.call_count, 1)
+        self.assertTrue(m_response.raise_for_status.called)
+
+    def test_iam_logout(self):
+        iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984')
+        # add a valid cookie to jar
+        iam.cookies.set_cookie(self._mock_cookie())
+        self.assertEqual(len(iam.cookies.keys()), 1)
+        iam.logout()
+        self.assertEqual(len(iam.cookies.keys()), 0)
+
+    @mock.patch('cloudant._common_util.ClientSession.get')
+    def test_iam_get_session_info(self, m_get):
+        m_info = {'ok': True, 'info': {'authentication_db': '_users'}}
+
+        m_response = mock.MagicMock()
+        m_response.json.return_value = m_info
+        m_get.return_value = m_response
+
+        iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984')
+        info = iam.info()
+
+        m_get.assert_called_once_with(iam._session_url)
+
+        self.assertEqual(info, m_info)
+        self.assertTrue(m_response.raise_for_status.called)
+
+    @mock.patch('cloudant._common_util.IAMSession.login')
+    @mock.patch('cloudant._common_util.ClientSession.request')
+    def test_iam_first_request(self, m_req, m_login):
+        # mock 200
+        m_response_ok = mock.MagicMock()
+        type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
+        m_response_ok.json.return_value = {'ok': True}
+
+        m_req.return_value = m_response_ok
+
+        iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984', auto_renew=True)
+        iam.login()
+
+        self.assertEqual(m_login.call_count, 1)
+        self.assertEqual(m_req.call_count, 0)
+
+        # add a valid cookie to jar
+        iam.cookies.set_cookie(self._mock_cookie())
+
+        resp = iam.request('GET', 'http://127.0.0.1:5984/mydb1')
+
+        self.assertEqual(m_login.call_count, 1)
+        self.assertEqual(m_req.call_count, 1)
+        self.assertEqual(resp.status_code, 200)
+
+    @mock.patch('cloudant._common_util.IAMSession.login')
+    @mock.patch('cloudant._common_util.ClientSession.request')
+    def test_iam_renew_cookie_on_expiry(self, m_req, m_login):
+        # mock 200
+        m_response_ok = mock.MagicMock()
+        type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
+        m_response_ok.json.return_value = {'ok': True}
+
+        m_req.return_value = m_response_ok
+
+        iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984', auto_renew=True)
+        iam.login()
+
+        # add an expired cookie to jar
+        iam.cookies.set_cookie(self._mock_cookie(expires_secs=-300))
+
+        resp = iam.request('GET', 'http://127.0.0.1:5984/mydb1')
+
+        self.assertEqual(m_login.call_count, 2)
+        self.assertEqual(m_req.call_count, 1)
+        self.assertEqual(resp.status_code, 200)
+
+    @mock.patch('cloudant._common_util.IAMSession.login')
+    @mock.patch('cloudant._common_util.ClientSession.request')
+    def test_iam_renew_cookie_on_401_success(self, m_req, m_login):
+        # mock 200
+        m_response_ok = mock.MagicMock()
+        type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
+        m_response_ok.json.return_value = {'ok': True}
+        # mock 401
+        m_response_bad = mock.MagicMock()
+        type(m_response_bad).status_code = mock.PropertyMock(return_value=401)
+
+        m_req.side_effect = [m_response_bad, m_response_ok, m_response_ok]
+
+        iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984', auto_renew=True)
+        iam.login()
+        self.assertEqual(m_login.call_count, 1)
+
+        # add a valid cookie to jar
+        iam.cookies.set_cookie(self._mock_cookie())
+
+        resp = iam.request('GET', 'http://127.0.0.1:5984/mydb1')
+        self.assertEqual(resp.status_code, 200)
+        self.assertEqual(m_login.call_count, 2)
+        self.assertEqual(m_req.call_count, 2)
+
+        resp = iam.request('GET', 'http://127.0.0.1:5984/mydb1')
+        self.assertEqual(resp.status_code, 200)
+        self.assertEqual(m_login.call_count, 2)
+        self.assertEqual(m_req.call_count, 3)
+
+    @mock.patch('cloudant._common_util.IAMSession.login')
+    @mock.patch('cloudant._common_util.ClientSession.request')
+    def test_iam_renew_cookie_on_401_failure(self, m_req, m_login):
+        # mock 401
+        m_response_bad = mock.MagicMock()
+        type(m_response_bad).status_code = mock.PropertyMock(return_value=401)
+
+        m_req.return_value = m_response_bad
+
+        iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984', auto_renew=True)
+        iam.login()
+        self.assertEqual(m_login.call_count, 1)
+
+        # add a valid cookie to jar
+        iam.cookies.set_cookie(self._mock_cookie())
+
+        resp = iam.request('GET', 'http://127.0.0.1:5984/mydb1')
+        self.assertEqual(resp.status_code, 401)
+        self.assertEqual(m_login.call_count, 2)
+        self.assertEqual(m_req.call_count, 2)
+
+        resp = iam.request('GET', 'http://127.0.0.1:5984/mydb1')
+        self.assertEqual(resp.status_code, 401)
+        self.assertEqual(m_login.call_count, 3)
+        self.assertEqual(m_req.call_count, 4)
+
+    @mock.patch('cloudant._common_util.IAMSession.login')
+    @mock.patch('cloudant._common_util.ClientSession.request')
+    def test_iam_renew_cookie_disabled(self, m_req, m_login):
+        # mock 401
+        m_response_bad = mock.MagicMock()
+        type(m_response_bad).status_code = mock.PropertyMock(return_value=401)
+
+        m_req.return_value = m_response_bad
+
+        iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984', auto_renew=False)
+        iam.login()
+        self.assertEqual(m_login.call_count, 1)
+
+        resp = iam.request('GET', 'http://127.0.0.1:5984/mydb1')
+        self.assertEqual(resp.status_code, 401)
+        self.assertEqual(m_login.call_count, 1)  # no attempt to renew
+        self.assertEqual(m_req.call_count, 1)
+
+        resp = iam.request('GET', 'http://127.0.0.1:5984/mydb1')
+        self.assertEqual(resp.status_code, 401)
+        self.assertEqual(m_login.call_count, 1)  # no attempt to renew
+        self.assertEqual(m_req.call_count, 2)
+
+    @mock.patch('cloudant._common_util.IAMSession.login')
+    @mock.patch('cloudant._common_util.ClientSession.request')
+    def test_iam_client_create(self, m_req, m_login):
+        # mock 200
+        m_response_ok = mock.MagicMock()
+        type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
+        m_response_ok.json.return_value = ['animaldb']
+
+        m_req.return_value = m_response_ok
+
+        # create IAM client
+        client = Cloudant.iam('foo', MOCK_API_KEY)
+        client.connect()
+
+        # add a valid cookie to jar
+        client.r_session.cookies.set_cookie(self._mock_cookie())
+
+        dbs = client.all_dbs()
+
+        self.assertEqual(m_login.call_count, 1)
+        self.assertEqual(m_req.call_count, 1)
+        self.assertEqual(dbs, ['animaldb'])
+
+    @mock.patch('cloudant._common_util.IAMSession.login')
+    @mock.patch('cloudant._common_util.IAMSession.set_credentials')
+    def test_iam_client_session_login(self, m_set, m_login):
+        # create IAM client
+        client = Cloudant.iam('foo', MOCK_API_KEY)
+        client.connect()
+
+        # add a valid cookie to jar
+        client.r_session.cookies.set_cookie(self._mock_cookie())
+
+        client.session_login()
+
+        m_set.assert_called_with(None, None)
+        self.assertEqual(m_login.call_count, 2)
+        self.assertEqual(m_set.call_count, 2)
+
+    @mock.patch('cloudant._common_util.IAMSession.login')
+    @mock.patch('cloudant._common_util.IAMSession.set_credentials')
+    def test_iam_client_session_login_with_new_credentials(self, m_set, m_login):
+        # create IAM client
+        client = Cloudant.iam('foo', MOCK_API_KEY)
+        client.connect()
+
+        # add a valid cookie to jar
+        client.r_session.cookies.set_cookie(self._mock_cookie())
+
+        client.session_login('bar', 'baz')  # new creds
+
+        m_set.assert_called_with('bar', 'baz')
+        self.assertEqual(m_login.call_count, 2)
+        self.assertEqual(m_set.call_count, 2)
+
+
+if __name__ == '__main__':
+    unittest.main()

pip install -e .
parse_log_pytest
requirements.txt
mock==1.3.0 nose sphinx pylint flaky pytest
3.5
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
astroid==2.11.7
attrs==22.2.0
Babel==2.11.0
certifi==2021.5.30
charset-normalizer==2.0.12
-e git+https://github.com/cloudant/python-cloudant.git@56795a119f7f8b884b001f9b2f740d67d550ea41#egg=cloudant
dill==0.3.4
docutils==0.18.1
flaky==3.8.1
idna==3.10
imagesize==1.4.1
importlib-metadata==4.8.3
iniconfig==1.1.1
isort==5.10.1
Jinja2==3.0.3
lazy-object-proxy==1.7.1
MarkupSafe==2.0.1
mccabe==0.7.0
mock==1.3.0
nose==1.3.7
packaging==21.3
pbr==6.1.1
platformdirs==2.4.0
pluggy==1.0.0
py==1.11.0
Pygments==2.14.0
pylint==2.13.9
pyparsing==3.1.4
pytest==7.0.1
pytz==2025.2
requests==2.27.1
six==1.17.0
snowballstemmer==2.2.0
Sphinx==5.3.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
tomli==1.2.3
typed-ast==1.5.5
typing_extensions==4.1.1
urllib3==1.26.20
wrapt==1.16.0
zipp==3.6.0

name: python-cloudant
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - astroid==2.11.7
      - attrs==22.2.0
      - babel==2.11.0
      - charset-normalizer==2.0.12
      - dill==0.3.4
      - docutils==0.18.1
      - flaky==3.8.1
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - isort==5.10.1
      - jinja2==3.0.3
      - lazy-object-proxy==1.7.1
      - markupsafe==2.0.1
      - mccabe==0.7.0
      - mock==1.3.0
      - nose==1.3.7
      - packaging==21.3
      - pbr==6.1.1
      - platformdirs==2.4.0
      - pluggy==1.0.0
      - py==1.11.0
      - pygments==2.14.0
      - pylint==2.13.9
      - pyparsing==3.1.4
      - pytest==7.0.1
      - pytz==2025.2
      - requests==2.27.1
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==5.3.0
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - tomli==1.2.3
      - typed-ast==1.5.5
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - wrapt==1.16.0
      - zipp==3.6.0
prefix: /opt/conda/envs/python-cloudant

tests/unit/client_tests.py::CloudantClientExceptionTests::test_raise_using_invalid_code tests/unit/client_tests.py::CloudantClientExceptionTests::test_raise_with_proper_code_and_args tests/unit/client_tests.py::CloudantClientExceptionTests::test_raise_without_args tests/unit/client_tests.py::CloudantClientExceptionTests::test_raise_without_code tests/unit/client_tests.py::ClientTests::test_constructor_with_url tests/unit/iam_auth_tests.py::IAMAuthTests::test_iam_client_create tests/unit/iam_auth_tests.py::IAMAuthTests::test_iam_client_session_login tests/unit/iam_auth_tests.py::IAMAuthTests::test_iam_client_session_login_with_new_credentials tests/unit/iam_auth_tests.py::IAMAuthTests::test_iam_first_request tests/unit/iam_auth_tests.py::IAMAuthTests::test_iam_get_access_token tests/unit/iam_auth_tests.py::IAMAuthTests::test_iam_get_session_info tests/unit/iam_auth_tests.py::IAMAuthTests::test_iam_login tests/unit/iam_auth_tests.py::IAMAuthTests::test_iam_logout tests/unit/iam_auth_tests.py::IAMAuthTests::test_iam_renew_cookie_disabled tests/unit/iam_auth_tests.py::IAMAuthTests::test_iam_renew_cookie_on_401_failure tests/unit/iam_auth_tests.py::IAMAuthTests::test_iam_renew_cookie_on_401_success tests/unit/iam_auth_tests.py::IAMAuthTests::test_iam_renew_cookie_on_expiry tests/unit/iam_auth_tests.py::IAMAuthTests::test_iam_set_credentials
tests/unit/client_tests.py::ClientTests::test_set_non_db_value_via_setitem tests/unit/client_tests.py::ClientTests::test_all_dbs tests/unit/client_tests.py::ClientTests::test_auto_connect tests/unit/client_tests.py::ClientTests::test_auto_renew_enabled tests/unit/client_tests.py::ClientTests::test_auto_renew_enabled_with_auto_connect tests/unit/client_tests.py::ClientTests::test_basic_auth_str tests/unit/client_tests.py::ClientTests::test_connect tests/unit/client_tests.py::ClientTests::test_couchdb_context_helper tests/unit/client_tests.py::ClientTests::test_create_db_via_setitem tests/unit/client_tests.py::ClientTests::test_create_delete_database tests/unit/client_tests.py::ClientTests::test_create_existing_database tests/unit/client_tests.py::ClientTests::test_db_updates_feed_call tests/unit/client_tests.py::ClientTests::test_delete_cached_db_object_via_delitem tests/unit/client_tests.py::ClientTests::test_delete_non_existing_database tests/unit/client_tests.py::ClientTests::test_delete_remote_db_via_delitem tests/unit/client_tests.py::ClientTests::test_get_cached_db_object_via_get tests/unit/client_tests.py::ClientTests::test_get_db_via_getitem tests/unit/client_tests.py::ClientTests::test_get_non_existing_db_via_getitem tests/unit/client_tests.py::ClientTests::test_get_remote_db_via_get tests/unit/client_tests.py::ClientTests::test_keys tests/unit/client_tests.py::ClientTests::test_local_set_db_value_via_setitem tests/unit/client_tests.py::ClientTests::test_multiple_connect tests/unit/client_tests.py::ClientTests::test_session tests/unit/client_tests.py::ClientTests::test_session_cookie
src/cloudant/_2to3.py src/cloudant/_common_util.py docs/getting_started.rst src/cloudant/client.py src/cloudant/__init__.py
src/cloudant/_2to3.py src/cloudant/_common_util.py docs/getting_started.rst src/cloudant/client.py src/cloudant/__init__.py