From 6d020e7535b3145d96962b78b1d7d0818e8332ac Mon Sep 17 00:00:00 2001 From: Benjamin Ross Date: Thu, 29 Nov 2018 19:50:57 -0700 Subject: [PATCH] Rebased with master and updated everything to be CBPRO instead of GDAX. Updated to check errors in send_message instead of _get --- README.md | 32 ++-- cbpro/__init__.py | 1 + cbpro/exceptions.py | 81 ++++++++++ cbpro/public_client.py | 30 ++-- gdax/authenticated_client.py | 289 ----------------------------------- gdax/exceptions.py | 81 ---------- requirements.txt | 9 -- setup.py | 5 - tests/test_error_handling.py | 24 +-- 9 files changed, 119 insertions(+), 433 deletions(-) create mode 100644 cbpro/exceptions.py delete mode 100644 gdax/authenticated_client.py delete mode 100644 gdax/exceptions.py delete mode 100644 requirements.txt diff --git a/README.md b/README.md index 6ff824c..3885f0c 100644 --- a/README.md +++ b/README.md @@ -354,7 +354,6 @@ time.sleep(10) order_book.close() ``` -<<<<<<< HEAD ### Testing Unit tests are under development using the pytest framework. Contributions are welcome! @@ -365,37 +364,32 @@ directory run: python -m pytest ``` -## Change Log -*1.1.2* **Current PyPI release** -- Refactor project for Coinbase Pro -- Major overhaul on how pagination is handled - -*1.0* -======= ### Error Handling Error handling has been added in version 2.0. Currently, the only HTTP error codes that are handled are the ones documented on the GDAX API error section: [here](https://docs.gdax.com/?python#errors) -- HTTP STATUS CODE 400: Raises InvalidGdaxRequest -- HTTP STATUS CODE 401: Raises UnauthorizedGdaxRequest -- HTTP STATUS CODE 403: Raises ForbiddenGdaxRequest -- HTTP STATUS CODE 404: Raises NotFoundGdaxRequest -- HTTP STATUS CODE 429: Raises GdaxRateLimitRequest -- HTTP STATUS CODE 4XX: Raises UnknownGdaxClientRequest -- HTTP STATUS CODE 5XX: Raises InternalErrorGdaxRequest +- HTTP STATUS CODE 400: Raises InvalidCbproRequest +- HTTP STATUS CODE 401: Raises UnauthorizedCbproRequest +- HTTP STATUS CODE 403: Raises ForbiddenCbproRequest +- HTTP STATUS CODE 404: Raises NotFoundCbproRequest +- HTTP STATUS CODE 429: Raises CbproRateLimitRequest +- HTTP STATUS CODE 4XX: Raises UnknownCbproClientRequest +- HTTP STATUS CODE 5XX: Raises InternalErrorCbproRequest All HTTP requests from both the public client and authenticated client run through `_determine_response`, which either returns the JSON body or raises the appropriate error. -Please consider creating new Exception classes and mapping as Gdax error states are discovered. - +Please consider creating new Exception classes and mapping as Cbpro error states are discovered. ## Change Log *2.0* - Added error handling to all HTTP requests [Error Handling Docs](#error-handling) -*1.0* **Current PyPI release** ->>>>>>> Added README info, added self to contributors, bumped version +*1.1.2* **Current PyPI release** +- Refactor project for Coinbase Pro +- Major overhaul on how pagination is handled + +*1.0* - The first release that is not backwards compatible - Refactored to follow PEP 8 Standards - Improved Documentation diff --git a/cbpro/__init__.py b/cbpro/__init__.py index 00937f5..a29f2e3 100644 --- a/cbpro/__init__.py +++ b/cbpro/__init__.py @@ -3,3 +3,4 @@ from cbpro.websocket_client import WebsocketClient from cbpro.order_book import OrderBook from cbpro.cbpro_auth import CBProAuth +from cbpro.exceptions import InvalidCbproRequest, UnauthorizedCbproRequest, ForbiddenCbproRequest, NotFoundCbproRequest, CbproRateLimitRequest, UnknownCbproClientRequest, InternalErrorCbproRequest diff --git a/cbpro/exceptions.py b/cbpro/exceptions.py new file mode 100644 index 0000000..70ec0c0 --- /dev/null +++ b/cbpro/exceptions.py @@ -0,0 +1,81 @@ +class CbproException(Exception): + """ + Base Coinbase Pro Exception + Raised when Bad Response returned from Coinbase Pro + See: https://docs.pro.coinbase.com/?r=1#errors + """ + + def __init__(self, message, code): + """ + :param message: Message from Coinbase Pro response + :type message: str + :param code: HTTP Code + :type code: int + """ + self._message = message + self._code = code + + @property + def message(self): + return self._message + + @message.setter + def message(self, message): + self._message = message + + @property + def code(self): + return self._code + + @message.setter + def code(self, code): + self._code = code + + +class InvalidCbproRequest(CbproException): + """ + Raised on 400 response from Coinbase Pro + """ + pass + + +class UnauthorizedCbproRequest(CbproException): + """ + Raised on 401 response from Coinbase Pro + """ + pass + + +class ForbiddenCbproRequest(CbproException): + """ + Raised on 403 response from Coinbase Pro + """ + pass + + +class NotFoundCbproRequest(CbproException): + """ + Raised on 404 response from Coinbase Pro + """ + pass + + +class CbproRateLimitRequest(CbproException): + """ + Raised on 429 response from Coinbase Pro + """ + pass + + +class UnknownCbproClientRequest(CbproException): + """ + Raised on 4XX responses not tracked + """ + pass + + +class InternalErrorCbproRequest(CbproException): + """ + Raised on 500 response from Coinbase Pro + """ + pass diff --git a/cbpro/public_client.py b/cbpro/public_client.py index ffdda42..8b311b6 100644 --- a/cbpro/public_client.py +++ b/cbpro/public_client.py @@ -6,7 +6,7 @@ from http import HTTPStatus import requests -from gdax import exceptions +from cbpro import exceptions class PublicClient(object): @@ -46,9 +46,9 @@ def _is_http_server_error(self, code): def _determine_response(self, response): """ - Determines if GDAX response is success or error + Determines if CBPRO response is success or error If success, returns response json - If error, raises appropriate GdaxException + If error, raises appropriate CbproException """ if self._is_http_success(response.status_code): return response.json() @@ -56,35 +56,29 @@ def _determine_response(self, response): body = response.json() message = body.get('message') if response.status_code == HTTPStatus.BAD_REQUEST: - raise exceptions.InvalidGdaxRequest(message, + raise exceptions.InvalidCbproRequest(message, HTTPStatus.BAD_REQUEST) elif response.status_code == HTTPStatus.UNAUTHORIZED: - raise exceptions.UnauthorizedGdaxRequest(message, + raise exceptions.UnauthorizedCbproRequest(message, HTTPStatus.UNAUTHORIZED) elif response.status_code == HTTPStatus.FORBIDDEN: - raise exceptions.ForbiddenGdaxRequest(message, + raise exceptions.ForbiddenCbproRequest(message, HTTPStatus.FORBIDDEN) elif response.status_code == HTTPStatus.NOT_FOUND: - raise exceptions.NotFoundGdaxRequest(message, + raise exceptions.NotFoundCbproRequest(message, HTTPStatus.NOT_FOUND) elif response.status_code == HTTPStatus.TOO_MANY_REQUESTS: - raise exceptions.GdaxRateLimitRequest(message, + raise exceptions.CbproRateLimitRequest(message, HTTPStatus.TOO_MANY_REQUESTS) else: # Other 4XX response not yet mapped - raise exceptions.UnknownGdaxClientRequest(message, + raise exceptions.UnknownCbproClientRequest(message, response.status_code) elif self._is_http_server_error(response.status_code): body = response.json() - raise exceptions.InternalErrorGdaxRequest(body.get('message'), + raise exceptions.InternalErrorCbproRequest(body.get('message'), HTTPStatus.INTERNAL_SERVER_ERROR) - def _get(self, path, params=None): - """Perform get request""" - - r = requests.get(self.url + path, params=params, timeout=self.timeout) - return self._determine_response(r) - def get_products(self): """Get a list of available currency pairs for trading. @@ -323,7 +317,7 @@ def _send_message(self, method, endpoint, params=None, data=None): url = self.url + endpoint r = self.session.request(method, url, params=params, data=data, auth=self.auth, timeout=30) - return r.json() + return self._determine_response(r) def _send_paginated_message(self, endpoint, params=None): """ Send API message that results in a paginated response. @@ -353,7 +347,7 @@ def _send_paginated_message(self, endpoint, params=None): url = self.url + endpoint while True: r = self.session.get(url, params=params, auth=self.auth, timeout=30) - results = r.json() + results = self._determine_response(r) for result in results: yield result # If there are no more pages, we're done. Otherwise update `after` diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py deleted file mode 100644 index 2fc222b..0000000 --- a/gdax/authenticated_client.py +++ /dev/null @@ -1,289 +0,0 @@ -# -# gdax/AuthenticatedClient.py -# Daniel Paquin -# -# For authenticated requests to the gdax exchange - -import hmac -import hashlib -import time -import requests -import base64 -import json -from requests.auth import AuthBase -from gdax.public_client import PublicClient -from gdax.gdax_auth import GdaxAuth - - -class AuthenticatedClient(PublicClient): - def __init__(self, key, b64secret, passphrase, api_url="https://api.gdax.com", timeout=30): - super(AuthenticatedClient, self).__init__(api_url) - self.auth = GdaxAuth(key, b64secret, passphrase) - self.timeout = timeout - - def get_account(self, account_id): - r = requests.get(self.url + '/accounts/' + account_id, auth=self.auth, timeout=self.timeout) - return self._determine_response(r) - - def get_accounts(self): - return self.get_account('') - - def get_account_history(self, account_id): - result = [] - r = requests.get(self.url + '/accounts/{}/ledger'.format(account_id), auth=self.auth, timeout=self.timeout) - r_json = self._determine_response(r) - result.append(r_json) - if "cb-after" in r.headers: - self.history_pagination(account_id, result, r.headers["cb-after"]) - return result - - def history_pagination(self, account_id, result, after): - r = requests.get(self.url + '/accounts/{}/ledger?after={}'.format(account_id, str(after)), auth=self.auth, timeout=self.timeout) - r_json = self._determine_response(r) - if r_json: - result.append(r_json) - if "cb-after" in r.headers: - self.history_pagination(account_id, result, r.headers["cb-after"]) - return result - - def get_account_holds(self, account_id): - result = [] - r = requests.get(self.url + '/accounts/{}/holds'.format(account_id), auth=self.auth, timeout=self.timeout) - r_json = self._determine_response(r) - result.append(r_json) - if "cb-after" in r.headers: - self.holds_pagination(account_id, result, r.headers["cb-after"]) - return result - - def holds_pagination(self, account_id, result, after): - r = requests.get(self.url + '/accounts/{}/holds?after={}'.format(account_id, str(after)), auth=self.auth, timeout=self.timeout) - r_json = self._determine_response(r) - if r_json: - result.append(r_json) - if "cb-after" in r.headers: - self.holds_pagination(account_id, result, r.headers["cb-after"]) - return result - - def buy(self, **kwargs): - kwargs["side"] = "buy" - if "product_id" not in kwargs: - kwargs["product_id"] = self.product_id - r = requests.post(self.url + '/orders', - data=json.dumps(kwargs), - auth=self.auth, - timeout=self.timeout) - return self._determine_response(r) - - def sell(self, **kwargs): - kwargs["side"] = "sell" - r = requests.post(self.url + '/orders', - data=json.dumps(kwargs), - auth=self.auth, - timeout=self.timeout) - return self._determine_response(r) - - def cancel_order(self, order_id): - r = requests.delete(self.url + '/orders/' + order_id, auth=self.auth, timeout=self.timeout) - return self._determine_response(r) - - def cancel_all(self, product_id=''): - url = self.url + '/orders/' - params = {} - if product_id: - params["product_id"] = product_id - r = requests.delete(url, auth=self.auth, params=params, timeout=self.timeout) - return self._determine_response(r) - - def get_order(self, order_id): - r = requests.get(self.url + '/orders/' + order_id, auth=self.auth, timeout=self.timeout) - return self._determine_response(r) - - def get_orders(self, product_id='', status=[]): - result = [] - url = self.url + '/orders/' - params = {} - if product_id: - params["product_id"] = product_id - if status: - params["status"] = status - r = requests.get(url, auth=self.auth, params=params, timeout=self.timeout) - r_json = self._determine_response(r) - result.append(r_json) - if 'cb-after' in r.headers: - self.paginate_orders(product_id, status, result, r.headers['cb-after']) - return result - - def paginate_orders(self, product_id, status, result, after): - url = self.url + '/orders' - - params = { - "after": str(after), - } - if product_id: - params["product_id"] = product_id - if status: - params["status"] = status - r = requests.get(url, auth=self.auth, params=params, timeout=self.timeout) - r_json = self._determine_response(r) - if r_json: - result.append(r_json) - if 'cb-after' in r.headers: - self.paginate_orders(product_id, status, result, r.headers['cb-after']) - return result - - def get_fills(self, order_id='', product_id='', before='', after='', limit=''): - result = [] - url = self.url + '/fills?' - if order_id: - url += "order_id={}&".format(str(order_id)) - if product_id: - url += "product_id={}&".format(product_id) - if before: - url += "before={}&".format(str(before)) - if after: - url += "after={}&".format(str(after)) - if limit: - url += "limit={}&".format(str(limit)) - r = requests.get(url, auth=self.auth, timeout=self.timeout) - r_json = self._determine_response(r) - result.append(r_json) - if 'cb-after' in r.headers and limit is not len(r_json): - return self.paginate_fills(result, r.headers['cb-after'], order_id=order_id, product_id=product_id) - return result - - def paginate_fills(self, result, after, order_id='', product_id=''): - url = self.url + '/fills?after={}&'.format(str(after)) - if order_id: - url += "order_id={}&".format(str(order_id)) - if product_id: - url += "product_id={}&".format(product_id) - r = requests.get(url, auth=self.auth, timeout=self.timeout) - r_json = self._determine_response(r) - if r_json: - result.append(r_json) - if 'cb-after' in r.headers: - return self.paginate_fills(result, r.headers['cb-after'], order_id=order_id, product_id=product_id) - return result - - def get_fundings(self, result='', status='', after=''): - if not result: - result = [] - url = self.url + '/funding?' - if status: - url += "status={}&".format(str(status)) - if after: - url += 'after={}&'.format(str(after)) - r = requests.get(url, auth=self.auth, timeout=self.timeout) - r_json = self._determine_response(r) - result.append(r_json) - if 'cb-after' in r.headers: - return self.get_fundings(result, status=status, after=r.headers['cb-after']) - return result - - def repay_funding(self, amount='', currency=''): - payload = { - "amount": amount, - "currency": currency # example: USD - } - r = requests.post(self.url + "/funding/repay", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) - return self._determine_response(r) - - def margin_transfer(self, margin_profile_id="", transfer_type="", currency="", amount=""): - payload = { - "margin_profile_id": margin_profile_id, - "type": transfer_type, - "currency": currency, # example: USD - "amount": amount - } - r = requests.post(self.url + "/profiles/margin-transfer", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) - return self._determine_response(r) - - def get_position(self): - r = requests.get(self.url + "/position", auth=self.auth, timeout=self.timeout) - return self._determine_response(r) - - def close_position(self, repay_only=""): - payload = { - "repay_only": repay_only or False - } - r = requests.post(self.url + "/position/close", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) - return self._determine_response(r) - - def deposit(self, amount="", currency="", payment_method_id=""): - payload = { - "amount": amount, - "currency": currency, - "payment_method_id": payment_method_id - } - r = requests.post(self.url + "/deposits/payment-method", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) - return self._determine_response(r) - - def coinbase_deposit(self, amount="", currency="", coinbase_account_id=""): - payload = { - "amount": amount, - "currency": currency, - "coinbase_account_id": coinbase_account_id - } - r = requests.post(self.url + "/deposits/coinbase-account", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) - return self._determine_response(r) - - def withdraw(self, amount="", currency="", payment_method_id=""): - payload = { - "amount": amount, - "currency": currency, - "payment_method_id": payment_method_id - } - r = requests.post(self.url + "/withdrawals/payment-method", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) - return self._determine_response(r) - - def coinbase_withdraw(self, amount="", currency="", coinbase_account_id=""): - payload = { - "amount": amount, - "currency": currency, - "coinbase_account_id": coinbase_account_id - } - r = requests.post(self.url + "/withdrawals/coinbase-account", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) - return self._determine_response(r) - - def crypto_withdraw(self, amount="", currency="", crypto_address=""): - payload = { - "amount": amount, - "currency": currency, - "crypto_address": crypto_address - } - r = requests.post(self.url + "/withdrawals/crypto", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) - return self._determine_response(r) - - def get_payment_methods(self): - r = requests.get(self.url + "/payment-methods", auth=self.auth, timeout=self.timeout) - return self._determine_response(r) - - def get_coinbase_accounts(self): - r = requests.get(self.url + "/coinbase-accounts", auth=self.auth, timeout=self.timeout) - return self._determine_response(r) - - def create_report(self, report_type="", start_date="", end_date="", product_id="", account_id="", report_format="", - email=""): - payload = { - "type": report_type, - "start_date": start_date, - "end_date": end_date, - "product_id": product_id, - "account_id": account_id, - "format": report_format, - "email": email - } - r = requests.post(self.url + "/reports", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) - return self._determine_response(r) - - def get_report(self, report_id=""): - r = requests.get(self.url + "/reports/" + report_id, auth=self.auth, timeout=self.timeout) - return self._determine_response(r) - - def get_trailing_volume(self): - r = requests.get(self.url + "/users/self/trailing-volume", auth=self.auth, timeout=self.timeout) - return self._determine_response(r) - - def get_deposit_address(self, account_id): - r = requests.post(self.url + '/coinbase-accounts/{}/addresses'.format(account_id), auth=self.auth, timeout=self.timeout) - return self._determine_response(r) diff --git a/gdax/exceptions.py b/gdax/exceptions.py deleted file mode 100644 index ab167ad..0000000 --- a/gdax/exceptions.py +++ /dev/null @@ -1,81 +0,0 @@ -class GdaxException(Exception): - """ - Base GDAX Exception - Raised when Bad Response returned from GDAX - See: https://docs.gdax.com/?python#errors - """ - - def __init__(self, message, code): - """ - :param message: Message from GDAX response - :type message: str - :param code: HTTP Code - :type code: int - """ - self._message = message - self._code = code - - @property - def message(self): - return self._message - - @message.setter - def message(self, message): - self._message = message - - @property - def code(self): - return self._code - - @message.setter - def code(self, code): - self._code = code - - -class InvalidGdaxRequest(GdaxException): - """ - Raised on 400 response from GDAX - """ - pass - - -class UnauthorizedGdaxRequest(GdaxException): - """ - Raised on 401 response from GDAX - """ - pass - - -class ForbiddenGdaxRequest(GdaxException): - """ - Raised on 403 response from GDAX - """ - pass - - -class NotFoundGdaxRequest(GdaxException): - """ - Raised on 404 response from GDAX - """ - pass - - -class GdaxRateLimitRequest(GdaxException): - """ - Raised on 429 response from GDAX - """ - pass - - -class UnknownGdaxClientRequest(GdaxException): - """ - Raised on 4XX responses not tracked - """ - pass - - -class InternalErrorGdaxRequest(GdaxException): - """ - Raised on 500 response from GDAX - """ - pass diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 5705bac..0000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -sortedcontainers>=1.5.9 -bintrees==2.0.7 -python-dateutil==2.6.1 -requests==2.13.0 -six==1.10.0 -websocket-client==0.40.0 -pymongo==3.5.1 -pytest>=3.3.0 -pytest-cov>=2.5.0 diff --git a/setup.py b/setup.py index c78c97f..53610c2 100644 --- a/setup.py +++ b/setup.py @@ -19,13 +19,8 @@ long_description = fh.read() setup( -<<<<<<< HEAD name='cbpro', - version='1.1.4', -======= - name='gdax', version='2.0.0', ->>>>>>> Added README info, added self to contributors, bumped version author='Daniel Paquin', author_email='dpaq34@gmail.com', license='MIT', diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py index 7a8fce5..a4aec3c 100644 --- a/tests/test_error_handling.py +++ b/tests/test_error_handling.py @@ -1,23 +1,23 @@ from unittest.mock import patch import pytest -import gdax +import cbpro @pytest.fixture def client(): - return gdax.PublicClient() + return cbpro.PublicClient() @pytest.mark.parametrize("code, exception",[ - (400, gdax.exceptions.InvalidGdaxRequest), - (401, gdax.exceptions.UnauthorizedGdaxRequest), - (403, gdax.exceptions.ForbiddenGdaxRequest), - (404, gdax.exceptions.NotFoundGdaxRequest), - (422, gdax.exceptions.UnknownGdaxClientRequest), - (429, gdax.exceptions.GdaxRateLimitRequest), - (500, gdax.exceptions.InternalErrorGdaxRequest)]) -@patch('requests.get') -def test_gdax_exceptions(mock_request, client, code, exception): + (400, cbpro.exceptions.InvalidCbproRequest), + (401, cbpro.exceptions.UnauthorizedCbproRequest), + (403, cbpro.exceptions.ForbiddenCbproRequest), + (404, cbpro.exceptions.NotFoundCbproRequest), + (422, cbpro.exceptions.UnknownCbproClientRequest), + (429, cbpro.exceptions.CbproRateLimitRequest), + (500, cbpro.exceptions.InternalErrorCbproRequest)]) +@patch('requests.Session.request') +def test_cbpro_exceptions(mock_request, client, code, exception): mock_request.return_value.status_code = code with pytest.raises(exception): - client.get_products() + response = client.get_products()