Skip to content

Commit f86c72a

Browse files
authored
Add config options to python client (#5)
* Add config options to python client
1 parent ff651d0 commit f86c72a

File tree

11 files changed

+205
-36
lines changed

11 files changed

+205
-36
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
1.1.0 October 4, 2016
2+
- Added config options: hostname, port, secure, timeout
13
1.0.2 August 11, 2016
24
- Initial Release

README.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,29 @@ instance:
6565
print(response.to_dict())
6666
# {'status': open, 'btn_ref': None, 'line_items': [], ...}
6767
68+
Configuration
69+
-------------
70+
71+
You may optionally supply a config argument with your API key:
72+
73+
.. code:: python
74+
75+
from pybutton import Client
76+
77+
client = Client("sk-XXX", {
78+
'hostname': 'api.testsite.com',
79+
'port': 80,
80+
'secure': False,
81+
'timeout': 5, # seconds
82+
})
83+
84+
The supported options are as follows:
85+
86+
* ``hostname``: Defaults to ``api.usebutton.com``.
87+
* ``port``: Defaults to ``443`` if ``config.secure``, else defaults to ``80``.
88+
* ``secure``: Whether or not to use HTTPS. Defaults to ``True``. **N.B: Button's API is only exposed through HTTPS. This option is provided purely as a convenience for testing and development.**
89+
* ``timeout``: The time in seconds that may elapse before network requests abort. Defaults to ``None``.
90+
6891
Resources
6992
---------
7093

pybutton/client.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ class Client(object):
1717
api_key (string): Your organization's API key. Do find yours at
1818
https://app.usebutton.com/settings/organization.
1919
20+
config (dict): Configuration options for the client. Options include:
21+
hostname: Defaults to api.usebutton.com.
22+
port: Defaults to 443 if config.secure, else defaults to 80.
23+
secure: Whether or not to use HTTPS. Defaults to True.
24+
timeout: The time in seconds for network requests to abort.
25+
Defaults to None.
26+
(N.B: Button's API is only exposed through HTTPS. This option is
27+
provided purely as a convenience for testing and development.)
28+
2029
Attributes:
2130
orders (pybutton.Resource): Resource for managing Button Orders.
2231
@@ -25,12 +34,29 @@ class Client(object):
2534
2635
'''
2736

28-
def __init__(self, api_key):
37+
def __init__(self, api_key, config=None):
2938

3039
if not api_key:
3140
raise ButtonClientError((
3241
'Must provide a Button API key. Find yours at'
3342
' https://app.usebutton.com/settings/organization'
3443
))
3544

36-
self.orders = Orders(api_key)
45+
if config is None:
46+
config = {}
47+
48+
config = config_with_defaults(config)
49+
50+
self.orders = Orders(api_key, config)
51+
52+
53+
def config_with_defaults(config):
54+
secure = config.get('secure', True)
55+
defaultPort = 443 if secure else 80
56+
57+
return {
58+
'secure': secure,
59+
'timeout': config.get('timeout'),
60+
'hostname': config.get('hostname', 'api.usebutton.com'),
61+
'port': config.get('port', defaultPort),
62+
}

pybutton/request.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
from urllib.request import Request
2121
from urllib.request import urlopen
2222
from urllib.error import HTTPError
23+
from urllib.parse import urlunsplit
2324

24-
def request(url, method, headers, data=None):
25+
def request(url, method, headers, data=None, timeout=None):
2526
''' Make an HTTP request in Python 3.x
2627
2728
This method will abstract the underlying organization and invocation of
@@ -50,21 +51,20 @@ def request(url, method, headers, data=None):
5051
if data:
5152
request.add_header('Content-Type', 'application/json')
5253

53-
response = urlopen(request).read().decode('utf8')
54+
response = urlopen(request, timeout=timeout).read().decode('utf8')
5455

5556
try:
5657
return json.loads(response)
5758
except ValueError:
5859
raise ButtonClientError('Invalid response: {0}'.format(response))
5960

60-
__all__ = [Request, urlopen, HTTPError, request]
61-
6261
else:
6362
from urllib2 import Request
6463
from urllib2 import urlopen
6564
from urllib2 import HTTPError
65+
from urlparse import urlunsplit
6666

67-
def request(url, method, headers, data=None):
67+
def request(url, method, headers, data=None, timeout=None):
6868
''' Make an HTTP request in Python 2.x
6969
7070
This method will abstract the underlying organization and invocation of
@@ -96,11 +96,30 @@ def request(url, method, headers, data=None):
9696
request.add_header('Content-Type', 'application/json')
9797
request.add_data(json.dumps(data))
9898

99-
response = urlopen(request).read()
99+
response = urlopen(request, timeout=timeout).read()
100100

101101
try:
102102
return json.loads(response)
103103
except ValueError:
104104
raise ButtonClientError('Invalid response: {0}'.format(response))
105105

106-
__all__ = [Request, urlopen, HTTPError, request]
106+
107+
def request_url(secure, hostname, port, path):
108+
'''
109+
Combines url components into a url passable into the request function.
110+
111+
Args:
112+
secure (boolean): Whether or not to use HTTPS.
113+
hostname (str): The host name for the url.
114+
port (int): The port number, as an integer.
115+
path (str): The hierarchical path.
116+
117+
Returns:
118+
(str) A complete url made up of the arguments.
119+
'''
120+
scheme = 'https' if secure else 'http'
121+
netloc = '{0}:{1}'.format(hostname, port)
122+
123+
return urlunsplit((scheme, netloc, path, '', ''))
124+
125+
__all__ = [Request, urlopen, HTTPError, request, request_url]

pybutton/resources/orders.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ class Orders(Resource):
1313
api_key (string): Your organization's API key. Do find yours at
1414
https://app.usebutton.com/settings/organization.
1515
16+
config (dict): Configuration options for the client. Options include:
17+
hostname: Defaults to api.usebutton.com.
18+
port: Defaults to 443 if config.secure, else defaults to 80.
19+
secure: Whether or not to use HTTPS. Defaults to True.
20+
timeout: The time in seconds for network requests to abort.
21+
Defaults to None.
22+
(N.B: Button's API is only exposed through HTTPS. This option is
23+
provided purely as a convenience for testing and development.)
24+
1625
Raises:
1726
pybutton.ButtonClientError
1827

pybutton/resources/resource.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from ..error import ButtonClientError
1212
from ..version import VERSION
1313
from ..request import request
14+
from ..request import request_url
1415
from ..request import HTTPError
1516

1617
USER_AGENT = 'pybutton/{0} python/{1}'.format(VERSION, python_version())
@@ -26,15 +27,23 @@ class Resource(object):
2627
api_key (string): Your organization's API key. Do find yours at
2728
https://app.usebutton.com/settings/organization.
2829
30+
config (dict): Configuration options for the client. Options include:
31+
hostname: Defaults to api.usebutton.com.
32+
port: Defaults to 443 if config.secure, else defaults to 80.
33+
secure: Whether or not to use HTTPS. Defaults to True.
34+
timeout: The time in seconds for network requests to abort.
35+
Defaults to None.
36+
(N.B: Button's API is only exposed through HTTPS. This option is
37+
provided purely as a convenience for testing and development.)
38+
2939
Raises:
3040
pybutton.ButtonClientError
3141
3242
'''
3343

34-
API_BASE = 'https://api.usebutton.com'
35-
36-
def __init__(self, api_key):
44+
def __init__(self, api_key, config):
3745
self.api_key = api_key
46+
self.config = config
3847

3948
def api_get(self, path):
4049
'''Make an HTTP GET request
@@ -91,7 +100,12 @@ def _api_request(self, path, method, data=None):
91100
92101
'''
93102

94-
url = '{0}{1}'.format(self.API_BASE, path)
103+
url = request_url(
104+
self.config['secure'],
105+
self.config['hostname'],
106+
self.config['port'],
107+
path
108+
)
95109
api_key_bytes = '{0}:'.format(self.api_key).encode()
96110
authorization = b64encode(api_key_bytes).decode()
97111

@@ -101,7 +115,14 @@ def _api_request(self, path, method, data=None):
101115
}
102116

