8000 Add Accounts resource by oritheorca · Pull Request #6 · button/button-client-python · GitHub
[go: up one dir, main page]

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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
2.0.0 October 13, 2016
- Added accounts resource
- Breaking changes to pybutton.Response class
1.1.0 October 4, 2016
- Added config options: hostname, port, secure, timeout
1.0.2 August 11, 2016
Expand Down
117 changes: 116 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ The supported options are as follows:
Resources
---------

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

Orders
~~~~~~
Expand Down Expand Up @@ -163,6 +163,121 @@ Delete
print(response)
# <class pybutton.Response >

Accounts
Copy link
Contributor

Choose a reason for hiding this comment

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

A section in the readme talking about the nature of our pybutton.Response class is worthwhile at this point, I think!

~~~~~~~~

All
'''

.. code:: python

from pybutton import Client

client = Client('sk-XXX')

response = client.accounts.all()

print(response)
# <class pybutton.Response [2 elements]>

Transactions
''''''''''''

Along with the required account ID, you may also
pass the following optional arguments:

* ``cursor`` (string): An API cursor to fetch a specific set of results.
* ``start`` (ISO-8601 datetime string): Fetch transactions after this time.
* ``end`` (ISO-8601 datetime string): Fetch transactions before this time.

.. code:: python

from pybutton import Client

client = Client('sk-XXX')

response = client.accounts.transactions(
'acc-123',
start='2016-07-15T00:00:00.000Z',
end='2016-09-30T00:00:00.000Z'
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: trailing commas

)

print(response)
# <class pybutton.Response [100 elements]>

Response
--------

Methods
~~~~~~~

data
''''

.. code:: python

from pybutton import Client

client = Client('sk-XXX')

response = client.orders.get('btnorder-XXX')

print(response.data())
# {'total': 50, 'currency': 'USD', 'status': 'open' ... }

response = client.accounts.all()

print(response.data())
# [{'id': 'acc-123', ... }, {'id': 'acc-234', ... }]

next_cursor
''''''''''

For any paged resource, ``next_cursor()`` will return a cursor to
supply for the next page of results. If ``next_cursor()`` returns ``None``,
there are no more results.

.. code:: python

from pybutton import Client

client = Client('sk-XXX')

response = client.accounts.transactions('acc-123')
cursor = response.next_cursor()

# loop through and print all transactions
while cursor:
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a cool method we can add later to the Response class... all_pages or something, where it returns a generator that yields all pages.

response = client.accounts.transactions('acc-123', cursor=cursor)
print(response.data())
cursor = response.next_cursor()

prev_cursor
''''''''''

For any paged resource, ``prev_cursor()`` will return a cursor to
supply for the next page of results. If ``prev_cursor()`` returns
``None``, there are no more previous results.

.. code:: python

from pybutton import Client

client = Client('sk-XXX')

response = client.accounts.transactions('acc-123', cursor='xyz')

print(response)
# <class pybutton.Response [25 elements]>

cursor = response.prev_cursor()

response = client.accounts.transactions('acc-123', cursor=cursor)

print(response)
# <class pybutton.Response [100 elements]>


Contributing
------------

Expand Down
2 changes: 2 additions & 0 deletions pybutton/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import print_function
from __future__ import unicode_literals

from .resources import Accounts
from .resources import Orders
from .error import ButtonClientError

Expand Down Expand Up @@ -48,6 +49,7 @@ def __init__(self, api_key, config=None):
config = config_with_defaults(config)

self.orders = Orders(api_key, config)
self.accounts = Accounts(api_key, config)


def config_with_defaults(config):
Expand Down
43 changes: 40 additions & 3 deletions pybutton/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
from urllib.request import Request
from urllib.request import urlopen
from urllib.error import HTTPError
from urllib.parse import urlencode
from urllib.parse import urlunsplit
from urllib.parse import urlparse
from urllib.parse import parse_qs

def request(url, method, headers, data=None, timeout=None):
''' Make an HTTP request in Python 3.x
Expand Down Expand Up @@ -62,7 +65,10 @@ def request(url, method, headers, data=None, timeout=None):
from urllib2 import Request
from urllib2 import urlopen
from urllib2 import HTTPError
from urllib import urlencode
from urlparse import urlunsplit
from urlparse import urlparse
from urlparse import parse_qs

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


def request_url(secure, hostname, port, path):
def request_url(secure, hostname, port, path, query=None):
'''
Combines url components into a url passable into the request function.

Expand All @@ -113,13 +119,44 @@ def request_url(secure, hostname, port, path):
hostname (str): The host name for the url.
port (int): The port number, as an integer.
path (str): The hierarchical path.
query (dict): A dict of query parameters.

