Repository: cloudant/python-cloudant

Issue:
Implement method to create Cloudant client from VCAP services environment variable
Add an additional method to parse a `VCAP_SERVICES` environment variable allowing easy binding of a service instance to a Cloud Foundry application.

Example `VCAP_SERVICES` variable is JSON:
```json
{
    "cloudantNoSQLDB": [
        {
            "credentials": {
                "username": "example",
                "password": "xxxxxxx",
                "host": "example.cloudant.com",
                "port": 443,
                "url": "https://example:xxxxxxx@example.cloudant.com"
            },
            "syslog_drain_url": null,
            "label": "cloudantNoSQLDB",
            "provider": null,
            "plan": "Lite",
            "name": "Cloudant NoSQL DB"
        }
    ]
}
```

Discussion:

Patch:
diff --git a/src/cloudant/__init__.py b/src/cloudant/__init__.py
index 1ed58b4..caed558 100644
--- a/src/cloudant/__init__.py
+++ b/src/cloudant/__init__.py
@@ -63,19 +63,21 @@ def cloudant(user, passwd, **kwargs):
     cloudant_session.disconnect()
 
 @contextlib.contextmanager
-def cloudant_bluemix(bm_service_name=None, **kwargs):
+def cloudant_bluemix(vcap_services, instance_name=None, **kwargs):
     """
     Provides a context manager to create a Cloudant session and provide access
     to databases, docs etc.
 
-    :param str bm_service_name: Optional Bluemix service instance name. Only
-        required if multiple Cloudant services are available.
+    :param vcap_services: VCAP_SERVICES environment variable
+    :type vcap_services: dict or str
+    :param str instance_name: Optional Bluemix instance name. Only required if
+        multiple Cloudant instances are available.
     :param str encoder: Optional json Encoder object used to encode
         documents for storage. Defaults to json.JSONEncoder.
 
-    Loads all configuration from the VCAP_SERVICES Cloud Foundry environment
-    variable. The VCAP_SERVICES variable contains connection information to
-    access a service instance. For example:
+    Loads all configuration from the specified VCAP_SERVICES Cloud Foundry
+    environment variable. The VCAP_SERVICES variable contains connection
+    information to access a service instance. For example:
 
     .. code-block:: json
 
@@ -102,8 +104,23 @@ def cloudant_bluemix(bm_service_name=None, **kwargs):
 
     See `Cloud Foundry Environment Variables <http://docs.cloudfoundry.org/
     devguide/deploy-apps/environment-variable.html#VCAP-SERVICES>`_.
+
+    Example usage:
+
+    .. code-block:: python
+
+        import os
+
+        # cloudant_bluemix context manager
+        from cloudant import cloudant_bluemix
+
+        with cloudant_bluemix(os.getenv('VCAP_SERVICES'), 'Cloudant NoSQL DB') as client:
+            # Context handles connect() and disconnect() for you.
+            # Perform library operations within this context.  Such as:
+            print client.all_dbs()
+            # ...
     """
-    service = CloudFoundryService(bm_service_name)
+    service = CloudFoundryService(vcap_services, instance_name)
     cloudant_session = Cloudant(
         username=service.username,
         password=service.password,
diff --git a/src/cloudant/_common_util.py b/src/cloudant/_common_util.py
index ad84b4f..2e8ad78 100644
--- a/src/cloudant/_common_util.py
+++ b/src/cloudant/_common_util.py
@@ -17,7 +17,6 @@ Module containing miscellaneous classes, functions, and constants used
 throughout the library.
 """
 
-import os
 import sys
 import platform
 from collections import Sequence
@@ -340,9 +339,12 @@ class InfiniteSession(Session):
 class CloudFoundryService(object):
     """ Manages Cloud Foundry service configuration. """
 
-    def __init__(self, name=None):
+    def __init__(self, vcap_services, name=None):
         try:
-            services = json.loads(os.getenv('VCAP_SERVICES', '{}'))
+            services = vcap_services
+            if not isinstance(vcap_services, dict):
+                services = json.loads(vcap_services)
+
             cloudant_services = services.get('cloudantNoSQLDB', [])
 
             # use first service if no name given and only one service present


Test patch:
diff --git a/tests/unit/cloud_foundry_tests.py b/tests/unit/cloud_foundry_tests.py
index f70eb70..043949f 100644
--- a/tests/unit/cloud_foundry_tests.py
+++ b/tests/unit/cloud_foundry_tests.py
@@ -19,7 +19,6 @@ Unit tests for the CloudFoundryService class.
 """
 
 import json
-import mock
 import unittest
 
 from cloudant._common_util import CloudFoundryService
@@ -93,94 +92,101 @@ class CloudFoundryServiceTests(unittest.TestCase):
             }
         ]})
 
