Repository: ithaka/apiron

Issue:
Add pass-through auth argument to ServiceCaller.call
**Is your feature request related to a problem? Please describe.**
It's currently fairly cumbersome to perform HTTP Basic Auth with `ServiceCaller.call`—doing so requires some amount of string manipulation, encoding, and bytes<->str casting. `requests` supports ease of use here via the `auth` argument to the `Request` constructor.

**Describe the solution you'd like**
`ServiceCaller.call` accepts an optional `auth` argument that, when supplied, is passed to the underlying `requests` call. This would allow other types of [custom authentication](http://docs.python-requests.org/en/master/user/advanced/#custom-authentication) as well.

**Describe alternatives you've considered**
This is currently _possible_ through HTTP headers via the `headers` argument, but requires some manual work by the developer since the header needs to be something of the form: `Authorization: Basic <b64encode(bytes('username:password'.encode('utf-8')))>`. This is somewhat cumbersome.


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index db753f4..3217015 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+### Added
+- Expose pass-through `auth` argument to `requests`
+
 ### Changed
 - Moved all tests to a top-level `tests` package and switched to using the `pytest` runner
 - Added missing parameter in docstring for `apiron.client.ServiceCaller.call`
diff --git a/apiron/client.py b/apiron/client.py
index 9c6dffd..42199a7 100644
--- a/apiron/client.py
+++ b/apiron/client.py
@@ -86,6 +86,7 @@ class ServiceCaller:
         data=None,
         headers=None,
         cookies=None,
+        auth=None,
     ):
         host = cls.choose_host(service=service)
 
@@ -104,6 +105,7 @@ class ServiceCaller:
             data=data,
             headers=headers,
             cookies=cookies,
+            auth=auth,
         )
 
         return session.prepare_request(request)
@@ -120,6 +122,7 @@ class ServiceCaller:
         data=None,
         headers=None,
         cookies=None,
+        auth=None,
         retry_spec=DEFAULT_RETRY,
         timeout_spec=DEFAULT_TIMEOUT,
         logger=None,
@@ -153,6 +156,8 @@ class ServiceCaller:
         :param dict cookies:
             Cookies to send to the endpoint
             (default ``None``)
+        :param auth:
+            An object suitable for the :class:`requests.Request` object's ``auth`` argument
         :param urllib3.util.retry.Retry retry_spec:
             (optional)
             An override of the retry behavior for this call.
@@ -190,6 +195,7 @@ class ServiceCaller:
             data=data,
             headers=headers,
             cookies=cookies,
+            auth=auth,
         )
 
         logger.info('{method} {url}'.format(


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
index f4a95a8..8338fbc 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -47,6 +47,7 @@ class ClientTestCase(unittest.TestCase):
         data = 'I am a data'
         headers = {'Accept': 'stuff'}
         cookies = {'chocolate-chip': 'yes'}
+        auth = mock.Mock()
 
         mock_get_required_headers.return_value = {'header': 'value'}
         expected_headers = {}
@@ -63,6 +64,7 @@ class ClientTestCase(unittest.TestCase):
                 data=data,
                 headers=headers,
                 cookies=cookies,
+                auth=auth,
             )
 
             mock_request_constructor.assert_called_once_with(
@@ -72,6 +74,7 @@ class ClientTestCase(unittest.TestCase):
                 cookies=cookies,
                 params=params,
                 data=data,
+                auth=auth,
             )
 
             self.assertEqual(1, mock_prepare_request.call_count)