Repository: simple-salesforce/simple-salesforce

Issue:
Salesforce.restful call is broken for POST
Specifying the method for `.restful` was recently added, however, the way the `params` end up getting passed down to the `requests` library needs to change depending on if it's a `GET` or `POST` request.

Currently, if I call `Salesforce.restful(url, {'foo': 1}, method='POST')`, it results in the payload getting passed in as query parameters rather than the body.


Comments:

Patch:
diff --git a/README.rst b/README.rst
index e5722d6..2388168 100644
--- a/README.rst
+++ b/README.rst
@@ -15,7 +15,7 @@ You can find out more regarding the format of the results in the `Official Sales
 
 .. _Official Salesforce.com REST API Documentation: http://www.salesforce.com/us/developer/docs/api_rest/index.htm
 
-Example
+Examples
 -------
 There are two ways to gain access to Salesforce
 
@@ -51,23 +51,23 @@ To login using IP-whitelist Organization ID method, simply use your Salesforce u
     from simple_salesforce import Salesforce
     sf = Salesforce(password='password', username='myemail@example.com', organizationId='OrgId')
 
-If you'd like to enter a sandbox, simply add ``sandbox=True`` to your ``Salesforce()`` call.
+If you'd like to enter a sandbox, simply add ``domain='test'`` to your ``Salesforce()`` call.
 
 For example:
 
 .. code-block:: python
 
     from simple_salesforce import Salesforce
-    sf = Salesforce(username='myemail@example.com.sandbox', password='password', security_token='token', sandbox=True)
+    sf = Salesforce(username='myemail@example.com.sandbox', password='password', security_token='token', domain='test')
 
-Note that specifying if you want to use a sandbox is only necessary if you are using the built-in username/password/security token authentication and is used exclusively during the authentication step.
+Note that specifying if you want to use a domain is only necessary if you are using the built-in username/password/security token authentication and is used exclusively during the authentication step.
 
 If you'd like to keep track where your API calls are coming from, simply add ``client_id='My App'`` to your ``Salesforce()`` call.
 
 .. code-block:: python
 
     from simple_salesforce import Salesforce
-    sf = Salesforce(username='myemail@example.com.sandbox', password='password', security_token='token', sandbox=True, client_id='My App')
+    sf = Salesforce(username='myemail@example.com.sandbox', password='password', security_token='token', client_id='My App', domain='test')
 
 If you view the API calls in your Salesforce instance by Client Id it will be prefixed with ``RestForce/``, for example ``RestForce/My App``.
 
@@ -310,7 +310,7 @@ Additional Features
 
 There are a few helper classes that are used internally and available to you.
 
-Included in them are ``SalesforceLogin``, which takes in a username, password, security token, optional boolean sandbox indicator and optional version and returns a tuple of ``(session_id, sf_instance)`` where `session_id` is the session ID to use for authentication to Salesforce and ``sf_instance`` is the domain of the instance of Salesforce to use for the session.
+Included in them are ``SalesforceLogin``, which takes in a username, password, security token, optional version and optional domain and returns a tuple of ``(session_id, sf_instance)`` where `session_id` is the session ID to use for authentication to Salesforce and ``sf_instance`` is the domain of the instance of Salesforce to use for the session.
 
 For example, to use SalesforceLogin for a sandbox account you'd use:
 
@@ -321,9 +321,9 @@ For example, to use SalesforceLogin for a sandbox account you'd use:
         username='myemail@example.com.sandbox',
         password='password',
         security_token='token',
-        sandbox=True)
+        domain='test')
 
-Simply leave off the final ``True`` if you do not wish to use a sandbox.
+Simply leave off the final domain if you do not wish to use a sandbox.
 
 Also exposed is the ``SFType`` class, which is used internally by the ``__getattr__()`` method in the ``Salesforce()`` class and represents a specific SObject type. ``SFType`` requires ``object_name`` (i.e. ``Contact``), ``session_id`` (an authentication ID), ``sf_instance`` (hostname of your Salesforce instance), and an optional ``sf_version``
 
diff --git a/docs/user_guide/additional_features.rst b/docs/user_guide/additional_features.rst
index 15c3716..2b0f6f8 100644
--- a/docs/user_guide/additional_features.rst
+++ b/docs/user_guide/additional_features.rst
@@ -3,7 +3,7 @@ Additional Features
 
 There are a few helper classes that are used internally and available to you.
 
-Included in them are ``SalesforceLogin``, which takes in a username, password, security token, optional boolean sandbox indicator and optional version and returns a tuple of ``(session_id, sf_instance)`` where `session_id` is the session ID to use for authentication to Salesforce and ``sf_instance`` is the domain of the instance of Salesforce to use for the session.
+Included in them are ``SalesforceLogin``, which takes in a username, password, security token, optional version and optional domain and returns a tuple of ``(session_id, sf_instance)`` where `session_id` is the session ID to use for authentication to Salesforce and ``sf_instance`` is the domain of the instance of Salesforce to use for the session.
 
 For example, to use SalesforceLogin for a sandbox account you'd use:
 
@@ -14,9 +14,9 @@ For example, to use SalesforceLogin for a sandbox account you'd use:
         username='myemail@example.com.sandbox',
         password='password',
         security_token='token',
-        sandbox=True)
+        domain='test')
 
-Simply leave off the final ``True`` if you do not wish to use a sandbox.
+Simply leave off the final domain if you do not wish to use a sandbox.
 
 Also exposed is the ``SFType`` class, which is used internally by the ``__getattr__()`` method in the ``Salesforce()`` class and represents a specific SObject type. ``SFType`` requires ``object_name`` (i.e. ``Contact``), ``session_id`` (an authentication ID), ``sf_instance`` (hostname of your Salesforce instance), and an optional ``sf_version``
 
diff --git a/docs/user_guide/examples.rst b/docs/user_guide/examples.rst
index 13472bb..b24d6e4 100644
--- a/docs/user_guide/examples.rst
+++ b/docs/user_guide/examples.rst
@@ -34,23 +34,23 @@ To login using IP-whitelist Organization ID method, simply use your Salesforce u
     from simple_salesforce import Salesforce
     sf = Salesforce(password='password', username='myemail@example.com', organizationId='OrgId')
 
-If you'd like to enter a sandbox, simply add ``sandbox=True`` to your ``Salesforce()`` call.
+If you'd like to enter a sandbox, simply add ``domain='test'`` to your ``Salesforce()`` call.
 
 For example:
 
 .. code-block:: python
 
     from simple_salesforce import Salesforce
-    sf = Salesforce(username='myemail@example.com.sandbox', password='password', security_token='token', sandbox=True)
+    sf = Salesforce(username='myemail@example.com.sandbox', password='password', security_token='token', domain='test')
 
-Note that specifying if you want to use a sandbox is only necessary if you are using the built-in username/password/security token authentication and is used exclusively during the authentication step.
+Note that specifying if you want to use a domain is only necessary if you are using the built-in username/password/security token authentication and is used exclusively during the authentication step.
 
 If you'd like to keep track where your API calls are coming from, simply add ``client_id='My App'`` to your ``Salesforce()`` call.
 
 .. code-block:: python
 
     from simple_salesforce import Salesforce
-    sf = Salesforce(username='myemail@example.com.sandbox', password='password', security_token='token', sandbox=True, client_id='My App')
+    sf = Salesforce(username='myemail@example.com.sandbox', password='password', security_token='token', client_id='My App', domain='test')
 
 If you view the API calls in your Salesforce instance by Client Id it will be prefixed with ``RestForce/``, for example ``RestForce/My App``.
 
diff --git a/simple_salesforce/api.py b/simple_salesforce/api.py
index 73c1afd..87af64f 100644
--- a/simple_salesforce/api.py
+++ b/simple_salesforce/api.py
@@ -59,8 +59,8 @@ class Salesforce(object):
     def __init__(
             self, username=None, password=None, security_token=None,
             session_id=None, instance=None, instance_url=None,
-            organizationId=None, sandbox=False, version=DEFAULT_API_VERSION,
-            proxies=None, session=None, client_id=None):
+            organizationId=None, sandbox=None, version=DEFAULT_API_VERSION,
+            proxies=None, session=None, client_id=None, domain=None):
         """Initialize the instance with the given parameters.
 
         Available kwargs
@@ -70,8 +70,11 @@ class Salesforce(object):
         * username -- the Salesforce username to use for authentication
         * password -- the password for the username
         * security_token -- the security token for the username
-        * sandbox -- True if you want to login to `test.salesforce.com`, False
-                     if you want to login to `login.salesforce.com`.
+        * sandbox -- DEPRECATED: Use domain instead.
+        * domain -- The domain to using for connecting to Salesforce. Use
+                    common domains, such as 'login' or 'test', or
+                    Salesforce My domain. If not used, will default to
+                    'login'.
 
         Direct Session and Instance Access:
 
@@ -93,11 +96,26 @@ class Salesforce(object):
                      exposed by simple_salesforce.
 
         """
+        if (sandbox is not None) and (domain is not None):
+            raise ValueError("Both 'sandbox' and 'domain' arguments were "
+                             "supplied. Either may be supplied, but not "
+                             "both.")
+
+        if sandbox is not None:
+            warnings.warn("'sandbox' argument is deprecated. Use "
+                          "'domain' instead. Overriding 'domain' "
+                          "with 'sandbox' value.",
+                          DeprecationWarning)
 
-        # Determine if the user passed in the optional version and/or sandbox
-        # kwargs
+            domain = 'test' if sandbox else 'login'
+
+        if domain is None:
+            domain = 'login'
+
+        # Determine if the user passed in the optional version and/or
+        # domain kwargs
         self.sf_version = version
-        self.sandbox = sandbox
+        self.domain = domain
         self.session = session or requests.Session()
         self.proxies = self.session.proxies
         # override custom session proxies dance
@@ -122,10 +140,10 @@ class Salesforce(object):
                 username=username,
                 password=password,
                 security_token=security_token,
-                sandbox=self.sandbox,
                 sf_version=self.sf_version,
                 proxies=self.proxies,
-                client_id=client_id)
+                client_id=client_id,
+                domain=self.domain)
 
         elif all(arg is not None for arg in (
                 session_id, instance or instance_url)):
@@ -149,20 +167,18 @@ class Salesforce(object):
                 username=username,
                 password=password,
                 organizationId=organizationId,
-                sandbox=self.sandbox,
                 sf_version=self.sf_version,
                 proxies=self.proxies,
-                client_id=client_id)
+                client_id=client_id,
+                domain=self.domain)
 
         else:
             raise TypeError(
                 'You must provide login information or an instance and token'
             )
 
-        if self.sandbox:
-            self.auth_site = 'https://test.salesforce.com'
-        else:
-            self.auth_site = 'https://login.salesforce.com'
+        self.auth_site = ('https://{domain}.salesforce.com'
+                          .format(domain=self.domain))
 
         self.headers = {
             'Content-Type': 'application/json',
@@ -272,7 +288,7 @@ class Salesforce(object):
         return self.set_password(user, password)
 
     # Generic Rest Function
-    def restful(self, path, params, method='GET'):
+    def restful(self, path, params=None, method='GET', **kwargs):
         """Allows you to make a direct REST call if you know the path
 
         Arguments:
@@ -281,10 +297,12 @@ class Salesforce(object):
             Example: sobjects/User/ABC123/password'
         * params: dict of parameters to pass to the path
         * method: HTTP request method, default GET
+        * other arguments supported by requests.request (e.g. json, timeout)
         """
 
         url = self.base_url + path
-        result = self._call_salesforce(method, url, name=path, params=params)
+        result = self._call_salesforce(method, url, name=path, params=params,
+                                       **kwargs)
 
         json_result = result.json(object_pairs_hook=OrderedDict)
         if len(json_result) == 0:
diff --git a/simple_salesforce/login.py b/simple_salesforce/login.py
index 9106330..197f449 100644
--- a/simple_salesforce/login.py
+++ b/simple_salesforce/login.py
@@ -16,13 +16,14 @@ try:
 except ImportError:
     from cgi import escape
 import requests
+import warnings
 
 
 # pylint: disable=invalid-name,too-many-arguments,too-many-locals
 def SalesforceLogin(
         username=None, password=None, security_token=None,
-        organizationId=None, sandbox=False, sf_version=DEFAULT_API_VERSION,
-        proxies=None, session=None, client_id=None):
+        organizationId=None, sandbox=None, sf_version=DEFAULT_API_VERSION,
+        proxies=None, session=None, client_id=None, domain=None):
     """Return a tuple of `(session_id, sf_instance)` where `session_id` is the
     session ID to use for authentication to Salesforce and `sf_instance` is
     the domain of the instance of Salesforce to use for the session.
@@ -34,8 +35,7 @@ def SalesforceLogin(
     * security_token -- the security token for the username
     * organizationId -- the ID of your organization
             NOTE: security_token an organizationId are mutually exclusive
-    * sandbox -- True if you want to login to `test.salesforce.com`, False if
-                 you want to login to `login.salesforce.com`.
+    * sandbox -- DEPRECATED: Use domain instead.
     * sf_version -- the version of the Salesforce API to use, for example
                     "27.0"
     * proxies -- the optional map of scheme to proxy server
@@ -43,10 +43,28 @@ def SalesforceLogin(
                  enables the use of requets Session features not otherwise
                  exposed by simple_salesforce.
     * client_id -- the ID of this client
+    * domain -- The domain to using for connecting to Salesforce. Use
+                common domains, such as 'login' or 'test', or
+                Salesforce My domain. If not used, will default to
+                'login'.
     """
+    if (sandbox is not None) and (domain is not None):
+        raise ValueError("Both 'sandbox' and 'domain' arguments were "
+                         "supplied. Either may be supplied, but not "
+                         "both.")
+
+    if sandbox is not None:
+        warnings.warn("'sandbox' argument is deprecated. Use "
+                      "'domain' instead. Overriding 'domain' "
+                      "with 'sandbox' value.",
+                      DeprecationWarning)
+
+        domain = 'test' if sandbox else 'login'
+
+    if domain is None:
+        domain = 'login'
 
     soap_url = 'https://{domain}.salesforce.com/services/Soap/u/{sf_version}'
-    domain = 'test' if sandbox else 'login'
 
     if client_id:
         client_id = "{prefix}/{app_name}".format(
@@ -55,7 +73,8 @@ def SalesforceLogin(
     else:
         client_id = DEFAULT_CLIENT_ID_PREFIX
 
-    soap_url = soap_url.format(domain=domain, sf_version=sf_version)
+    soap_url = soap_url.format(domain=domain,
+                               sf_version=sf_version)
 
     # pylint: disable=E0012,deprecated-method
     username = escape(username)


Test patch:
diff --git a/simple_salesforce/tests/test_login.py b/simple_salesforce/tests/test_login.py
index 4ab18cd..904ef18 100644
--- a/simple_salesforce/tests/test_login.py
+++ b/simple_salesforce/tests/test_login.py
@@ -32,6 +32,141 @@ class TestSalesforceLogin(unittest.TestCase):
         self.mockrequest = request_patcher.start()
         self.addCleanup(request_patcher.stop)
 
+    @responses.activate
+    def test_default_domain_success(self):
+        """Test default domain logic and login"""
+        responses.add(
+            responses.POST,
+            re.compile(r'^https://login.*$'),
+            body=tests.LOGIN_RESPONSE_SUCCESS,
+            status=http.OK
+        )
+        session_state = {
+            'used': False,
+        }
+
+        # pylint: disable=missing-docstring,unused-argument
+        def on_response(*args, **kwargs):
+            session_state['used'] = True
+
+        session = requests.Session()
+        session.hooks = {
+            'response': on_response,
+        }
+        session_id, instance = SalesforceLogin(
+            session=session,
+            username='foo@bar.com',
+            password='password',
+            security_token='token')
+        self.assertTrue(session_state['used'])
+        self.assertEqual(session_id, tests.SESSION_ID)
+        self.assertEqual(instance, urlparse(tests.SERVER_URL).netloc)
+
+    @responses.activate
+    def test_custom_domain_success(self):
+        """Test custom domain login"""
+        responses.add(
+            responses.POST,
+            re.compile(r'^https://testdomain.my.*$'),
+            body=tests.LOGIN_RESPONSE_SUCCESS,
+            status=http.OK
+        )
+        session_state = {
+            'used': False,
+        }
+
+        # pylint: disable=missing-docstring,unused-argument
+        def on_response(*args, **kwargs):
+            session_state['used'] = True
+
+        session = requests.Session()
+        session.hooks = {
+            'response': on_response,
+        }
+        session_id, instance = SalesforceLogin(
+            session=session,
+            username='foo@bar.com',
+            password='password',
+            security_token='token',
+            domain='testdomain.my')
+        self.assertTrue(session_state['used'])
+        self.assertEqual(session_id, tests.SESSION_ID)
+        self.assertEqual(instance, urlparse(tests.SERVER_URL).netloc)
+
+    @responses.activate
+    def test_deprecated_sandbox_disabled_success(self):
+        """Test sandbox argument set to False"""
+        responses.add(
+            responses.POST,
+            re.compile(r'^https://login.*$'),
+            body=tests.LOGIN_RESPONSE_SUCCESS,
+            status=http.OK
+        )
+        session_state = {
+            'used': False,
+        }
+
+        # pylint: disable=missing-docstring,unused-argument
+        def on_response(*args, **kwargs):
+            session_state['used'] = True
+
+        session = requests.Session()
+        session.hooks = {
+            'response': on_response,
+        }
+        session_id, instance = SalesforceLogin(
+            session=session,
+            username='foo@bar.com',
+            password='password',
+            security_token='token',
+            sandbox=False)
+        self.assertTrue(session_state['used'])
+        self.assertEqual(session_id, tests.SESSION_ID)
+        self.assertEqual(instance, urlparse(tests.SERVER_URL).netloc)
+
+    @responses.activate
+    def test_deprecated_sandbox_enabled_success(self):
+        """Test sandbox argument set to True"""
+        responses.add(
+            responses.POST,
+            re.compile(r'^https://test.*$'),
+            body=tests.LOGIN_RESPONSE_SUCCESS,
+            status=http.OK
+        )
+        session_state = {
+            'used': False,
+        }
+
+        # pylint: disable=missing-docstring,unused-argument
+        def on_response(*args, **kwargs):
+            session_state['used'] = True
+
+        session = requests.Session()
+        session.hooks = {
+            'response': on_response,
+        }
+        session_id, instance = SalesforceLogin(
+            session=session,
+            username='foo@bar.com',
+            password='password',
+            security_token='token',
+            sandbox=True)
+        self.assertTrue(session_state['used'])
+        self.assertEqual(session_id, tests.SESSION_ID)
+        self.assertEqual(instance, urlparse(tests.SERVER_URL).netloc)
+
+    def test_domain_sandbox_mutual_exclusion_failure(self):
+        """Test sandbox and domain mutual exclusion"""
+
+        with self.assertRaises(ValueError):
+            SalesforceLogin(
+                username='myemail@example.com.sandbox',
+                password='password',
+                security_token='token',
+                domain='login',
+                sandbox=False
+            )
+
     @responses.activate
     def test_custom_session_success(self):
         """Test custom session"""
@@ -75,6 +210,6 @@ class TestSalesforceLogin(unittest.TestCase):
                 username='myemail@example.com.sandbox',
                 password='password',
                 security_token='token',
-                sandbox=True
+                domain='test'
             )
         self.assertTrue(self.mockrequest.post.called)