Skip to content

Commit

Permalink
Relocate and unify web errors, fixes #154
Browse files Browse the repository at this point in the history
  • Loading branch information
felix-hilden committed May 21, 2020
1 parent 5e3fde6 commit 18e66c5
Show file tree
Hide file tree
Showing 15 changed files with 165 additions and 84 deletions.
7 changes: 7 additions & 0 deletions docs/src/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ serialisation clear. (:issue:`149`)
- :meth:`pprint <tekore.model.serialise.Serialisable.pprint>` output is now
compact by default

Web exceptions
~~~~~~~~~~~~~~
Exceptions thrown by :mod:`auth <tekore.auth>` now match :mod:`client <tekore.client>`.
Because of that, :class:`OAuthError` was removed.
Web exceptions were moved to :mod:`error <tekore.error>`
and inherit from a common base class. (:issue:`154`)

1.7.0 (2020-04-28)
------------------
Added
Expand Down
4 changes: 2 additions & 2 deletions tekore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

from tekore import scope
from tekore.auth import Credentials, RefreshingCredentials
from tekore.auth.expiring import OAuthError
from tekore.client import Spotify
from tekore.client.decor.error import (
from tekore.error import (
HTTPError,
BadRequest,
Unauthorised,
Forbidden,
Expand Down
18 changes: 10 additions & 8 deletions tekore/auth/expiring.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,13 @@
from requests import HTTPError, Request, Response
from urllib.parse import urlencode

from tekore.error import errors
from tekore.sender import Sender, Client

OAUTH_AUTHORIZE_URL = 'https://accounts.spotify.com/authorize'
OAUTH_TOKEN_URL = 'https://accounts.spotify.com/api/token'


class OAuthError(HTTPError):
pass


def b64encode(msg: str) -> str:
"""
Base 64 encoding for Unicode strings.
Expand Down Expand Up @@ -94,16 +91,21 @@ def is_expiring(self) -> bool:


def handle_errors(response: Response) -> None:
if 400 <= response.status_code < 500:
if response.status_code < 400:
return

if response.status_code < 500:
content = response.json()
error_str = '{} {}: {}'.format(
response.status_code,
content['error'],
content['error_description']
)
raise OAuthError(error_str)
elif response.status_code >= 500:
raise HTTPError('Unexpected error!', response=response)
else:
error_str = 'Unexpected error!'

error_cls = errors.get(response.status_code, HTTPError)
raise error_cls(error_str, response=response)


def parse_token(response):
Expand Down
45 changes: 0 additions & 45 deletions tekore/client/decor/error.py

This file was deleted.

2 changes: 1 addition & 1 deletion tekore/client/decor/handle.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from requests import Request, Response, HTTPError

from tekore.error import errors
from tekore.model.error import PlayerErrorReason
from tekore.client.decor.error import errors

error_format = """Error in {url}:
{code}: {msg}
Expand Down
2 changes: 1 addition & 1 deletion tekore/client/paging.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import Generator, Optional

from tekore.model import Model
from tekore.error import NotFound
from tekore.client.base import SpotifyBase
from tekore.model.paging import Paging, OffsetPaging
from tekore.client.decor import send_and_process
from tekore.client.decor.error import NotFound


def parse_paging_result(result):
Expand Down
113 changes: 113 additions & 0 deletions tekore/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
Web exceptions for :mod:`auth <tekore.auth>` and :mod:`client <tekore.client>`.
Clients facing the Web API raise errors when recieving bad status codes.
Specific errors or all web errors can be caught.
.. code:: python
import tekore as tk
conf = tk.config_from_environment()
token = tk.request_client_token(*conf[:2])
spotify = tk.Spotify(token)
try:
spotify.album('not-a-real-album')
except tk.BadRequest:
print('Whoops, bad request!')
except tk.HTTPError:
print('How did you get here?')
"""

import requests


class HTTPError(requests.HTTPError):
"""
Base error for all web errors.
"""


class BadRequest(HTTPError):
"""
400 - Bad request
The request could not be understood by the server due to malformed syntax.
"""


class Unauthorised(HTTPError):
"""
401 - Unauthorised
The request requires user authentication or,
if the request included authorization credentials,
authorization has been refused for those credentials.
"""


class Forbidden(HTTPError):
"""
403 - Forbidden
The server understood the request, but is refusing to fulfill it.
"""


class NotFound(HTTPError):
"""
404 - Not found
The requested resource could not be found.
This error can be due to a temporary or permanent condition.
"""


class TooManyRequests(HTTPError):
"""
429 - Too many requests
Rate limiting has been applied.
"""


class InternalServerError(HTTPError):
"""
500 - Internal server error
You should never receive this error because the clever coders at Spotify
catch them all... But if you are unlucky enough to get one,
please report it to Spotify through their GitHub (spotify/web-api).
"""


class BadGateway(HTTPError):
"""
502 - Bad gateway
The server was acting as a gateway or proxy and received
an invalid response from the upstream server.
"""


class ServiceUnavailable(HTTPError):
"""
503 - Service unavailable
The server is currently unable to handle the request due to a temporary
condition which will be alleviated after some delay.
You can choose to resend the request again.
"""


errors = {
400: BadRequest,
401: Unauthorised,
403: Forbidden,
404: NotFound,
429: TooManyRequests,
500: InternalServerError,
502: BadGateway,
503: ServiceUnavailable,
}
37 changes: 20 additions & 17 deletions tests/_cred.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,9 @@
"""

import os
import tekore as tk

from unittest import TestCase, SkipTest
from requests.exceptions import HTTPError

from tekore.auth import Credentials
from tekore.util import config_from_environment
from tekore.client import Spotify
from tekore.sender import PersistentSender, RetryingSender

skip_is_fail = os.getenv('TEKORE_TEST_SKIP_IS_FAIL', None)

Expand All @@ -34,7 +29,7 @@ class TestCaseWithAppEnvironment(TestCase):
def setUpClass(cls):
super().setUpClass()

cred = config_from_environment()
cred = tk.config_from_environment()
if any(i is None for i in cred):
skip_or_fail(KeyError, 'No application credentials!')

Expand Down Expand Up @@ -72,19 +67,23 @@ class TestCaseWithCredentials(TestCaseWithAppEnvironment):
def setUpClass(cls):
super().setUpClass()

cls.cred = Credentials(
cls.cred = tk.Credentials(
cls.client_id,
cls.client_secret,
cls.redirect_uri
)

try:
cls.app_token = cls.cred.request_client_token()
except HTTPError as e:
skip_or_fail(HTTPError, 'Error in retrieving application token!', e)
except tk.HTTPError as error:
skip_or_fail(
tk.HTTPError,
'Error in retrieving application token!',
error
)

sender = RetryingSender(sender=PersistentSender())
cls.client = Spotify(cls.app_token, sender=sender)
sender = tk.RetryingSender(sender=tk.PersistentSender())
cls.client = tk.Spotify(cls.app_token, sender=sender)


class TestCaseWithUserCredentials(
Expand All @@ -100,16 +99,20 @@ def setUpClass(cls):

try:
cls.user_token = cls.cred.refresh_user_token(cls.user_refresh)
except HTTPError as e:
skip_or_fail(HTTPError, 'Error in retrieving user token!', e)
except tk.HTTPError as error:
skip_or_fail(
tk.HTTPError,
'Error in retrieving user token!',
error
)

cls.client.token = cls.user_token

try:
cls.current_user_id = cls.client.current_user().id
except HTTPError as e:
except tk.HTTPError as error:
skip_or_fail(
HTTPError,
tk.HTTPError,
'ID of current user could not be retrieved!',
e
error
)
8 changes: 4 additions & 4 deletions tests/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
from unittest import TestCase
from unittest.mock import MagicMock, patch

from requests import HTTPError
from tekore.auth.expiring import AccessToken, Token, Credentials, OAuthError
from tekore import HTTPError
from tekore.auth.expiring import AccessToken, Token, Credentials
from tekore.auth.refreshing import RefreshingCredentials, RefreshingToken

from tests._cred import TestCaseWithEnvironment, TestCaseWithCredentials
Expand Down Expand Up @@ -101,10 +101,10 @@ def test_async_refresh_user_token(self):
c = Credentials(self.client_id, self.client_secret, asynchronous=True)
run(c.refresh_user_token(self.user_refresh))

def test_bad_arguments_raises_oauth_error(self):
def test_bad_arguments_raises_error(self):
c = Credentials('id', 'secret')

with self.assertRaises(OAuthError):
with self.assertRaises(HTTPError):
c.request_client_token()


Expand Down
2 changes: 1 addition & 1 deletion tests/client/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import unittest
from unittest.mock import MagicMock

from requests import HTTPError
from tekore import HTTPError
from tekore.client.base import SpotifyBase
from tekore.model.error import PlayerErrorReason

Expand Down
2 changes: 1 addition & 1 deletion tests/client/episode.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from tests._cred import TestCaseWithCredentials, TestCaseWithUserCredentials
from ._resources import episode_id, episode_ids

from requests import HTTPError
from tekore import HTTPError


class TestSpotifyEpisodeAsApp(TestCaseWithCredentials):
Expand Down
2 changes: 1 addition & 1 deletion tests/client/full.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from unittest import TestCase
from unittest.mock import MagicMock

from tekore.client.decor.error import BadRequest
from tekore import BadRequest
from tekore.client.chunked import chunked, return_none, return_last
from tekore.client import Spotify
from tests._cred import TestCaseWithCredentials, TestCaseWithUserCredentials
Expand Down
3 changes: 2 additions & 1 deletion tests/client/player.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from time import sleep
from requests import HTTPError

from tests._cred import TestCaseWithUserCredentials, skip_or_fail
from ._resources import track_ids, album_id, episode_id

from tekore.convert import to_uri
from tekore import HTTPError


class TestSpotifyPlayerSequence(TestCaseWithUserCredentials):
Expand Down
2 changes: 1 addition & 1 deletion tests/client/search.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from requests import HTTPError
from tekore import HTTPError
from tests._cred import TestCaseWithUserCredentials


Expand Down

0 comments on commit 18e66c5

Please sign in to comment.