Skip to content

Commit

Permalink
Catch 404 in paging next, add exception hierarchy
Browse files Browse the repository at this point in the history
  • Loading branch information
felix-hilden committed Feb 17, 2020
1 parent 55b5171 commit 478bea4
Show file tree
Hide file tree
Showing 21 changed files with 234 additions and 126 deletions.
1 change: 1 addition & 0 deletions readme_pypi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Added

Fixed
*****
- Retrieving all items and pages of a search respects API limits (#145)
- Always return an awaitable in paging navigation (#146)

1.1.0 (2020-02-02)
Expand Down
3 changes: 2 additions & 1 deletion tekore/client/api/album.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from tekore.client.process import single, model_list
from tekore.client.base import SpotifyBase, send_and_process, maximise_limit
from tekore.client.decor import send_and_process, maximise_limit
from tekore.client.base import SpotifyBase
from tekore.serialise import ModelList
from tekore.model import FullAlbum, SimpleTrackPaging

Expand Down
3 changes: 2 additions & 1 deletion tekore/client/api/artist.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import List, Union

from tekore.client.process import single, model_list
from tekore.client.base import SpotifyBase, send_and_process, maximise_limit
from tekore.client.decor import send_and_process, maximise_limit
from tekore.client.base import SpotifyBase
from tekore.serialise import ModelList
from tekore.model import FullArtist, SimpleAlbumPaging, FullTrack, AlbumGroup

Expand Down
3 changes: 2 additions & 1 deletion tekore/client/api/browse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from tekore.client.api.browse.validate import validate_attributes
from tekore.client.process import single, top_item, multiple
from tekore.client.base import SpotifyBase, send_and_process, maximise_limit
from tekore.client.decor import send_and_process, maximise_limit
from tekore.client.base import SpotifyBase
from tekore.model import (
SimplePlaylistPaging,
SimpleAlbumPaging,
Expand Down
3 changes: 2 additions & 1 deletion tekore/client/api/follow.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import List

from tekore.client.process import single, nothing
from tekore.client.base import SpotifyBase, send_and_process, maximise_limit
from tekore.client.decor import send_and_process, maximise_limit
from tekore.client.base import SpotifyBase
from tekore.model import FullArtistCursorPaging


Expand Down
3 changes: 2 additions & 1 deletion tekore/client/api/library.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import List

from tekore.client.process import single, nothing
from tekore.client.base import SpotifyBase, send_and_process, maximise_limit
from tekore.client.decor import send_and_process, maximise_limit
from tekore.client.base import SpotifyBase
from tekore.model import SavedAlbumPaging, SavedTrackPaging


Expand Down
5 changes: 3 additions & 2 deletions tekore/client/api/personalisation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from tekore.model import FullArtistOffsetPaging, FullTrackPaging
from tekore.client.process import single
from tekore.client.base import SpotifyBase, send_and_process, maximise_limit
from tekore.client.decor import send_and_process, maximise_limit
from tekore.client.base import SpotifyBase
from tekore.model import FullArtistOffsetPaging, FullTrackPaging


class SpotifyPersonalisation(SpotifyBase):
Expand Down
3 changes: 2 additions & 1 deletion tekore/client/api/player/modify.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

from tekore.model import RepeatState
from tekore.convert import to_uri
from tekore.client.base import SpotifyBase
from tekore.client.decor import send_and_process
from tekore.client.process import nothing
from tekore.client.base import SpotifyBase, send_and_process


def offset_to_dict(offset: Union[int, str]):
Expand Down
3 changes: 2 additions & 1 deletion tekore/client/api/player/view.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import List

from tekore.client.process import single, model_list
from tekore.client.base import SpotifyBase, send_and_process, maximise_limit
from tekore.client.decor import send_and_process, maximise_limit
from tekore.client.base import SpotifyBase
from tekore.serialise import ModelList
from tekore.model import (
CurrentlyPlayingContext,
Expand Down
3 changes: 2 additions & 1 deletion tekore/client/api/playlist/modify.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from requests import Request

from tekore.client.process import single, nothing
from tekore.client.base import SpotifyBase, build_url, send_and_process
from tekore.client.decor import send_and_process
from tekore.client.base import SpotifyBase, build_url
from tekore.model import FullPlaylist


Expand Down
3 changes: 2 additions & 1 deletion tekore/client/api/playlist/tracks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import List, Tuple

from tekore.client.process import top_item, nothing
from tekore.client.base import SpotifyBase, send_and_process
from tekore.client.decor import send_and_process
from tekore.client.base import SpotifyBase
from tekore.convert import to_uri


Expand Down
3 changes: 2 additions & 1 deletion tekore/client/api/playlist/view.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Union

from tekore.client.process import single, model_list, single_or_dict
from tekore.client.base import SpotifyBase, send_and_process, maximise_limit
from tekore.client.decor import send_and_process, maximise_limit
from tekore.client.base import SpotifyBase
from tekore.serialise import ModelList
from tekore.model import (
SimplePlaylistPaging,
Expand Down
15 changes: 13 additions & 2 deletions tekore/client/api/search.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from tekore.client.base import SpotifyBase, send_and_process, maximise_limit
from tekore.client.decor import send_and_process, maximise_limit
from tekore.client.base import SpotifyBase
from tekore.model import (
FullArtistOffsetPaging,
FullTrackPaging,
Expand Down Expand Up @@ -37,13 +38,15 @@ def search(
Search for an item.
Requires the user-read-private scope.
Returns 404 - Not Found if limit+offset would be above 5000.
Parameters
----------
query
search query
types
items to return: 'artist', 'album', 'track' and/or 'playlist'
resources to search for, tuple of strings containing
artist, album, track and/or playlist
market
an ISO 3166-1 alpha-2 country code or 'from_token'
limit
Expand All @@ -58,6 +61,14 @@ def search(
tuple
paging objects containing the types of items searched for
in the order that they were specified in 'types'
Examples
--------
.. code:: python
tracks, = spotify.search('monty python')
artists, = spotify.search('sheeran', types=('artist',))
albums, tracks = spotify.search('piano', types=('album', 'track'))
"""
return self._get(
'search',
Expand Down
3 changes: 2 additions & 1 deletion tekore/client/api/track.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from tekore.client.process import single, model_list
from tekore.client.base import SpotifyBase, send_and_process
from tekore.client.decor import send_and_process
from tekore.client.base import SpotifyBase
from tekore.serialise import ModelList
from tekore.model import FullTrack, AudioFeatures, AudioAnalysis

Expand Down
3 changes: 2 additions & 1 deletion tekore/client/api/user.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from tekore.model import PublicUser, PrivateUser
from tekore.client.base import SpotifyBase
from tekore.client.decor import send_and_process
from tekore.client.process import single
from tekore.client.base import SpotifyBase, send_and_process


class SpotifyUser(SpotifyBase):
Expand Down
100 changes: 2 additions & 98 deletions tekore/client/base.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import json

from typing import Optional, Callable
from functools import wraps
from requests import Request, Response, HTTPError
from typing import Optional
from requests import Request

from tekore.sender import Sender, Client
from tekore.model.error import PlayerErrorReason

prefix = 'https://api.spotify.com/v1/'
error_format = """Error in {url}:
{code}: {msg}
"""


def build_url(url: str) -> str:
Expand All @@ -24,97 +19,6 @@ def parse_url_params(params: Optional[dict]) -> Optional[dict]:
return {k: v for k, v in params.items() if v is not None} or None


def parse_json(response):
try:
return response.json()
except ValueError:
return None


def parse_error_reason(response):
content = parse_json(response)
reason = getattr(response, 'reason', '')

if content is None:
return reason

error = content['error']
message = error.get('message', reason)
if 'reason' in error:
message += '\n' + PlayerErrorReason[error['reason']].value
return message


def handle_errors(request: Request, response: Response) -> None:
if response.status_code >= 400:
error_str = error_format.format(
url=response.url,
code=response.status_code,
msg=parse_error_reason(response)
)
raise HTTPError(error_str, request=request, response=response)


def send_and_process(post_func: Callable) -> Callable:
"""
Decorate a function to send a request and process its content.
The first parameter of a decorated function must be the instance (self)
of a client with a :meth:`_send` method.
The instance must also have :attr:`is_async`, based on which a synchronous
or an asynchronous function is used in the process.
The decorated function must return a :class:`requests.Request`.
The result of ``post_func`` is returned to the caller.
Parameters
----------
post_func
function to call with response JSON content
"""
def decorator(function: Callable[..., Request]) -> Callable:
async def async_send(self, request: Request):
response = await self._send(request)
handle_errors(request, response)
content = parse_json(response)
return post_func(content)

@wraps(function)
def wrapper(self, *args, **kwargs):
request = function(self, *args, **kwargs)

if self.is_async:
return async_send(self, request)

response = self._send(request)
handle_errors(request, response)
content = parse_json(response)
return post_func(content)
return wrapper
return decorator


def maximise_limit(max_limit: int) -> Callable:
"""
Decorate a function to maximise the value of a 'limit' argument.
Parameters
----------
max_limit
maximum value of the limit
"""
def decorator(function: Callable) -> Callable:
varnames = function.__code__.co_varnames
arg_pos = varnames.index('limit') - 1

@wraps(function)
def wrapper(self, *args, **kwargs):
if self.max_limits_on and len(args) < arg_pos:
kwargs.setdefault('limit', max_limit)
return function(self, *args, **kwargs)
return wrapper
return decorator


class SpotifyBase(Client):
def __init__(
self,
Expand Down
65 changes: 65 additions & 0 deletions tekore/client/decor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from typing import Callable
from functools import wraps

from requests import Request
from tekore.client.decor.handle import handle_errors, parse_json


def send_and_process(post_func: Callable) -> Callable:
"""
Decorate a function to send a request and process its content.
The first parameter of a decorated function must be the instance (self)
of a client with a :meth:`_send` method.
The instance must also have :attr:`is_async`, based on which a synchronous
or an asynchronous function is used in the process.
The decorated function must return a :class:`requests.Request`.
The result of ``post_func`` is returned to the caller.
Parameters
----------
post_func
function to call with response JSON content
"""
def decorator(function: Callable[..., Request]) -> Callable:
async def async_send(self, request: Request):
response = await self._send(request)
handle_errors(request, response)
content = parse_json(response)
return post_func(content)

@wraps(function)
def wrapper(self, *args, **kwargs):
request = function(self, *args, **kwargs)

if self.is_async:
return async_send(self, request)

response = self._send(request)
handle_errors(request, response)
content = parse_json(response)
return post_func(content)
return wrapper
return decorator


def maximise_limit(max_limit: int) -> Callable:
"""
Decorate a function to maximise the value of a 'limit' argument.
Parameters
----------
max_limit
maximum value of the limit
"""
def decorator(function: Callable) -> Callable:
varnames = function.__code__.co_varnames
arg_pos = varnames.index('limit') - 1

@wraps(function)
def wrapper(self, *args, **kwargs):
if self.max_limits_on and len(args) < arg_pos:
kwargs.setdefault('limit', max_limit)
return function(self, *args, **kwargs)
return wrapper
return decorator
Loading

0 comments on commit 478bea4

Please sign in to comment.