Repository: softlayer/softlayer-python

Issue:
Solidify Client Construction
I want to make the following changes:

* Rename SoftLayer.API.Client to SoftLayer.API.BaseClient
* Remove all other arguments from SoftLayer.API.BaseClient except for:
  * transport
  * auth
* Transports will now be constructed with endpoint_url, timeout, proxy and user_agent
* a new create_client_from_env() function will take all current arguments as SoftLayer.API.Client's __init__ arguments and behave the same. It will also have an alias to SoftLayer.API.Client in order to maintain backwards compatibility. Using SoftLayer.API.Client will be deprecated.

This accomplishes the following goals:
 * Generally, removes magic from initialization of an API client
 * Allows for the construction of an API client without possibly loading environmental and file-based configuration options. The fact that this isn't possible has lead to initializing of a client only to set the auth handler to None since it was automatically set by reading config file.
 * Makes transport-specific defaults for timeouts, endpoints, etc happen in an intuitive place.

Discussion:

Patch:
diff --git a/SoftLayer/API.py b/SoftLayer/API.py
index 5b75fd96..fc0494c6 100644
--- a/SoftLayer/API.py
+++ b/SoftLayer/API.py
@@ -12,11 +12,20 @@
 from SoftLayer import consts
 from SoftLayer import transports
 
+# pylint: disable=invalid-name
+
+
 API_PUBLIC_ENDPOINT = consts.API_PUBLIC_ENDPOINT
 API_PRIVATE_ENDPOINT = consts.API_PRIVATE_ENDPOINT