103117
try:
104-
resp = request(url, method, headers, data).get('object', {})
118+
resp = request(
119+
url,
120+
method,
121+
headers,
122+
data,
123+
self.config['timeout']
124+
).get('object', {})
125+
105126
return Response(resp)
106127
except HTTPError as e:
107128
response = e.read()

pybutton/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = '1.0.2'
1+
VERSION = '1.1.0'

test/client_test.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from unittest import TestCase
77

88
from pybutton.client import Client
9+
from pybutton.client import config_with_defaults
910
from pybutton import ButtonClientError
1011

1112

@@ -33,3 +34,40 @@ def test_requires_api_key(self):
3334
def test_orders(self):
3435
client = Client('sk-XXX')
3536
self.assertTrue(client.orders is not None)
37+
38+
def test_config(self):
39+
# Defaults
40+
config = config_with_defaults({})
41+
42+
self.assertEqual(config, {
43+
'hostname': 'api.usebutton.com',
44+
'port': 443,
45+
'secure': True,
46+
'timeout': None,
47+
})
48+
49+
# Port and timeout overrides
50+
config = config_with_defaults({
51+
'port': 88,
52+
'timeout': 5,
53+
})
54+
55+
self.assertEqual(config, {
56+
'hostname': 'api.usebutton.com',
57+
'port': 88,
58+
'secure': True,
59+
'timeout': 5,
60+
})
61+
62+
# Hostname and secure overrides
63+
config = config_with_defaults({
64+
'hostname': 'localhost',
65+
'secure': False,
66+
})
67+
68+
self.assertEqual(config, {
69+
'hostname': 'localhost',
70+
'port': 80,
71+
'secure': False,
72+
'timeout': None,
73+
})

