diff --git a/massive/exceptions.py b/massive/exceptions.py index b8960763..d5b1c40b 100644 --- a/massive/exceptions.py +++ b/massive/exceptions.py @@ -1,3 +1,9 @@ +from typing import Optional + +# the package star-imports this module; keep Optional out of its namespace +__all__ = ["AuthError", "BadResponse"] + + class AuthError(Exception): """ Empty or invalid API key @@ -9,6 +15,11 @@ class AuthError(Exception): class BadResponse(Exception): """ Non-200 response from API + + The message is the response body. ``status`` is the HTTP status code, or + None if not given. """ - pass + def __init__(self, *args: object, status: Optional[int] = None): + super().__init__(*args) + self.status = status diff --git a/massive/rest/base.py b/massive/rest/base.py index 294dd482..7b728685 100644 --- a/massive/rest/base.py +++ b/massive/rest/base.py @@ -152,7 +152,7 @@ def _get( ) if resp.status != 200: - raise BadResponse(resp.data.decode("utf-8")) + raise BadResponse(resp.data.decode("utf-8"), status=resp.status) if raw: return resp diff --git a/test_rest/test_bad_response.py b/test_rest/test_bad_response.py new file mode 100644 index 00000000..23c86e7b --- /dev/null +++ b/test_rest/test_bad_response.py @@ -0,0 +1,38 @@ +from massive.exceptions import BadResponse +from base import BaseTest + +import pook # type: ignore + + +class BadResponseTest(BaseTest): + def test_status_is_set(self): + cases = [ + (403, '{"status":"NOT_AUTHORIZED","message":"not entitled"}'), + (404, '{"status":"NOT_FOUND","message":"not found"}'), + ] + for status, body in cases: + with self.subTest(status=status): + ticker = f"ERR{status}" # not covered by the fixture mocks + pook.get( + self.c.BASE + "/v3/reference/tickers/" + ticker, + reply=status, + response_body=body, + ) + with self.assertRaises(BadResponse) as ctx: + self.c.get_ticker_details(ticker) + self.assertEqual(ctx.exception.status, status) + self.assertEqual(ctx.exception.args, (body,)) + self.assertEqual(str(ctx.exception), body) + + def test_status_defaults_to_none(self): + for args in [(), ("boom",), ("boom", 1)]: + with self.subTest(args=args): + exc = BadResponse(*args) + self.assertEqual(exc.args, args) + self.assertIsNone(exc.status) + + def test_status_does_not_change_message(self): + exc = BadResponse("boom", status=500) + self.assertEqual(exc.args, ("boom",)) + self.assertEqual(str(exc), "boom") + self.assertEqual(exc.status, 500)