Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add error handling, fix broken unit tests #260

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,27 @@ directory run:
python -m pytest
```

### 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 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 Cbpro error states are discovered.

## Change Log
*2.0*
Copy link

Choose a reason for hiding this comment

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

Why are you bumping the version to 2.0, is it not backwards compatible anymore?

Copy link
Author

Choose a reason for hiding this comment

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

This is not backwards compatible, as it will raise exceptions instead of returning a response object.

- Added error handling to all HTTP requests [Error Handling Docs](#error-handling)

*1.1.2* **Current PyPI release**
- Refactor project for Coinbase Pro
- Major overhaul on how pagination is handled
Expand Down
1 change: 1 addition & 0 deletions cbpro/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
81 changes: 81 additions & 0 deletions cbpro/exceptions.py
Original file line number Diff line number Diff line change
@@ -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
54 changes: 52 additions & 2 deletions cbpro/public_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
#
# For public requests to the Coinbase exchange

from http import HTTPStatus
import requests
from cbpro import exceptions


class PublicClient(object):
Expand All @@ -28,6 +30,54 @@ def __init__(self, api_url='https://api.pro.coinbase.com', timeout=30):
self.url = api_url.rstrip('/')
self.auth = None
self.session = requests.Session()
self.timeout = timeout

def _is_http_success(self, code):
# type: (int) -> bool
return code >= HTTPStatus.OK and code < HTTPStatus.MULTIPLE_CHOICES

def _is_http_client_error(self, code):
# type: (int) -> bool
return code >= HTTPStatus.BAD_REQUEST and code < HTTPStatus.INTERNAL_SERVER_ERROR

def _is_http_server_error(self, code):
# type: (int) -> bool
return code >= HTTPStatus.INTERNAL_SERVER_ERROR

def _determine_response(self, response):
"""
Determines if CBPRO response is success or error
If success, returns response json
If error, raises appropriate CbproException
"""
if self._is_http_success(response.status_code):
return response.json()
elif self._is_http_client_error(response.status_code):
body = response.json()
message = body.get('message')
if response.status_code == HTTPStatus.BAD_REQUEST:
raise exceptions.InvalidCbproRequest(message,
HTTPStatus.BAD_REQUEST)
elif response.status_code == HTTPStatus.UNAUTHORIZED:
raise exceptions.UnauthorizedCbproRequest(message,
HTTPStatus.UNAUTHORIZED)
elif response.status_code == HTTPStatus.FORBIDDEN:
raise exceptions.ForbiddenCbproRequest(message,
HTTPStatus.FORBIDDEN)
elif response.status_code == HTTPStatus.NOT_FOUND:
raise exceptions.NotFoundCbproRequest(message,
HTTPStatus.NOT_FOUND)
elif response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
raise exceptions.CbproRateLimitRequest(message,
HTTPStatus.TOO_MANY_REQUESTS)
else: # Other 4XX response not yet mapped
raise exceptions.UnknownCbproClientRequest(message,
response.status_code)

elif self._is_http_server_error(response.status_code):
body = response.json()
raise exceptions.InternalErrorCbproRequest(body.get('message'),
HTTPStatus.INTERNAL_SERVER_ERROR)

def get_products(self):
"""Get a list of available currency pairs for trading.
Expand Down Expand Up @@ -267,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.
Expand Down Expand Up @@ -297,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`
Expand Down
3 changes: 2 additions & 1 deletion contributors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ Leonard Lin
Jeff Gibson
David Caseria
Paul Mestemaker
Drew Rice
Drew Rice
Benjamin Ross
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

setup(
name='cbpro',
version='1.1.4',
version='2.0.0',
author='Daniel Paquin',
author_email='[email protected]',
license='MIT',
Expand Down
23 changes: 23 additions & 0 deletions tests/test_error_handling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from unittest.mock import patch
import pytest
import cbpro


@pytest.fixture
def client():
return cbpro.PublicClient()


@pytest.mark.parametrize("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):
response = client.get_products()