test/request_test.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from mock import patch
1111

1212
from pybutton.request import request
13+
from pybutton.request import request_url
1314
from pybutton import ButtonClientError
1415

1516

@@ -197,3 +198,19 @@ def test_raises_with_invalid_response_data(self, MockRequest,
197198
self.assertTrue(False)
198199
except ButtonClientError:
199200
pass
201+
202+
def test_request_url(self):
203+
path = request_url(
204+
True,
205+
'api.usebutton.com',
206+
443,
207+
'/v1/api/btnorder-XXX'
208+
)
209+
210+
self.assertEqual(
211+
path,
212+
'https://api.usebutton.com:443/v1/api/btnorder-XXX'
213+
)
214+
215+
path = request_url(False, 'localhost', 80, '/v1/api/btnorder-XXX')
216+
self.assertEqual(path, 'http://localhost:80/v1/api/btnorder-XXX')

test/resources/orders_test.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,23 @@
99

1010
from pybutton.resources import Orders
1111

12+
config = {
13+
'hostname': 'api.usebutton.com',
14+
'secure': True,
15+
'port': 443,
16+
'timeout': None
17+
}
18+
1219

1320
class OrdersTestCase(TestCase):
1421

1522
def test_path(self):
16-
order = Orders('sk-XXX')
23+
order = Orders('sk-XXX', config)
1724
self.assertEqual(order._path(), '/v1/order')
1825
self.assertEqual(order._path('btnorder-1'), '/v1/order/btnorder-1')
1926

2027
def test_get(self):
21-
order = Orders('sk-XXX')
28+
order = Orders('sk-XXX', config)
2229
order_response = {'a': 1}
2330

2431
api_get = Mock()
@@ -31,7 +38,7 @@ def test_get(self):
3138
api_get.assert_called_with('/v1/order/btnorder-XXX')
3239

3340
def test_create(self):
34-
order = Orders('sk-XXX')
41+
order = Orders('sk-XXX', config)
3542
order_payload = {'b': 2}
3643
order_response = {'a': 1}
3744

@@ -45,7 +52,7 @@ def test_create(self):
4552
api_post.assert_called_with('/v1/order', order_payload)
4653

4754
def test_update(self):
48-
order = Orders('sk-XXX')
55+
order = Orders('sk-XXX', config)
4956
order_payload = {'b': 2}
5057
order_response = {'a': 1}
5158

@@ -62,7 +69,7 @@ def test_update(self):
6269
)
6370

6471
def test_delete(self):
65-
order = Orders('sk-XXX')
72+
order = Orders('sk-XXX', config)
6673
order_response = {'a': 1}
6774

6875
api_delete = Mock()

0 commit comments

Comments
 (0)