-__all__ = ['Client', 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT']
-
-VALID_CALL_ARGS = set([
+__all__ = [
+    'create_client_from_env',
+    'Client',
+    'BaseClient',
+    'API_PUBLIC_ENDPOINT',
+    'API_PRIVATE_ENDPOINT',
+]
+
+VALID_CALL_ARGS = set((
     'id',
     'mask',
     'filter',
@@ -25,11 +34,22 @@
     'raw_headers',
     'limit',
     'offset',
-])
+))
 
 
-class Client(object):
-    """A SoftLayer API client.
+def create_client_from_env(username=None,
+                           api_key=None,
+                           endpoint_url=None,
+                           timeout=None,
+                           auth=None,
+                           config_file=None,
+                           proxy=None,
+                           user_agent=None,
+                           transport=None):
+    """Creates a SoftLayer API client using your environment.
+
+    Settings are loaded via keyword arguments, environemtal variables and
+    config file.
 
     :param username: an optional API username if you wish to bypass the
         package's built-in username
@@ -51,38 +71,62 @@ class Client(object):
     Usage:
 
         >>> import SoftLayer
-        >>> client = SoftLayer.Client(username="username", api_key="api_key")
+        >>> client = SoftLayer.create_client_from_env()
         >>> resp = client['Account'].getObject()
         >>> resp['companyName']
         'Your Company'
 
     """
+    settings = config.get_client_settings(username=username,
+                                          api_key=api_key,
+                                          endpoint_url=endpoint_url,
+                                          timeout=timeout,
+                                          proxy=proxy,
+                                          config_file=config_file)
+
+    # Default the transport to use XMLRPC
+    if transport is None:
+        transport = transports.XmlRpcTransport(
+            endpoint_url=settings.get('endpoint_url'),
+            proxy=settings.get('proxy'),
+            timeout=settings.get('timeout'),
+            user_agent=user_agent,
+        )
+
+    # If we have enough information to make an auth driver, let's do it
+    if auth is None and settings.get('username') and settings.get('api_key'):
+
+        auth = slauth.BasicAuthentication(
+            settings.get('username'),
+            settings.get('api_key'),
+        )
+
+    return BaseClient(auth=auth, transport=transport)
+
+
+def Client(**kwargs):
+    """Get a SoftLayer API Client using environmental settings.
+
+    Deprecated in favor of create_client_from_env()
+    """
+    warnings.warn("use SoftLayer.create_client_from_env() instead",
+                  DeprecationWarning)
+    return create_client_from_env(**kwargs)
+
+
+class BaseClient(object):
+    """Base SoftLayer API client.
+
+    :param auth: auth driver that looks like SoftLayer.auth.AuthenticationBase
+    :param transport: An object that's callable with this signature:
+                      transport(SoftLayer.transports.Request)
+    """
+
     _prefix = "SoftLayer_"
 
-    def __init__(self, username=None, api_key=None, endpoint_url=None,
-                 timeout=None, auth=None, config_file=None, proxy=None,
-                 user_agent=None, transport=None):
-
-        settings = config.get_client_settings(username=username,
-                                              api_key=api_key,
-                                              endpoint_url=endpoint_url,
-                                              timeout=timeout,
-                                              auth=auth,
-                                              proxy=proxy,
-                                              config_file=config_file)
-        self.auth = settings.get('auth')
-
-        self.endpoint_url = (settings.get('endpoint_url') or
-                             API_PUBLIC_ENDPOINT).rstrip('/')
-        self.transport = transport or transports.XmlRpcTransport()
-
-        self.timeout = None
-        if settings.get('timeout'):
-            self.timeout = float(settings.get('timeout'))
-        self.proxy = None
-        if settings.get('proxy'):
-            self.proxy = settings.get('proxy')
-        self.user_agent = user_agent
+    def __init__(self, auth=None, transport=None):
+        self.auth = auth
+        self.transport = transport
 
     def authenticate_with_password(self, username, password,
                                    security_question_id=None,
@@ -145,13 +189,10 @@ def call(self, service, method, *args, **kwargs):
             raise TypeError(
                 'Invalid keyword arguments: %s' % ','.join(invalid_kwargs))
 
-        if not service.startswith(self._prefix):
+        if self._prefix and not service.startswith(self._prefix):
             service = self._prefix + service
 
-        http_headers = {
-            'User-Agent': self.user_agent or consts.USER_AGENT,
-            'Content-Type': 'application/xml',
-        }
+        http_headers = {}
 
         if kwargs.get('compress', True):
             http_headers['Accept'] = '*/*'
@@ -161,13 +202,10 @@ def call(self, service, method, *args, **kwargs):
             http_headers.update(kwargs.get('raw_headers'))
 
         request = transports.Request()
-        request.endpoint = self.endpoint_url
         request.service = service
         request.method = method
         request.args = args
         request.transport_headers = http_headers
-        request.timeout = self.timeout
-        request.proxy = self.proxy
         request.identifier = kwargs.get('id')
         request.mask = kwargs.get('mask')
         request.filter = kwargs.get('filter')
@@ -244,8 +282,7 @@ def iter_call(self, service, method, *args, **kwargs):
                 break
 
     def __repr__(self):
-        return "<Client: endpoint=%s, user=%r>" % (self.endpoint_url,
-                                                   self.auth)
+        return "Client(transport=%r, auth=%r)" % (self.transport, self.auth)
 
     __str__ = __repr__
 
diff --git a/SoftLayer/CLI/config/__init__.py b/SoftLayer/CLI/config/__init__.py
index f309ad65..3a45cfff 100644
--- a/SoftLayer/CLI/config/__init__.py
+++ b/SoftLayer/CLI/config/__init__.py
@@ -12,8 +12,8 @@ def get_settings_from_client(client):
     settings = {
         'username': '',
         'api_key': '',
-        'timeout': client.timeout or None,
-        'endpoint_url': client.endpoint_url,
+        'timeout': '',
+        'endpoint_url': '',
     }
     try:
         settings['username'] = client.auth.username
@@ -21,6 +21,12 @@ def get_settings_from_client(client):
     except AttributeError:
         pass
 
+    try:
+        settings['timeout'] = client.transport.transport.timeout
+        settings['endpoint_url'] = client.transport.transport.endpoint_url
+    except AttributeError:
+        pass
+
     return settings
 
 
diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py
index 191351e0..66049232 100644
--- a/SoftLayer/auth.py
+++ b/SoftLayer/auth.py
@@ -7,7 +7,12 @@
 """
 # pylint: disable=no-self-use
 
-__all__ = ['BasicAuthentication', 'TokenAuthentication', 'AuthenticationBase']
+__all__ = [
+    'BasicAuthentication',
+    'TokenAuthentication',
+    'BasicHTTPAuthentication',
+    'AuthenticationBase',
+]
 
 
 class AuthenticationBase(object):
@@ -51,7 +56,7 @@ def get_request(self, request):
         return request
 
     def __repr__(self):
-        return "<TokenAuthentication: %s %s>" % (self.user_id, self.auth_token)
+        return "TokenAuthentication(%r)" % self.user_id
 
 
 class BasicAuthentication(AuthenticationBase):
@@ -73,4 +78,24 @@ def get_request(self, request):
         return request
 
     def __repr__(self):
-        return "<BasicAuthentication: %s>" % (self.username)
+        return "BasicAuthentication(username=%r)" % self.username
+
+
+class BasicHTTPAuthentication(AuthenticationBase):
+    """Token-based authentication class.
+
+        :param username str: a user's username
+        :param api_key str: a user's API key
+    """
+    def __init__(self, username, api_key):
+        self.username = username
+        self.api_key = api_key
+
+    def get_request(self, request):
+        """Sets token-based auth headers."""
+        request.transport_user = self.username
+        request.transport_password = self.api_key
+        return request
+
+    def __repr__(self):
+        return "BasicHTTPAuthentication(username=%r)" % self.username
diff --git a/SoftLayer/config.py b/SoftLayer/config.py
index 81030517..8c679e6c 100644
--- a/SoftLayer/config.py
+++ b/SoftLayer/config.py
@@ -8,7 +8,6 @@
 import os
 import os.path
 
-from SoftLayer import auth
 from SoftLayer import utils
 
 
@@ -17,17 +16,13 @@ def get_client_settings_args(**kwargs):
 
         :param \\*\\*kwargs: Arguments that are passed into the client instance
     """
-    settings = {
+    return {
         'endpoint_url': kwargs.get('endpoint_url'),
-        'timeout': kwargs.get('timeout'),
-        'auth': kwargs.get('auth'),
+        'timeout': float(kwargs.get('timeout') or 0),
         'proxy': kwargs.get('proxy'),
+        'username': kwargs.get('username'),
+        'api_key': kwargs.get('api_key'),
     }
-    username = kwargs.get('username')
-    api_key = kwargs.get('api_key')
-    if username and api_key and not settings['auth']:
-        settings['auth'] = auth.BasicAuthentication(username, api_key)
-    return settings
 
 
 def get_client_settings_env(**_):
@@ -35,14 +30,12 @@ def get_client_settings_env(**_):
 
         :param \\*\\*kwargs: Arguments that are passed into the client instance
     """
-    username = os.environ.get('SL_USERNAME')
-    api_key = os.environ.get('SL_API_KEY')
-    proxy = os.environ.get('https_proxy')
 
-    config = {'proxy': proxy}
-    if username and api_key:
-        config['auth'] = auth.BasicAuthentication(username, api_key)
-    return config
+    return {
+        'proxy': os.environ.get('https_proxy'),
+        'username': os.environ.get('SL_USERNAME'),
+        'api_key': os.environ.get('SL_API_KEY'),
+    }
 
 
 def get_client_settings_config_file(**kwargs):
@@ -58,7 +51,7 @@ def get_client_settings_config_file(**kwargs):
         'username': '',
         'api_key': '',
         'endpoint_url': '',
-        'timeout': '',
+        'timeout': '0',
         'proxy': '',
     })
     config.read(config_files)