-    @mock.patch('os.getenv')
-    def test_get_vcap_service_default_success(self, m_getenv):
-        m_getenv.return_value = self._test_vcap_services_single
-        service = CloudFoundryService()
+    def test_get_vcap_service_default_success(self):
+        service = CloudFoundryService(self._test_vcap_services_single)
         self.assertEqual('Cloudant NoSQL DB 1', service.name)
 
-    @mock.patch('os.getenv')
-    def test_get_vcap_service_default_failure_multiple_services(self, m_getenv):
-        m_getenv.return_value = self._test_vcap_services_multiple
+    def test_get_vcap_service_default_success_as_dict(self):
+        service = CloudFoundryService(
+            json.loads(self._test_vcap_services_single)
+        )
+        self.assertEqual('Cloudant NoSQL DB 1', service.name)
+
+    def test_get_vcap_service_default_failure_multiple_services(self):
         with self.assertRaises(CloudantException) as cm:
-            CloudFoundryService()
+            CloudFoundryService(self._test_vcap_services_multiple)
         self.assertEqual('Missing service in VCAP_SERVICES', str(cm.exception))
 
-    @mock.patch('os.getenv')
-    def test_get_vcap_service_instance_host(self, m_getenv):
-        m_getenv.return_value = self._test_vcap_services_multiple
-        service = CloudFoundryService('Cloudant NoSQL DB 1')
+    def test_get_vcap_service_instance_host(self):
+        service = CloudFoundryService(
+            self._test_vcap_services_multiple, 'Cloudant NoSQL DB 1'
+        )
         self.assertEqual('example.cloudant.com', service.host)
 
-    @mock.patch('os.getenv')
-    def test_get_vcap_service_instance_password(self, m_getenv):
-        m_getenv.return_value = self._test_vcap_services_multiple
-        service = CloudFoundryService('Cloudant NoSQL DB 1')
+    def test_get_vcap_service_instance_password(self):
+        service = CloudFoundryService(
+            self._test_vcap_services_multiple, 'Cloudant NoSQL DB 1'
+        )
         self.assertEqual('pa$$w0rd01', service.password)
 
-    @mock.patch('os.getenv')
-    def test_get_vcap_service_instance_port(self, m_getenv):
-        m_getenv.return_value = self._test_vcap_services_multiple
-        service = CloudFoundryService('Cloudant NoSQL DB 1')
+    def test_get_vcap_service_instance_port(self):
+        service = CloudFoundryService(
+            self._test_vcap_services_multiple, 'Cloudant NoSQL DB 1'
+        )
         self.assertEqual('1234', service.port)
 
-    @mock.patch('os.getenv')
-    def test_get_vcap_service_instance_port_default(self, m_getenv):
-        m_getenv.return_value = self._test_vcap_services_multiple
-        service = CloudFoundryService('Cloudant NoSQL DB 2')
+    def test_get_vcap_service_instance_port_default(self):
+        service = CloudFoundryService(
+            self._test_vcap_services_multiple, 'Cloudant NoSQL DB 2'
+        )
         self.assertEqual('443', service.port)
 
