Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
1.1.0 October 4, 2016
- Added config options: hostname, port, secure, timeout
1.0.2 August 11, 2016
- Initial Release
23 changes: 23 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,29 @@ 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

from pybutton import Client

client = Client("sk-XXX", {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you include from pybutton import Client? I'm trying to keep all code snippets atomically copy+pasteable

'hostname': 'api.testsite.com',
'port': 80,
'secure': False,
'timeout': 5, # seconds
})

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
---------

Expand Down
30 changes: 28 additions & 2 deletions pybutton/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ 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:
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:
orders (pybutton.Resource): Resource for managing Button Orders.

Expand All @@ -25,12 +34,29 @@ class Client(object):

'''

def __init__(self, api_key):
def __init__(self, api_key, config=None):

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

self.orders = Orders(api_key)
if config is None:
config = {}

config = config_with_defaults(config)

self.orders = Orders(api_key, config)


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),
}
33 changes: 26 additions & 7 deletions pybutton/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
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):
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
Expand Down Expand Up @@ -50,21 +51,20 @@ 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)
except ValueError:
raise ButtonClientError('Invalid response: {0}'.format(response))

__all__ = [Request, urlopen, HTTPError, request]

else:
from urllib2 import Request
from urllib2 import urlopen
from urllib2 import HTTPError
from urlparse import urlunsplit

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
Expand Down Expand Up @@ -96,11 +96,30 @@ 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)
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.

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)

return urlunsplit((scheme, netloc, path, '', ''))

__all__ = [Request, urlopen, HTTPError, request, request_url]
9 changes: 9 additions & 0 deletions pybutton/resources/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ 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

Expand Down
31 changes: 26 additions & 5 deletions pybutton/resources/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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())
Expand All @@ -26,15 +27,23 @@ 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

'''

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even though you're going to end up repeating it a lot, I think a docstring here explaining config makes sense (especially given all the keys are required at this point)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally. Added config to the docstring above.


def api_get(self, path):
'''Make an HTTP GET request
Expand Down Expand Up @@ -91,7 +100,12 @@ def _api_request(self, path, method, data=None):

'''

url = '{0}{1}'.format(self.API_BASE, 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()

Expand All @@ -101,7 +115,14 @@ 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()
Expand Down
2 changes: 1 addition & 1 deletion pybutton/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION = '1.0.2'
VERSION = '1.1.0'
38 changes: 38 additions & 0 deletions test/client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from unittest import TestCase

from pybutton.client import Client
from pybutton.client import config_with_defaults
from pybutton import ButtonClientError


Expand Down Expand Up @@ -33,3 +34,40 @@ 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):
# Defaults
config = config_with_defaults({})

self.assertEqual(config, {
'hostname': 'api.usebutton.com',
'port': 443,
'secure': True,
'timeout': None,
})

# Port and timeout overrides
config = 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 = config_with_defaults({
'hostname': 'localhost',
'secure': False,
})

self.assertEqual(config, {
'hostname': 'localhost',
'port': 80,
'secure': False,
'timeout': None,
})
17 changes: 17 additions & 0 deletions test/request_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from mock import patch

from pybutton.request import request
from pybutton.request import request_url
from pybutton import ButtonClientError


Expand Down Expand Up @@ -197,3 +198,19 @@ 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')
17 changes: 12 additions & 5 deletions test/resources/orders_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,23 @@

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()
Expand All @@ -31,7 +38,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}

Expand All @@ -45,7 +52,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}

Expand All @@ -62,7 +69,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()
Expand Down
Loading