From 024a6c96d4788d234b214e2f2fa3509d572dab33 Mon Sep 17 00:00:00 2001 From: Dj Date: Sun, 29 Mar 2020 09:18:23 -0700 Subject: [PATCH] Retries For All Endpoints, fixes #347 (#457) * test_improvements - Add __init__.py files to tests dirs so you can run all tests * test_improvements - added helpers file, restructured tests to work without previous data and to be grouped with api type * http_retries - Implement Retry for all requests * Readme - Update README with contributing info * PR Feedback - Added CONTRIBUTING.md, fixed README, fixed test --- CONTRIBUTING.md | 18 + requirements.txt | 2 +- spotipy/client.py | 115 ++-- tests/__init__.py | 0 tests/helpers.py | 19 + tests/integration/__init__.py | 0 tests/integration/test_non_user_endpoints.py | 17 +- tests/integration/test_user_endpoints.py | 539 ++++++++++--------- tests/unit/__init__.py | 0 9 files changed, 382 insertions(+), 328 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 tests/__init__.py create mode 100644 tests/helpers.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/unit/__init__.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1b94f52 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,18 @@ +## Contributing +If you would like to contribute to spotipy follow these steps: + +### Export the needed environment variables +```bash +export SPOTIPY_CLIENT_ID=client_id_here +export SPOTIPY_CLIENT_SECRET=client_secret_here +export SPOTIPY_CLIENT_USERNAME=client_username_here # This is actually an id not spotify display name +export SPOTIPY_REDIRECT_URI=http://localhost/ # Make url is set in app you created to get your ID and SECRET +``` + +### Create virtual enevironment, install dependencies, run tests: +```bash +$ virtualenv --python=python3.7 env +(env) $ pip install requirements.txt +(env) $ python -m unittest discover -v tests +``` + diff --git a/requirements.txt b/requirements.txt index 6ef1b3b..accca78 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ mock==2.0.0 requests==2.20.0 -six==1.10.0 \ No newline at end of file +six==1.10.0 diff --git a/spotipy/client.py b/spotipy/client.py index 3a08ca7..f9b7831 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -8,10 +8,10 @@ __all__ = ["Spotify", "SpotifyException"] import json import sys -import time import warnings import requests +import urllib3 import six @@ -53,7 +53,8 @@ class Spotify(object): trace = False # Enable tracing? trace_out = False - max_get_retries = 10 + max_retries = 3 + default_retry_codes = (429, 500, 502, 503, 504) def __init__( self, @@ -63,7 +64,11 @@ class Spotify(object): oauth_manager=None, auth_manager=None, proxies=None, - requests_timeout=None, + requests_timeout=5, + status_forcelist=None, + retries=max_retries, + status_retries=max_retries, + backoff_factor=0.3, ): """ Creates a Spotify API client. @@ -86,6 +91,15 @@ class Spotify(object): :param requests_timeout: Tell Requests to stop waiting for a response after a given number of seconds + :param status_forcelist: + Tell requests what type of status codes retries should occur on + :param retries: + Total number of retries to allow + :param status_retries: + Number of times to retry on bad status codes + :param backoff_factor: + A backoff factor to apply between attempts after the second try + See urllib3 https://urllib3.readthedocs.io/en/latest/reference/urllib3.util.html """ self.prefix = "https://api.spotify.com/v1/" self._auth = auth @@ -94,16 +108,18 @@ class Spotify(object): self.auth_manager = auth_manager self.proxies = proxies self.requests_timeout = requests_timeout + self.status_forcelist = status_forcelist or self.default_retry_codes + self.backoff_factor = backoff_factor + self.retries = retries + self.status_retries = status_retries if isinstance(requests_session, requests.Session): self._session = requests_session else: if requests_session: # Build a new session. - self._session = requests.Session() + self._build_session() else: # Use the Requests API module as a "session". - from requests import api - - self._session = api + self._session = requests.api @property def auth_manager(self): @@ -118,6 +134,20 @@ class Spotify(object): self.client_credentials_manager or self.oauth_manager ) + def _build_session(self): + self._session = requests.Session() + retry = urllib3.Retry( + total=self.retries, + connect=None, + read=False, + status=self.status_retries, + backoff_factor=self.backoff_factor, + status_forcelist=self.status_forcelist) + + adapter = requests.adapters.HTTPAdapter(max_retries=retry) + self._session.mount('http://', adapter) + self._session.mount('https://', adapter) + def _auth_headers(self): if self._auth: return {"Authorization": "Bearer {0}".format(self._auth)} @@ -129,7 +159,6 @@ class Spotify(object): def _internal_call(self, method, url, payload, params): args = dict(params=params) - args["timeout"] = self.requests_timeout if not url.startswith("http"): url = self.prefix + url headers = self._auth_headers() @@ -147,37 +176,44 @@ class Spotify(object): if self.trace_out: print(url) - with self._session.request( - method, url, headers=headers, proxies=self.proxies, **args - ) as r: + try: + response = self._session.request( + method, url, headers=headers, proxies=self.proxies, + timeout=self.requests_timeout, **args + ) if self.trace: # pragma: no cover print() print("Request headers:", headers) - print("Response headers:", r.headers) - print("HTTP status", r.status_code) - print(method, r.url) + print("Response headers:", response.headers) + print("HTTP status", response.status_code) + print(method, response.url) if payload: print("Data", json.dumps(payload)) + response.raise_for_status() + results = response.json() + except requests.exceptions.HTTPError: try: - r.raise_for_status() - except BaseException: - try: - msg = r.json()["error"]["message"] - except BaseException: - msg = "error" - raise SpotifyException( - r.status_code, - -1, - "%s:\n %s" % (r.url, msg), - headers=r.headers, - ) + msg = response.json()["error"]["message"] + except (ValueError, KeyError): + msg = "error" - try: - results = r.json() - except BaseException: - results = None + raise SpotifyException( + response.status_code, + -1, + "%s:\n %s" % (response.url, msg), + headers=response.headers, + ) + except requests.exceptions.RetryError: + raise SpotifyException( + 599, + -1, + "%s:\n %s" % (response.url, "Max Retries"), + headers=response.headers, + ) + except ValueError: + results = None if self.trace: # pragma: no cover print("Response:", results) @@ -187,23 +223,8 @@ class Spotify(object): def _get(self, url, args=None, payload=None, **kwargs): if args: kwargs.update(args) - retries = self.max_get_retries - delay = 0 - while retries > 0: - try: - return self._internal_call("GET", url, payload, kwargs) - except SpotifyException as e: - retries -= 1 - delay += 1 - status = e.http_status - # 429 means we hit a rate limit, back-off - if not (status == 429 or status >= 500 and status < 600): - raise - sleep_seconds = int( - e.headers.get("Retry-After", delay) - ) - print("retrying after..." + str(sleep_seconds) + "secs") - time.sleep(sleep_seconds + 1) + + return self._internal_call("GET", url, payload, kwargs) def _post(self, url, args=None, payload=None, **kwargs): if args: diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..0f42ad0 --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,19 @@ +import base64 +import requests + + +def get_spotify_playlist(spotify_object, playlist_name, username): + playlists = spotify_object.user_playlists(username) + while playlists: + for item in playlists['items']: + if item['name'] == playlist_name: + return item + playlists = spotify_object.next(playlists) + + +def create_spotify_playlist(spotify_object, playlist_name, username): + return spotify_object.user_playlist_create(username, playlist_name) + + +def get_as_base64(url): + return base64.b64encode(requests.get(url).content).decode("utf-8") diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/test_non_user_endpoints.py b/tests/integration/test_non_user_endpoints.py index 287f6b9..fdb5886 100644 --- a/tests/integration/test_non_user_endpoints.py +++ b/tests/integration/test_non_user_endpoints.py @@ -189,15 +189,11 @@ class AuthTestSpotipy(unittest.TestCase): def test_search_timeout(self): client_credentials_manager = SpotifyClientCredentials() - sp = spotipy.Spotify( - client_credentials_manager=client_credentials_manager, - requests_timeout=.01) + sp = spotipy.Spotify(requests_timeout=0.01, + client_credentials_manager=client_credentials_manager) - try: + with self.assertRaises(requests.exceptions.Timeout): sp.search(q='my*', type='track') - self.assertTrue(False, 'unexpected search timeout') - except requests.exceptions.Timeout: - self.assertTrue(True, 'expected search timeout') def test_album_search(self): results = self.spotify.search(q='weezer pinkerton', type='album') @@ -302,10 +298,9 @@ class AuthTestSpotipy(unittest.TestCase): sess.close() def test_force_no_requests_session(self): - from requests import Session with_no_session = spotipy.Spotify( client_credentials_manager=SpotifyClientCredentials(), requests_session=False) - self.assertFalse(isinstance(with_no_session._session, Session)) - self.assertTrue(with_no_session.user(user="akx") - ["uri"] == "spotify:user:akx") + self.assertNotIsInstance(with_no_session._session, requests.Session) + user = with_no_session.user(user="akx") + self.assertEqual(user["uri"], "spotify:user:akx") diff --git a/tests/integration/test_user_endpoints.py b/tests/integration/test_user_endpoints.py index 671335f..1705f6d 100644 --- a/tests/integration/test_user_endpoints.py +++ b/tests/integration/test_user_endpoints.py @@ -1,20 +1,4 @@ -# -*- coding: utf-8 -*- - -""" -These tests require user authentication - provide client credentials using the -following environment variables - -:: - - 'SPOTIPY_CLIENT_USERNAME' - 'SPOTIPY_CLIENT_ID' - 'SPOTIPY_CLIENT_SECRET' - 'SPOTIPY_REDIRECT_URI' -""" -from __future__ import print_function - import os -import sys from spotipy import ( CLIENT_CREDS_ENV_VARS as CCEV, @@ -23,63 +7,23 @@ from spotipy import ( SpotifyException, ) import unittest -import warnings import requests -from pprint import pprint # noqa +from tests import helpers -class AuthTestSpotipy(unittest.TestCase): - """ - These tests require user authentication - provide client credentials using - the following environment variables - - :: - - 'SPOTIPY_CLIENT_USERNAME' - 'SPOTIPY_CLIENT_ID' - 'SPOTIPY_CLIENT_SECRET' - 'SPOTIPY_REDIRECT_URI' - """ - - playlist = "spotify:user:plamere:playlist:2oCEWyyAPbZp9xhVSxZavx" - playlist_new_id = "spotify:playlist:7GlxpQjjxRjmbb3RP2rDqI" - four_tracks = ["spotify:track:6RtPijgfPKROxEzTHNRiDp", - "spotify:track:7IHOIqZUUInxjVkko181PB", - "4VrWlk8IQxevMvERoX08iC", - "http://open.spotify.com/track/3cySlItpiPiIAzU3NyHCJf"] - - two_tracks = ["spotify:track:6RtPijgfPKROxEzTHNRiDp", - "spotify:track:7IHOIqZUUInxjVkko181PB"] - - other_tracks = ["spotify:track:2wySlB6vMzCbQrRnNGOYKa", - "spotify:track:29xKs5BAHlmlX1u4gzQAbJ", - "spotify:track:1PB7gRWcvefzu7t3LJLUlf"] - - album_ids = ["spotify:album:6kL09DaURb7rAoqqaA51KU", - "spotify:album:6RTzC0rDbvagTSJLlY7AKl"] - - bad_id = 'BAD_ID' - +class SpotipyPlaylistApiTest(unittest.TestCase): @classmethod - def setUpClass(self): - if sys.version_info >= (3, 2): - # >= Python3.2 only - warnings.filterwarnings( - "ignore", - category=ResourceWarning, # noqa - message="unclosed.*") + def setUpClass(cls): + cls.four_tracks = ["spotify:track:6RtPijgfPKROxEzTHNRiDp", + "spotify:track:7IHOIqZUUInxjVkko181PB", + "4VrWlk8IQxevMvERoX08iC", + "http://open.spotify.com/track/3cySlItpiPiIAzU3NyHCJf"] + cls.other_tracks = ["spotify:track:2wySlB6vMzCbQrRnNGOYKa", + "spotify:track:29xKs5BAHlmlX1u4gzQAbJ", + "spotify:track:1PB7gRWcvefzu7t3LJLUlf"] + cls.username = os.getenv(CCEV['client_username']) - missing = list(filter(lambda var: not os.getenv(CCEV[var]), CCEV)) - - if missing: - raise Exception( - ('Please set the client credentials for the test application' - ' using the following environment variables: {}').format( - CCEV.values())) - - self.username = os.getenv(CCEV['client_username']) - - self.scope = ( + scope = ( 'playlist-modify-public ' 'user-library-read ' 'user-follow-read ' @@ -88,52 +32,25 @@ class AuthTestSpotipy(unittest.TestCase): 'user-top-read ' 'user-follow-modify ' 'user-read-recently-played ' - 'ugc-image-upload' + 'ugc-image-upload ' + 'user-read-playback-state' ) - self.token = prompt_for_user_token(self.username, scope=self.scope) + token = prompt_for_user_token(cls.username, scope=scope) - self.spotify = Spotify(auth=self.token) + cls.spotify = Spotify(auth=token) - # Helper - def get_or_create_spotify_playlist(self, playlist_name): - playlists = self.spotify.user_playlists(self.username) - while playlists: - for item in playlists['items']: - if item['name'] == playlist_name: - return item - playlists = self.spotify.next(playlists) - return self.spotify.user_playlist_create( - self.username, playlist_name) - - # Helper - def get_as_base64(self, url): - import base64 - return base64.b64encode(requests.get(url).content).decode("utf-8") - - def test_track_bad_id(self): - try: - self.spotify.track(self.bad_id) - self.assertTrue(False) - except SpotifyException: - self.assertTrue(True) - - def test_basic_user_profile(self): - user = self.spotify.user(self.username) - self.assertTrue(user['id'] == self.username.lower()) - - def test_current_user(self): - user = self.spotify.current_user() - self.assertTrue(user['id'] == self.username.lower()) - - def test_me(self): - user = self.spotify.me() - self.assertTrue(user['id'] == self.username.lower()) + cls.new_playlist_name = 'spotipy-playlist-test' + cls.new_playlist = helpers.get_spotify_playlist( + cls.spotify, cls.new_playlist_name, cls.username) or \ + helpers.create_spotify_playlist( + cls.spotify, cls.new_playlist_name, cls.username) + cls.new_playlist_uri = cls.new_playlist['uri'] def test_user_playlists(self): playlists = self.spotify.user_playlists(self.username, limit=5) self.assertTrue('items' in playlists) - self.assertTrue(len(playlists['items']) == 5) + self.assertGreaterEqual(len(playlists['items']), 1) def test_user_playlist_tracks(self): playlists = self.spotify.user_playlists(self.username, limit=5) @@ -142,53 +59,135 @@ class AuthTestSpotipy(unittest.TestCase): user = playlist['owner']['id'] pid = playlist['id'] results = self.spotify.user_playlist_tracks(user, pid) - self.assertTrue(len(results['items']) >= 0) - - def test_current_user_saved_albums(self): - # List - albums = self.spotify.current_user_saved_albums() - self.assertTrue(len(albums['items']) > 1) - - # Add - self.spotify.current_user_saved_albums_add(self.album_ids) - - # Contains - self.assertTrue( - self.spotify.current_user_saved_albums_contains( - self.album_ids) == [ - True, True]) - - # Remove - self.spotify.current_user_saved_albums_delete(self.album_ids) - albums = self.spotify.current_user_saved_albums() - self.assertTrue(len(albums['items']) > 1) + self.assertEquals(len(results['items']), 0) def test_current_user_playlists(self): playlists = self.spotify.current_user_playlists(limit=10) self.assertTrue('items' in playlists) - self.assertTrue(len(playlists['items']) == 10) + self.assertGreaterEqual(len(playlists['items']), 1) + self.assertLessEqual(len(playlists['items']), 10) def test_user_playlist_follow(self): + user_to_follow = 'plamere' + user_to_follow_id = '4erXB04MxwRAVqcUEpu30O' self.spotify.user_playlist_follow_playlist( - 'plamere', '4erXB04MxwRAVqcUEpu30O') + user_to_follow, user_to_follow_id) follows = self.spotify.user_playlist_is_following( - 'plamere', '4erXB04MxwRAVqcUEpu30O', [ - self.spotify.current_user()['id']]) + user_to_follow, user_to_follow_id, [self.username]) self.assertTrue(len(follows) == 1, 'proper follows length') self.assertTrue(follows[0], 'is following') self.spotify.user_playlist_unfollow( - 'plamere', '4erXB04MxwRAVqcUEpu30O') + user_to_follow, user_to_follow_id) follows = self.spotify.user_playlist_is_following( - 'plamere', '4erXB04MxwRAVqcUEpu30O', [ - self.spotify.current_user()['id']]) + user_to_follow, user_to_follow_id, [self.username]) self.assertTrue(len(follows) == 1, 'proper follows length') self.assertFalse(follows[0], 'is no longer following') + def test_user_playlist_replace_tracks(self): + # add tracks to playlist + self.spotify.user_playlist_add_tracks( + self.username, self.new_playlist['id'], self.four_tracks) + playlist = self.spotify.user_playlist(self.username, self.new_playlist['id']) + self.assertEqual(playlist['tracks']['total'], 4) + self.assertEqual(len(playlist['tracks']['items']), 4) + + # replace with 3 other tracks + self.spotify.user_playlist_replace_tracks(self.username, + self.new_playlist['id'], + self.other_tracks) + playlist = self.spotify.user_playlist(self.username, + self.new_playlist['id']) + self.assertEqual(playlist['tracks']['total'], 3) + self.assertEqual(len(playlist['tracks']['items']), 3) + + self.spotify.user_playlist_remove_all_occurrences_of_tracks( + self.username, playlist['id'], self.other_tracks) + playlist = self.spotify.user_playlist(self.username, self.new_playlist['id']) + self.assertEqual(playlist["tracks"]["total"], 0) + + def test_get_playlist_by_id(self): + pl = self.spotify.playlist(self.new_playlist['id']) + self.assertEqual(pl["tracks"]["total"], 0) + + def test_playlist_add_tracks(self): + # add tracks to playlist + self.spotify.user_playlist_add_tracks( + self.username, self.new_playlist['id'], self.other_tracks) + playlist = self.spotify.user_playlist(self.username, self.new_playlist['id']) + self.assertEqual(playlist['tracks']['total'], 3) + self.assertEqual(len(playlist['tracks']['items']), 3) + + pl = self.spotify.playlist_tracks(self.new_playlist['id'], limit=2) + self.assertEqual(len(pl["items"]), 2) + + self.spotify.user_playlist_remove_all_occurrences_of_tracks( + self.username, playlist['id'], self.other_tracks) + playlist = self.spotify.user_playlist(self.username, self.new_playlist['id']) + self.assertEqual(playlist["tracks"]["total"], 0) + + def test_playlist_cover_image(self): + # Upload random dog image + r = requests.get('https://dog.ceo/api/breeds/image/random') + dog_base64 = helpers.get_as_base64(r.json()['message']) + self.spotify.playlist_upload_cover_image(self.new_playlist_uri, dog_base64) + + res = self.spotify.playlist_cover_image(self.new_playlist_uri) + self.assertEquals(len(res), 1) + first_image = res[0] + self.assertIn('width', first_image) + self.assertIn('height', first_image) + self.assertIn('url', first_image) + + def test_deprecated_starred(self): + pl = self.spotify.user_playlist(self.username) + self.assertTrue(pl["tracks"] is None) + self.assertTrue(pl["owner"] is None) + + def test_deprecated_user_playlist(self): + # Test without user due to change from + # https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ + pl = self.spotify.user_playlist(None, self.new_playlist['id']) + self.assertEqual(pl["tracks"]["total"], 0) + + +class SpotipyLibraryApiTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.four_tracks = ["spotify:track:6RtPijgfPKROxEzTHNRiDp", + "spotify:track:7IHOIqZUUInxjVkko181PB", + "4VrWlk8IQxevMvERoX08iC", + "http://open.spotify.com/track/3cySlItpiPiIAzU3NyHCJf"] + cls.album_ids = ["spotify:album:6kL09DaURb7rAoqqaA51KU", + "spotify:album:6RTzC0rDbvagTSJLlY7AKl"] + cls.username = os.getenv(CCEV['client_username']) + + scope = ( + 'playlist-modify-public ' + 'user-library-read ' + 'user-follow-read ' + 'user-library-modify ' + 'user-read-private ' + 'user-top-read ' + 'user-follow-modify ' + 'user-read-recently-played ' + 'ugc-image-upload ' + 'user-read-playback-state' + ) + + token = prompt_for_user_token(cls.username, scope=scope) + + cls.spotify = Spotify(auth=token) + + def test_track_bad_id(self): + with self.assertRaises(SpotifyException): + self.spotify.track('BadID123') + def test_current_user_saved_tracks(self): + # TODO make this not fail if someone doesnthave saved tracks tracks = self.spotify.current_user_saved_tracks() - self.assertTrue(len(tracks['items']) > 0) + self.assertGreater(len(tracks['items']), 0) def test_current_user_save_and_unsave_tracks(self): tracks = self.spotify.current_user_saved_tracks() @@ -197,168 +196,146 @@ class AuthTestSpotipy(unittest.TestCase): tracks = self.spotify.current_user_saved_tracks() new_total = tracks['total'] - self.assertTrue(new_total - total == len(self.four_tracks)) + self.assertEquals(new_total - total, len(self.four_tracks)) tracks = self.spotify.current_user_saved_tracks_delete( self.four_tracks) tracks = self.spotify.current_user_saved_tracks() new_total = tracks['total'] - self.assertTrue(new_total == total) + self.assertEquals(new_total, total) - def test_categories(self): - response = self.spotify.categories() - self.assertTrue(len(response['categories']) > 0) + def test_current_user_saved_albums(self): + # Add + self.spotify.current_user_saved_albums_add(self.album_ids) + albums = self.spotify.current_user_saved_albums() + self.assertGreaterEqual(len(albums['items']), 2) - def test_category_playlists(self): - response = self.spotify.categories() - for cat in response['categories']['items']: - cat_id = cat['id'] - response = self.spotify.category_playlists(category_id=cat_id) - if len(response['playlists']["items"]) > 0: - break - self.assertTrue(True) + # Contains + resp = self.spotify.current_user_saved_albums_contains(self.album_ids) + self.assertEquals(resp, [True, True]) - def test_new_releases(self): - response = self.spotify.new_releases() - self.assertTrue(len(response['albums']) > 0) + # Remove + self.spotify.current_user_saved_albums_delete(self.album_ids) + resp = self.spotify.current_user_saved_albums_contains(self.album_ids) + self.assertEquals(resp, [False, False]) - def test_featured_releases(self): - response = self.spotify.featured_playlists() - self.assertTrue(len(response['playlists']) > 0) - def test_current_user_follows(self): - response = self.spotify.current_user_followed_artists() - artists = response['artists'] - self.assertTrue(len(artists['items']) > 0) +class SpotipyUserApiTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.username = os.getenv(CCEV['client_username']) + + scope = ( + 'playlist-modify-public ' + 'user-library-read ' + 'user-follow-read ' + 'user-library-modify ' + 'user-read-private ' + 'user-top-read ' + 'user-follow-modify ' + 'user-read-recently-played ' + 'ugc-image-upload ' + 'user-read-playback-state' + ) + + token = prompt_for_user_token(cls.username, scope=scope) + + cls.spotify = Spotify(auth=token) + + def test_basic_user_profile(self): + user = self.spotify.user(self.username) + self.assertEquals(user['id'], self.username.lower()) + + def test_current_user(self): + user = self.spotify.current_user() + self.assertEquals(user['id'], self.username.lower()) + + def test_me(self): + user = self.spotify.me() + self.assertTrue(user['id'] == self.username.lower()) def test_current_user_top_tracks(self): response = self.spotify.current_user_top_tracks() items = response['items'] - self.assertTrue(len(items) > 0) + self.assertGreater(len(items), 0) def test_current_user_top_artists(self): response = self.spotify.current_user_top_artists() items = response['items'] - self.assertTrue(len(items) > 0) + self.assertGreater(len(items), 0) - def test_current_user_recently_played(self): - # No cursor - res = self.spotify.current_user_recently_played() - self.assertTrue(len(res['items']) <= 50) - played_at = res['items'][0]['played_at'] - # Using `before` gives tracks played before - res = self.spotify.current_user_recently_played( - before=res['cursors']['after']) - self.assertTrue(len(res['items']) <= 50) - self.assertTrue(res['items'][0]['played_at'] < played_at) - played_at = res['items'][0]['played_at'] +class SpotipyBrowseApiTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + username = os.getenv(CCEV['client_username']) + token = prompt_for_user_token(username) + cls.spotify = Spotify(auth=token) - # Using `after` gives tracks played after - res = self.spotify.current_user_recently_played( - after=res['cursors']['before']) - self.assertTrue(len(res['items']) <= 50) - self.assertTrue(res['items'][0]['played_at'] > played_at) + def test_categories(self): + response = self.spotify.categories() + self.assertGreater(len(response['categories']), 0) - def test_user_playlist_ops(self): - sp = self.spotify - # create empty playlist - playlist = self.get_or_create_spotify_playlist( - 'spotipy-testing-playlist-1') - playlist_id = playlist['id'] + def test_category_playlists(self): + response = self.spotify.categories() + category = 'rock' + for cat in response['categories']['items']: + cat_id = cat['id'] + if cat_id == category: + response = self.spotify.category_playlists(category_id=cat_id) + self.assertGreater(len(response['playlists']["items"]), 0) - # remove all tracks from it - sp.user_playlist_replace_tracks( - self.username, playlist_id, []) - playlist = sp.user_playlist(self.username, playlist_id) - self.assertTrue(playlist['tracks']['total'] == 0) - self.assertTrue(len(playlist['tracks']['items']) == 0) + def test_new_releases(self): + response = self.spotify.new_releases() + self.assertGreater(len(response['albums']), 0) - # add tracks to it - sp.user_playlist_add_tracks( - self.username, playlist_id, self.four_tracks) - playlist = sp.user_playlist(self.username, playlist_id) - self.assertTrue(playlist['tracks']['total'] == 4) - self.assertTrue(len(playlist['tracks']['items']) == 4) + def test_featured_releases(self): + response = self.spotify.featured_playlists() + self.assertGreater(len(response['playlists']), 0) - # remove two tracks from it - sp.user_playlist_remove_all_occurrences_of_tracks(self.username, - playlist_id, - self.two_tracks) - playlist = sp.user_playlist(self.username, playlist_id) - self.assertTrue(playlist['tracks']['total'] == 2) - self.assertTrue(len(playlist['tracks']['items']) == 2) +class SpotipyFollowApiTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.username = os.getenv(CCEV['client_username']) - # replace with 3 other tracks - sp.user_playlist_replace_tracks(self.username, - playlist_id, - self.other_tracks) - playlist = sp.user_playlist(self.username, playlist_id) - self.assertTrue(playlist['tracks']['total'] == 3) - self.assertTrue(len(playlist['tracks']['items']) == 3) + scope = ( + 'playlist-modify-public ' + 'user-library-read ' + 'user-follow-read ' + 'user-library-modify ' + 'user-read-private ' + 'user-top-read ' + 'user-follow-modify ' + 'user-read-recently-played ' + 'ugc-image-upload ' + 'user-read-playback-state' + ) - def test_playlist(self): - # New playlist ID - pl = self.spotify.playlist(self.playlist_new_id) - self.assertTrue(pl["tracks"]["total"] > 0) + token = prompt_for_user_token(cls.username, scope=scope) - # Old playlist ID - pl = self.spotify.playlist(self.playlist) - self.assertTrue(pl["tracks"]["total"] > 0) + cls.spotify = Spotify(auth=token) - def test_playlist_tracks(self): - # New playlist ID - pl = self.spotify.playlist_tracks(self.playlist_new_id, limit=2) - self.assertTrue(len(pl["items"]) == 2) - self.assertTrue(pl["total"] > 0) - - # Old playlist ID - pl = self.spotify.playlist_tracks(self.playlist, limit=2) - self.assertTrue(len(pl["items"]) == 2) - self.assertTrue(pl["total"] > 0) - - def test_playlist_upload_cover_image(self): - pl1 = self.get_or_create_spotify_playlist('spotipy-testing-playlist-1') - plid = pl1['uri'] - old_b64 = pl1['images'][0]['url'] - - # Upload random dog image - r = requests.get('https://dog.ceo/api/breeds/image/random') - dog_base64 = self.get_as_base64(r.json()['message']) - self.spotify.playlist_upload_cover_image(plid, dog_base64) - - # Image must be different - pl1 = self.spotify.playlist(plid) - new_b64 = self.get_as_base64(pl1['images'][0]['url']) - self.assertTrue(old_b64 != new_b64) - - def test_playlist_cover_image(self): - pl = self.get_or_create_spotify_playlist('spotipy-testing-playlist-1') - plid = pl['uri'] - res = self.spotify.playlist_cover_image(plid) - - self.assertTrue(len(res) > 0) - first_image = res[0] - self.assertTrue('width' in first_image) - self.assertTrue('height' in first_image) - self.assertTrue('url' in first_image) + def test_current_user_follows(self): + response = self.spotify.current_user_followed_artists() + artists = response['artists'] + self.assertGreater(len(artists['items']), 0) def test_user_follows_and_unfollows_artist(self): # Initially follows 1 artist res = self.spotify.current_user_followed_artists() - self.assertTrue(res['artists']['total'] == 1) + self.assertEqual(res['artists']['total'], 1) # Follow 2 more artists artists = ["6DPYiyq5kWVQS4RGwxzPC7", "0NbfKEOTQCcwd6o7wSDOHI"] self.spotify.user_follow_artists(artists) res = self.spotify.current_user_followed_artists() - self.assertTrue(res['artists']['total'] == 3) + self.assertEqual(res['artists']['total'], 3) # Unfollow these 2 artists self.spotify.user_unfollow_artists(artists) res = self.spotify.current_user_followed_artists() - self.assertTrue(res['artists']['total'] == 1) + self.assertEqual(res['artists']['total'], 1) def test_user_follows_and_unfollows_user(self): # TODO improve after implementing `me/following/contains` @@ -370,25 +347,49 @@ class AuthTestSpotipy(unittest.TestCase): # Unfollow these 2 users self.spotify.user_unfollow_users(users) - def test_deprecated_starred(self): - pl = self.spotify.user_playlist(self.username) - self.assertTrue(pl["tracks"] is None) - self.assertTrue(pl["owner"] is None) - def test_deprecated_user_playlist(self): - # Test without user due to change from - # https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ - pl = self.spotify.user_playlist(None, self.playlist) - self.assertTrue(pl["tracks"]["total"] > 0) +class SpotipyPlayerApiTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.username = os.getenv(CCEV['client_username']) - def test_deprecated_user_playlis(self): - # Test without user due to change from - # https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ - pl = self.spotify.user_playlist_tracks(None, self.playlist, limit=2) - self.assertTrue(len(pl["items"]) == 2) - self.assertTrue(pl["total"] > 0) + scope = ( + 'playlist-modify-public ' + 'user-library-read ' + 'user-follow-read ' + 'user-library-modify ' + 'user-read-private ' + 'user-top-read ' + 'user-follow-modify ' + 'user-read-recently-played ' + 'ugc-image-upload ' + 'user-read-playback-state' + ) + + token = prompt_for_user_token(cls.username, scope=scope) + + cls.spotify = Spotify(auth=token) def test_devices(self): # No devices playing by default res = self.spotify.devices() - self.assertEqual(len(res["devices"]), 0) + self.assertEquals(len(res["devices"]), 0) + + def test_current_user_recently_played(self): + # No cursor + res = self.spotify.current_user_recently_played() + self.assertLessEqual(len(res['items']), 50) + played_at = res['items'][0]['played_at'] + + # Using `before` gives tracks played before + res = self.spotify.current_user_recently_played( + before=res['cursors']['after']) + self.assertLessEqual(len(res['items']), 50) + self.assertTrue(res['items'][0]['played_at'] < played_at) + played_at = res['items'][0]['played_at'] + + # Using `after` gives tracks played after + res = self.spotify.current_user_recently_played( + after=res['cursors']['before']) + self.assertLessEqual(len(res['items']), 50) + self.assertGreater(res['items'][0]['played_at'], played_at) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29