Skip to content

Commit 82a7485

Browse files
authored
Add Accounts resource (#6)
* Added accounts resource * Breaking changes to pybutton.Response class
1 parent f86c72a commit 82a7485

File tree

15 files changed

+449
-50
lines changed

15 files changed

+449
-50
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
2.0.0 October 13, 2016
2+
- Added accounts resource
3+
- Breaking changes to pybutton.Response class
14
1.1.0 October 4, 2016
25
- Added config options: hostname, port, secure, timeout
36
1.0.2 August 11, 2016

README.rst

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ The supported options are as follows:
9191
Resources
9292
---------
9393

94-
We currently expose only one resource to manage, ``Orders``.
94+
We currently expose two resources to manage, ``Orders`` and ``Accounts``.
9595

9696
Orders
9797
~~~~~~
@@ -163,6 +163,121 @@ Delete
163163
print(response)
164164
# <class pybutton.Response >
165165
166+
Accounts
167+
~~~~~~~~
168+
169+
All
170+
'''
171+
172+
.. code:: python
173+
174+
from pybutton import Client
175+
176+
client = Client('sk-XXX')
177+
178+
response = client.accounts.all()
179+
180+
print(response)
181+
# <class pybutton.Response [2 elements]>
182+
183+
Transactions
184+
''''''''''''
185+
186+
Along with the required account ID, you may also
187+
pass the following optional arguments:
188+
189+
* ``cursor`` (string): An API cursor to fetch a specific set of results.
190+
* ``start`` (ISO-8601 datetime string): Fetch transactions after this time.
191+
* ``end`` (ISO-8601 datetime string): Fetch transactions before this time.
192+
193+
.. code:: python
194+
195+
from pybutton import Client
196+
197+
client = Client('sk-XXX')
198+
199+
response = client.accounts.transactions(
200+
'acc-123',
201+
start='2016-07-15T00:00:00.000Z',
202+
end='2016-09-30T00:00:00.000Z'
203+
)
204+
205+
print(response)
206+
# <class pybutton.Response [100 elements]>
207+
208+
Response
209+
--------
210+
211+
Methods
212+
~~~~~~~
213+
214+
data
215+
''''
216+
217+
.. code:: python
218+
219+
from pybutton import Client
220+
221+
client = Client('sk-XXX')
222+
223+
response = client.orders.get('btnorder-XXX')
224+
225+
print(response.data())
226+
# {'total': 50, 'currency': 'USD', 'status': 'open' ... }
227+
228+
response = client.accounts.all()
229+
230+
print(response.data())
231+
# [{'id': 'acc-123', ... }, {'id': 'acc-234', ... }]
232+
233+
next_cursor
234+
''''''''''
235+
236+
For any paged resource, ``next_cursor()`` will return a cursor to
237+
supply for the next page of results. If ``next_cursor()`` returns ``None``,
238+
there are no more results.
239+
240+
.. code:: python
241+
242+
from pybutton import Client
243+
244+
client = Client('sk-XXX')
245+
246+
response = client.accounts.transactions('acc-123')
247+
cursor = response.next_cursor()
248+
249+
# loop through and print all transactions
250+
while cursor:
251+
response = client.accounts.transactions('acc-123', cursor=cursor)
252+
print(response.data())
253+
cursor = response.next_cursor()
254+
255+
prev_cursor
256+
''''''''''
257+
258+
For any paged resource, ``prev_cursor()`` will return a cursor to
259+
supply for the next page of results. If ``prev_cursor()`` returns
260+
``None``, there are no more previous results.
261+
262+
.. code:: python
263+
264+
from pybutton import Client
265+
266+
client = Client('sk-XXX')
267+
268+
response = client.accounts.transactions('acc-123', cursor='xyz')
269+
270+
print(response)
271+
# <class pybutton.Response [25 elements]>
272+
273+
cursor = response.prev_cursor()
274+
275+
response = client.accounts.transactions('acc-123', cursor=cursor)
276+
277+
print(response)
278+
# <class pybutton.Response [100 elements]>
279+
280+
166281
Contributing
167282
------------
168283

pybutton/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import print_function
44
from __future__ import unicode_literals
55

6+
from .resources import Accounts
67
from .resources import Orders
78
from .error import ButtonClientError
89

@@ -48,6 +49,7 @@ def __init__(self, api_key, config=None):
4849
config = config_with_defaults(config)
4950

5051
self.orders = Orders(api_key, config)
52+
self.accounts = Accounts(api_key, config)
5153

5254

5355
def config_with_defaults(config):

pybutton/request.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
from urllib.request import Request
2121
from urllib.request import urlopen
2222
from urllib.error import HTTPError
23+
from urllib.parse import urlencode
2324
from urllib.parse import urlunsplit
25+
from urllib.parse import urlparse
26+
from urllib.parse import parse_qs
2427

2528
def request(url, method, headers, data=None, timeout=None):
2629
''' Make an HTTP request in Python 3.x
@@ -62,7 +65,10 @@ def request(url, method, headers, data=None, timeout=None):
6265
from urllib2 import Request
6366
from urllib2 import urlopen
6467
from urllib2 import HTTPError
68+
from urllib import urlencode
6569
from urlparse import urlunsplit
70+
from urlparse import urlparse
71+
from urlparse import parse_qs
6672

6773
def request(url, method, headers, data=None, timeout=None):
6874
''' Make an HTTP request in Python 2.x
@@ -104,7 +110,7 @@ def request(url, method, headers, data=None, timeout=None):
104110
raise ButtonClientError('Invalid response: {0}'.format(response))
105111

106112

107-
def request_url(secure, hostname, port, path):
113+
def request_url(secure, hostname, port, path, query=None):
108114
'''
109115
Combines url components into a url passable into the request function.
110116
@@ -113,13 +119,44 @@ def request_url(secure, hostname, port, path):
113119
hostname (str): The host name for the url.
114120
port (int): The port number, as an integer.
115121
path (str): The hierarchical path.
122+
query (dict): A dict of query parameters.
116123
117124
Returns:
118125
(str) A complete url made up of the arguments.
119126
'''
127+
encoded_query = urlencode(query) if query else ''
120128
scheme = 'https' if secure else 'http'
121129
netloc = '{0}:{1}'.format(hostname, port)
122130

123-
return urlunsplit((scheme, netloc, path, '', ''))
131+
return urlunsplit((scheme, netloc, path, encoded_query, ''))
124132

125-
__all__ = [Request, urlopen, HTTPError, request, request_url]
133+
134+
def query_dict(url):
135+
'''
136+
Given a url, returns a dictionary of its query parameters.
137+
138+
Args:
139+
url (string): The url to extract query parameters from.
140+
141+
Returns:
142+
(dict) A dictionary of query parameters, formatted as follows:
143+
{
144+
query_name: [ list of values ],
145+
...
146+
}
147+
148+
'''
149+
url_components = urlparse(url)
150+
151+
if (url_components):
152+
query_string = url_components.query
153+
return parse_qs(query_string)
154+
155+
__all__ = [
156+
Request,
157+
urlopen,
158+
HTTPError,
159+
request,
160+
request_url,
161+
query_dict,
162+
]

pybutton/resources/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
from __future__ import unicode_literals
55

66
from .orders import Orders
7+
from .accounts import Accounts
78

8-
__all__ = [Orders]
9+
__all__ = [Orders, Accounts]

pybutton/resources/accounts.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from __future__ import absolute_import
2+
from __future__ import division
3+
from __future__ import print_function
4+
from __future__ import unicode_literals
5+
6+
from .resource import Resource
7+
8+
9+
class Accounts(Resource):
10+
'''Manages interacting with Button Accounts with the Button API
11+
12+
Args:
13+
api_key (string): Your organization's API key. Do find yours at
14+
https://app.usebutton.com/settings/organization.
15+
config (dict): Configuration options for the client. Options include:
16+
hostname: Defaults to api.usebutton.com.
17+
port: Defaults to 443 if config.secure, else defaults to 80.
18+
secure: Whether or not to use HTTPS. Defaults to True.
19+
timeout: The time in seconds for network requests to abort.
20+
Defaults to None.
21+
(N.B: Button's API is only exposed through HTTPS. This option is
22+
provided purely as a convenience for testing and development.)
23+
24+
Raises:
25+
pybutton.ButtonClientError
26+
27+
'''
28+
29+
def all(self):
30+
'''Get a list of available accounts
31+
32+
Raises:
33+
pybutton.ButtonClientError
34+
35+
Returns:
36+
(pybutton.Response) The API response
37+
38+
'''
39+
40+
return self.api_get('/v1/affiliation/accounts')
41+
42+
def transactions(self, account_id, cursor=None, start=None, end=None):
43+
'''Get a list of transactions.
44+
To paginate transactions, pass the result of response.next_cursor() as
45+
the cursor argument.
46+
47+
48+
Args:
49+
account_id (str) optional: A Button account id ('acc-XXX')
50+
cursor (str) optional: An opaque string that lets you view a
51+
consistent list of transactions.
52+
start (ISO-8601 datetime str) optional: Filter out transactions
53+
created at or after this time.
54+
end (ISO-8601 datetime str) optional: Filter out transactions
55+
created before this time.
56+
57+
Raises:
58+
pybutton.ButtonClientError
59+
60+
Returns:
61+
(pybutton.Response) The API response
62+
63+
'''
64+
65+
query = {}
66+
67+
if cursor:
68+
query['cursor'] = cursor
69+
if start:
70+
query['start'] = start
71+
if end:
72+
query['end'] = end
73+
74+
path = '/v1/affiliation/accounts/{0}/transactions'.format(
75+
account_id
76+
)
77+
78+
return self.api_get(path, query=query)

pybutton/resources/orders.py

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

pybutton/resources/resource.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def __init__(self, api_key, config):
4545
self.api_key = api_key
4646
self.config = config
4747

48-
def api_get(self, path):
48+
def api_get(self, path, query=None):
4949
'''Make an HTTP GET request
5050
5151
Args:
@@ -55,7 +55,7 @@ def api_get(self, path):
5555
(pybutton.Response): The API response
5656
5757
'''
58-
return self._api_request(path, 'GET')
58+
return self._api_request(path, 'GET', query=query)
5959

6060
def api_post(self, path, data):
6161
'''Make an HTTP POST request
@@ -82,7 +82,7 @@ def api_delete(self, path):
8282
'''
8383
return self._api_request(path, 'DELETE')
8484

85-
def _api_request(self, path, method, data=None):
85+
def _api_request(self, path, method, data=None, query=None):
8686
'''Make an HTTP request
8787
8888
Any data provided will be JSON encoded an included as part of the
@@ -104,14 +104,15 @@ def _api_request(self, path, method, data=None):
104104
self.config['secure'],
105105
self.config['hostname'],
106106
self.config['port'],
107-
path
107+
path,
108+
query,
108109
)
109110
api_key_bytes = '{0}:'.format(self.api_key).encode()
110111
authorization = b64encode(api_key_bytes).decode()
111112

112113
headers = {
113114
'Authorization': 'Basic {0}'.format(authorization),
114-
'User-Agent': USER_AGENT
115+
'User-Agent': USER_AGENT,
115116
}
116117

117118
try:
@@ -120,10 +121,15 @@ def _api_request(self, path, method, data=None):
120121
method,
121122
headers,
122123
data,
123-
self.config['timeout']
124-
).get('object', {})
125-
126-
return Response(resp)
124+
self.config['timeout'],
125+
)
126+
127+
return Response(
128+
resp.get('meta', {}),
129+
# Response info may have 'object' or 'objects' key, depending
130+
# on whether there are 1 or multiple results.
131+
resp.get('object', resp.get('objects'))
132+
)
127133
except HTTPError as e:
128134
response = e.read()
129135
fallback = '{0} {1}'.format(e.code, e.msg)

0 commit comments

Comments
 (0)