Skip to content

Commit aaa52af

Browse files
committed
Implements Client Improvements #514
1 parent d6612c3 commit aaa52af

13 files changed

Lines changed: 386 additions & 281 deletions

File tree

SoftLayer/API.py

Lines changed: 78 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,20 @@
1212
from SoftLayer import consts
1313
from SoftLayer import transports
1414

15+
# pylint: disable=invalid-name
16+
17+
1518
API_PUBLIC_ENDPOINT = consts.API_PUBLIC_ENDPOINT
1619
API_PRIVATE_ENDPOINT = consts.API_PRIVATE_ENDPOINT
17-
__all__ = ['Client', 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT']
18-
19-
VALID_CALL_ARGS = set([
20+
__all__ = [
21+
'create_client_from_env',
22+
'Client',
23+
'BaseClient',
24+
'API_PUBLIC_ENDPOINT',
25+
'API_PRIVATE_ENDPOINT',
26+
]
27+
28+
VALID_CALL_ARGS = set((
2029
'id',
2130
'mask',
2231
'filter',
@@ -25,11 +34,22 @@
2534
'raw_headers',
2635
'limit',
2736
'offset',
28-
])
37+
))
2938

3039

31-
class Client(object):
32-
"""A SoftLayer API client.
40+
def create_client_from_env(username=None,
41+
api_key=None,
42+
endpoint_url=None,
43+
timeout=None,
44+
auth=None,
45+
config_file=None,
46+
proxy=None,
47+
user_agent=None,
48+
transport=None):
49+
"""Creates a SoftLayer API client using your environment.
50+
51+
Settings are loaded via keyword arguments, environemtal variables and
52+
config file.
3353
3454
:param username: an optional API username if you wish to bypass the
3555
package's built-in username
@@ -51,38 +71,62 @@ class Client(object):
5171
Usage:
5272
5373
>>> import SoftLayer
54-
>>> client = SoftLayer.Client(username="username", api_key="api_key")
74+
>>> client = SoftLayer.create_client_from_env()
5575
>>> resp = client['Account'].getObject()
5676
>>> resp['companyName']
5777
'Your Company'
5878
5979
"""
80+
settings = config.get_client_settings(username=username,
81+
api_key=api_key,
82+
endpoint_url=endpoint_url,
83+
timeout=timeout,
84+
proxy=proxy,
85+
config_file=config_file)
86+
87+
# Default the transport to use XMLRPC
88+
if transport is None:
89+
transport = transports.XmlRpcTransport(
90+
endpoint_url=settings.get('endpoint_url'),
91+
proxy=settings.get('proxy'),
92+
timeout=settings.get('timeout'),
93+
user_agent=user_agent,
94+
)
95+
96+
# If we have enough information to make an auth driver, let's do it
97+
if auth is None and settings.get('username') and settings.get('api_key'):
98+
99+
auth = slauth.BasicAuthentication(
100+
settings.get('username'),
101+
settings.get('api_key'),
102+
)
103+
104+
return BaseClient(auth=auth, transport=transport)
105+
106+
107+
def Client(**kwargs):
108+
"""Get a SoftLayer API Client using environmental settings.
109+
110+
Deprecated in favor of create_client_from_env()
111+
"""
112+
warnings.warn("use SoftLayer.create_client_from_env() instead",
113+
DeprecationWarning)
114+
return create_client_from_env(**kwargs)
115+
116+
117+
class BaseClient(object):
118+
"""Base SoftLayer API client.
119+
120+
:param auth: auth driver that looks like SoftLayer.auth.AuthenticationBase
121+
:param transport: An object that's callable with this signature:
122+
transport(SoftLayer.transports.Request)
123+
"""
124+
60125
_prefix = "SoftLayer_"
61126

62-
def __init__(self, username=None, api_key=None, endpoint_url=None,
63-
timeout=None, auth=None, config_file=None, proxy=None,
64-
user_agent=None, transport=None):
65-
66-
settings = config.get_client_settings(username=username,
67-
api_key=api_key,
68-
endpoint_url=endpoint_url,
69-
timeout=timeout,
70-
auth=auth,
71-
proxy=proxy,
72-
config_file=config_file)
73-
self.auth = settings.get('auth')
74-
75-
self.endpoint_url = (settings.get('endpoint_url') or
76-
API_PUBLIC_ENDPOINT).rstrip('/')
77-
self.transport = transport or transports.XmlRpcTransport()
78-
79-
self.timeout = None
80-
if settings.get('timeout'):
81-
self.timeout = float(settings.get('timeout'))
82-
self.proxy = None
83-
if settings.get('proxy'):
84-
self.proxy = settings.get('proxy')
85-
self.user_agent = user_agent
127+
def __init__(self, auth=None, transport=None):
128+
self.auth = auth
129+
self.transport = transport
86130

87131
def authenticate_with_password(self, username, password,
88132
security_question_id=None,
@@ -145,13 +189,10 @@ def call(self, service, method, *args, **kwargs):
145189
raise TypeError(
146190
'Invalid keyword arguments: %s' % ','.join(invalid_kwargs))
147191

148-
if not service.startswith(self._prefix):
192+
if self._prefix and not service.startswith(self._prefix):
149193
service = self._prefix + service
150194

151-
http_headers = {
152-
'User-Agent': self.user_agent or consts.USER_AGENT,
153-
'Content-Type': 'application/xml',
154-
}
195+
http_headers = {}
155196

156197
if kwargs.get('compress', True):
157198
http_headers['Accept'] = '*/*'
@@ -161,13 +202,10 @@ def call(self, service, method, *args, **kwargs):
161202
http_headers.update(kwargs.get('raw_headers'))
162203

163204
request = transports.Request()
164-
request.endpoint = self.endpoint_url
165205
request.service = service
166206
request.method = method
167207
request.args = args
168208
request.transport_headers = http_headers
169-
request.timeout = self.timeout
170-
request.proxy = self.proxy
171209
request.identifier = kwargs.get('id')
172210
request.mask = kwargs.get('mask')
173211
request.filter = kwargs.get('filter')
@@ -244,8 +282,7 @@ def iter_call(self, service, method, *args, **kwargs):
244282
break
245283

246284
def __repr__(self):
247-
return "<Client: endpoint=%s, user=%r>" % (self.endpoint_url,
248-
self.auth)
285+
return "Client(transport=%r, auth=%r)" % (self.transport, self.auth)
249286

250287
__str__ = __repr__
251288

SoftLayer/CLI/config/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,21 @@ def get_settings_from_client(client):
1212
settings = {
1313
'username': '',
1414
'api_key': '',
15-
'timeout': client.timeout or None,
16-
'endpoint_url': client.endpoint_url,
15+
'timeout': '',
16+
'endpoint_url': '',
1717
}
1818
try:
1919
settings['username'] = client.auth.username
2020
settings['api_key'] = client.auth.api_key
2121
except AttributeError:
2222
pass
2323

24+
try:
25+
settings['timeout'] = client.transport.transport.timeout
26+
settings['endpoint_url'] = client.transport.transport.endpoint_url
27+
except AttributeError:
28+
pass
29+
2430
return settings
2531

2632

SoftLayer/auth.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77
"""
88
# pylint: disable=no-self-use
99

10-
__all__ = ['BasicAuthentication', 'TokenAuthentication', 'AuthenticationBase']
10+
__all__ = [
11+
'BasicAuthentication',
12+
'TokenAuthentication',
13+
'BasicHTTPAuthentication',
14+
'AuthenticationBase',
15+
]
1116

1217

1318
class AuthenticationBase(object):
@@ -51,7 +56,7 @@ def get_request(self, request):
5156
return request
5257

5358
def __repr__(self):
54-
return "<TokenAuthentication: %s %s>" % (self.user_id, self.auth_token)
59+
return "TokenAuthentication(%r)" % self.user_id
5560

5661

5762
class BasicAuthentication(AuthenticationBase):
@@ -73,4 +78,24 @@ def get_request(self, request):
7378
return request
7479

7580
def __repr__(self):
76-
return "<BasicAuthentication: %s>" % (self.username)
81+
return "BasicAuthentication(username=%r)" % self.username
82+
83+
84+
class BasicHTTPAuthentication(AuthenticationBase):
85+
"""Token-based authentication class.
86+
87+
:param username str: a user's username
88+
:param api_key str: a user's API key
89+
"""
90+
def __init__(self, username, api_key):
91+
self.username = username
92+
self.api_key = api_key
93+
94+
def get_request(self, request):
95+
"""Sets token-based auth headers."""
96+
request.transport_user = self.username
97+
request.transport_password = self.api_key
98+
return request
99+
100+
def __repr__(self):
101+
return "BasicHTTPAuthentication(username=%r)" % self.username

SoftLayer/config.py

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import os
99
import os.path
1010

11-
from SoftLayer import auth
1211
from SoftLayer import utils
1312

1413

@@ -17,32 +16,26 @@ def get_client_settings_args(**kwargs):
1716
1817
:param \\*\\*kwargs: Arguments that are passed into the client instance
1918
"""
20-
settings = {
19+
return {
2120
'endpoint_url': kwargs.get('endpoint_url'),
22-
'timeout': kwargs.get('timeout'),
23-
'auth': kwargs.get('auth'),
21+
'timeout': float(kwargs.get('timeout') or 0),
2422
'proxy': kwargs.get('proxy'),
23+
'username': kwargs.get('username'),
24+
'api_key': kwargs.get('api_key'),
2525
}
26-
username = kwargs.get('username')
27-
api_key = kwargs.get('api_key')
28-
if username and api_key and not settings['auth']:
29-
settings['auth'] = auth.BasicAuthentication(username, api_key)
30-
return settings
3126

3227

3328
def get_client_settings_env(**_):
3429
"""Retrieve client settings from environment settings.
3530
3631
:param \\*\\*kwargs: Arguments that are passed into the client instance
3732
"""
38-
username = os.environ.get('SL_USERNAME')
39-
api_key = os.environ.get('SL_API_KEY')
40-
proxy = os.environ.get('https_proxy')
4133

42-
config = {'proxy': proxy}
43-
if username and api_key:
44-
config['auth'] = auth.BasicAuthentication(username, api_key)
45-
return config
34+
return {
35+
'proxy': os.environ.get('https_proxy'),
36+
'username': os.environ.get('SL_USERNAME'),
37+
'api_key': os.environ.get('SL_API_KEY'),
38+
}
4639

4740

4841
def get_client_settings_config_file(**kwargs):
@@ -58,24 +51,22 @@ def get_client_settings_config_file(**kwargs):
5851
'username': '',
5952
'api_key': '',
6053
'endpoint_url': '',
61-
'timeout': '',
54+
'timeout': '0',
6255
'proxy': '',
6356
})
6457
config.read(config_files)
6558

6659
if not config.has_section('softlayer'):
6760
return
6861

69-
settings = {
62+
return {
7063
'endpoint_url': config.get('softlayer', 'endpoint_url'),
71-
'timeout': config.get('softlayer', 'timeout'),
64+
'timeout': config.getfloat('softlayer', 'timeout'),
7265
'proxy': config.get('softlayer', 'proxy'),
66+
'username': config.get('softlayer', 'username'),
67+
'api_key': config.get('softlayer', 'api_key'),
7368
}
74-
username = config.get('softlayer', 'username')
75-
api_key = config.get('softlayer', 'api_key')
76-
if username and api_key:
77-
settings['auth'] = auth.BasicAuthentication(username, api_key)
78-
return settings
69+
7970

8071
SETTING_RESOLVERS = [get_client_settings_args,
8172
get_client_settings_env,
@@ -86,8 +77,7 @@ def get_client_settings(**kwargs):
8677
"""Parse client settings.
8778
8879
Parses settings from various input methods, preferring earlier values
89-
to later ones. Once an 'auth' value is found, it returns the gathered
90-
settings. The settings currently come from explicit user arguments,
80+
to later ones. The settings currently come from explicit user arguments,
9181
environmental variables and config files.
9282
9383
:param \\*\\*kwargs: Arguments that are passed into the client instance
@@ -98,6 +88,5 @@ def get_client_settings(**kwargs):
9888
if settings:
9989
settings.update((k, v) for k, v in all_settings.items() if v)
10090
all_settings = settings
101-
if all_settings.get('auth'):
102-
break
91+
10392
return all_settings

SoftLayer/managers/metadata.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,13 @@ class MetadataManager(object):
5656
attribs = METADATA_MAPPING
5757

5858
def __init__(self, client=None, timeout=5):
59-
url = consts.API_PRIVATE_ENDPOINT_REST.rstrip('/')
6059
if client is None:
61-
client = SoftLayer.Client(endpoint_url=url,
62-
timeout=timeout,
63-
transport=transports.RestTransport())
60+
transport = transports.RestTransport(
61+
timeout=timeout,
62+
endpoint_url=consts.API_PRIVATE_ENDPOINT_REST,
63+
)
64+
client = SoftLayer.BaseClient(transport=transport)
65+
6466
self.client = client
6567

6668
def get(self, name, param=None):
@@ -83,7 +85,7 @@ def get(self, name, param=None):
8385
try:
8486
return self.client.call('Resource_Metadata',
8587
self.attribs[name]['call'],
86-
id=param)
88+
param)
8789
except exceptions.SoftLayerAPIError as ex:
8890
if ex.faultCode == 404:
8991
return None

SoftLayer/tests/CLI/modules/config_tests.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ def test_show(self):
2424
self.assertEqual(json.loads(result.output),
2525
{'Username': 'default-user',
2626
'API Key': 'default-key',
27-
'Endpoint URL': 'default-endpoint-url',
28-
'Timeout': 10.0})
27+
'Endpoint URL': 'not set',
28+
'Timeout': 'not set'})
2929

3030

3131
class TestHelpSetup(testing.TestCase):
@@ -80,7 +80,6 @@ def test_get_user_input_private(self, input, getpass):
8080
self.assertEqual(username, 'user')
8181
self.assertEqual(secret, 'A' * 64)
8282
self.assertEqual(endpoint_url, consts.API_PRIVATE_ENDPOINT)
83-
self.assertEqual(timeout, 10)
8483

8584
@mock.patch('SoftLayer.CLI.environment.Environment.getpass')
8685
@mock.patch('SoftLayer.CLI.environment.Environment.input')

0 commit comments

Comments
 (0)