Returns:
(str) A complete url made up of the arguments.
'''
encoded_query = urlencode(query) if query else ''
scheme = 'https' if secure else 'http'
netloc = '{0}:{1}'.format(hostname, port)

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

__all__ = [Request, urlopen, HTTPError, request, request_url]

def query_dict(url):
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: docstring

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: docstring

'''
Given a url, returns a dictionary of its query parameters.

Args:
url (string): The url to extract query parameters from.

Returns:
(dict) A dictionary of query parameters, formatted as follows:
{
query_name: [ list of values ],
...
}

'''
url_components = urlparse(url)

if (url_components):
query_string = url_components.query
return parse_qs(query_string)

__all__ = [
Request,
urlopen,
HTTPError,
request,
request_url,
query_dict,
]
3 changes: 2 additions & 1 deletion pybutton/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
from __future__ import unicode_literals

from .orders import Orders
from .accounts import Accounts

__all__ = [Orders]
__all__ = [Orders, Accounts]
78 changes: 78 additions & 0 deletions pybutton/resources/accounts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

from .resource import Resource


class Accounts(Resource):
'''Manages interacting with Button Accounts with the Button API

Args:
api_key (string): Your organization's API key. Do find yours at
https://app.usebutton.com/settings/organization.
config (dict): Configurati 9E71 on 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

'''

def all(self):
'''Get a list of available accounts

Raises:
pybutton.ButtonClientError

Returns:
(pybutton.Response) The API response

'''

return self.api_get('/v1/affiliation/accounts')

def transactions(self, account_id, cursor=None, start=None, end=None):
Copy link
Contributor

Choose a reason for hiding this comment

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

May be worth spelling out in the docs what arguments are required and what are optional.

'''Get a list of transactions.
To paginate transactions, pass the result of response.next_cursor() as
the cursor argument.


Args:
account_id (str) optional: A Button account id ('acc-XXX')
cursor (str) optional: An opaque string that lets you view a
consistent list of transactions.
start (ISO-8601 datetime str) optional: Filter out transactions
created at or after this time.
end (ISO-8601 datetime str) optional: Filter out transactions
created before this time.

Raises:
pybutton.ButtonClientError

Returns:
(pybutton.Response) The API response

'''

query = {}

if cursor:
query['cursor'] = cursor
if start:
query['start'] = start
if end:
query['end'] = end

path = '/v1/affiliation/accounts/{0}/transactions'.format(
account_id
)

return self.api_get(path, query=query)
8 changes: 8 additions & 0 deletions pybutton/resources/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ class Orders(Resource):
Args:
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.)

config (dict): Configuration options for the client. Options include:
hostname: Defaults to api.usebutton.com.
Expand Down
24 changes: 15 additions & 9 deletions pybutton/resources/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def __init__(self, api_key, config):
self.api_key = api_key
self.config = config

def api_get(self, path):
def api_get(self, path, query=None):
'''Make an HTTP GET request

Args:
Expand All @@ -55,7 +55,7 @@ def api_get(self, path):
(pybutton.Response): The API response

'''
return self._api_request(path, 'GET')
return self._api_request(path, 'GET', query=query)

def api_post(self, path, data):
'''Make an HTTP POST request
Expand All @@ -82,7 +82,7 @@ def api_delete(self, path):
'''
return self._api_request(path, 'DELETE')

def _api_request(self, path, method, data=None):
def _api_request(self, path, method, data=None, query=None):
'''Make an HTTP request

Any data provided will be JSON encoded an included as part of the
Expand All @@ -104,14 +104,15 @@ def _api_request(self, path, method, data=None):
self.config['secure'],
self.config['hostname'],
self.config['port'],
path
path,
query,
)
api_key_bytes = '{0}:'.format(self.api_key).encode()
authorization = b64encode(api_key_bytes).decode()

headers = {
'Authorization': 'Basic {0}'.format(authorization),
'User-Agent': USER_AGENT
'User-Agent': USER_AGENT,
}

try:
Expand All @@ -120,10 +121,15 @@ def _api_request(self, path, method, data=None):
method,
headers,
data,
self.config['timeout']
).get('object', {})

return Response(resp)
self.config['timeout'],
)

return Response(
resp.get('meta', {}),
# Response info may have 'object' or 'objects' key, depending
# on whether there are 1 or multiple results.
resp.get('object', resp.get('objects'))
)
except HTTPError as e:
response = e.read()
fallback = '{0} {1}'.format(e.code, e.msg)
Expand Down
Loading
0