@@ -66,16 +59,14 @@ def get_client_settings_config_file(**kwargs):
     if not config.has_section('softlayer'):
         return
 
-    settings = {
+    return {
         'endpoint_url': config.get('softlayer', 'endpoint_url'),
-        'timeout': config.get('softlayer', 'timeout'),
+        'timeout': config.getfloat('softlayer', 'timeout'),
         'proxy': config.get('softlayer', 'proxy'),
+        'username': config.get('softlayer', 'username'),
+        'api_key': config.get('softlayer', 'api_key'),
     }
-    username = config.get('softlayer', 'username')
-    api_key = config.get('softlayer', 'api_key')
-    if username and api_key:
-        settings['auth'] = auth.BasicAuthentication(username, api_key)
-    return settings
+
 
 SETTING_RESOLVERS = [get_client_settings_args,
                      get_client_settings_env,
@@ -86,8 +77,7 @@ def get_client_settings(**kwargs):
     """Parse client settings.
 
     Parses settings from various input methods, preferring earlier values
-    to later ones. Once an 'auth' value is found, it returns the gathered
-    settings. The settings currently come from explicit user arguments,
+    to later ones. The settings currently come from explicit user arguments,
     environmental variables and config files.
 
         :param \\*\\*kwargs: Arguments that are passed into the client instance
@@ -98,6 +88,5 @@ def get_client_settings(**kwargs):
         if settings:
             settings.update((k, v) for k, v in all_settings.items() if v)
             all_settings = settings
-            if all_settings.get('auth'):
-                break
+
     return all_settings
diff --git a/SoftLayer/managers/metadata.py b/SoftLayer/managers/metadata.py
index acad47b7..405f88da 100644
--- a/SoftLayer/managers/metadata.py
+++ b/SoftLayer/managers/metadata.py
@@ -56,11 +56,13 @@ class MetadataManager(object):
     attribs = METADATA_MAPPING
 
     def __init__(self, client=None, timeout=5):
-        url = consts.API_PRIVATE_ENDPOINT_REST.rstrip('/')
         if client is None:
-            client = SoftLayer.Client(endpoint_url=url,
-                                      timeout=timeout,
-                                      transport=transports.RestTransport())
+            transport = transports.RestTransport(
+                timeout=timeout,
+                endpoint_url=consts.API_PRIVATE_ENDPOINT_REST,
+            )
+            client = SoftLayer.BaseClient(transport=transport)
+
         self.client = client
 
     def get(self, name, param=None):
@@ -83,7 +85,7 @@ def get(self, name, param=None):
         try:
             return self.client.call('Resource_Metadata',
                                     self.attribs[name]['call'],
-                                    id=param)
+                                    param)
         except exceptions.SoftLayerAPIError as ex:
             if ex.faultCode == 404:
                 return None
diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py
index d73e59b7..b30d5552 100644
--- a/SoftLayer/transports.py
+++ b/SoftLayer/transports.py
@@ -5,6 +5,7 @@
 
     :license: MIT, see LICENSE for more details.
 """
+from SoftLayer import consts
 from SoftLayer import exceptions
 from SoftLayer import utils
 
@@ -19,20 +20,19 @@
 # transports.Request does have a lot of instance attributes. :(
 # pylint: disable=too-many-instance-attributes
 
-__all__ = ['Request',
-           'XmlRpcTransport',
-           'RestTransport',
-           'TimingTransport',
-           'FixtureTransport']
+__all__ = [
+    'Request',
+    'XmlRpcTransport',
+    'RestTransport',
+    'TimingTransport',
+    'FixtureTransport',
+]
 
 
 class Request(object):
     """Transport request object."""
 
     def __init__(self):
-        #: The SoftLayer endpoint address.
-        self.endpoint = None
-
         #: API service name. E.G. SoftLayer_Account
         self.service = None
 
@@ -45,14 +45,14 @@ def __init__(self):
         #: API headers, used for authentication, masks, limits, offsets, etc.
         self.headers = {}
 
-        #: Transport headers.
-        self.transport_headers = {}
+        #: Transport user.
+        self.transport_user = None
 
-        #: Integer timeout.
-        self.timeout = None
+        #: Transport password.
+        self.transport_password = None
 
-        #: URL to proxy API requests to.
-        self.proxy = None
+        #: Transport headers.
+        self.transport_headers = {}
 
         #: Boolean specifying if the server certificate should be verified.
         self.verify = True
@@ -78,58 +78,69 @@ def __init__(self):
 
 class XmlRpcTransport(object):
     """XML-RPC transport."""
+    def __init__(self,
+                 endpoint_url=None,
+                 timeout=None,
+                 proxy=None,
+                 user_agent=None):
+
+        self.endpoint_url = (endpoint_url or
+                             consts.API_PUBLIC_ENDPOINT).rstrip('/')
+        self.timeout = timeout or None
+        self.proxy = proxy
+        self.user_agent = user_agent or consts.USER_AGENT
 
     def __call__(self, request):
         """Makes a SoftLayer API call against the XML-RPC endpoint.
 
         :param request request: Request object
         """
-        try:
-            largs = list(request.args)
+        largs = list(request.args)
 
-            headers = request.headers
+        headers = request.headers
 
-            if request.identifier is not None:
-                header_name = request.service + 'InitParameters'
-                headers[header_name] = {'id': request.identifier}
+        if request.identifier is not None:
+            header_name = request.service + 'InitParameters'
+            headers[header_name] = {'id': request.identifier}
 
-            if request.mask is not None:
-                headers.update(_format_object_mask(request.mask,
-                                                   request.service))
+        if request.mask is not None:
+            headers.update(_format_object_mask(request.mask, request.service))
 
-            if request.filter is not None:
-                headers['%sObjectFilter' % request.service] = request.filter
+        if request.filter is not None:
+            headers['%sObjectFilter' % request.service] = request.filter
 
-            if request.limit:
-                headers['resultLimit'] = {
-                    'limit': request.limit,
-                    'offset': request.offset or 0,
-                }
+        if request.limit:
+            headers['resultLimit'] = {
+                'limit': request.limit,
+                'offset': request.offset or 0,
+            }
 
-            largs.insert(0, {'headers': headers})
+        largs.insert(0, {'headers': headers})
+        request.transport_headers.setdefault('Content-Type', 'application/xml')
+        request.transport_headers.setdefault('User-Agent', self.user_agent)
 
-            url = '/'.join([request.endpoint, request.service])
-            payload = utils.xmlrpc_client.dumps(tuple(largs),
-                                                methodname=request.method,
-                                                allow_none=True)
-            LOGGER.debug("=== REQUEST ===")
-            LOGGER.info('POST %s', url)
-            LOGGER.debug(request.transport_headers)
-            LOGGER.debug(payload)
+        url = '/'.join([self.endpoint_url, request.service])
+        payload = utils.xmlrpc_client.dumps(tuple(largs),
+                                            methodname=request.method,
+                                            allow_none=True)
+        LOGGER.debug("=== REQUEST ===")
+        LOGGER.info('POST %s', url)
+        LOGGER.debug(request.transport_headers)
+        LOGGER.debug(payload)
 
+        try:
             response = requests.request('POST', url,
                                         data=payload,
                                         headers=request.transport_headers,
-                                        timeout=request.timeout,
+                                        timeout=self.timeout,
                                         verify=request.verify,
                                         cert=request.cert,
-                                        proxies=_proxies_dict(request.proxy))
+                                        proxies=_proxies_dict(self.proxy))
             LOGGER.debug("=== RESPONSE ===")
             LOGGER.debug(response.headers)
             LOGGER.debug(response.content)
             response.raise_for_status()
-            result = utils.xmlrpc_client.loads(response.content,)[0][0]
-            return result
+            return utils.xmlrpc_client.loads(response.content)[0][0]
         except utils.xmlrpc_client.Fault as ex:
             # These exceptions are formed from the XML-RPC spec
             # http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
@@ -154,7 +165,23 @@ def __call__(self, request):
 
 
 class RestTransport(object):
-    """REST transport."""
+    """REST transport.
+
+    Currently only supports GET requests (no POST, PUT, DELETE) and lacks
+    support for masks, filters, limits and offsets.
+    """
+
+    def __init__(self,
+                 endpoint_url=None,
+                 timeout=None,
+                 proxy=None,
+                 user_agent=None):
+
+        self.endpoint_url = (endpoint_url or
+                             consts.API_PUBLIC_ENDPOINT_REST).rstrip('/')
+        self.timeout = timeout or None
+        self.proxy = proxy
+        self.user_agent = user_agent or consts.USER_AGENT
 
     def __call__(self, request):
         """Makes a SoftLayer API call against the REST endpoint.
@@ -163,9 +190,17 @@ def __call__(self, request):
 
         :param request request: Request object
         """
-        url_parts = [request.endpoint, request.service, request.method]
+        url_parts = [self.endpoint_url, request.service]
         if request.identifier is not None:
             url_parts.append(str(request.identifier))
+        if request.method is not None:
+            url_parts.append(request.method)
+        for arg in request.args:
+            url_parts.append(str(arg))
+
+        request.transport_headers.setdefault('Content-Type',
+                                             'application/json')
+        request.transport_headers.setdefault('User-Agent', self.user_agent)
 
         url = '%s.%s' % ('/'.join(url_parts), 'json')
 
@@ -175,10 +210,10 @@ def __call__(self, request):
         try:
             resp = requests.request('GET', url,
                                     headers=request.transport_headers,
-                                    timeout=request.timeout,
+                                    timeout=self.timeout,
                                     verify=request.verify,
                                     cert=request.cert,
-                                    proxies=_proxies_dict(request.proxy))
+                                    proxies=_proxies_dict(self.proxy))
             LOGGER.debug("=== RESPONSE ===")
             LOGGER.debug(resp.headers)
             LOGGER.debug(resp.content)