-    @mock.patch('os.getenv')
-    def test_get_vcap_service_instance_url(self, m_getenv):
-        m_getenv.return_value = self._test_vcap_services_multiple
-        service = CloudFoundryService('Cloudant NoSQL DB 1')
+    def test_get_vcap_service_instance_url(self):
+        service = CloudFoundryService(
+            self._test_vcap_services_multiple, 'Cloudant NoSQL DB 1'
+        )
         self.assertEqual('https://example.cloudant.com:1234', service.url)
 
-    @mock.patch('os.getenv')
-    def test_get_vcap_service_instance_username(self, m_getenv):
-        m_getenv.return_value = self._test_vcap_services_multiple
-        service = CloudFoundryService('Cloudant NoSQL DB 1')
+    def test_get_vcap_service_instance_username(self):
+        service = CloudFoundryService(
+            self._test_vcap_services_multiple, 'Cloudant NoSQL DB 1'
+        )
         self.assertEqual('example', service.username)
 
-    @mock.patch('os.getenv')
-    def test_raise_error_for_missing_host(self, m_getenv):
-        m_getenv.return_value = self._test_vcap_services_multiple
+    def test_raise_error_for_missing_host(self):
         with self.assertRaises(CloudantException):
-            CloudFoundryService('Cloudant NoSQL DB 3')
+            CloudFoundryService(
+                self._test_vcap_services_multiple, 'Cloudant NoSQL DB 3'
+            )
 
-    @mock.patch('os.getenv')
-    def test_raise_error_for_missing_password(self, m_getenv):
-        m_getenv.return_value = self._test_vcap_services_multiple
+    def test_raise_error_for_missing_password(self):
         with self.assertRaises(CloudantException) as cm:
-            CloudFoundryService('Cloudant NoSQL DB 4')
+            CloudFoundryService(
+                self._test_vcap_services_multiple, 'Cloudant NoSQL DB 4'
+            )
         self.assertEqual(
             "Invalid service: 'password' missing",
             str(cm.exception)
         )
 
-    @mock.patch('os.getenv')
-    def test_raise_error_for_missing_username(self, m_getenv):
-        m_getenv.return_value = self._test_vcap_services_multiple
+    def test_raise_error_for_missing_username(self):
         with self.assertRaises(CloudantException) as cm:
-            CloudFoundryService('Cloudant NoSQL DB 5')
+            CloudFoundryService(
+                self._test_vcap_services_multiple, 'Cloudant NoSQL DB 5'
+            )
         self.assertEqual(
             "Invalid service: 'username' missing",
             str(cm.exception)
         )
 
-    @mock.patch('os.getenv')
-    def test_raise_error_for_invalid_credentials_type(self, m_getenv):
-        m_getenv.return_value = self._test_vcap_services_multiple
+    def test_raise_error_for_invalid_credentials_type(self):
         with self.assertRaises(CloudantException) as cm:
-            CloudFoundryService('Cloudant NoSQL DB 6')
+            CloudFoundryService(
+                self._test_vcap_services_multiple, 'Cloudant NoSQL DB 6'
+            )
         self.assertEqual(
             'Failed to decode VCAP_SERVICES service credentials',
             str(cm.exception)
         )
 
-    @mock.patch('os.getenv')
-    def test_raise_error_for_missing_service(self, m_getenv):
-        m_getenv.return_value = self._test_vcap_services_multiple
+    def test_raise_error_for_missing_service(self):
         with self.assertRaises(CloudantException) as cm:
-            CloudFoundryService('Cloudant NoSQL DB 7')
+            CloudFoundryService(
+                self._test_vcap_services_multiple, 'Cloudant NoSQL DB 7'
+            )
         self.assertEqual('Missing service in VCAP_SERVICES', str(cm.exception))
+
+    def test_raise_error_for_invalid_vcap(self):
+        with self.assertRaises(CloudantException) as cm:
+            CloudFoundryService('{', 'Cloudant NoSQL DB 1')  # invalid JSON
+        self.assertEqual('Failed to decode VCAP_SERVICES JSON', str(cm.exception))