From 4b38c33013fc667bde4205c34518ed089534de3e Mon Sep 17 00:00:00 2001 From: vkhougaz Date: Sat, 4 Jan 2014 22:17:57 -0800 Subject: [PATCH 01/12] Initial code and tests for Button and API support --- coinbase/__init__.py | 128 ++++++++++++++++++++++++++++++++- coinbase/config.py | 6 +- coinbase/models/__init__.py | 4 +- coinbase/models/button.py | 46 ++++++++++++ coinbase/models/order.py | 38 ++++++++++ coinbase/models/transaction.py | 18 ++--- coinbase/tests.py | 78 ++++++++++++++++++-- 7 files changed, 302 insertions(+), 16 deletions(-) create mode 100644 coinbase/models/button.py create mode 100644 coinbase/models/order.py diff --git a/coinbase/__init__.py b/coinbase/__init__.py index 8530ba5..0788eee 100644 --- a/coinbase/__init__.py +++ b/coinbase/__init__.py @@ -44,8 +44,8 @@ #TODO: Switch to decimals from floats #from decimal import Decimal -from coinbase.config import COINBASE_ENDPOINT -from coinbase.models import CoinbaseAmount, CoinbaseTransaction, CoinbaseUser, CoinbaseTransfer, CoinbaseError +from .config import COINBASE_ENDPOINT, COINBASE_ITEMS_PER_PAGE +from .models import CoinbaseAmount, CoinbaseTransaction, CoinbaseUser, CoinbaseTransfer, CoinbaseError, CoinbaseButton, CoinbaseOrder class CoinbaseAccount(object): @@ -464,5 +464,129 @@ def generate_receive_address(self, callback_url=None): response = self.session.post(url=url, data=json.dumps(request_data), params=self.global_request_params) return response.json()['address'] + def create_button(self, name, + price, + currency='BTC', + type=None, + style=None, + text=None, + description=None, + custom=None, + callback_url=None, + success_url=None, + cancel_url=None, + info_url=None, + variable_price=None, + choose_price=None, + include_address=None, + include_email=None, + price1=None, price2=None, price3=None, price4=None, price5=None): + """ + Create a new button + :param name: The name of the item for which you are collecting bitcoin. + :param price: The price of the item + :param currency: The currency to charge + :param type: One of buy_now, donation, and subscription. Default is buy_now. + :param style: One of buy_now_large, buy_now_small, donation_large, donation_small, subscription_large, subscription_small, custom_large, custom_small, and none. Default is buy_now_large. none is used if you plan on triggering the payment modal yourself using your own button or link. + :param text: Allows you to customize the button text on custom_large and custom_small styles. Default is Pay With Bitcoin. + :param description: Longer description of the item in case you want it added to the user's transaction notes. + :param custom: An optional custom parameter. Usually an Order, User, or Product ID corresponding to a record in your database. + :param callback_url: A custom callback URL specific to this button. + :param success_url: A custom success URL specific to this button. The user will be redirected to this URL after a successful payment. + :param cancel_url: A custom cancel URL specific to this button. The user will be redirected to this URL after a canceled order. + :param info_url: A custom info URL specific to this button. Displayed to the user after a successful purchase for sharing. + :param variable_price: Allow users to change the price on the generated button. + :param choose_price: Show some suggested prices + :param include_address: Collect shipping address from customer (not for use with inline iframes). + :param include_email: Collect email address from customer (not for use with inline iframes). + :param price1: Suggested price 1 + :param price2: Suggested price 2 + :param price3: Suggested price 3 + :param price4: Suggested price 4 + :param price5: Suggested price 5 + :return: A CoinbaseButton object + """ + url = COINBASE_ENDPOINT + '/buttons' + price = str(price) + request_data = { + "button": { + "name": name, + "price": price, + "currency": currency, + "type": type, + "style": style, + "text": text, + "description": description, + "custom": custom, + "callback_url": callback_url, + "success_url": success_url, + "cancel_url": cancel_url, + "info_url": info_url, + "variable_price": variable_price, + "choose_price": choose_price, + "include_address": include_address, + "include_email": include_email, + "price1": price1, + "price2": price2, + "price3": price3, + "price4": price4, + "price5": price5 + } + } + none_keys = [key for key in request_data['button'].keys() if request_data['button'][key] is None] + for key in none_keys: + del request_data['button'][key] + + response = self.session.post(url=url, data=json.dumps(request_data), params=self.global_request_params) + return CoinbaseButton(response.json()['button']) + + def create_order(self, code): + """ + Generate a new order from a button + :param code: The code of the button for which you wish to create an order + :return: A CoinbaseOrder object + """ + url = COINBASE_ENDPOINT + '/buttons/{code}/create_order'.format(code=code) + + response = self.session.post(url=url, params=self.global_request_params) + return CoinbaseOrder(response.json()['order']) + + def get_order(self, order_id): + """ + Get an order by id + :param order_id: The order id to be retrieved + :return: A CoinbaseOrder object + """ + url = COINBASE_ENDPOINT + '/orders/{id}'.format(id=order_id) + + response = self.session.get(url=url, params=self.global_request_params) + return CoinbaseOrder(response.json()['order']) + + def orders(self, count=30): + """ + Retrieve the list of orders for the current account + :param count: How many orders to retrieve + :return: List of CoinbaseOrder objects + """ + url = COINBASE_ENDPOINT + '/orders' + pages = count / 30 + 1 + orders = [] + + reached_final_page = False + + for page in xrange(1, pages + 1): + + if not reached_final_page: + params = {'page': page} + params.update(self.global_request_params) + response = self.session.get(url=url, params=params) + parsed_orders = response.json() + + if parsed_orders['num_pages'] == page: + reached_final_page = True + + for order in parsed_orders['orders']: + orders.append(CoinbaseOrder(order['order'])) + return orders \ No newline at end of file diff --git a/coinbase/config.py b/coinbase/config.py index b4af013..4dccee3 100644 --- a/coinbase/config.py +++ b/coinbase/config.py @@ -7,4 +7,8 @@ TEMP_CREDENTIALS = ''' -{"_module": "oauth2client.client", "token_expiry": "2013-03-24T02:37:50Z", "access_token": "2a02d1fc82b1c42d4ea94d6866b5a232b53a3a50ad4ee899ead9afa6144c2ca3", "token_uri": "https://www.coinbase.com/oauth/token", "invalid": false, "token_response": {"access_token": "2a02d1fc82b1c42d4ea94d6866b5a232b53a3a50ad4ee899ead9afa6144c2ca3", "token_type": "bearer", "expires_in": 7200, "refresh_token": "ffec0153da773468c8cb418d07ced54c13ca8deceae813c9be0b90d25e7c3d71", "scope": "all"}, "client_id": "2df06cb383f4ffffac20e257244708c78a1150d128f37d420f11fdc069a914fc", "id_token": null, "client_secret": "7caedd79052d7e29aa0f2700980247e499ce85381e70e4a44de0c08f25bded8a", "revoke_uri": "https://accounts.google.com/o/oauth2/revoke", "_class": "OAuth2Credentials", "refresh_token": "ffec0153da773468c8cb418d07ced54c13ca8deceae813c9be0b90d25e7c3d71", "user_agent": null}''' \ No newline at end of file +{"_module": "oauth2client.client", "token_expiry": "2013-03-24T02:37:50Z", "access_token": "2a02d1fc82b1c42d4ea94d6866b5a232b53a3a50ad4ee899ead9afa6144c2ca3", "token_uri": "https://www.coinbase.com/oauth/token", "invalid": false, "token_response": {"access_token": "2a02d1fc82b1c42d4ea94d6866b5a232b53a3a50ad4ee899ead9afa6144c2ca3", "token_type": "bearer", "expires_in": 7200, "refresh_token": "ffec0153da773468c8cb418d07ced54c13ca8deceae813c9be0b90d25e7c3d71", "scope": "all"}, "client_id": "2df06cb383f4ffffac20e257244708c78a1150d128f37d420f11fdc069a914fc", "id_token": null, "client_secret": "7caedd79052d7e29aa0f2700980247e499ce85381e70e4a44de0c08f25bded8a", "revoke_uri": "https://accounts.google.com/o/oauth2/revoke", "_class": "OAuth2Credentials", "refresh_token": "ffec0153da773468c8cb418d07ced54c13ca8deceae813c9be0b90d25e7c3d71", "user_agent": null}''' + +CENTS_PER_BITCOIN = 100000000 +CENTS_PER_OTHER = 100 +COINBASE_ITEMS_PER_PAGE = 30 \ No newline at end of file diff --git a/coinbase/models/__init__.py b/coinbase/models/__init__.py index 1c5a3ca..7689ea4 100644 --- a/coinbase/models/__init__.py +++ b/coinbase/models/__init__.py @@ -5,4 +5,6 @@ from transfer import CoinbaseTransfer from contact import CoinbaseContact from user import CoinbaseUser -from error import CoinbaseError \ No newline at end of file +from error import CoinbaseError +from button import CoinbaseButton +from order import CoinbaseOrder \ No newline at end of file diff --git a/coinbase/models/button.py b/coinbase/models/button.py new file mode 100644 index 0000000..361a0ad --- /dev/null +++ b/coinbase/models/button.py @@ -0,0 +1,46 @@ +__author__ = 'vkhougaz' + +from amount import CoinbaseAmount +from decimal import Decimal +from coinbase.config import CENTS_PER_BITCOIN, CENTS_PER_OTHER + +class CoinbaseButton(object): + + def __init__(self, button): + # Sometimes it's called code (create button) and sometimes id (sub item of create button) + # so we map them together + if 'id' in button: + self.button_id = button['id'] + if 'code' in button: + self.button_id = button['code'] + self.code = self.button_id + + self.name = button['name'] + if 'price' in button: + price = Decimal(button['price']['cents']) + price_currency_iso = button['price']['currency_iso'] + if price_currency_iso == 'BTC': + price /= CENTS_PER_BITCOIN + else: + price /= CENTS_PER_OTHER + self.price = CoinbaseAmount(price, price_currency_iso) + else: + self.price = None + self.type = button.get('type', None) + self.style = button.get('style', None) + self.text = button.get('text', None) + self.description = button.get('description', None) + self.custom = button.get('custom', None) + self.callback_url = button.get('callback_url', None) + self.success_url = button.get('success_url', None) + self.cancel_url = button.get('cancel_url', None) + self.info_url = button.get('info_url', None) + self.variable_price = button.get('variable_price', None) + self.choose_price = button.get('choose_price', None) + self.include_address = button.get('include_address', None) + self.include_email = button.get('include_email', None) + self.price1 = button.get('price1', None) + self.price2 = button.get('price2', None) + self.price3 = button.get('price3', None) + self.price4 = button.get('price4', None) + self.price5 = button.get('price4', None) diff --git a/coinbase/models/order.py b/coinbase/models/order.py new file mode 100644 index 0000000..4c5a1f4 --- /dev/null +++ b/coinbase/models/order.py @@ -0,0 +1,38 @@ +__author__ = 'vkhougaz' + +from amount import CoinbaseAmount +from button import CoinbaseButton +from transaction import CoinbaseTransaction +from decimal import Decimal +from coinbase.config import CENTS_PER_BITCOIN, CENTS_PER_OTHER +# from datetime import datetime + +class CoinbaseOrder(object): + def __init__(self, order): + self.id = order['id'] + # TODO: Account for timezone properly + #self.created_at = datetime.strptime(order['created_at'], '%Y-%m-%dT%H:%M:%S-08:00') + self.created_at = order['created_at'] + self.status = order['status'] + + btc_cents = Decimal(order['total_btc']['cents']) + btc_currency_iso = order['total_btc']['currency_iso'] + if btc_currency_iso == 'BTC': + btc_cents /= CENTS_PER_BITCOIN + else: + btc_cents /= CENTS_PER_OTHER + self.total_btc = CoinbaseAmount(btc_cents, btc_currency_iso) + + native_cents = order['total_native']['cents'] + native_currency_iso = order['total_native']['currency_iso'] + if native_currency_iso == 'BTC': + native_cents /= CENTS_PER_BITCOIN + else: + native_cents /= CENTS_PER_OTHER + self.total_native = CoinbaseAmount(native_cents, native_currency_iso) + + self.button = CoinbaseButton(order['button']) + if 'transaction' in order and order['transaction'] is not None: + self.transaction = CoinbaseTransaction(order['transaction']) + else: + self.transaction = None diff --git a/coinbase/models/transaction.py b/coinbase/models/transaction.py index c5db34a..50d356d 100644 --- a/coinbase/models/transaction.py +++ b/coinbase/models/transaction.py @@ -8,16 +8,18 @@ class CoinbaseTransaction(object): def __init__(self, transaction): self.transaction_id = transaction['id'] - self.created_at = transaction['created_at'] - self.notes = transaction['notes'] + self.created_at = transaction.get('created_at', None) + self.notes = transaction.get('notes', '') - transaction_amount = transaction['amount']['amount'] - transaction_currency = transaction['amount']['currency'] - - self.amount = CoinbaseAmount(transaction_amount, transaction_currency) + if 'amount' in transaction: + transaction_amount = transaction['amount']['amount'] + transaction_currency = transaction['amount']['currency'] + self.amount = CoinbaseAmount(transaction_amount, transaction_currency) + else: + self.amount = None - self.status = transaction['status'] - self.request = transaction['request'] + self.status = transaction.get('status', None) + self.request = transaction.get('request', None) #Sender Information diff --git a/coinbase/tests.py b/coinbase/tests.py index ad22826..7353f4c 100644 --- a/coinbase/tests.py +++ b/coinbase/tests.py @@ -142,8 +142,7 @@ def test_send_bitcoin(self): this(new_transaction_with_btc_address.recipient_address).should.equal('15yHmnB5vY68sXpAU9pR71rnyPAGLLWeRP') HTTPretty.register_uri(HTTPretty.POST, "https://coinbase.com/api/v1/transactions/send_money", - body='''{"success":true,"transaction":{"id":"5158b2920b974ea4cb000003","created_at":"2013-03-31T15:02:58-07:00","hsh":null,"notes":"","amount":{"amount":"-0.10000000","currency":"BTC"},"request":false,"status":"pending","sender":{"id":"509e01ca12838e0200000212","email":"gsibble@gmail.com","name":"gsibble@gmail.com"},"recipient":{"id":"4efec8d7bedd320001000003","email":"brian@coinbase.com","name":"Brian Armstrong"},"recipient_address":"brian@coinbase.com"}} -''', + body='''{"success":true,"transaction":{"id":"5158b2920b974ea4cb000003","created_at":"2013-03-31T15:02:58-07:00","hsh":null,"notes":"","amount":{"amount":"-0.10000000","currency":"BTC"},"request":false,"status":"pending","sender":{"id":"509e01ca12838e0200000212","email":"gsibble@gmail.com","name":"gsibble@gmail.com"},"recipient":{"id":"4efec8d7bedd320001000003","email":"brian@coinbase.com","name":"Brian Armstrong"},"recipient_address":"brian@coinbase.com"}}''', content_type='text/json') new_transaction_with_email = self.account.send('brian@coinbase.com', amount=0.1) @@ -155,7 +154,7 @@ def test_transaction_list(self): HTTPretty.register_uri(HTTPretty.GET, "https://coinbase.com/api/v1/transactions", body='''{"current_user":{"id":"509e01ca12838e0200000212","email":"gsibble@gmail.com","name":"gsibble@gmail.com"},"balance":{"amount":"0.00000000","currency":"BTC"},"total_count":4,"num_pages":1,"current_page":1,"transactions":[{"transaction":{"id":"514e4c37802e1bf69100000e","created_at":"2013-03-23T17:43:35-07:00","hsh":null,"notes":"Testing","amount":{"amount":"1.00000000","currency":"BTC"},"request":true,"status":"pending","sender":{"id":"514e4c1c802e1bef9800001e","email":"george@atlasr.com","name":"george@atlasr.com"},"recipient":{"id":"509e01ca12838e0200000212","email":"gsibble@gmail.com","name":"gsibble@gmail.com"}}},{"transaction":{"id":"514e4c1c802e1bef98000020","created_at":"2013-03-23T17:43:08-07:00","hsh":null,"notes":"Testing","amount":{"amount":"1.00000000","currency":"BTC"},"request":true,"status":"pending","sender":{"id":"514e4c1c802e1bef9800001e","email":"george@atlasr.com","name":"george@atlasr.com"},"recipient":{"id":"509e01ca12838e0200000212","email":"gsibble@gmail.com","name":"gsibble@gmail.com"}}},{"transaction":{"id":"514b9fb1b8377ee36500000d","created_at":"2013-03-21T17:02:57-07:00","hsh":"42dd65a18dbea0779f32021663e60b1fab8ee0f859db7172a078d4528e01c6c8","notes":"You gave me this a while ago. It's turning into a fair amount of cash and thought you might want it back :) Building something on your API this weekend. Take care!","amount":{"amount":"-1.00000000","currency":"BTC"},"request":false,"status":"complete","sender":{"id":"509e01ca12838e0200000212","email":"gsibble@gmail.com","name":"gsibble@gmail.com"},"recipient":{"id":"4efec8d7bedd320001000003","email":"brian@coinbase.com","name":"Brian Armstrong"},"recipient_address":"brian@coinbase.com"}},{"transaction":{"id":"509e01cb12838e0200000224","created_at":"2012-11-09T23:27:07-08:00","hsh":"ac9b0ffbe36dbe12c5ca047a5bdf9cadca3c9b89b74751dff83b3ac863ccc0b3","notes":"","amount":{"amount":"1.00000000","currency":"BTC"},"request":false,"status":"complete","sender":{"id":"4efec8d7bedd320001000003","email":"brian@coinbase.com","name":"Brian Armstrong"},"recipient":{"id":"509e01ca12838e0200000212","email":"gsibble@gmail.com","name":"gsibble@gmail.com"},"recipient_address":"gsibble@gmail.com"}}]}''', - content_type='text/json') + content_type='text/json') transaction_list = self.account.transactions() @@ -183,4 +182,75 @@ def test_getting_user_details(self): user = self.account.get_user_details() this(user.id).should.equal("509f01da12837e0201100212") - this(user.balance).should.equal(1225.86084181) \ No newline at end of file + this(user.balance).should.equal(1225.86084181) + + # The following tests use the example request/responses from the coinbase API docs: + # test_creating_button, test_creating_order, test_getting_order + @httprettified + def test_creating_button(self): + + HTTPretty.register_uri(HTTPretty.POST, "https://coinbase.com/api/v1/buttons", + body='''{"success":true,"button":{"code":"93865b9cae83706ae59220c013bc0afd","type":"buy_now","style":"custom_large","text":"Pay With Bitcoin","name":"test","description":"Sample description","custom":"Order123","callback_url":"http://www.example.com/my_custom_button_callback","price":{"cents":123,"currency_iso":"USD"}}}''', + content_type='text/json') + + button = self.account.create_button( + name="test", + type="buy_now", + price=1.23, + currency="USD", + custom="Order123", + callback_url="http://www.example.com/my_custom_button_callback", + description="Sample description", + style="custom_large", + include_email=True + ) + this(button.code).should.equal("93865b9cae83706ae59220c013bc0afd") + this(button.name).should.equal("test") + this(button.price).should.equal(1.23) + this(button.price.currency).should.equal("USD") + this(button.include_address).should.equal(None) + + @httprettified + def test_creating_order(self): + + HTTPretty.register_uri(HTTPretty.POST, "https://coinbase.com/api/v1/buttons/93865b9cae83706ae59220c013bc0afd/create_order", + body='''{"success":true,"order":{"id":"7RTTRDVP","created_at":"2013-11-09T22:47:10-08:00","status":"new","total_btc":{"cents":100000000,"currency_iso":"BTC"},"total_native":{"cents":100000000,"currency_iso":"BTC"},"custom":"Order123","receive_address":"mgrmKftH5CeuFBU3THLWuTNKaZoCGJU5jQ","button":{"type":"buy_now","name":"test","description":"Sample description","id":"93865b9cae83706ae59220c013bc0afd"},"transaction":null}}''', + content_type='text/json') + + order = self.account.create_order("93865b9cae83706ae59220c013bc0afd") + + this(order.id).should.equal("7RTTRDVP") + this(order.total_btc).should.equal(1) + this(order.total_btc.currency).should.equal("BTC") + this(order.button.button_id).should.equal("93865b9cae83706ae59220c013bc0afd") + this(order.transaction).should.equal(None) + + @httprettified + def test_order_list(self): + + HTTPretty.register_uri(HTTPretty.GET, "https://coinbase.com/api/v1/orders", + body='''{"orders":[{"order":{"id":"A7C52JQT","created_at":"2013-03-11T22:04:37-07:00","status":"completed","total_btc":{"cents":100000000,"currency_iso":"BTC"},"total_native":{"cents":3000,"currency_iso":"USD"},"custom":"","button":{"type":"buy_now","name":"Order #1234","description":"order description","id":"eec6d08e9e215195a471eae432a49fc7"},"transaction":{"id":"513eb768f12a9cf27400000b","hash":"4cc5eec20cd692f3cdb7fc264a0e1d78b9a7e3d7b862dec1e39cf7e37ababc14","confirmations":0}}}],"total_count":1,"num_pages":1,"current_page":1}''', + content_type='text/json') + + orders = self.account.orders() + this(orders).should.be.an(list) + this(orders[0].id).should.equal("A7C52JQT") + + @httprettified + def test_getting_order(self): + + HTTPretty.register_uri(HTTPretty.GET, "https://coinbase.com/api/v1/orders/A7C52JQT", + body='''{"order":{"id":"A7C52JQT","created_at":"2013-03-11T22:04:37-07:00","status":"completed","total_btc":{"cents":10000000,"currency_iso":"BTC"},"total_native":{"cents":10000000,"currency_iso":"BTC"},"custom":"","button":{"type":"buy_now","name":"test","description":"","id":"eec6d08e9e215195a471eae432a49fc7"},"transaction":{"id":"513eb768f12a9cf27400000b","hash":"4cc5eec20cd692f3cdb7fc264a0e1d78b9a7e3d7b862dec1e39cf7e37ababc14","confirmations":0}}}''', + content_type='text/json') + + order = self.account.get_order("A7C52JQT") + + this(order.id).should.equal("A7C52JQT") + this(order.status).should.equal("completed") + this(order.total_btc).should.equal(.1) + this(order.total_native.currency).should.equal("BTC") + this(order.button.name).should.equal("test") + this(order.transaction.transaction_id).should.equal("513eb768f12a9cf27400000b") + +if __name__ == '__main__': + unittest.main() From b4de975b0e96eceaec443fbe7b1891f355069212 Mon Sep 17 00:00:00 2001 From: vkhougaz Date: Sun, 5 Jan 2014 02:17:52 -0800 Subject: [PATCH 02/12] Fixed missing custom parameter in order, implemented Decimal --- coinbase/models/amount.py | 11 +++++++++-- coinbase/models/order.py | 1 + coinbase/tests.py | 40 ++++++++++++++++++++++----------------- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/coinbase/models/amount.py b/coinbase/models/amount.py index e940dc5..02214d2 100644 --- a/coinbase/models/amount.py +++ b/coinbase/models/amount.py @@ -1,10 +1,17 @@ __author__ = 'gsibble' -class CoinbaseAmount(float): +from decimal import Decimal + +class CoinbaseAmount(Decimal): def __new__(self, amount, currency): - return float.__new__(self, amount) + return Decimal.__new__(self, amount) def __init__(self, amount, currency): super(CoinbaseAmount, self).__init__() self.currency = currency + + def __eq__(self, other, *args, **kwargs): + if isinstance(other, self.__class__) and self.currency != other.currency: + return False + return super(CoinbaseAmount, self).__eq__(other, *args, **kwargs) \ No newline at end of file diff --git a/coinbase/models/order.py b/coinbase/models/order.py index 4c5a1f4..c6bf6fb 100644 --- a/coinbase/models/order.py +++ b/coinbase/models/order.py @@ -14,6 +14,7 @@ def __init__(self, order): #self.created_at = datetime.strptime(order['created_at'], '%Y-%m-%dT%H:%M:%S-08:00') self.created_at = order['created_at'] self.status = order['status'] + self.custom = order['custom'] btc_cents = Decimal(order['total_btc']['cents']) btc_currency_iso = order['total_btc']['currency_iso'] diff --git a/coinbase/tests.py b/coinbase/tests.py index 7353f4c..af8e0cb 100644 --- a/coinbase/tests.py +++ b/coinbase/tests.py @@ -6,7 +6,8 @@ from httpretty import HTTPretty, httprettified from coinbase import CoinbaseAccount -from models import CoinbaseAmount +from coinbase.models import CoinbaseAmount +from decimal import Decimal TEMP_CREDENTIALS = '''{"_module": "oauth2client.client", "token_expiry": "2014-03-31T23:27:40Z", "access_token": "c15a9f84e471db9b0b8fb94f3cb83f08867b4e00cb823f49ead771e928af5c79", "token_uri": "https://www.coinbase.com/oauth/token", "invalid": false, "token_response": {"access_token": "c15a9f84e471db9b0b8fb94f3cb83f08867b4e00cb823f49ead771e928af5c79", "token_type": "bearer", "expires_in": 7200, "refresh_token": "90cb2424ddc39f6668da41a7b46dfd5a729ac9030e19e05fd95bb1880ad07e65", "scope": "all"}, "client_id": "2df06cb383f4ffffac20e257244708c78a1150d128f37d420f11fdc069a914fc", "id_token": null, "client_secret": "7caedd79052d7e29aa0f2700980247e499ce85381e70e4a44de0c08f25bded8a", "revoke_uri": "https://accounts.google.com/o/oauth2/revoke", "_class": "OAuth2Credentials", "refresh_token": "90cb2424ddc39f6668da41a7b46dfd5a729ac9030e19e05fd95bb1880ad07e65", "user_agent": null}''' @@ -16,9 +17,15 @@ def setUp(self): self.cb_amount = CoinbaseAmount(1, 'BTC') def test_cb_amount_class(self): - this(self.cb_amount).should.equal(1) + this(self.cb_amount).should.be.an(Decimal) + this(Decimal(self.cb_amount)).should.equal(Decimal('1')) this(self.cb_amount.currency).should.equal('BTC') + def test_cb_amount_equality(self): + this(CoinbaseAmount(1, 'BTC')).should.equal(CoinbaseAmount(1, 'BTC')) + this(CoinbaseAmount(1, 'BTC')).doesnt.equal(CoinbaseAmount(1, 'USD')) + assert CoinbaseAmount(1, 'BTC') == 1 + class CoinBaseAPIKeyTests(unittest.TestCase): def setUp(self): @@ -31,7 +38,7 @@ def test_api_key_balance(self): body='''{"amount":"1.00000000","currency":"BTC"}''', content_type='text/json') - this(self.account.balance).should.equal(1.0) + this(float(self.account.balance)).should.equal(1) class CoinBaseLibraryTests(unittest.TestCase): @@ -45,7 +52,7 @@ def test_retrieve_balance(self): body='''{"amount":"0.00000000","currency":"BTC"}''', content_type='text/json') - this(self.account.balance).should.equal(0.0) + this(float(self.account.balance)).should.equal(0.0) this(self.account.balance.currency).should.equal('BTC') #TODO: Switch to decimals @@ -76,7 +83,7 @@ def test_buy_price_1(self): content_type='text/json') buy_price_1 = self.account.buy_price(1) - this(buy_price_1).should.be.an(float) + this(buy_price_1).should.be.an(Decimal) this(buy_price_1).should.be.lower_than(100) this(buy_price_1.currency).should.equal('USD') @@ -98,7 +105,7 @@ def test_sell_price(self): content_type='text/json') sell_price_1 = self.account.sell_price(1) - this(sell_price_1).should.be.an(float) + this(sell_price_1).should.be.an(Decimal) this(sell_price_1).should.be.lower_than(100) this(sell_price_1.currency).should.equal('USD') @@ -120,7 +127,7 @@ def test_request_bitcoin(self): new_request = self.account.request('george@atlasr.com', 1, 'Testing') - this(new_request.amount).should.equal(1) + this(new_request.amount).should.equal(CoinbaseAmount(Decimal(1), 'BTC')) this(new_request.request).should.equal(True) this(new_request.sender.email).should.equal('george@atlasr.com') this(new_request.recipient.email).should.equal('gsibble@gmail.com') @@ -135,7 +142,7 @@ def test_send_bitcoin(self): new_transaction_with_btc_address = self.account.send('15yHmnB5vY68sXpAU9pR71rnyPAGLLWeRP', amount=0.1) - this(new_transaction_with_btc_address.amount).should.equal(-0.1) + this(new_transaction_with_btc_address.amount).should.equal(CoinbaseAmount(Decimal('-0.1'), 'BTC')) this(new_transaction_with_btc_address.request).should.equal(False) this(new_transaction_with_btc_address.sender.email).should.equal('gsibble@gmail.com') this(new_transaction_with_btc_address.recipient).should.equal(None) @@ -170,7 +177,7 @@ def test_getting_transaction(self): transaction = self.account.get_transaction('5158b227802669269c000009') this(transaction.status).should.equal('pending') - this(transaction.amount).should.equal(-0.1) + this(transaction.amount).should.equal(CoinbaseAmount(Decimal("-0.1"), "BTC")) @httprettified def test_getting_user_details(self): @@ -182,13 +189,12 @@ def test_getting_user_details(self): user = self.account.get_user_details() this(user.id).should.equal("509f01da12837e0201100212") - this(user.balance).should.equal(1225.86084181) + this(user.balance).should.equal(CoinbaseAmount(Decimal("1225.86084181"), "BTC")) # The following tests use the example request/responses from the coinbase API docs: # test_creating_button, test_creating_order, test_getting_order @httprettified def test_creating_button(self): - HTTPretty.register_uri(HTTPretty.POST, "https://coinbase.com/api/v1/buttons", body='''{"success":true,"button":{"code":"93865b9cae83706ae59220c013bc0afd","type":"buy_now","style":"custom_large","text":"Pay With Bitcoin","name":"test","description":"Sample description","custom":"Order123","callback_url":"http://www.example.com/my_custom_button_callback","price":{"cents":123,"currency_iso":"USD"}}}''', content_type='text/json') @@ -206,8 +212,7 @@ def test_creating_button(self): ) this(button.code).should.equal("93865b9cae83706ae59220c013bc0afd") this(button.name).should.equal("test") - this(button.price).should.equal(1.23) - this(button.price.currency).should.equal("USD") + this(button.price).should.equal(CoinbaseAmount(Decimal('1.23'), "USD")) this(button.include_address).should.equal(None) @httprettified @@ -220,8 +225,8 @@ def test_creating_order(self): order = self.account.create_order("93865b9cae83706ae59220c013bc0afd") this(order.id).should.equal("7RTTRDVP") - this(order.total_btc).should.equal(1) - this(order.total_btc.currency).should.equal("BTC") + this(order.total_btc).should.equal(CoinbaseAmount(1, 'BTC')) + this(order.button.button_id).should.equal("93865b9cae83706ae59220c013bc0afd") this(order.transaction).should.equal(None) @@ -247,10 +252,11 @@ def test_getting_order(self): this(order.id).should.equal("A7C52JQT") this(order.status).should.equal("completed") - this(order.total_btc).should.equal(.1) - this(order.total_native.currency).should.equal("BTC") + this(order.total_btc).should.equal(CoinbaseAmount(Decimal(".1"), "BTC")) this(order.button.name).should.equal("test") this(order.transaction.transaction_id).should.equal("513eb768f12a9cf27400000b") if __name__ == '__main__': + logging.basicConfig( stream=sys.stderr ) + logging.getLogger( "SomeTest.testSomething" ).setLevel( logging.DEBUG ) unittest.main() From 10e169545b94b94ad7f27ea41d4a886ce60ff858 Mon Sep 17 00:00:00 2001 From: vkhougaz Date: Sun, 5 Jan 2014 02:36:21 -0800 Subject: [PATCH 03/12] Implemented from_cents method, fixed possible bug in transfer --- coinbase/models/amount.py | 10 +++++++++- coinbase/models/button.py | 10 ++-------- coinbase/models/order.py | 16 +++------------- coinbase/models/transfer.py | 8 ++++---- 4 files changed, 18 insertions(+), 26 deletions(-) diff --git a/coinbase/models/amount.py b/coinbase/models/amount.py index 02214d2..d748b38 100644 --- a/coinbase/models/amount.py +++ b/coinbase/models/amount.py @@ -1,6 +1,7 @@ __author__ = 'gsibble' from decimal import Decimal +from coinbase.config import CENTS_PER_BITCOIN, CENTS_PER_OTHER class CoinbaseAmount(Decimal): @@ -14,4 +15,11 @@ def __init__(self, amount, currency): def __eq__(self, other, *args, **kwargs): if isinstance(other, self.__class__) and self.currency != other.currency: return False - return super(CoinbaseAmount, self).__eq__(other, *args, **kwargs) \ No newline at end of file + return super(CoinbaseAmount, self).__eq__(other, *args, **kwargs) + + @classmethod + def from_cents(cls, cents, currency): + if currency == 'BTC': + return cls(Decimal(cents)/CENTS_PER_BITCOIN, currency) + else: + return cls(Decimal(cents)/CENTS_PER_OTHER, currency) diff --git a/coinbase/models/button.py b/coinbase/models/button.py index 361a0ad..c74833a 100644 --- a/coinbase/models/button.py +++ b/coinbase/models/button.py @@ -1,8 +1,6 @@ __author__ = 'vkhougaz' from amount import CoinbaseAmount -from decimal import Decimal -from coinbase.config import CENTS_PER_BITCOIN, CENTS_PER_OTHER class CoinbaseButton(object): @@ -17,13 +15,9 @@ def __init__(self, button): self.name = button['name'] if 'price' in button: - price = Decimal(button['price']['cents']) + price_cents = button['price']['cents'] price_currency_iso = button['price']['currency_iso'] - if price_currency_iso == 'BTC': - price /= CENTS_PER_BITCOIN - else: - price /= CENTS_PER_OTHER - self.price = CoinbaseAmount(price, price_currency_iso) + self.price = CoinbaseAmount.from_cents(price_cents, price_currency_iso) else: self.price = None self.type = button.get('type', None) diff --git a/coinbase/models/order.py b/coinbase/models/order.py index c6bf6fb..16717c5 100644 --- a/coinbase/models/order.py +++ b/coinbase/models/order.py @@ -3,8 +3,6 @@ from amount import CoinbaseAmount from button import CoinbaseButton from transaction import CoinbaseTransaction -from decimal import Decimal -from coinbase.config import CENTS_PER_BITCOIN, CENTS_PER_OTHER # from datetime import datetime class CoinbaseOrder(object): @@ -16,21 +14,13 @@ def __init__(self, order): self.status = order['status'] self.custom = order['custom'] - btc_cents = Decimal(order['total_btc']['cents']) + btc_cents = order['total_btc']['cents'] btc_currency_iso = order['total_btc']['currency_iso'] - if btc_currency_iso == 'BTC': - btc_cents /= CENTS_PER_BITCOIN - else: - btc_cents /= CENTS_PER_OTHER - self.total_btc = CoinbaseAmount(btc_cents, btc_currency_iso) + self.total_btc = CoinbaseAmount.from_cents(btc_cents, btc_currency_iso) native_cents = order['total_native']['cents'] native_currency_iso = order['total_native']['currency_iso'] - if native_currency_iso == 'BTC': - native_cents /= CENTS_PER_BITCOIN - else: - native_cents /= CENTS_PER_OTHER - self.total_native = CoinbaseAmount(native_cents, native_currency_iso) + self.total_native = CoinbaseAmount.from_cents(native_cents, native_currency_iso) self.button = CoinbaseButton(order['button']) if 'transaction' in order and order['transaction'] is not None: diff --git a/coinbase/models/transfer.py b/coinbase/models/transfer.py index 0fdacb5..b037d07 100644 --- a/coinbase/models/transfer.py +++ b/coinbase/models/transfer.py @@ -11,14 +11,14 @@ def __init__(self, transfer): fees_coinbase_cents = transfer['fees']['coinbase']['cents'] fees_coinbase_currency_iso = transfer['fees']['coinbase']['currency_iso'] - self.fees_coinbase = CoinbaseAmount(fees_coinbase_cents, fees_coinbase_currency_iso) + self.fees_coinbase = CoinbaseAmount.from_cents(fees_coinbase_cents, fees_coinbase_currency_iso) fees_bank_cents = transfer['fees']['bank']['cents'] fees_bank_currency_iso = transfer['fees']['bank']['currency_iso'] - self.fees_bank = CoinbaseAmount(fees_bank_cents, fees_bank_currency_iso) + self.fees_bank = CoinbaseAmount.from_cents(fees_bank_cents, fees_bank_currency_iso) self.payout_date = transfer['payout_date'] - self.transaction_id = transfer.get('transaction_id','') + self.transaction_id = transfer.get('transaction_id', '') self.status = transfer['status'] btc_amount = transfer['btc']['amount'] @@ -33,7 +33,7 @@ def __init__(self, transfer): total_currency = transfer['total']['currency'] self.total_amount = CoinbaseAmount(total_amount, total_currency) - self.description = transfer.get('description','') + self.description = transfer.get('description', '') def refresh(self): pass From c847601b59a63d318377113838fd8f4900d0ae2a Mon Sep 17 00:00:00 2001 From: vkhougaz Date: Mon, 6 Jan 2014 12:09:38 -0800 Subject: [PATCH 04/12] Made order_id more consistant and made models store their original data --- coinbase/models/button.py | 1 + coinbase/models/order.py | 4 +++- coinbase/models/transaction.py | 1 + coinbase/models/transfer.py | 2 ++ coinbase/tests.py | 6 +++--- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/coinbase/models/button.py b/coinbase/models/button.py index c74833a..e275850 100644 --- a/coinbase/models/button.py +++ b/coinbase/models/button.py @@ -5,6 +5,7 @@ class CoinbaseButton(object): def __init__(self, button): + self.data = button # Sometimes it's called code (create button) and sometimes id (sub item of create button) # so we map them together if 'id' in button: diff --git a/coinbase/models/order.py b/coinbase/models/order.py index 16717c5..02bab3c 100644 --- a/coinbase/models/order.py +++ b/coinbase/models/order.py @@ -7,7 +7,9 @@ class CoinbaseOrder(object): def __init__(self, order): - self.id = order['id'] + self.data = order + + self.order_id = order['id'] # TODO: Account for timezone properly #self.created_at = datetime.strptime(order['created_at'], '%Y-%m-%dT%H:%M:%S-08:00') self.created_at = order['created_at'] diff --git a/coinbase/models/transaction.py b/coinbase/models/transaction.py index 50d356d..878c7a0 100644 --- a/coinbase/models/transaction.py +++ b/coinbase/models/transaction.py @@ -6,6 +6,7 @@ class CoinbaseTransaction(object): def __init__(self, transaction): + self.data = transaction self.transaction_id = transaction['id'] self.created_at = transaction.get('created_at', None) diff --git a/coinbase/models/transfer.py b/coinbase/models/transfer.py index b037d07..844ceba 100644 --- a/coinbase/models/transfer.py +++ b/coinbase/models/transfer.py @@ -5,6 +5,8 @@ class CoinbaseTransfer(object): def __init__(self, transfer): + self.data = transfer + self.type = transfer['type'] self.code = transfer['code'] self.created_at = transfer['created_at'] diff --git a/coinbase/tests.py b/coinbase/tests.py index af8e0cb..66b53c8 100644 --- a/coinbase/tests.py +++ b/coinbase/tests.py @@ -224,7 +224,7 @@ def test_creating_order(self): order = self.account.create_order("93865b9cae83706ae59220c013bc0afd") - this(order.id).should.equal("7RTTRDVP") + this(order.order_id).should.equal("7RTTRDVP") this(order.total_btc).should.equal(CoinbaseAmount(1, 'BTC')) this(order.button.button_id).should.equal("93865b9cae83706ae59220c013bc0afd") @@ -239,7 +239,7 @@ def test_order_list(self): orders = self.account.orders() this(orders).should.be.an(list) - this(orders[0].id).should.equal("A7C52JQT") + this(orders[0].order_id).should.equal("A7C52JQT") @httprettified def test_getting_order(self): @@ -250,7 +250,7 @@ def test_getting_order(self): order = self.account.get_order("A7C52JQT") - this(order.id).should.equal("A7C52JQT") + this(order.order_id).should.equal("A7C52JQT") this(order.status).should.equal("completed") this(order.total_btc).should.equal(CoinbaseAmount(Decimal(".1"), "BTC")) this(order.button.name).should.equal("test") From a608a14fc84ab7dd914782f60ab8a0b9527f01bc Mon Sep 17 00:00:00 2001 From: vkhougaz Date: Tue, 7 Jan 2014 10:33:46 -0800 Subject: [PATCH 05/12] Cleaned button, added some error handling --- coinbase/__init__.py | 80 ++++++++++++++++++++------------------- coinbase/models/button.py | 4 +- 2 files changed, 44 insertions(+), 40 deletions(-) diff --git a/coinbase/__init__.py b/coinbase/__init__.py index 0788eee..938e0cb 100644 --- a/coinbase/__init__.py +++ b/coinbase/__init__.py @@ -168,8 +168,8 @@ def balance(self): url = COINBASE_ENDPOINT + '/account/balance' response = self.session.get(url, params=self.global_request_params) - results = response.json() - return CoinbaseAmount(results['amount'], results['currency']) + response_parsed = response.json() + return CoinbaseAmount(response_parsed['amount'], response_parsed['currency']) @property def receive_address(self): @@ -180,7 +180,8 @@ def receive_address(self): """ url = COINBASE_ENDPOINT + '/account/receive_address' response = self.session.get(url, params=self.global_request_params) - return response.json()['address'] + response_parsed = response.json() + return response_parsed['address'] @property def contacts(self): @@ -220,8 +221,8 @@ def sell_price(self, qty=1): params = {'qty': qty} params.update(self.global_request_params) response = self.session.get(url, params=params) - results = response.json() - return CoinbaseAmount(results['amount'], results['currency']) + response_parsed = response.json() + return CoinbaseAmount(response_parsed['amount'], response_parsed['currency']) # @property # def user(self): @@ -246,9 +247,8 @@ def buy_btc(self, qty, pricevaries=False): } response = self.session.post(url=url, data=json.dumps(request_data), params=self.global_request_params) response_parsed = response.json() - if response_parsed['success'] == False: + if response_parsed['success'] is False: return CoinbaseError(response_parsed['errors']) - return CoinbaseTransfer(response_parsed['transfer']) @@ -265,9 +265,8 @@ def sell_btc(self, qty): } response = self.session.post(url=url, data=json.dumps(request_data), params=self.global_request_params) response_parsed = response.json() - if response_parsed['success'] == False: + if response_parsed['success'] is False: return CoinbaseError(response_parsed['errors']) - return CoinbaseTransfer(response_parsed['transfer']) @@ -302,10 +301,8 @@ def request(self, from_email, amount, notes='', currency='BTC'): response = self.session.post(url=url, data=json.dumps(request_data), params=self.global_request_params) response_parsed = response.json() - if response_parsed['success'] == False: - pass - #DO ERROR HANDLING and raise something - + if response_parsed['success'] is False: + return CoinbaseError(response_parsed['errors']) return CoinbaseTransaction(response_parsed['transaction']) def send(self, to_address, amount, notes='', currency='BTC'): @@ -340,10 +337,8 @@ def send(self, to_address, amount, notes='', currency='BTC'): response = self.session.post(url=url, data=json.dumps(request_data), params=self.global_request_params) response_parsed = response.json() - - if response_parsed['success'] == False: - raise RuntimeError('Transaction Failed') - + if response_parsed['success'] is False: + return CoinbaseError(response_parsed['errors']) return CoinbaseTransaction(response_parsed['transaction']) @@ -365,12 +360,12 @@ def transactions(self, count=30): params = {'page': page} params.update(self.global_request_params) response = self.session.get(url=url, params=params) - parsed_transactions = response.json() + response_parsed = response.json() - if parsed_transactions['num_pages'] == page: + if response_parsed['num_pages'] == page: reached_final_page = True - for transaction in parsed_transactions['transactions']: + for transaction in response_parsed['transactions']: transactions.append(CoinbaseTransaction(transaction['transaction'])) return transactions @@ -393,12 +388,14 @@ def transfers(self, count=30): params = {'page': page} params.update(self.global_request_params) response = self.session.get(url=url, params=params) - parsed_transfers = response.json() + response_parsed = response.json() + if response_parsed['success'] is False: + return CoinbaseError(response_parsed['errors']) - if parsed_transfers['num_pages'] == page: + if response_parsed['num_pages'] == page: reached_final_page = True - for transfer in parsed_transfers['transfers']: + for transfer in response_parsed['transfers']: transfers.append(CoinbaseTransfer(transfer['transfer'])) return transfers @@ -411,13 +408,8 @@ def get_transaction(self, transaction_id): """ url = COINBASE_ENDPOINT + '/transactions/' + str(transaction_id) response = self.session.get(url, params=self.global_request_params) - results = response.json() - - if results.get('success', True) == False: - pass - #TODO: Add error handling - - return CoinbaseTransaction(results['transaction']) + response_parsed = response.json() + return CoinbaseTransaction(response_parsed['transaction']) def get_user_details(self): """ @@ -427,9 +419,9 @@ def get_user_details(self): """ url = COINBASE_ENDPOINT + '/users' response = self.session.get(url, params=self.global_request_params) - results = response.json() + response_parsed = response.json() - user_details = results['users'][0]['user'] + user_details = response_parsed['users'][0]['user'] #Convert our balance and limits to proper amounts balance = CoinbaseAmount(user_details['balance']['amount'], user_details['balance']['currency']) @@ -462,7 +454,10 @@ def generate_receive_address(self, callback_url=None): } } response = self.session.post(url=url, data=json.dumps(request_data), params=self.global_request_params) - return response.json()['address'] + response_parsed = response.json() + if response_parsed['success'] is False: + return CoinbaseError(response_parsed['errors']) + return response_parsed['address'] def create_button(self, name, price, @@ -539,7 +534,10 @@ def create_button(self, name, del request_data['button'][key] response = self.session.post(url=url, data=json.dumps(request_data), params=self.global_request_params) - return CoinbaseButton(response.json()['button']) + response_parsed = response.json() + if response_parsed['success'] is False: + return CoinbaseError(response_parsed['errors']) + return CoinbaseButton(response_parsed['button']) def create_order(self, code): """ @@ -550,7 +548,10 @@ def create_order(self, code): url = COINBASE_ENDPOINT + '/buttons/{code}/create_order'.format(code=code) response = self.session.post(url=url, params=self.global_request_params) - return CoinbaseOrder(response.json()['order']) + response_parsed = response.json() + if response_parsed['success'] is False: + return CoinbaseError(response_parsed['errors']) + return CoinbaseOrder(response_parsed['order']) def get_order(self, order_id): """ @@ -561,7 +562,8 @@ def get_order(self, order_id): url = COINBASE_ENDPOINT + '/orders/{id}'.format(id=order_id) response = self.session.get(url=url, params=self.global_request_params) - return CoinbaseOrder(response.json()['order']) + response_parsed = response.json() + return CoinbaseOrder(response_parsed['order']) def orders(self, count=30): """ @@ -581,12 +583,12 @@ def orders(self, count=30): params = {'page': page} params.update(self.global_request_params) response = self.session.get(url=url, params=params) - parsed_orders = response.json() + response_parsed = response.json() - if parsed_orders['num_pages'] == page: + if response_parsed['num_pages'] == page: reached_final_page = True - for order in parsed_orders['orders']: + for order in response_parsed['orders']: orders.append(CoinbaseOrder(order['order'])) return orders \ No newline at end of file diff --git a/coinbase/models/button.py b/coinbase/models/button.py index e275850..30deb9f 100644 --- a/coinbase/models/button.py +++ b/coinbase/models/button.py @@ -10,8 +10,10 @@ def __init__(self, button): # so we map them together if 'id' in button: self.button_id = button['id'] - if 'code' in button: + elif 'code' in button: self.button_id = button['code'] + else: + self.button_id = None self.code = self.button_id self.name = button['name'] From 329aea9836f1431cdeba964d1e027de522bc6292 Mon Sep 17 00:00:00 2001 From: vkhougaz Date: Tue, 7 Jan 2014 11:33:04 -0800 Subject: [PATCH 06/12] Added more error handling via _get and _post methods, made CoinbaseError an exception --- coinbase/__init__.py | 167 ++++++++++++++------------------------- coinbase/models/error.py | 4 +- coinbase/tests.py | 47 ++++++++--- 3 files changed, 95 insertions(+), 123 deletions(-) diff --git a/coinbase/__init__.py b/coinbase/__init__.py index 938e0cb..7e1ce2c 100644 --- a/coinbase/__init__.py +++ b/coinbase/__init__.py @@ -40,9 +40,7 @@ import json import os import inspect - -#TODO: Switch to decimals from floats -#from decimal import Decimal +import copy from .config import COINBASE_ENDPOINT, COINBASE_ITEMS_PER_PAGE from .models import CoinbaseAmount, CoinbaseTransaction, CoinbaseUser, CoinbaseTransfer, CoinbaseError, CoinbaseButton, CoinbaseOrder @@ -158,17 +156,47 @@ def _prepare_request(self): #Check if the oauth token is expired and refresh it if necessary self._check_oauth_expired() + _get = lambda self, url, data=None, params=None: self.make_request(self.session.get , url, data, params) + _post = lambda self, url, data=None, params=None: self.make_request(self.session.post , url, data, params) + _put = lambda self, url, data=None, params=None: self.make_request(self.session.put , url, data, params) + _delete = lambda self, url, data=None, params=None: self.make_request(self.session.delete, url, data, params) + + def make_request(self, request_func, url, data=None, params=None): + if params: + # We don't want to change this object, it is passed by reference + params = copy.copy(params) + params.update(self.global_request_params) + else: + params = self.global_request_params + + response = request_func(COINBASE_ENDPOINT + url, data=data, params=params) + response_parsed = response.json() + + if response.status_code != 200: + if 'error' in response_parsed: + raise CoinbaseError(response_parsed['error']) + else: + raise CoinbaseError('Response code not 200, was {}'.format(response.status_code)) + + if 'success' in response_parsed and not response_parsed['success']: + if 'error' in response_parsed: + raise CoinbaseError(response_parsed['error']) + elif 'errors' in response_parsed: + raise CoinbaseError(response_parsed['errors']) + else: + raise CoinbaseError('Success was false in response, unknown error') + + return response_parsed + + @property def balance(self): """ Retrieve coinbase's account balance - :return: CoinbaseAmount (float) with currency attribute + :return: CoinbaseAmount with currency attribute """ - - url = COINBASE_ENDPOINT + '/account/balance' - response = self.session.get(url, params=self.global_request_params) - response_parsed = response.json() + response_parsed = self._get('/account/balance') return CoinbaseAmount(response_parsed['amount'], response_parsed['currency']) @property @@ -178,9 +206,7 @@ def receive_address(self): :return: String address of account """ - url = COINBASE_ENDPOINT + '/account/receive_address' - response = self.session.get(url, params=self.global_request_params) - response_parsed = response.json() + response_parsed = self._get('/account/receive_address') return response_parsed['address'] @property @@ -190,65 +216,41 @@ def contacts(self): :return: List of contacts in the account """ - url = COINBASE_ENDPOINT + '/contacts' - response = self.session.get(url, params=self.global_request_params) - return [contact['contact'] for contact in response.json()['contacts']] - - - - + response_parsed = self._get('/contacts') + return [contact['contact'] for contact in response_parsed['contacts']] def buy_price(self, qty=1): """ Return the buy price of BitCoin in USD :param qty: Quantity of BitCoin to price - :return: CoinbaseAmount (float) with currency attribute + :return: CoinbaseAmount with currency attribute """ - url = COINBASE_ENDPOINT + '/prices/buy' - params = {'qty': qty} - params.update(self.global_request_params) - response = self.session.get(url, params=params) - results = response.json() - return CoinbaseAmount(results['amount'], results['currency']) + response_parsed = self._get('/prices/buy', data=json.dumps({"qty": qty})) + return CoinbaseAmount(response_parsed['amount'], response_parsed['currency']) def sell_price(self, qty=1): """ Return the sell price of BitCoin in USD :param qty: Quantity of BitCoin to price - :return: CoinbaseAmount (float) with currency attribute + :return: CoinbaseAmount with currency attribute """ - url = COINBASE_ENDPOINT + '/prices/sell' - params = {'qty': qty} - params.update(self.global_request_params) - response = self.session.get(url, params=params) - response_parsed = response.json() + response_parsed = self._get('/prices/sell', data=json.dumps({"qty": qty})) return CoinbaseAmount(response_parsed['amount'], response_parsed['currency']) - # @property - # def user(self): - # url = COINBASE_ENDPOINT + '/account/receive_address' - # response = self.session.get(url) - # return response.json() - - def buy_btc(self, qty, pricevaries=False): """ Buy BitCoin from Coinbase for USD :param qty: BitCoin quantity to be bought :param pricevaries: Boolean value that indicates whether or not the transaction should - be processed if Coinbase cannot gaurentee the current price. + be processed if Coinbase cannot guarantee the current price. :return: CoinbaseTransfer with all transfer details on success or CoinbaseError with the error list received from Coinbase on failure """ - url = COINBASE_ENDPOINT + '/buys' request_data = { "qty": qty, "agree_btc_amount_varies": pricevaries } - response = self.session.post(url=url, data=json.dumps(request_data), params=self.global_request_params) - response_parsed = response.json() - if response_parsed['success'] is False: - return CoinbaseError(response_parsed['errors']) + response_parsed = self._post('/buys', data=json.dumps(request_data)) return CoinbaseTransfer(response_parsed['transfer']) @@ -259,14 +261,7 @@ def sell_btc(self, qty): :return: CoinbaseTransfer with all transfer details on success or CoinbaseError with the error list received from Coinbase on failure """ - url = COINBASE_ENDPOINT + '/sells' - request_data = { - "qty": qty, - } - response = self.session.post(url=url, data=json.dumps(request_data), params=self.global_request_params) - response_parsed = response.json() - if response_parsed['success'] is False: - return CoinbaseError(response_parsed['errors']) + response_parsed = self._post('/sells', data=json.dumps({"qty": qty})) return CoinbaseTransfer(response_parsed['transfer']) @@ -279,7 +274,6 @@ def request(self, from_email, amount, notes='', currency='BTC'): :param currency: Currency of the request :return: CoinbaseTransaction with status and details """ - url = COINBASE_ENDPOINT + '/transactions/request_money' if currency == 'BTC': request_data = { @@ -299,10 +293,7 @@ def request(self, from_email, amount, notes='', currency='BTC'): } } - response = self.session.post(url=url, data=json.dumps(request_data), params=self.global_request_params) - response_parsed = response.json() - if response_parsed['success'] is False: - return CoinbaseError(response_parsed['errors']) + response_parsed = self._post('/transactions/request_money', data=json.dumps(request_data)) return CoinbaseTransaction(response_parsed['transaction']) def send(self, to_address, amount, notes='', currency='BTC'): @@ -314,7 +305,6 @@ def send(self, to_address, amount, notes='', currency='BTC'): :param currency: Currency to send :return: CoinbaseTransaction with status and details """ - url = COINBASE_ENDPOINT + '/transactions/send_money' if currency == 'BTC': request_data = { @@ -335,10 +325,7 @@ def send(self, to_address, amount, notes='', currency='BTC'): } } - response = self.session.post(url=url, data=json.dumps(request_data), params=self.global_request_params) - response_parsed = response.json() - if response_parsed['success'] is False: - return CoinbaseError(response_parsed['errors']) + response_parsed = self._post('/transactions/send_money', data=json.dumps(request_data)) return CoinbaseTransaction(response_parsed['transaction']) @@ -348,7 +335,6 @@ def transactions(self, count=30): :param count: How many transactions to retrieve :return: List of CoinbaseTransaction objects """ - url = COINBASE_ENDPOINT + '/transactions' pages = count / 30 + 1 transactions = [] @@ -357,10 +343,7 @@ def transactions(self, count=30): for page in xrange(1, pages + 1): if not reached_final_page: - params = {'page': page} - params.update(self.global_request_params) - response = self.session.get(url=url, params=params) - response_parsed = response.json() + response_parsed = self._get('/transactions', params={'page': page}) if response_parsed['num_pages'] == page: reached_final_page = True @@ -376,7 +359,6 @@ def transfers(self, count=30): :param count: How many transfers to retrieve :return: List of CoinbaseTransfer objects """ - url = COINBASE_ENDPOINT + '/transfers' pages = count / 30 + 1 transfers = [] @@ -385,12 +367,7 @@ def transfers(self, count=30): for page in xrange(1, pages + 1): if not reached_final_page: - params = {'page': page} - params.update(self.global_request_params) - response = self.session.get(url=url, params=params) - response_parsed = response.json() - if response_parsed['success'] is False: - return CoinbaseError(response_parsed['errors']) + response_parsed = self._get('/transfers', params={'page': page}) if response_parsed['num_pages'] == page: reached_final_page = True @@ -406,9 +383,7 @@ def get_transaction(self, transaction_id): :param transaction_id: Unique transaction identifier :return: CoinbaseTransaction object with transaction details """ - url = COINBASE_ENDPOINT + '/transactions/' + str(transaction_id) - response = self.session.get(url, params=self.global_request_params) - response_parsed = response.json() + response_parsed = self._get('/transactions/{id}'.format(id=transaction_id)) return CoinbaseTransaction(response_parsed['transaction']) def get_user_details(self): @@ -417,9 +392,7 @@ def get_user_details(self): :return: CoinbaseUser object with user details """ - url = COINBASE_ENDPOINT + '/users' - response = self.session.get(url, params=self.global_request_params) - response_parsed = response.json() + response_parsed = self._get('/users') user_details = response_parsed['users'][0]['user'] @@ -447,16 +420,12 @@ def generate_receive_address(self, callback_url=None): :param callback_url: The URL to receive instant payment notifications :return: The new string address """ - url = COINBASE_ENDPOINT + '/account/generate_receive_address' request_data = { "address": { "callback_url": callback_url } } - response = self.session.post(url=url, data=json.dumps(request_data), params=self.global_request_params) - response_parsed = response.json() - if response_parsed['success'] is False: - return CoinbaseError(response_parsed['errors']) + response_parsed = self._post('/account/generate_receive_address', data=json.dumps(request_data)) return response_parsed['address'] def create_button(self, name, @@ -501,13 +470,10 @@ def create_button(self, name, :param price5: Suggested price 5 :return: A CoinbaseButton object """ - url = COINBASE_ENDPOINT + '/buttons' - price = str(price) - request_data = { "button": { "name": name, - "price": price, + "price": str(price), "currency": currency, "type": type, "style": style, @@ -533,10 +499,7 @@ def create_button(self, name, for key in none_keys: del request_data['button'][key] - response = self.session.post(url=url, data=json.dumps(request_data), params=self.global_request_params) - response_parsed = response.json() - if response_parsed['success'] is False: - return CoinbaseError(response_parsed['errors']) + response_parsed = self._post('/buttons', data=json.dumps(request_data)) return CoinbaseButton(response_parsed['button']) def create_order(self, code): @@ -545,12 +508,7 @@ def create_order(self, code): :param code: The code of the button for which you wish to create an order :return: A CoinbaseOrder object """ - url = COINBASE_ENDPOINT + '/buttons/{code}/create_order'.format(code=code) - - response = self.session.post(url=url, params=self.global_request_params) - response_parsed = response.json() - if response_parsed['success'] is False: - return CoinbaseError(response_parsed['errors']) + response_parsed = self._post('/buttons/{code}/create_order'.format(code=code)) return CoinbaseOrder(response_parsed['order']) def get_order(self, order_id): @@ -559,10 +517,7 @@ def get_order(self, order_id): :param order_id: The order id to be retrieved :return: A CoinbaseOrder object """ - url = COINBASE_ENDPOINT + '/orders/{id}'.format(id=order_id) - - response = self.session.get(url=url, params=self.global_request_params) - response_parsed = response.json() + response_parsed = self._get('/orders/{id}'.format(id=order_id)) return CoinbaseOrder(response_parsed['order']) def orders(self, count=30): @@ -571,7 +526,6 @@ def orders(self, count=30): :param count: How many orders to retrieve :return: List of CoinbaseOrder objects """ - url = COINBASE_ENDPOINT + '/orders' pages = count / 30 + 1 orders = [] @@ -580,10 +534,7 @@ def orders(self, count=30): for page in xrange(1, pages + 1): if not reached_final_page: - params = {'page': page} - params.update(self.global_request_params) - response = self.session.get(url=url, params=params) - response_parsed = response.json() + response_parsed = self._get('/orders', params={'page': page}) if response_parsed['num_pages'] == page: reached_final_page = True diff --git a/coinbase/models/error.py b/coinbase/models/error.py index 4231b45..f60fd71 100644 --- a/coinbase/models/error.py +++ b/coinbase/models/error.py @@ -1,8 +1,6 @@ __author__ = 'kroberts' - - -class CoinbaseError(object): +class CoinbaseError(BaseException): def __init__(self, errorList): self.error = errorList diff --git a/coinbase/tests.py b/coinbase/tests.py index 66b53c8..14b5a8c 100644 --- a/coinbase/tests.py +++ b/coinbase/tests.py @@ -5,7 +5,7 @@ import unittest from httpretty import HTTPretty, httprettified -from coinbase import CoinbaseAccount +from coinbase import CoinbaseAccount, CoinbaseError from coinbase.models import CoinbaseAmount from decimal import Decimal @@ -17,7 +17,7 @@ def setUp(self): self.cb_amount = CoinbaseAmount(1, 'BTC') def test_cb_amount_class(self): - this(self.cb_amount).should.be.an(Decimal) + this(self.cb_amount).should.be.a(Decimal) this(Decimal(self.cb_amount)).should.equal(Decimal('1')) this(self.cb_amount.currency).should.equal('BTC') @@ -45,6 +45,35 @@ class CoinBaseLibraryTests(unittest.TestCase): def setUp(self): self.account = CoinbaseAccount(oauth2_credentials=TEMP_CREDENTIALS) + @httprettified + def test_status_error(self): + + HTTPretty.register_uri(HTTPretty.GET, "https://coinbase.com/api/v1/account/balance", + body='''{"error": "Invalid api_key"}''', + content_type='text/json', + status=401) + + try: + self.account.balance + except CoinbaseError as e: + e.error.should.equal("Invalid api_key") + except Exception: + assert False + + @httprettified + def test_success_false(self): + # Success is added when posting, putting or deleting + HTTPretty.register_uri(HTTPretty.GET, "https://coinbase.com/api/v1/account/balance", + body='''{"success": false, "errors":["Error 1","Error 2"]}''', + content_type='text/json') + + try: + self.account.balance + except CoinbaseError as e: + e.error.should.equal(["Error 1", "Error 2"]) + except Exception: + assert False + @httprettified def test_retrieve_balance(self): @@ -55,10 +84,6 @@ def test_retrieve_balance(self): this(float(self.account.balance)).should.equal(0.0) this(self.account.balance.currency).should.equal('BTC') - #TODO: Switch to decimals - #this(self.account.balance).should.equal(CoinbaseAmount('0.00000000', 'USD')) - #this(self.account.balance.currency).should.equal(CoinbaseAmount('0.00000000', 'USD').currency) - @httprettified def test_receive_addresses(self): @@ -83,7 +108,7 @@ def test_buy_price_1(self): content_type='text/json') buy_price_1 = self.account.buy_price(1) - this(buy_price_1).should.be.an(Decimal) + this(buy_price_1).should.be.a(Decimal) this(buy_price_1).should.be.lower_than(100) this(buy_price_1.currency).should.equal('USD') @@ -105,7 +130,7 @@ def test_sell_price(self): content_type='text/json') sell_price_1 = self.account.sell_price(1) - this(sell_price_1).should.be.an(Decimal) + this(sell_price_1).should.be.a(Decimal) this(sell_price_1).should.be.lower_than(100) this(sell_price_1.currency).should.equal('USD') @@ -165,7 +190,7 @@ def test_transaction_list(self): transaction_list = self.account.transactions() - this(transaction_list).should.be.an(list) + this(transaction_list).should.be.a(list) @httprettified def test_getting_transaction(self): @@ -238,7 +263,7 @@ def test_order_list(self): content_type='text/json') orders = self.account.orders() - this(orders).should.be.an(list) + this(orders).should.be.a(list) this(orders[0].order_id).should.equal("A7C52JQT") @httprettified @@ -257,6 +282,4 @@ def test_getting_order(self): this(order.transaction.transaction_id).should.equal("513eb768f12a9cf27400000b") if __name__ == '__main__': - logging.basicConfig( stream=sys.stderr ) - logging.getLogger( "SomeTest.testSomething" ).setLevel( logging.DEBUG ) unittest.main() From 51c3bfeaec37efd201e2a4007497a80311a97382 Mon Sep 17 00:00:00 2001 From: vkhougaz Date: Tue, 7 Jan 2014 11:39:31 -0800 Subject: [PATCH 07/12] Gave CoinbaseError a more informative string representation --- coinbase/models/error.py | 6 ++++++ coinbase/tests.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/coinbase/models/error.py b/coinbase/models/error.py index f60fd71..440b39a 100644 --- a/coinbase/models/error.py +++ b/coinbase/models/error.py @@ -4,3 +4,9 @@ class CoinbaseError(BaseException): def __init__(self, errorList): self.error = errorList + + def __unicode__(self): + return 'CoinbaseError: {}'.format(unicode(self.error)) + + def __str__(self): + return 'CoinbaseError: {}'.format(str(self.error)) \ No newline at end of file diff --git a/coinbase/tests.py b/coinbase/tests.py index 14b5a8c..21755a4 100644 --- a/coinbase/tests.py +++ b/coinbase/tests.py @@ -57,6 +57,8 @@ def test_status_error(self): self.account.balance except CoinbaseError as e: e.error.should.equal("Invalid api_key") + unicode(e).should.equal(u"CoinbaseError: Invalid api_key") + str(e).should.equal("CoinbaseError: Invalid api_key") except Exception: assert False From be2b6abf8c592ad2d17c748f12e0aa09216d82b2 Mon Sep 17 00:00:00 2001 From: vkhougaz Date: Tue, 7 Jan 2014 11:40:58 -0800 Subject: [PATCH 08/12] Gave CoinbaseError a LESS informative string representation --- coinbase/models/error.py | 4 ++-- coinbase/tests.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/coinbase/models/error.py b/coinbase/models/error.py index 440b39a..1289cbe 100644 --- a/coinbase/models/error.py +++ b/coinbase/models/error.py @@ -6,7 +6,7 @@ def __init__(self, errorList): self.error = errorList def __unicode__(self): - return 'CoinbaseError: {}'.format(unicode(self.error)) + return unicode(self.error) def __str__(self): - return 'CoinbaseError: {}'.format(str(self.error)) \ No newline at end of file + return str(self.error) \ No newline at end of file diff --git a/coinbase/tests.py b/coinbase/tests.py index 21755a4..f56c0b0 100644 --- a/coinbase/tests.py +++ b/coinbase/tests.py @@ -57,8 +57,8 @@ def test_status_error(self): self.account.balance except CoinbaseError as e: e.error.should.equal("Invalid api_key") - unicode(e).should.equal(u"CoinbaseError: Invalid api_key") - str(e).should.equal("CoinbaseError: Invalid api_key") + unicode(e).should.equal(u"Invalid api_key") + str(e).should.equal("Invalid api_key") except Exception: assert False From 857cf5e8820c53d446d59fddc196a5619f758e6c Mon Sep 17 00:00:00 2001 From: vkhougaz Date: Tue, 7 Jan 2014 18:02:44 -0800 Subject: [PATCH 09/12] Fixed bug where qty was passed as data not a parameter --- coinbase/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coinbase/__init__.py b/coinbase/__init__.py index 7e1ce2c..380e5f5 100644 --- a/coinbase/__init__.py +++ b/coinbase/__init__.py @@ -225,7 +225,7 @@ def buy_price(self, qty=1): :param qty: Quantity of BitCoin to price :return: CoinbaseAmount with currency attribute """ - response_parsed = self._get('/prices/buy', data=json.dumps({"qty": qty})) + response_parsed = self._get('/prices/buy', params={"qty": qty}) return CoinbaseAmount(response_parsed['amount'], response_parsed['currency']) def sell_price(self, qty=1): @@ -234,7 +234,7 @@ def sell_price(self, qty=1): :param qty: Quantity of BitCoin to price :return: CoinbaseAmount with currency attribute """ - response_parsed = self._get('/prices/sell', data=json.dumps({"qty": qty})) + response_parsed = self._get('/prices/sell', params={"qty": qty}) return CoinbaseAmount(response_parsed['amount'], response_parsed['currency']) def buy_btc(self, qty, pricevaries=False): From 1fcb9b597584158dff152fa94a2e9032b191f17b Mon Sep 17 00:00:00 2001 From: Sylvain BLOT Date: Fri, 14 Feb 2014 14:32:26 +0100 Subject: [PATCH 10/12] Get rid of "Price can't be blank" CoinbaseError: [u"Price can't be blank"] --- coinbase/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coinbase/__init__.py b/coinbase/__init__.py index 380e5f5..ac1c390 100644 --- a/coinbase/__init__.py +++ b/coinbase/__init__.py @@ -474,6 +474,7 @@ def create_button(self, name, "button": { "name": name, "price": str(price), + "price_string": str(price), "currency": currency, "type": type, "style": style, @@ -542,4 +543,4 @@ def orders(self, count=30): for order in response_parsed['orders']: orders.append(CoinbaseOrder(order['order'])) - return orders \ No newline at end of file + return orders From f787054fb43974b8f045ebb2b90ca7af8c7906f1 Mon Sep 17 00:00:00 2001 From: Sylvain BLOT Date: Sat, 15 Feb 2014 08:36:42 +0100 Subject: [PATCH 11/12] price_currency_iso in button api --- coinbase/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/coinbase/__init__.py b/coinbase/__init__.py index ac1c390..270447f 100644 --- a/coinbase/__init__.py +++ b/coinbase/__init__.py @@ -476,6 +476,7 @@ def create_button(self, name, "price": str(price), "price_string": str(price), "currency": currency, + "price_currency_iso": currency, "type": type, "style": style, "text": text, From 3dd2e4858fc07795f021f3bd40849c5acc221935 Mon Sep 17 00:00:00 2001 From: vkhougaz Date: Wed, 12 Mar 2014 18:00:50 -0700 Subject: [PATCH 12/12] Removed support for old api_key sustem, added support for new api_key + api_secret HMAC authentication --- coinbase/__init__.py | 46 +++++++++++++++++++++++++++----------------- coinbase/tests.py | 3 ++- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/coinbase/__init__.py b/coinbase/__init__.py index 270447f..be7c31e 100644 --- a/coinbase/__init__.py +++ b/coinbase/__init__.py @@ -40,7 +40,11 @@ import json import os import inspect -import copy +from urllib import urlencode +import hashlib +import hmac +import time + from .config import COINBASE_ENDPOINT, COINBASE_ITEMS_PER_PAGE from .models import CoinbaseAmount, CoinbaseTransaction, CoinbaseUser, CoinbaseTransfer, CoinbaseError, CoinbaseButton, CoinbaseOrder @@ -55,7 +59,7 @@ class CoinbaseAccount(object): def __init__(self, oauth2_credentials=None, - api_key=None): + api_key=None, api_secret=None): """ :param oauth2_credentials: JSON representation of Coinbase oauth2 credentials @@ -94,18 +98,15 @@ def __init__(self, #Set our request parameters to be empty self.global_request_params = {} - elif api_key: - if type(api_key) is str: - + elif api_key and api_secret: + if type(api_key) is str and type(api_secret) is str: #Set our API Key self.api_key = api_key - - #Set our global_request_params - self.global_request_params = {'api_key':api_key} + self.api_secret = api_secret else: - print "Your api_key must be a string" + print "Your api_key and api_secret must be strings" else: - print "You must pass either an api_key or oauth_credentials" + print "You must pass either api_key and api_secret or oauth_credentials" def _check_oauth_expired(self): """ @@ -162,14 +163,23 @@ def _prepare_request(self): _delete = lambda self, url, data=None, params=None: self.make_request(self.session.delete, url, data, params) def make_request(self, request_func, url, data=None, params=None): - if params: - # We don't want to change this object, it is passed by reference - params = copy.copy(params) - params.update(self.global_request_params) - else: - params = self.global_request_params - - response = request_func(COINBASE_ENDPOINT + url, data=data, params=params) + # We need body as a string to compute the hmac signature + body = json.dumps(data) if data else '' + # We also need the full url, so we urlencode the params here + url = COINBASE_ENDPOINT + url + ('?' + urlencode(params) if params else '') + + if hasattr(self, 'api_key'): + nonce = str(int(time.time() * 1e6)) + + message = nonce + url + body + signature = hmac.new(self.api_secret, message, hashlib.sha256).hexdigest() + self.session.headers.update({ + 'ACCESS_KEY': self.api_key, + 'ACCESS_SIGNATURE': signature, + 'ACCESS_NONCE': nonce + }) + + response = request_func(url, data=body) response_parsed = response.json() if response.status_code != 200: diff --git a/coinbase/tests.py b/coinbase/tests.py index f56c0b0..944f5f7 100644 --- a/coinbase/tests.py +++ b/coinbase/tests.py @@ -29,7 +29,8 @@ def test_cb_amount_equality(self): class CoinBaseAPIKeyTests(unittest.TestCase): def setUp(self): - self.account = CoinbaseAccount(api_key='f64223978e5fd99d07cded069db2189a38c17142fee35625f6ab3635585f61ab') + self.account = CoinbaseAccount(api_key='f64223978e5fd99d07cded069db2189a38c17142fee35625f6ab3635585f61ab', + api_secret='made up string') @httprettified def test_api_key_balance(self):