Test patch:
diff --git a/SoftLayer/tests/CLI/modules/config_tests.py b/SoftLayer/tests/CLI/modules/config_tests.py
index 8299d7b9..73baf54a 100644
--- a/SoftLayer/tests/CLI/modules/config_tests.py
+++ b/SoftLayer/tests/CLI/modules/config_tests.py
@@ -24,8 +24,8 @@ def test_show(self):
         self.assertEqual(json.loads(result.output),
                          {'Username': 'default-user',
                           'API Key': 'default-key',
-                          'Endpoint URL': 'default-endpoint-url',
-                          'Timeout': 10.0})
+                          'Endpoint URL': 'not set',
+                          'Timeout': 'not set'})
 
 
 class TestHelpSetup(testing.TestCase):
@@ -80,7 +80,6 @@ def test_get_user_input_private(self, input, getpass):
         self.assertEqual(username, 'user')
         self.assertEqual(secret, 'A' * 64)
         self.assertEqual(endpoint_url, consts.API_PRIVATE_ENDPOINT)
-        self.assertEqual(timeout, 10)
 
     @mock.patch('SoftLayer.CLI.environment.Environment.getpass')
     @mock.patch('SoftLayer.CLI.environment.Environment.input')
diff --git a/SoftLayer/tests/api_tests.py b/SoftLayer/tests/api_tests.py
index 6df36510..daae5aed 100644
--- a/SoftLayer/tests/api_tests.py
+++ b/SoftLayer/tests/api_tests.py
@@ -8,7 +8,6 @@
 
 import SoftLayer
 import SoftLayer.API
