From aa8e608ef5f9e63764e407d0378240bdb278c620 Mon Sep 17 00:00:00 2001 From: gckwan Date: Tue, 27 Sep 2016 16:03:30 -0400 Subject: [PATCH 1/9] Add config options to python client --- pybutton/client.py | 24 ++++++++++++++-- pybutton/request.py | 8 +++--- pybutton/resources/resource.py | 14 +++++---- test/client_test.py | 39 +++++++++++++++++++++++++ test/resources/orders_test.py | 16 +++++++---- test/resources/resource_test.py | 51 ++++++++++++++++++++++----------- 6 files changed, 120 insertions(+), 32 deletions(-) diff --git a/pybutton/client.py b/pybutton/client.py index 7466947..264b50b 100644 --- a/pybutton/client.py +++ b/pybutton/client.py @@ -17,6 +17,13 @@ class Client(object): api_key (string): Your organization's API key. Do find yours at https://app.usebutton.com/settings/organization. + config (dict): Configuration options for the client. Options include: + timeout: The time in ms for network requests to abort. Defaults to None. + hostname: Defaults to api.usebutton.com. + port: Defaults to 443 if config.secure, else defaults to 80. + 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.) + Attributes: orders (pybutton.Resource): Resource for managing Button Orders. @@ -25,7 +32,7 @@ class Client(object): ''' - def __init__(self, api_key): + def __init__(self, api_key, config={}): if not api_key: raise ButtonClientError(( @@ -33,4 +40,17 @@ def __init__(self, api_key): ' https://app.usebutton.com/settings/organization' )) - self.orders = Orders(api_key) + config = self._config_with_defaults(config) + + self.orders = Orders(api_key, config) + + def _config_with_defaults(self, config): + secure = config.get('secure', True) + defaultPort = 443 if secure else 80 + + return { + 'secure': secure, + 'timeout': config.get('timeout'), + 'hostname': config.get('hostname', 'api.usebutton.com'), + 'port': config.get('port', defaultPort) + } diff --git a/pybutton/request.py b/pybutton/request.py index 02ba6a6..1d7d0da 100644 --- a/pybutton/request.py +++ b/pybutton/request.py @@ -21,7 +21,7 @@ from urllib.request import urlopen from urllib.error import HTTPError - def request(url, method, headers, data=None): + def request(url, method, headers, data=None, timeout=None): ''' Make an HTTP request in Python 3.x This method will abstract the underlying organization and invocation of @@ -50,7 +50,7 @@ def request(url, method, headers, data=None): if data: request.add_header('Content-Type', 'application/json') - response = urlopen(request).read().decode('utf8') + response = urlopen(request, timeout=timeout).read().decode('utf8') try: return json.loads(response) @@ -64,7 +64,7 @@ def request(url, method, headers, data=None): from urllib2 import urlopen from urllib2 import HTTPError - def request(url, method, headers, data=None): + def request(url, method, headers, data=None, timeout=None): ''' Make an HTTP request in Python 2.x This method will abstract the underlying organization and invocation of @@ -96,7 +96,7 @@ def request(url, method, headers, data=None): request.add_header('Content-Type', 'application/json') request.add_data(json.dumps(data)) - response = urlopen(request).read() + response = urlopen(request, timeout=timeout).read() try: return json.loads(response) diff --git a/pybutton/resources/resource.py b/pybutton/resources/resource.py index a0b85d0..c038108 100644 --- a/pybutton/resources/resource.py +++ b/pybutton/resources/resource.py @@ -31,10 +31,9 @@ class Resource(object): ''' - API_BASE = 'https://api.usebutton.com' - - def __init__(self, api_key): + def __init__(self, api_key, config): self.api_key = api_key + self.config = config def api_get(self, path): '''Make an HTTP GET request @@ -91,7 +90,8 @@ def _api_request(self, path, method, data=None): ''' - url = '{0}{1}'.format(self.API_BASE, path) + url = self._request_url(path) + api_key_bytes = '{0}:'.format(self.api_key).encode() authorization = b64encode(api_key_bytes).decode() @@ -101,7 +101,7 @@ def _api_request(self, path, method, data=None): } try: - resp = request(url, method, headers, data).get('object', {}) + resp = request(url, method, headers, data, self.config['timeout']).get('object', {}) return Response(resp) except HTTPError as e: response = e.read() @@ -115,3 +115,7 @@ def _api_request(self, path, method, data=None): error = json.loads(data).get('error', {}) message = error.get('message', fallback) raise ButtonClientError(message) + + def _request_url(self, path): + protocol = 'https://' if self.config['secure'] else 'http://' + return '{0}{1}:{2}{3}'.format(protocol, self.config['hostname'], self.config['port'], path) diff --git a/test/client_test.py b/test/client_test.py index c8c152d..764e0ba 100644 --- a/test/client_test.py +++ b/test/client_test.py @@ -33,3 +33,42 @@ def test_requires_api_key(self): def test_orders(self): client = Client('sk-XXX') self.assertTrue(client.orders is not None) + + def test_config(self): + client = Client('sk-XXX') + + # Defaults + config = client._config_with_defaults({}) + + self.assertEqual(config, { + 'hostname': 'api.usebutton.com', + 'port': 443, + 'secure': True, + 'timeout': None + }) + + # Port and timeout overrides + config = client._config_with_defaults({ + 'port': 88, + 'timeout': 5 + }) + + self.assertEqual(config, { + 'hostname': 'api.usebutton.com', + 'port': 88, + 'secure': True, + 'timeout': 5 + }) + + # Hostname and secure overrides + config = client._config_with_defaults({ + 'hostname': 'localhost', + 'secure': False + }) + + self.assertEqual(config, { + 'hostname': 'localhost', + 'port': 80, + 'secure': False, + 'timeout': None + }) diff --git a/test/resources/orders_test.py b/test/resources/orders_test.py index 0d4ee03..e75d19c 100644 --- a/test/resources/orders_test.py +++ b/test/resources/orders_test.py @@ -9,16 +9,22 @@ from pybutton.resources import Orders +config = { + 'hostname': 'api.usebutton.com', + 'secure': True, + 'port': 443, + 'timeout': None +} class OrdersTestCase(TestCase): def test_path(self): - order = Orders('sk-XXX') + order = Orders('sk-XXX', config) self.assertEqual(order._path(), '/v1/order') self.assertEqual(order._path('btnorder-1'), '/v1/order/btnorder-1') def test_get(self): - order = Orders('sk-XXX') + order = Orders('sk-XXX', config) order_response = {'a': 1} api_get = Mock() @@ -31,7 +37,7 @@ def test_get(self): api_get.assert_called_with('/v1/order/btnorder-XXX') def test_create(self): - order = Orders('sk-XXX') + order = Orders('sk-XXX', config) order_payload = {'b': 2} order_response = {'a': 1} @@ -45,7 +51,7 @@ def test_create(self): api_post.assert_called_with('/v1/order', order_payload) def test_update(self): - order = Orders('sk-XXX') + order = Orders('sk-XXX', config) order_payload = {'b': 2} order_response = {'a': 1} @@ -62,7 +68,7 @@ def test_update(self): ) def test_delete(self): - order = Orders('sk-XXX') + order = Orders('sk-XXX', config) order_response = {'a': 1} api_delete = Mock() diff --git a/test/resources/resource_test.py b/test/resources/resource_test.py index 572a906..dfb98e7 100644 --- a/test/resources/resource_test.py +++ b/test/resources/resource_test.py @@ -11,6 +11,12 @@ from pybutton.resources.resource import Resource from pybutton.error import ButtonClientError +config = { + 'hostname': 'api.usebutton.com', + 'secure': True, + 'port': 443, + 'timeout': None +} class ResourceTestCase(TestCase): @@ -18,12 +24,12 @@ class ResourceTestCase(TestCase): def test_api_request(self, request): resource_response = {'a': 1} request.return_value = {'object': resource_response} - resource = Resource('sk-XXX') + resource = Resource('sk-XXX', config) response = resource._api_request('/v1/api', 'GET') args = request.call_args[0] self.assertEqual(response.to_dict(), resource_response) - self.assertEqual(args[0], 'https://api.usebutton.com/v1/api') + self.assertEqual(args[0], 'https://api.usebutton.com:443/v1/api') self.assertEqual(args[1], 'GET') self.assertTrue(len(args[2]['User-Agent']) != 0) self.assertEqual(args[2]['Authorization'], 'Basic c2stWFhYOg==') @@ -33,12 +39,12 @@ def test_api_request(self, request): def test_api_request_with_other_methods(self, request): resource_response = {'a': 1} request.return_value = {'object': resource_response} - resource = Resource('sk-XXX') + resource = Resource('sk-XXX', config) response = resource._api_request('/v1/api', 'POST') args = request.call_args[0] self.assertEqual(response.to_dict(), resource_response) - self.assertEqual(args[0], 'https://api.usebutton.com/v1/api') + self.assertEqual(args[0], 'https://api.usebutton.com:443/v1/api') self.assertEqual(args[1], 'POST') self.assertTrue(len(args[2]['User-Agent']) != 0) self.assertEqual(args[2]['Authorization'], 'Basic c2stWFhYOg==') @@ -48,12 +54,12 @@ def test_api_request_with_other_methods(self, request): def test_api_request_with_other_paths(self, request): resource_response = {'a': 1} request.return_value = {'object': resource_response} - resource = Resource('sk-XXX') + resource = Resource('sk-XXX', config) response = resource._api_request('/v2/api', 'GET') args = request.call_args[0] self.assertEqual(response.to_dict(), resource_response) - self.assertEqual(args[0], 'https://api.usebutton.com/v2/api') + self.assertEqual(args[0], 'https://api.usebutton.com:443/v2/api') self.assertEqual(args[1], 'GET') self.assertTrue(len(args[2]['User-Agent']) != 0) self.assertEqual(args[2]['Authorization'], 'Basic c2stWFhYOg==') @@ -64,12 +70,12 @@ def test_api_request_with_data(self, request): data = {'c': 3} resource_response = {'a': 1} request.return_value = {'object': resource_response} - resource = Resource('sk-XXX') + resource = Resource('sk-XXX', config) response = resource._api_request('/v2/api', 'GET', data) args = request.call_args[0] self.assertEqual(response.to_dict(), resource_response) - self.assertEqual(args[0], 'https://api.usebutton.com/v2/api') + self.assertEqual(args[0], 'https://api.usebutton.com:443/v2/api') self.assertEqual(args[1], 'GET') self.assertTrue(len(args[2]['User-Agent']) != 0) self.assertEqual(args[2]['Authorization'], 'Basic c2stWFhYOg==') @@ -86,7 +92,7 @@ def side_effect(*args): raise HTTPError('url', 404, 'bloop', {}, fp) request.side_effect = side_effect - resource = Resource('sk-XXX') + resource = Resource('sk-XXX', config) try: resource._api_request('/v2/api', 'GET', data) @@ -107,7 +113,7 @@ def side_effect(*args): raise HTTPError('url', 404, 'bloop', {}, fp) request.side_effect = side_effect - resource = Resource('sk-XXX') + resource = Resource('sk-XXX', config) try: resource._api_request('/v2/api', 'GET', data) @@ -119,12 +125,12 @@ def side_effect(*args): def test_api_get(self, request): resource_response = {'a': 1} request.return_value = {'object': resource_response} - resource = Resource('sk-XXX') + resource = Resource('sk-XXX', config) response = resource.api_get('/v1/api') args = request.call_args[0] self.assertEqual(response.to_dict(), resource_response) - self.assertEqual(args[0], 'https://api.usebutton.com/v1/api') + self.assertEqual(args[0], 'https://api.usebutton.com:443/v1/api') self.assertEqual(args[1], 'GET') self.assertTrue(len(args[2]['User-Agent']) != 0) self.assertEqual(args[2]['Authorization'], 'Basic c2stWFhYOg==') @@ -135,12 +141,12 @@ def test_api_post(self, request): request_payload = {'c': 3} resource_response = {'a': 1} request.return_value = {'object': resource_response} - resource = Resource('sk-XXX') + resource = Resource('sk-XXX', config) response = resource.api_post('/v1/api', request_payload) args = request.call_args[0] self.assertEqual(response.to_dict(), resource_response) - self.assertEqual(args[0], 'https://api.usebutton.com/v1/api') + self.assertEqual(args[0], 'https://api.usebutton.com:443/v1/api') self.assertEqual(args[1], 'POST') self.assertTrue(len(args[2]['User-Agent']) != 0) self.assertEqual(args[2]['Authorization'], 'Basic c2stWFhYOg==') @@ -150,13 +156,26 @@ def test_api_post(self, request): def test_api_delete(self, request): resource_response = {'a': 1} request.return_value = {'object': resource_response} - resource = Resource('sk-XXX') + resource = Resource('sk-XXX', config) response = resource.api_delete('/v1/api') args = request.call_args[0] self.assertEqual(response.to_dict(), resource_response) - self.assertEqual(args[0], 'https://api.usebutton.com/v1/api') + self.assertEqual(args[0], 'https://api.usebutton.com:443/v1/api') self.assertEqual(args[1], 'DELETE') self.assertTrue(len(args[2]['User-Agent']) != 0) self.assertEqual(args[2]['Authorization'], 'Basic c2stWFhYOg==') self.assertEqual(args[3], None) + + def test_request_url(self): + resource = Resource('sk-XXX', config) + path = resource._request_url('/v1/api/btnorder-XXX') + self.assertEqual(path, 'https://api.usebutton.com:443/v1/api/btnorder-XXX') + + resource = Resource('sk-XXX', { + 'hostname': 'localhost', + 'port': 80, + 'secure': False + }) + path = resource._request_url('/v1/api/btnorder-XXX') + self.assertEqual(path, 'http://localhost:80/v1/api/btnorder-XXX') From d43dedf8d1eaab615ebf1610e324e479b45cce8c Mon Sep 17 00:00:00 2001 From: gckwan Date: Tue, 27 Sep 2016 16:25:23 -0400 Subject: [PATCH 2/9] Update README --- README.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.rst b/README.rst index e8c0613..17a7955 100644 --- a/README.rst +++ b/README.rst @@ -65,6 +65,27 @@ instance: print(response.to_dict()) # {'status': open, 'btn_ref': None, 'line_items': [], ...} +Configuration +--------- + +You may optionally supply a config argument with your API key: + +.. code:: python + + client = Client("sk-XXX", { + 'hostname': 'api.testsite.com', + 'port': 80, + 'secure': False, + 'timeout': 5 + }) + +The supported options are as follows: + +* ``hostname``: Defaults to ``api.usebutton.com``. +* ``port``: Defaults to ``443`` if ``config.secure``, else defaults to ``80``. +* ``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.** +* ``timeout``: The time in seconds that may elapse before network requests abort. Defaults to ``None``. + Resources --------- From d2fda7a3f57f06f5f00fc75826f7eaff3c05e1ae Mon Sep 17 00:00:00 2001 From: gckwan Date: Wed, 28 Sep 2016 10:56:32 -0400 Subject: [PATCH 3/9] Address comments --- CHANGELOG.md | 3 +++ README.rst | 6 ++++-- pybutton/client.py | 28 ++++++++++++++++------------ pybutton/resources/resource.py | 12 ++++++++++-- pybutton/version.py | 2 +- test/client_test.py | 9 ++++----- 6 files changed, 38 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2b422f..074a78c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,5 @@ 1.0.2 August 11, 2016 - Initial Release +1.1.0 September 28, 2016 + - Added config options: hostname, port, secure, timeout + diff --git a/README.rst b/README.rst index 17a7955..ee165ae 100644 --- a/README.rst +++ b/README.rst @@ -66,17 +66,19 @@ instance: # {'status': open, 'btn_ref': None, 'line_items': [], ...} Configuration ---------- +------------- You may optionally supply a config argument with your API key: .. code:: python + from pybutton import Client + client = Client("sk-XXX", { 'hostname': 'api.testsite.com', 'port': 80, 'secure': False, - 'timeout': 5 + 'timeout': 5 # seconds }) The supported options are as follows: diff --git a/pybutton/client.py b/pybutton/client.py index 264b50b..79f4be3 100644 --- a/pybutton/client.py +++ b/pybutton/client.py @@ -18,10 +18,10 @@ class Client(object): https://app.usebutton.com/settings/organization. config (dict): Configuration options for the client. Options include: - timeout: The time in ms for network requests to abort. Defaults to None. hostname: Defaults to api.usebutton.com. port: Defaults to 443 if config.secure, else defaults to 80. secure: Whether or not to use HTTPS. Defaults to True. + timeout: The time in seconds for network requests to abort. Defaults to None. (N.B: Button's API is only exposed through HTTPS. This option is provided purely as a convenience for testing and development.) Attributes: @@ -32,7 +32,7 @@ class Client(object): ''' - def __init__(self, api_key, config={}): + def __init__(self, api_key, config=None): if not api_key: raise ButtonClientError(( @@ -40,17 +40,21 @@ def __init__(self, api_key, config={}): ' https://app.usebutton.com/settings/organization' )) - config = self._config_with_defaults(config) + if config is None: + config = {} + + config = _config_with_defaults(config) self.orders = Orders(api_key, config) - def _config_with_defaults(self, config): - secure = config.get('secure', True) - defaultPort = 443 if secure else 80 - return { - 'secure': secure, - 'timeout': config.get('timeout'), - 'hostname': config.get('hostname', 'api.usebutton.com'), - 'port': config.get('port', defaultPort) - } +def _config_with_defaults(config): + secure = config.get('secure', True) + defaultPort = 443 if secure else 80 + + return { + 'secure': secure, + 'timeout': config.get('timeout'), + 'hostname': config.get('hostname', 'api.usebutton.com'), + 'port': config.get('port', defaultPort) + } diff --git a/pybutton/resources/resource.py b/pybutton/resources/resource.py index c038108..428aa34 100644 --- a/pybutton/resources/resource.py +++ b/pybutton/resources/resource.py @@ -6,6 +6,7 @@ from base64 import b64encode from platform import python_version import json +import urlparse from ..response import Response from ..error import ButtonClientError @@ -26,6 +27,13 @@ class Resource(object): api_key (string): Your organization's API key. Do find yours at https://app.usebutton.com/settings/organization. + config (dict): Configuration options for the client. Options include: + hostname: Defaults to api.usebutton.com. + port: Defaults to 443 if config.secure, else defaults to 80. + secure: Whether or not to use HTTPS. Defaults to True. + timeout: The time in seconds for network requests to abort. Defaults to None. + (N.B: Button's API is only exposed through HTTPS. This option is provided purely as a convenience for testing and development.) + Raises: pybutton.ButtonClientError @@ -117,5 +125,5 @@ def _api_request(self, path, method, data=None): raise ButtonClientError(message) def _request_url(self, path): - protocol = 'https://' if self.config['secure'] else 'http://' - return '{0}{1}:{2}{3}'.format(protocol, self.config['hostname'], self.config['port'], path) + protocol = 'https' if self.config['secure'] else 'http' + return urlparse.urlunsplit((protocol, '{0}:{1}'.format(self.config['hostname'], self.config['port']), path, '', '')) diff --git a/pybutton/version.py b/pybutton/version.py index 6732d5a..9cff1bc 100644 --- a/pybutton/version.py +++ b/pybutton/version.py @@ -1 +1 @@ -VERSION = '1.0.2' +VERSION = '1.1.0' diff --git a/test/client_test.py b/test/client_test.py index 764e0ba..bc6fcad 100644 --- a/test/client_test.py +++ b/test/client_test.py @@ -6,6 +6,7 @@ from unittest import TestCase from pybutton.client import Client +from pybutton.client import _config_with_defaults from pybutton import ButtonClientError @@ -35,10 +36,8 @@ def test_orders(self): self.assertTrue(client.orders is not None) def test_config(self): - client = Client('sk-XXX') - # Defaults - config = client._config_with_defaults({}) + config = _config_with_defaults({}) self.assertEqual(config, { 'hostname': 'api.usebutton.com', @@ -48,7 +47,7 @@ def test_config(self): }) # Port and timeout overrides - config = client._config_with_defaults({ + config = _config_with_defaults({ 'port': 88, 'timeout': 5 }) @@ -61,7 +60,7 @@ def test_config(self): }) # Hostname and secure overrides - config = client._config_with_defaults({ + config = _config_with_defaults({ 'hostname': 'localhost', 'secure': False }) From 05084cf44ca7a7483fd84c3cf5233bb976cf6975 Mon Sep 17 00:00:00 2001 From: gckwan Date: Fri, 30 Sep 2016 11:20:40 -0400 Subject: [PATCH 4/9] move request_url into requests --- pybutton/request.py | 20 ++++++++++++++++++-- pybutton/resources/orders.py | 7 +++++++ pybutton/resources/resource.py | 9 ++------- test/request_test.py | 8 ++++++++ test/resources/resource_test.py | 13 ------------- 5 files changed, 35 insertions(+), 22 deletions(-) diff --git a/pybutton/request.py b/pybutton/request.py index 1d7d0da..c3cf35d 100644 --- a/pybutton/request.py +++ b/pybutton/request.py @@ -20,6 +20,7 @@ from urllib.request import Request from urllib.request import urlopen from urllib.error import HTTPError + from urllib.parse import urlunsplit def request(url, method, headers, data=None, timeout=None): ''' Make an HTTP request in Python 3.x @@ -57,12 +58,20 @@ def request(url, method, headers, data=None, timeout=None): except ValueError: raise ButtonClientError('Invalid response: {0}'.format(response)) - __all__ = [Request, urlopen, HTTPError, request] + def request_url(secure, hostname, port, path): + '''Combines url components into a url passable into the request function.''' + scheme = 'https' if secure else 'http' + netloc = '{0}:{1}'.format(hostname, port) + + return urlunsplit((scheme, netloc, path, '', '')) + + __all__ = [Request, urlopen, HTTPError, request, request_url] else: from urllib2 import Request from urllib2 import urlopen from urllib2 import HTTPError + from urlparse import urlunsplit def request(url, method, headers, data=None, timeout=None): ''' Make an HTTP request in Python 2.x @@ -103,4 +112,11 @@ def request(url, method, headers, data=None, timeout=None): except ValueError: raise ButtonClientError('Invalid response: {0}'.format(response)) - __all__ = [Request, urlopen, HTTPError, request] + def request_url(secure, hostname, port, path): + '''Combines url components into a url passable into the request function.''' + scheme = 'https' if secure else 'http' + netloc = '{0}:{1}'.format(hostname, port) + + return urlunsplit((scheme, netloc, path, '', '')) + + __all__ = [Request, urlopen, HTTPError, request, request_url] diff --git a/pybutton/resources/orders.py b/pybutton/resources/orders.py index 4f8c49f..2932800 100644 --- a/pybutton/resources/orders.py +++ b/pybutton/resources/orders.py @@ -13,6 +13,13 @@ class Orders(Resource): api_key (string): Your organization's API key. Do find yours at https://app.usebutton.com/settings/organization. + config (dict): Configuration options for the client. Options include: + hostname: Defaults to api.usebutton.com. + port: Defaults to 443 if config.secure, else defaults to 80. + secure: Whether or not to use HTTPS. Defaults to True. + timeout: The time in seconds for network requests to abort. Defaults to None. + (N.B: Button's API is only exposed through HTTPS. This option is provided purely as a convenience for testing and development.) + Raises: pybutton.ButtonClientError diff --git a/pybutton/resources/resource.py b/pybutton/resources/resource.py index 428aa34..aa9e761 100644 --- a/pybutton/resources/resource.py +++ b/pybutton/resources/resource.py @@ -6,12 +6,12 @@ from base64 import b64encode from platform import python_version import json -import urlparse from ..response import Response from ..error import ButtonClientError from ..version import VERSION from ..request import request +from ..request import request_url from ..request import HTTPError USER_AGENT = 'pybutton/{0} python/{1}'.format(VERSION, python_version()) @@ -98,8 +98,7 @@ def _api_request(self, path, method, data=None): ''' - url = self._request_url(path) - + url = request_url(self.config['secure'], self.config['hostname'], self.config['port'], path) api_key_bytes = '{0}:'.format(self.api_key).encode() authorization = b64encode(api_key_bytes).decode() @@ -123,7 +122,3 @@ def _api_request(self, path, method, data=None): error = json.loads(data).get('error', {}) message = error.get('message', fallback) raise ButtonClientError(message) - - def _request_url(self, path): - protocol = 'https' if self.config['secure'] else 'http' - return urlparse.urlunsplit((protocol, '{0}:{1}'.format(self.config['hostname'], self.config['port']), path, '', '')) diff --git a/test/request_test.py b/test/request_test.py index 0ac16ee..dca255f 100644 --- a/test/request_test.py +++ b/test/request_test.py @@ -10,6 +10,7 @@ from mock import patch from pybutton.request import request +from pybutton.request import request_url from pybutton import ButtonClientError @@ -197,3 +198,10 @@ def test_raises_with_invalid_response_data(self, MockRequest, self.assertTrue(False) except ButtonClientError: pass + + def test_request_url(self): + path = request_url(True, 'api.usebutton.com', 443, '/v1/api/btnorder-XXX') + self.assertEqual(path, 'https://api.usebutton.com:443/v1/api/btnorder-XXX') + + path = request_url(False, 'localhost', 80, '/v1/api/btnorder-XXX') + self.assertEqual(path, 'http://localhost:80/v1/api/btnorder-XXX') diff --git a/test/resources/resource_test.py b/test/resources/resource_test.py index dfb98e7..3450c58 100644 --- a/test/resources/resource_test.py +++ b/test/resources/resource_test.py @@ -166,16 +166,3 @@ def test_api_delete(self, request): self.assertTrue(len(args[2]['User-Agent']) != 0) self.assertEqual(args[2]['Authorization'], 'Basic c2stWFhYOg==') self.assertEqual(args[3], None) - - def test_request_url(self): - resource = Resource('sk-XXX', config) - path = resource._request_url('/v1/api/btnorder-XXX') - self.assertEqual(path, 'https://api.usebutton.com:443/v1/api/btnorder-XXX') - - resource = Resource('sk-XXX', { - 'hostname': 'localhost', - 'port': 80, - 'secure': False - }) - path = resource._request_url('/v1/api/btnorder-XXX') - self.assertEqual(path, 'http://localhost:80/v1/api/btnorder-XXX') From afd9d75a802ffebf96ad55c9f6020e6d7e69cc09 Mon Sep 17 00:00:00 2001 From: gckwan Date: Tue, 4 Oct 2016 13:21:37 -0400 Subject: [PATCH 5/9] Fix lint errors --- CHANGELOG.md | 4 ++-- pybutton/client.py | 10 ++++++---- pybutton/request.py | 7 +++++-- pybutton/resources/orders.py | 6 ++++-- pybutton/resources/resource.py | 22 ++++++++++++++++++---- test/client_test.py | 8 ++++---- test/request_test.py | 13 +++++++++++-- test/resources/orders_test.py | 1 + test/resources/resource_test.py | 1 + 9 files changed, 52 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 074a78c..bf3aa6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ -1.0.2 August 11, 2016 - - Initial Release 1.1.0 September 28, 2016 - Added config options: hostname, port, secure, timeout +1.0.2 August 11, 2016 + - Initial Release diff --git a/pybutton/client.py b/pybutton/client.py index 79f4be3..044c041 100644 --- a/pybutton/client.py +++ b/pybutton/client.py @@ -21,8 +21,10 @@ class Client(object): hostname: Defaults to api.usebutton.com. port: Defaults to 443 if config.secure, else defaults to 80. secure: Whether or not to use HTTPS. Defaults to True. - timeout: The time in seconds for network requests to abort. Defaults to None. - (N.B: Button's API is only exposed through HTTPS. This option is provided purely as a convenience for testing and development.) + timeout: The time in seconds for network requests to abort. + Defaults to None. + (N.B: Button's API is only exposed through HTTPS. This option is + provided purely as a convenience for testing and development.) Attributes: orders (pybutton.Resource): Resource for managing Button Orders. @@ -43,12 +45,12 @@ def __init__(self, api_key, config=None): if config is None: config = {} - config = _config_with_defaults(config) + config = config_with_defaults(config) self.orders = Orders(api_key, config) -def _config_with_defaults(config): +def config_with_defaults(config): secure = config.get('secure', True) defaultPort = 443 if secure else 80 diff --git a/pybutton/request.py b/pybutton/request.py index c3cf35d..8c30c10 100644 --- a/pybutton/request.py +++ b/pybutton/request.py @@ -59,7 +59,9 @@ def request(url, method, headers, data=None, timeout=None): raise ButtonClientError('Invalid response: {0}'.format(response)) def request_url(secure, hostname, port, path): - '''Combines url components into a url passable into the request function.''' + ''' Combines url components into a url passable into the request + function. ''' + scheme = 'https' if secure else 'http' netloc = '{0}:{1}'.format(hostname, port) @@ -113,7 +115,8 @@ def request(url, method, headers, data=None, timeout=None): raise ButtonClientError('Invalid response: {0}'.format(response)) def request_url(secure, hostname, port, path): - '''Combines url components into a url passable into the request function.''' + ''' Combines url components into a url passable into the request + function. ''' scheme = 'https' if secure else 'http' netloc = '{0}:{1}'.format(hostname, port) diff --git a/pybutton/resources/orders.py b/pybutton/resources/orders.py index 2932800..011d1c5 100644 --- a/pybutton/resources/orders.py +++ b/pybutton/resources/orders.py @@ -17,8 +17,10 @@ class Orders(Resource): hostname: Defaults to api.usebutton.com. port: Defaults to 443 if config.secure, else defaults to 80. secure: Whether or not to use HTTPS. Defaults to True. - timeout: The time in seconds for network requests to abort. Defaults to None. - (N.B: Button's API is only exposed through HTTPS. This option is provided purely as a convenience for testing and development.) + timeout: The time in seconds for network requests to abort. + Defaults to None. + (N.B: Button's API is only exposed through HTTPS. This option is + provided purely as a convenience for testing and development.) Raises: pybutton.ButtonClientError diff --git a/pybutton/resources/resource.py b/pybutton/resources/resource.py index aa9e761..26831aa 100644 --- a/pybutton/resources/resource.py +++ b/pybutton/resources/resource.py @@ -31,8 +31,10 @@ class Resource(object): hostname: Defaults to api.usebutton.com. port: Defaults to 443 if config.secure, else defaults to 80. secure: Whether or not to use HTTPS. Defaults to True. - timeout: The time in seconds for network requests to abort. Defaults to None. - (N.B: Button's API is only exposed through HTTPS. This option is provided purely as a convenience for testing and development.) + timeout: The time in seconds for network requests to abort. + Defaults to None. + (N.B: Button's API is only exposed through HTTPS. This option is + provided purely as a convenience for testing and development.) Raises: pybutton.ButtonClientError @@ -98,7 +100,12 @@ def _api_request(self, path, method, data=None): ''' - url = request_url(self.config['secure'], self.config['hostname'], self.config['port'], path) + url = request_url( + self.config['secure'], + self.config['hostname'], + self.config['port'], + path + ) api_key_bytes = '{0}:'.format(self.api_key).encode() authorization = b64encode(api_key_bytes).decode() @@ -108,7 +115,14 @@ def _api_request(self, path, method, data=None): } try: - resp = request(url, method, headers, data, self.config['timeout']).get('object', {}) + resp = request( + url, + method, + headers, + data, + self.config['timeout'] + ).get('object', {}) + return Response(resp) except HTTPError as e: response = e.read() diff --git a/test/client_test.py b/test/client_test.py index bc6fcad..77909c8 100644 --- a/test/client_test.py +++ b/test/client_test.py @@ -6,7 +6,7 @@ from unittest import TestCase from pybutton.client import Client -from pybutton.client import _config_with_defaults +from pybutton.client import config_with_defaults from pybutton import ButtonClientError @@ -37,7 +37,7 @@ def test_orders(self): def test_config(self): # Defaults - config = _config_with_defaults({}) + config = config_with_defaults({}) self.assertEqual(config, { 'hostname': 'api.usebutton.com', @@ -47,7 +47,7 @@ def test_config(self): }) # Port and timeout overrides - config = _config_with_defaults({ + config = config_with_defaults({ 'port': 88, 'timeout': 5 }) @@ -60,7 +60,7 @@ def test_config(self): }) # Hostname and secure overrides - config = _config_with_defaults({ + config = config_with_defaults({ 'hostname': 'localhost', 'secure': False }) diff --git a/test/request_test.py b/test/request_test.py index dca255f..f7ec1cb 100644 --- a/test/request_test.py +++ b/test/request_test.py @@ -200,8 +200,17 @@ def test_raises_with_invalid_response_data(self, MockRequest, pass def test_request_url(self): - path = request_url(True, 'api.usebutton.com', 443, '/v1/api/btnorder-XXX') - self.assertEqual(path, 'https://api.usebutton.com:443/v1/api/btnorder-XXX') + path = request_url( + True, + 'api.usebutton.com', + 443, + '/v1/api/btnorder-XXX' + ) + + self.assertEqual( + path, + 'https://api.usebutton.com:443/v1/api/btnorder-XXX' + ) path = request_url(False, 'localhost', 80, '/v1/api/btnorder-XXX') self.assertEqual(path, 'http://localhost:80/v1/api/btnorder-XXX') diff --git a/test/resources/orders_test.py b/test/resources/orders_test.py index e75d19c..a988d76 100644 --- a/test/resources/orders_test.py +++ b/test/resources/orders_test.py @@ -16,6 +16,7 @@ 'timeout': None } + class OrdersTestCase(TestCase): def test_path(self): diff --git a/test/resources/resource_test.py b/test/resources/resource_test.py index 3450c58..d427935 100644 --- a/test/resources/resource_test.py +++ b/test/resources/resource_test.py @@ -18,6 +18,7 @@ 'timeout': None } + class ResourceTestCase(TestCase): @patch('pybutton.resources.resource.request') From 8c59aeb24e104ff78afbf1b0a13693a311131a90 Mon Sep 17 00:00:00 2001 From: gckwan Date: Tue, 4 Oct 2016 15:16:41 -0400 Subject: [PATCH 6/9] Single definition for request_url --- pybutton/request.py | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/pybutton/request.py b/pybutton/request.py index 8c30c10..257b5f4 100644 --- a/pybutton/request.py +++ b/pybutton/request.py @@ -58,17 +58,6 @@ def request(url, method, headers, data=None, timeout=None): except ValueError: raise ButtonClientError('Invalid response: {0}'.format(response)) - def request_url(secure, hostname, port, path): - ''' Combines url components into a url passable into the request - function. ''' - - scheme = 'https' if secure else 'http' - netloc = '{0}:{1}'.format(hostname, port) - - return urlunsplit((scheme, netloc, path, '', '')) - - __all__ = [Request, urlopen, HTTPError, request, request_url] - else: from urllib2 import Request from urllib2 import urlopen @@ -114,12 +103,13 @@ def request(url, method, headers, data=None, timeout=None): except ValueError: raise ButtonClientError('Invalid response: {0}'.format(response)) - def request_url(secure, hostname, port, path): - ''' Combines url components into a url passable into the request - function. ''' - scheme = 'https' if secure else 'http' - netloc = '{0}:{1}'.format(hostname, port) - return urlunsplit((scheme, netloc, path, '', '')) +def request_url(secure, hostname, port, path): + ''' Combines url components into a url passable into the request + function. ''' + scheme = 'https' if secure else 'http' + netloc = '{0}:{1}'.format(hostname, port) + + return urlunsplit((scheme, netloc, path, '', '')) - __all__ = [Request, urlopen, HTTPError, request, request_url] +__all__ = [Request, urlopen, HTTPError, request, request_url] From f657497be88c1dc76b4c7d3ea6062d865d9442cd Mon Sep 17 00:00:00 2001 From: gckwan Date: Tue, 4 Oct 2016 15:31:33 -0400 Subject: [PATCH 7/9] Trailing commas, update changelog date --- CHANGELOG.md | 3 +-- README.rst | 2 +- pybutton/client.py | 2 +- test/client_test.py | 10 +++++----- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf3aa6a..4ec8d19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,4 @@ -1.1.0 September 28, 2016 +1.1.0 October 4, 2016 - Added config options: hostname, port, secure, timeout 1.0.2 August 11, 2016 - Initial Release - diff --git a/README.rst b/README.rst index ee165ae..2a78242 100644 --- a/README.rst +++ b/README.rst @@ -78,7 +78,7 @@ You may optionally supply a config argument with your API key: 'hostname': 'api.testsite.com', 'port': 80, 'secure': False, - 'timeout': 5 # seconds + 'timeout': 5, # seconds }) The supported options are as follows: diff --git a/pybutton/client.py b/pybutton/client.py index 044c041..5e32313 100644 --- a/pybutton/client.py +++ b/pybutton/client.py @@ -58,5 +58,5 @@ def config_with_defaults(config): 'secure': secure, 'timeout': config.get('timeout'), 'hostname': config.get('hostname', 'api.usebutton.com'), - 'port': config.get('port', defaultPort) + 'port': config.get('port', defaultPort), } diff --git a/test/client_test.py b/test/client_test.py index 77909c8..b5c4b0c 100644 --- a/test/client_test.py +++ b/test/client_test.py @@ -43,31 +43,31 @@ def test_config(self): 'hostname': 'api.usebutton.com', 'port': 443, 'secure': True, - 'timeout': None + 'timeout': None, }) # Port and timeout overrides config = config_with_defaults({ 'port': 88, - 'timeout': 5 + 'timeout': 5, }) self.assertEqual(config, { 'hostname': 'api.usebutton.com', 'port': 88, 'secure': True, - 'timeout': 5 + 'timeout': 5, }) # Hostname and secure overrides config = config_with_defaults({ 'hostname': 'localhost', - 'secure': False + 'secure': False, }) self.assertEqual(config, { 'hostname': 'localhost', 'port': 80, 'secure': False, - 'timeout': None + 'timeout': None, }) From 54215031b51f1f6d73e53f312b91157aa53218e3 Mon Sep 17 00:00:00 2001 From: gckwan Date: Tue, 4 Oct 2016 15:43:09 -0400 Subject: [PATCH 8/9] request_url docstring --- pybutton/request.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pybutton/request.py b/pybutton/request.py index 257b5f4..34f4847 100644 --- a/pybutton/request.py +++ b/pybutton/request.py @@ -105,8 +105,18 @@ def request(url, method, headers, data=None, timeout=None): def request_url(secure, hostname, port, path): - ''' Combines url components into a url passable into the request - function. ''' + ''' + Combines url components into a url passable into the request function. + + Args: + secure (boolean): Whether or not to use HTTPS. + hostname (str): The host name for the url. + port (int): The port number, as an integer. + path (str): The hierarchical path. + + Returns: + (str) A complete url made up of the arguments. + ''' scheme = 'https' if secure else 'http' netloc = '{0}:{1}'.format(hostname, port) From 26c64f6940503d5c37b84859f55bfc998407c5ef Mon Sep 17 00:00:00 2001 From: gckwan Date: Tue, 4 Oct 2016 16:45:30 -0400 Subject: [PATCH 9/9] one more comma --- test/resources/resource_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/resources/resource_test.py b/test/resources/resource_test.py index d427935..e6fbe23 100644 --- a/test/resources/resource_test.py +++ b/test/resources/resource_test.py @@ -15,7 +15,7 @@ 'hostname': 'api.usebutton.com', 'secure': True, 'port': 443, - 'timeout': None + 'timeout': None, }