-from SoftLayer import consts
 from SoftLayer import testing
 
 TEST_AUTH_HEADERS = {
@@ -24,22 +23,21 @@ def test_init(self):
         self.assertIsInstance(client.auth, SoftLayer.BasicAuthentication)
         self.assertEqual(client.auth.username, 'doesnotexist')
         self.assertEqual(client.auth.api_key, 'issurelywrong')
-        self.assertEqual(client.endpoint_url,
+        self.assertEqual(client.transport.endpoint_url,
                          SoftLayer.API_PUBLIC_ENDPOINT.rstrip('/'))
-        self.assertEqual(client.timeout, 10)
+        self.assertEqual(client.transport.timeout, 10)
 
     @mock.patch('SoftLayer.config.get_client_settings')
     def test_env(self, get_client_settings):
         auth = mock.Mock()
         get_client_settings.return_value = {
-            'auth': auth,
             'timeout': 10,
             'endpoint_url': 'http://endpoint_url/',
         }
-        client = SoftLayer.Client()
+        client = SoftLayer.Client(auth=auth)
         self.assertEqual(client.auth.get_headers(), auth.get_headers())
-        self.assertEqual(client.timeout, 10)
-        self.assertEqual(client.endpoint_url, 'http://endpoint_url')
+        self.assertEqual(client.transport.timeout, 10)
+        self.assertEqual(client.transport.endpoint_url, 'http://endpoint_url')
 
 
 class ClientMethods(testing.TestCase):
@@ -76,7 +74,6 @@ def test_simple_call(self):
 
         self.assertEqual(resp, {"test": "result"})
         self.assert_called_with('SoftLayer_SERVICE', 'METHOD',
-                                endpoint=self.client.endpoint_url,
                                 mask=None,
                                 filter=None,
                                 identifier=None,
@@ -101,7 +98,6 @@ def test_complex(self):
 
         self.assertEqual(resp, {"test": "result"})
         self.assert_called_with('SoftLayer_SERVICE', 'METHOD',
-                                endpoint=self.client.endpoint_url,
                                 mask={'object': {'attribute': ''}},
                                 filter=_filter,
                                 identifier=5678,
@@ -111,22 +107,22 @@ def test_complex(self):
                                 headers=TEST_AUTH_HEADERS,
                                 )
 
-    @mock.patch('SoftLayer.API.Client.iter_call')
+    @mock.patch('SoftLayer.API.BaseClient.iter_call')
     def test_iterate(self, _iter_call):
         self.client['SERVICE'].METHOD(iter=True)
         _iter_call.assert_called_with('SERVICE', 'METHOD')
 
-    @mock.patch('SoftLayer.API.Client.iter_call')
+    @mock.patch('SoftLayer.API.BaseClient.iter_call')
     def test_service_iter_call(self, _iter_call):
         self.client['SERVICE'].iter_call('METHOD', 'ARG')
         _iter_call.assert_called_with('SERVICE', 'METHOD', 'ARG')
 
-    @mock.patch('SoftLayer.API.Client.iter_call')
+    @mock.patch('SoftLayer.API.BaseClient.iter_call')
     def test_service_iter_call_with_chunk(self, _iter_call):
         self.client['SERVICE'].iter_call('METHOD', 'ARG', chunk=2)
         _iter_call.assert_called_with('SERVICE', 'METHOD', 'ARG', chunk=2)
 
-    @mock.patch('SoftLayer.API.Client.call')
+    @mock.patch('SoftLayer.API.BaseClient.call')
     def test_iter_call(self, _call):
         # chunk=100, no limit
         _call.side_effect = [list(range(100)), list(range(100, 125))]
@@ -207,10 +203,8 @@ def test_call_compression_enabled(self):
         self.client['SERVICE'].METHOD(compress=True)
 
         expected_headers = {
-            'Content-Type': 'application/xml',
             'Accept-Encoding': 'gzip, deflate, compress',
             'Accept': '*/*',
-            'User-Agent': consts.USER_AGENT,
         }
         self.assert_called_with('SoftLayer_SERVICE', 'METHOD',
                                 transport_headers=expected_headers)
@@ -223,9 +217,7 @@ def test_call_compression_override(self):
             raw_headers={'Accept-Encoding': 'gzip'})
 
         expected_headers = {
-            'Content-Type': 'application/xml',
             'Accept-Encoding': 'gzip',
-            'User-Agent': consts.USER_AGENT,
         }
         self.assert_called_with('SoftLayer_SERVICE', 'METHOD',
                                 transport_headers=expected_headers)
@@ -245,9 +237,9 @@ def test_init(self, get_client_settings):
     def test_init_with_proxy(self, get_client_settings):
         get_client_settings.return_value = {'proxy': 'http://localhost:3128'}
         client = SoftLayer.Client()
-        self.assertEqual(client.proxy, 'http://localhost:3128')
+        self.assertEqual(client.transport.proxy, 'http://localhost:3128')
 
-    @mock.patch('SoftLayer.API.Client.call')
+    @mock.patch('SoftLayer.API.BaseClient.call')
     def test_authenticate_with_password(self, _call):
         _call.return_value = {
             'userId': 12345,
diff --git a/SoftLayer/tests/auth_tests.py b/SoftLayer/tests/auth_tests.py
index 240af3a9..6bac999f 100644
--- a/SoftLayer/tests/auth_tests.py
+++ b/SoftLayer/tests/auth_tests.py
@@ -63,4 +63,23 @@ def test_repr(self):
         s = repr(self.auth)
         self.assertIn('TokenAuthentication', s)
         self.assertIn('12345', s)
-        self.assertIn('TOKEN', s)
+
+
+class TestBasicHTTPAuthentication(testing.TestCase):
+    def set_up(self):
+        self.auth = auth.BasicHTTPAuthentication('USERNAME', 'APIKEY')
+
+    def test_attribs(self):
+        self.assertEqual(self.auth.username, 'USERNAME')
+        self.assertEqual(self.auth.api_key, 'APIKEY')
+
+    def test_get_request(self):
+        req = transports.Request()
+        authed_req = self.auth.get_request(req)
+        self.assertEqual(authed_req.transport_user, 'USERNAME')
+        self.assertEqual(authed_req.transport_password, 'APIKEY')
+
+    def test_repr(self):
+        s = repr(self.auth)
+        self.assertIn('BasicHTTPAuthentication', s)
+        self.assertIn('USERNAME', s)
diff --git a/SoftLayer/tests/config_tests.py b/SoftLayer/tests/config_tests.py
index 3db988c0..3ef805e9 100644
--- a/SoftLayer/tests/config_tests.py
+++ b/SoftLayer/tests/config_tests.py
@@ -50,28 +50,10 @@ def test_username_api_key(self):
 
         self.assertEqual(result['endpoint_url'], 'http://endpoint/')
         self.assertEqual(result['timeout'], 10)
-        self.assertEqual(result['auth'].username, 'username')
-        self.assertEqual(result['auth'].api_key, 'api_key')
+        self.assertEqual(result['username'], 'username')
+        self.assertEqual(result['api_key'], 'api_key')
         self.assertEqual(result['proxy'], 'https://localhost:3128')
 
-    def test_no_auth(self):
-        result = config.get_client_settings_args()
-
-        self.assertEqual(result, {
-            'endpoint_url': None,
-            'timeout': None,
-            'proxy': None,
-            'auth': None,
-        })
-
-    def test_with_auth(self):
-        auth = mock.Mock()
-        result = config.get_client_settings_args(auth=auth)
-
-        self.assertEqual(result['endpoint_url'], None)
-        self.assertEqual(result['timeout'], None)
-        self.assertEqual(result['auth'], auth)
-
 
 class TestGetClientSettingsEnv(testing.TestCase):
 
@@ -81,15 +63,8 @@ class TestGetClientSettingsEnv(testing.TestCase):
     def test_username_api_key(self):
         result = config.get_client_settings_env()
 
-        self.assertEqual(result['auth'].username, 'username')
-        self.assertEqual(result['auth'].api_key, 'api_key')
-
-    @mock.patch.dict('os.environ', {'SL_USERNAME': '', 'SL_API_KEY': ''})
-    def test_no_auth(self):
-        result = config.get_client_settings_env()
-
-        # proxy might get ANY value depending on test env.
-        self.assertEqual(result, {'proxy': mock.ANY})
+        self.assertEqual(result['username'], 'username')
+        self.assertEqual(result['api_key'], 'api_key')
 
 
 class TestGetClientSettingsConfigFile(testing.TestCase):
@@ -99,10 +74,10 @@ def test_username_api_key(self, config_parser):
         result = config.get_client_settings_config_file()
 
         self.assertEqual(result['endpoint_url'], config_parser().get())
-        self.assertEqual(result['timeout'], config_parser().get())
+        self.assertEqual(result['timeout'], config_parser().getfloat())
         self.assertEqual(result['proxy'], config_parser().get())
-        self.assertEqual(result['auth'].username, config_parser().get())
-        self.assertEqual(result['auth'].api_key, config_parser().get())
+        self.assertEqual(result['username'], config_parser().get())
+        self.assertEqual(result['api_key'], config_parser().get())
 
     @mock.patch('six.moves.configparser.RawConfigParser')
     def test_no_section(self, config_parser):
diff --git a/SoftLayer/tests/functional_tests.py b/SoftLayer/tests/functional_tests.py
index 52383431..53abe6eb 100644
--- a/SoftLayer/tests/functional_tests.py
+++ b/SoftLayer/tests/functional_tests.py
@@ -38,13 +38,14 @@ def test_failed_auth(self):
     def test_no_hostname(self):
         try:
             request = transports.Request()
-            request.endpoint = 'http://notvalidsoftlayer.com'
             request.service = 'SoftLayer_Account'
             request.method = 'getObject'
             request.id = 1234
 
             # This test will fail if 'notvalidsoftlayer.com' becomes a thing
-            transport = transports.XmlRpcTransport()
+            transport = transports.XmlRpcTransport(
+                endpoint_url='http://notvalidsoftlayer.com',
+            )
             transport(request)
         except SoftLayer.SoftLayerAPIError as ex:
             self.assertIn('not known', str(ex))
diff --git a/SoftLayer/tests/managers/metadata_tests.py b/SoftLayer/tests/managers/metadata_tests.py
index 8a5c28cb..9d471265 100644
--- a/SoftLayer/tests/managers/metadata_tests.py
+++ b/SoftLayer/tests/managers/metadata_tests.py
@@ -21,7 +21,6 @@ def test_get(self):
 
         self.assertEqual('dal01', resp)
         self.assert_called_with('SoftLayer_Resource_Metadata', 'Datacenter',
-                                timeout=10.0,
                                 identifier=None)
 
     def test_no_param(self):
@@ -36,7 +35,7 @@ def test_w_param(self):
 
         self.assertEqual([10, 124], resp)
         self.assert_called_with('SoftLayer_Resource_Metadata', 'Vlans',
-                                identifier='1:2:3:4:5')
+                                args=('1:2:3:4:5',))
 
     def test_user_data(self):
         resp = self.metadata.get('user_data')
diff --git a/SoftLayer/tests/transport_tests.py b/SoftLayer/tests/transport_tests.py
index 29b9a7e7..c4977ff0 100644
--- a/SoftLayer/tests/transport_tests.py
+++ b/SoftLayer/tests/transport_tests.py
@@ -10,6 +10,7 @@
 import requests
 
 import SoftLayer
+from SoftLayer import consts
 from SoftLayer import testing
 from SoftLayer import transports
 
@@ -17,7 +18,9 @@
 class TestXmlRpcAPICall(testing.TestCase):
 
     def set_up(self):
-        self.transport = transports.XmlRpcTransport()
+        self.transport = transports.XmlRpcTransport(
+            endpoint_url='http://something.com',
+        )
         self.response = mock.MagicMock()
         self.response.content = '''<?xml version="1.0" encoding="utf-8"?>
 <params>
@@ -52,14 +55,14 @@ def test_call(self, request):
 '''
 
         req = transports.Request()
-        req.endpoint = 'http://something.com'
         req.service = 'SoftLayer_Service'
         req.method = 'getObject'
         resp = self.transport(req)
 
         request.assert_called_with('POST',
                                    'http://something.com/SoftLayer_Service',
-                                   headers={},
+                                   headers={'Content-Type': 'application/xml',
+                                            'User-Agent': consts.USER_AGENT},
                                    proxies=None,
                                    data=data,
                                    timeout=None,
@@ -69,7 +72,6 @@ def test_call(self, request):
 
     def test_proxy_without_protocol(self):
         req = transports.Request()
-        req.endpoint = 'http://something.com'
         req.service = 'SoftLayer_Service'
         req.method = 'Resource'
         req.proxy = 'localhost:3128'
@@ -83,21 +85,20 @@ def test_proxy_without_protocol(self):
     @mock.patch('requests.request')
     def test_valid_proxy(self, request):
         request.return_value = self.response
+        self.transport.proxy = 'http://localhost:3128'
 
         req = transports.Request()
-        req.endpoint = 'http://something.com'
         req.service = 'SoftLayer_Service'
         req.method = 'Resource'
-        req.proxy = 'http://localhost:3128'
         self.transport(req)
 
         request.assert_called_with(
             'POST',
             mock.ANY,
-            headers={},
             proxies={'https': 'http://localhost:3128',
                      'http': 'http://localhost:3128'},
             data=mock.ANY,
+            headers=mock.ANY,
             timeout=None,
             cert=None,
             verify=True)
@@ -237,7 +238,6 @@ def test_request_exception(self, request):
         request().raise_for_status.side_effect = e
 
         req = transports.Request()
-        req.endpoint = 'http://something.com'
         req.service = 'SoftLayer_Service'
         req.method = 'getObject'
 
@@ -247,13 +247,14 @@ def test_request_exception(self, request):
 class TestRestAPICall(testing.TestCase):
 
     def set_up(self):
-        self.transport = transports.RestTransport()
+        self.transport = transports.RestTransport(
+            endpoint_url='http://something.com',
+        )
 
     @mock.patch('requests.request')
     def test_basic(self, request):
         request().content = '{}'
         req = transports.Request()
-        req.endpoint = 'http://something.com'
         req.service = 'SoftLayer_Service'
         req.method = 'Resource'
 
@@ -261,7 +262,7 @@ def test_basic(self, request):
         self.assertEqual(resp, {})
         request.assert_called_with(
             'GET', 'http://something.com/SoftLayer_Service/Resource.json',
-            headers={},
+            headers=mock.ANY,
             verify=True,
             cert=None,
             proxies=None,
@@ -281,7 +282,6 @@ def test_basic(self, request):
 
     def test_proxy_without_protocol(self):
         req = transports.Request()
-        req.endpoint = 'http://something.com'
         req.service = 'SoftLayer_Service'
         req.method = 'Resource'
         req.proxy = 'localhost:3128'
@@ -295,12 +295,11 @@ def test_proxy_without_protocol(self):
     @mock.patch('requests.request')
     def test_valid_proxy(self, request):
         request().content = '{}'
+        self.transport.proxy = 'http://localhost:3128'
 
         req = transports.Request()
-        req.endpoint = 'http://something.com'
         req.service = 'SoftLayer_Service'
         req.method = 'Resource'
-        req.proxy = 'http://localhost:3128'
 
         self.transport(req)
         request.assert_called_with(
@@ -317,7 +316,6 @@ def test_with_id(self, request):
         request().content = '{}'
 
         req = transports.Request()
-        req.endpoint = 'http://something.com'
         req.service = 'SoftLayer_Service'
         req.method = 'getObject'
         req.identifier = 2
@@ -327,8 +325,29 @@ def test_with_id(self, request):
         self.assertEqual(resp, {})
         request.assert_called_with(
             'GET',
-            'http://something.com/SoftLayer_Service/getObject/2.json',
-            headers={},
+            'http://something.com/SoftLayer_Service/2/getObject.json',
+            headers=mock.ANY,
+            verify=True,
+            cert=None,
+            proxies=None,
+            timeout=None)
+
+    @mock.patch('requests.request')
+    def test_with_args(self, request):
+        request().content = '{}'
+
+        req = transports.Request()
+        req.service = 'SoftLayer_Service'
+        req.method = 'getObject'
+        req.args = ('test', 1)
+
+        resp = self.transport(req)
+
+        self.assertEqual(resp, {})
+        request.assert_called_with(
+            'GET',
+            'http://something.com/SoftLayer_Service/getObject/test/1.json',
+            headers=mock.ANY,
             verify=True,
             cert=None,
             proxies=None,
@@ -343,7 +362,6 @@ def test_unknown_error(self, request):
         request().raise_for_status.side_effect = e
 
         req = transports.Request()
-        req.endpoint = 'http://something.com'
         req.service = 'SoftLayer_Service'
         req.method = 'getObject'