From c8e045891bb157afde75b5eb545aa8cb9d663d47 Mon Sep 17 00:00:00 2001 From: Peter-Schorn Date: Wed, 14 Apr 2021 12:40:08 -0500 Subject: [PATCH] Renamed the `auth` parameter of `Spotify.__init__` to `access_token` for better clarity. Removed the `client_credentials_manager` and `oauth_manager` parameters because they are redundant. Replaced the `set_auth` and `auth_manager` properties with standard attributes. Removed the following deprecated methods from `Spotify`: * `playlist_tracks` * `user_playlist` * `user_playlist_tracks` * `user_playlist_change_details` * `user_playlist_unfollow` * `user_playlist_add_tracks` * `user_playlist_replace_tracks` * `user_playlist_reorder_tracks` * `user_playlist_remove_all_occurrences_of_tracks` * `user_playlist_remove_specific_occurrences_of_tracks` * `user_playlist_follow_playlist` * `user_playlist_is_following` Removed the deprecated `as_dict` parameter from the `get_access_token` method of `SpotifyOAuth` and `SpotifyPKCE`. Removed the deprecated `get_cached_token` and `_save_token_info` methods of `SpotifyOAuth` and `SpotifyPKCE`. Removed `SpotifyImplicitGrant`. Removed `prompt_for_user_token`. --- examples/artist_albums.py | 2 +- examples/artist_discography.py | 4 +- examples/artist_recommendations.py | 4 +- examples/audio_analysis_for_track.py | 4 +- examples/audio_features.py | 4 +- examples/audio_features_for_track.py | 4 +- examples/client_credentials_flow.py | 4 +- examples/multiple_accounts.py | 10 - examples/player.py | 2 +- examples/playlist_all_non_local_tracks.py | 2 +- examples/playlist_tracks.py | 2 +- examples/read_a_playlist.py | 4 +- examples/search.py | 2 +- examples/show_album.py | 2 +- examples/show_artist.py | 2 +- examples/show_artist_top_tracks.py | 2 +- examples/show_related.py | 4 +- examples/show_track_info.py | 2 +- examples/show_tracks.py | 4 +- examples/show_user.py | 4 +- examples/simple_artist_albums.py | 4 +- examples/simple_artist_top_tracks.py | 4 +- examples/simple_search_artist.py | 4 +- examples/simple_search_artist_image_url.py | 2 +- examples/title_chain.py | 4 +- examples/tracks.py | 4 +- examples/user_public_playlists.py | 4 +- spotipy/client.py | 362 +------------- spotipy/oauth2.py | 480 +++---------------- spotipy/util.py | 137 +----- tests/integration/non_user_endpoints/test.py | 17 +- tests/integration/user_endpoints/test.py | 79 ++- tests/unit/test_oauth.py | 204 +------- tests/unit/test_scopes.py | 25 + 34 files changed, 202 insertions(+), 1196 deletions(-) delete mode 100644 examples/multiple_accounts.py diff --git a/examples/artist_albums.py b/examples/artist_albums.py index c5cc7a6..81d56df 100644 --- a/examples/artist_albums.py +++ b/examples/artist_albums.py @@ -7,7 +7,7 @@ import spotipy logger = logging.getLogger('examples.artist_albums') logging.basicConfig(level='INFO') -sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) +sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials()) def get_args(): diff --git a/examples/artist_discography.py b/examples/artist_discography.py index 84f9c26..3104af5 100644 --- a/examples/artist_discography.py +++ b/examples/artist_discography.py @@ -69,6 +69,6 @@ def main(): if __name__ == '__main__': - client_credentials_manager = SpotifyClientCredentials() - sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) + auth_manager = SpotifyClientCredentials() + sp = spotipy.Spotify(auth_manager=auth_manager) main() diff --git a/examples/artist_recommendations.py b/examples/artist_recommendations.py index 40a95a2..4b4c6e0 100644 --- a/examples/artist_recommendations.py +++ b/examples/artist_recommendations.py @@ -8,8 +8,8 @@ from spotipy.oauth2 import SpotifyClientCredentials logger = logging.getLogger('examples.artist_recommendations') logging.basicConfig(level='INFO') -client_credentials_manager = SpotifyClientCredentials() -sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) +auth_manager = SpotifyClientCredentials() +sp = spotipy.Spotify(auth_manager=auth_manager) def get_args(): diff --git a/examples/audio_analysis_for_track.py b/examples/audio_analysis_for_track.py index 1bef5e9..9f13e1f 100644 --- a/examples/audio_analysis_for_track.py +++ b/examples/audio_analysis_for_track.py @@ -7,8 +7,8 @@ import time import sys -client_credentials_manager = SpotifyClientCredentials() -sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) +auth_manager = SpotifyClientCredentials() +sp = spotipy.Spotify(auth_manager=auth_manager) if len(sys.argv) > 1: tid = sys.argv[1] diff --git a/examples/audio_features.py b/examples/audio_features.py index 4657a97..c81b23d 100644 --- a/examples/audio_features.py +++ b/examples/audio_features.py @@ -7,8 +7,8 @@ import time import sys -client_credentials_manager = SpotifyClientCredentials() -sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) +auth_manager = SpotifyClientCredentials() +sp = spotipy.Spotify(auth_manager=auth_manager) sp.trace = False if len(sys.argv) > 1: diff --git a/examples/audio_features_for_track.py b/examples/audio_features_for_track.py index e345ca6..ddebda6 100644 --- a/examples/audio_features_for_track.py +++ b/examples/audio_features_for_track.py @@ -7,8 +7,8 @@ import time import sys -client_credentials_manager = SpotifyClientCredentials() -sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) +auth_manager = SpotifyClientCredentials() +sp = spotipy.Spotify(auth_manager=auth_manager) sp.trace = True if len(sys.argv) > 1: diff --git a/examples/client_credentials_flow.py b/examples/client_credentials_flow.py index 856bf5e..cd7e56c 100644 --- a/examples/client_credentials_flow.py +++ b/examples/client_credentials_flow.py @@ -2,8 +2,8 @@ from spotipy.oauth2 import SpotifyClientCredentials import spotipy from pprint import pprint -client_credentials_manager = SpotifyClientCredentials() -sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) +auth_manager = SpotifyClientCredentials() +sp = spotipy.Spotify(auth_manager=auth_manager) search_str = 'Muse' result = sp.search(search_str) diff --git a/examples/multiple_accounts.py b/examples/multiple_accounts.py deleted file mode 100644 index 03f4732..0000000 --- a/examples/multiple_accounts.py +++ /dev/null @@ -1,10 +0,0 @@ -import spotipy -import spotipy.util as util - -from pprint import pprint - -while True: - username = input("Type the Spotify user ID to use: ") - token = util.prompt_for_user_token(username, show_dialog=True) - sp = spotipy.Spotify(token) - pprint(sp.me()) diff --git a/examples/player.py b/examples/player.py index 7ca38dd..82a12ed 100644 --- a/examples/player.py +++ b/examples/player.py @@ -4,7 +4,7 @@ from pprint import pprint from time import sleep scope = "user-read-playback-state,user-modify-playback-state" -sp = spotipy.Spotify(client_credentials_manager=SpotifyOAuth(scope=scope)) +sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) # Shows playing devices res = sp.devices() diff --git a/examples/playlist_all_non_local_tracks.py b/examples/playlist_all_non_local_tracks.py index e0bcb21..1349c91 100644 --- a/examples/playlist_all_non_local_tracks.py +++ b/examples/playlist_all_non_local_tracks.py @@ -6,7 +6,7 @@ import spotipy PlaylistExample = '37i9dQZEVXbMDoHDwVN2tF' # create spotipy client -sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) +sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials()) # load the first 100 songs tracks = [] diff --git a/examples/playlist_tracks.py b/examples/playlist_tracks.py index 360f974..00ae96b 100644 --- a/examples/playlist_tracks.py +++ b/examples/playlist_tracks.py @@ -2,7 +2,7 @@ from spotipy.oauth2 import SpotifyClientCredentials import spotipy from pprint import pprint -sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) +sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials()) pl_id = 'spotify:playlist:5RIbzhG2QqdkaP24iXLnZX' offset = 0 diff --git a/examples/read_a_playlist.py b/examples/read_a_playlist.py index 06cad00..506760b 100644 --- a/examples/read_a_playlist.py +++ b/examples/read_a_playlist.py @@ -2,8 +2,8 @@ from spotipy.oauth2 import SpotifyClientCredentials import spotipy import json -client_credentials_manager = SpotifyClientCredentials() -sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) +auth_manager = SpotifyClientCredentials() +sp = spotipy.Spotify(auth_manager=auth_manager) playlist_id = 'spotify:user:spotifycharts:playlist:37i9dQZEVXbJiZcmkrIHGU' results = sp.playlist(playlist_id) diff --git a/examples/search.py b/examples/search.py index cf81208..f1e3d9b 100644 --- a/examples/search.py +++ b/examples/search.py @@ -10,6 +10,6 @@ if len(sys.argv) > 1: else: search_str = 'Radiohead' -sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) +sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials()) result = sp.search(search_str) pprint.pprint(result) diff --git a/examples/show_album.py b/examples/show_album.py index 8f5e617..f689b5e 100644 --- a/examples/show_album.py +++ b/examples/show_album.py @@ -10,6 +10,6 @@ if len(sys.argv) > 1: else: urn = 'spotify:album:5yTx83u3qerZF7GRJu7eFk' -sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) +sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials()) album = sp.album(urn) pprint(album) diff --git a/examples/show_artist.py b/examples/show_artist.py index 72f18cc..5b270ac 100644 --- a/examples/show_artist.py +++ b/examples/show_artist.py @@ -10,7 +10,7 @@ if len(sys.argv) > 1: else: urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu' -sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) +sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials()) artist = sp.artist(urn) pprint(artist) diff --git a/examples/show_artist_top_tracks.py b/examples/show_artist_top_tracks.py index 97d2f92..7f2df16 100644 --- a/examples/show_artist_top_tracks.py +++ b/examples/show_artist_top_tracks.py @@ -10,7 +10,7 @@ if len(sys.argv) > 1: else: urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu' -sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) +sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials()) response = sp.artist_top_tracks(urn) for track in response['tracks']: diff --git a/examples/show_related.py b/examples/show_related.py index 8791404..423af3b 100644 --- a/examples/show_related.py +++ b/examples/show_related.py @@ -9,8 +9,8 @@ if len(sys.argv) > 1: else: artist_name = 'weezer' -client_credentials_manager = SpotifyClientCredentials() -sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) +auth_manager = SpotifyClientCredentials() +sp = spotipy.Spotify(auth_manager=auth_manager) result = sp.search(q='artist:' + artist_name, type='artist') try: name = result['artists']['items'][0]['name'] diff --git a/examples/show_track_info.py b/examples/show_track_info.py index 353172c..13a709e 100644 --- a/examples/show_track_info.py +++ b/examples/show_track_info.py @@ -10,7 +10,7 @@ if len(sys.argv) > 1: else: urn = 'spotify:track:0Svkvt5I79wficMFgaqEQJ' -sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) +sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials()) track = sp.track(urn) pprint(track) diff --git a/examples/show_tracks.py b/examples/show_tracks.py index efee1d2..bf184e8 100644 --- a/examples/show_tracks.py +++ b/examples/show_tracks.py @@ -15,8 +15,8 @@ if __name__ == '__main__': file = sys.stdin tids = file.read().split() - client_credentials_manager = SpotifyClientCredentials() - sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) + auth_manager = SpotifyClientCredentials() + sp = spotipy.Spotify(auth_manager=auth_manager) for start in range(0, len(tids), max_tracks_per_call): results = sp.tracks(tids[start: start + max_tracks_per_call]) for track in results['tracks']: diff --git a/examples/show_user.py b/examples/show_user.py index 9c010ea..288f271 100644 --- a/examples/show_user.py +++ b/examples/show_user.py @@ -10,8 +10,8 @@ if len(sys.argv) > 1: else: username = 'plamere' -client_credentials_manager = SpotifyClientCredentials() -sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) +auth_manager = SpotifyClientCredentials() +sp = spotipy.Spotify(auth_manager=auth_manager) sp.trace = True user = sp.user(username) pprint.pprint(user) diff --git a/examples/simple_artist_albums.py b/examples/simple_artist_albums.py index 3f8323d..8b99ce6 100644 --- a/examples/simple_artist_albums.py +++ b/examples/simple_artist_albums.py @@ -3,8 +3,8 @@ import spotipy birdy_uri = 'spotify:artist:2WX2uTcsvV5OnS0inACecP' -client_credentials_manager = SpotifyClientCredentials() -sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) +auth_manager = SpotifyClientCredentials() +sp = spotipy.Spotify(auth_manager=auth_manager) results = sp.artist_albums(birdy_uri, album_type='album') albums = results['items'] diff --git a/examples/simple_artist_top_tracks.py b/examples/simple_artist_top_tracks.py index caf5fed..6943d41 100644 --- a/examples/simple_artist_top_tracks.py +++ b/examples/simple_artist_top_tracks.py @@ -3,8 +3,8 @@ import spotipy lz_uri = 'spotify:artist:36QJpDe2go2KgaRleHCDTp' -client_credentials_manager = SpotifyClientCredentials() -sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) +auth_manager = SpotifyClientCredentials() +sp = spotipy.Spotify(auth_manager=auth_manager) results = sp.artist_top_tracks(lz_uri) diff --git a/examples/simple_search_artist.py b/examples/simple_search_artist.py index a89ca42..e0b0e1e 100644 --- a/examples/simple_search_artist.py +++ b/examples/simple_search_artist.py @@ -1,8 +1,8 @@ from spotipy.oauth2 import SpotifyClientCredentials import spotipy -client_credentials_manager = SpotifyClientCredentials() -sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) +auth_manager = SpotifyClientCredentials() +sp = spotipy.Spotify(auth_manager=auth_manager) results = sp.search(q='weezer', limit=20) for i, t in enumerate(results['tracks']['items']): diff --git a/examples/simple_search_artist_image_url.py b/examples/simple_search_artist_image_url.py index f1b8eb3..c3a9cb3 100644 --- a/examples/simple_search_artist_image_url.py +++ b/examples/simple_search_artist_image_url.py @@ -4,7 +4,7 @@ import sys from spotipy.oauth2 import SpotifyClientCredentials import spotipy -sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) +sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials()) if len(sys.argv) > 1: name = ' '.join(sys.argv[1:]) diff --git a/examples/title_chain.py b/examples/title_chain.py index 6cf3b8e..30ce5a6 100644 --- a/examples/title_chain.py +++ b/examples/title_chain.py @@ -9,8 +9,8 @@ import random usage: python title_chain.py [song name] ''' -client_credentials_manager = SpotifyClientCredentials() -sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) +auth_manager = SpotifyClientCredentials() +sp = spotipy.Spotify(auth_manager=auth_manager) skiplist = {'dm', 'remix'} diff --git a/examples/tracks.py b/examples/tracks.py index 33e943e..372e7ca 100644 --- a/examples/tracks.py +++ b/examples/tracks.py @@ -6,8 +6,8 @@ from spotipy.oauth2 import SpotifyClientCredentials import spotipy import sys -client_credentials_manager = SpotifyClientCredentials() -sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) +auth_manager = SpotifyClientCredentials() +sp = spotipy.Spotify(auth_manager=auth_manager) if len(sys.argv) > 1: artist_name = ' '.join(sys.argv[1:]) diff --git a/examples/user_public_playlists.py b/examples/user_public_playlists.py index 2d7f64f..5685bdd 100644 --- a/examples/user_public_playlists.py +++ b/examples/user_public_playlists.py @@ -6,8 +6,8 @@ import sys import spotipy from spotipy.oauth2 import SpotifyClientCredentials -client_credentials_manager = SpotifyClientCredentials() -sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) +auth_manager = SpotifyClientCredentials() +sp = spotipy.Spotify(auth_manager=auth_manager) user = 'spotify' diff --git a/spotipy/client.py b/spotipy/client.py index 3415841..7bf3b22 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -125,11 +125,9 @@ class Spotify: def __init__( self, - auth=None, - requests_session=True, - client_credentials_manager=None, - oauth_manager=None, + access_token=None, auth_manager=None, + requests_session=True, proxies=None, requests_timeout=5, status_forcelist=None, @@ -141,19 +139,16 @@ class Spotify: """ Creates a Spotify API client. - :param auth: An access token (optional) + :param access_token: An access token (optional). If not None, then this parameter + will override the auth_manager parameter. Prefer `auth_manager` over this parameter + because otherwise you cannot refresh the `access_token`. + :param auth_manager: + SpotifyOauth, SpotifyClientCredentials, or SpotifyPKCE object :param requests_session: - A Requests session object or a truthy value to create one. - A falsy value disables sessions. + A Requests session object or a true value to create one. + A false value disables sessions. It should generally be a good idea to keep sessions enabled for performance reasons (connection pooling). - :param client_credentials_manager: - SpotifyClientCredentials object - :param oauth_manager: - SpotifyOAuth object - :param auth_manager: - SpotifyOauth, SpotifyClientCredentials, - or SpotifyImplicitGrant object :param proxies: Definition of proxies (optional). See Requests doc https://2.python-requests.org/en/master/user/advanced/#proxies @@ -173,17 +168,23 @@ class Spotify: The language parameter advertises what language the user prefers to see. See ISO-639-1 language code: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes """ + + if access_token is not None and auth_manager is not None: + warnings.warn( + "Either `access_token` or `auth_manager` should be provided, " + "not both. `auth_manager` will be ignored.", + UserWarning + ) + self.prefix = "https://api.spotify.com/v1/" - self._auth = auth - self.client_credentials_manager = client_credentials_manager - self.oauth_manager = oauth_manager + self.access_token = access_token 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 + self.backoff_factor = backoff_factor self.language = language if isinstance(requests_session, requests.Session): @@ -194,21 +195,6 @@ class Spotify: else: # Use the Requests API module as a "session". self._session = requests.api - def set_auth(self, auth): - self._auth = auth - - @property - def auth_manager(self): - return self._auth_manager - - @auth_manager.setter - def auth_manager(self, auth_manager): - if auth_manager is not None: - self._auth_manager = auth_manager - else: - self._auth_manager = ( - self.client_credentials_manager or self.oauth_manager - ) def __del__(self): """Make sure the connection (pool) gets closed""" @@ -234,12 +220,12 @@ class Spotify: self._session.mount('https://', adapter) def _auth_headers(self): - if self._auth: - return {"Authorization": f"Bearer {self._auth}"} + if self.access_token: + return {"Authorization": "Bearer {0}".format(self.access_token)} if not self.auth_manager: return {} try: - token = self.auth_manager.get_access_token(as_dict=False) + token = self.auth_manager.get_access_token() except TypeError: token = self.auth_manager.get_access_token() return {"Authorization": f"Bearer {token}"} @@ -670,34 +656,6 @@ class Spotify: additional_types=",".join(additional_types), ) - def playlist_tracks( - self, - playlist_id, - fields=None, - limit=100, - offset=0, - market=None, - additional_types=("track",) - ): - """ Get full details of the tracks of a playlist. - - Parameters: - - playlist_id - the playlist ID, URI or URL - - fields - which fields to return - - limit - the maximum number of tracks to return - - offset - the index of the first track to return - - market - an ISO 3166-1 alpha-2 country code. - - additional_types - list of item types to return. - valid types are: track and episode - """ - warnings.warn( - "You should use `playlist_items(playlist_id, ...," - "additional_types=('track',))` instead", - DeprecationWarning, - ) - return self.playlist_items(playlist_id, fields, limit, offset, - market, additional_types) - def playlist_items( self, playlist_id, @@ -752,55 +710,6 @@ class Spotify: content_type="image/jpeg", ) - def user_playlist(self, user, playlist_id=None, fields=None, market=None): - warnings.warn( - "You should use `playlist(playlist_id)` instead", - DeprecationWarning, - ) - - """ Gets a single playlist of a user - - Parameters: - - user - the id of the user - - playlist_id - the id of the playlist - - fields - which fields to return - """ - if playlist_id is None: - return self._get(f"users/{user}/starred") - return self.playlist(playlist_id, fields=fields, market=market) - - def user_playlist_tracks( - self, - user=None, - playlist_id=None, - fields=None, - limit=100, - offset=0, - market=None, - ): - warnings.warn( - "You should use `playlist_tracks(playlist_id)` instead", - DeprecationWarning, - ) - - """ Get full details of the tracks of a playlist owned by a user. - - Parameters: - - user - the id of the user - - playlist_id - the id of the playlist - - fields - which fields to return - - limit - the maximum number of tracks to return - - offset - the index of the first track to return - - market - an ISO 3166-1 alpha-2 country code. - """ - return self.playlist_tracks( - playlist_id, - limit=limit, - offset=offset, - fields=fields, - market=market, - ) - def user_playlists(self, user, limit=50, offset=0): """ Gets playlists of a user @@ -832,233 +741,6 @@ class Spotify: return self._post(f"users/{user}/playlists", payload=data) - def user_playlist_change_details( - self, - user, - playlist_id, - name=None, - public=None, - collaborative=None, - description=None, - ): - """ This function is no longer in use, please use the recommended function in the warning! - - Changes a playlist's name and/or public/private state - - Parameters: - - user - the id of the user - - playlist_id - the id of the playlist - - name - optional name of the playlist - - public - optional is the playlist public - - collaborative - optional is the playlist collaborative - - description - optional description of the playlist - """ - warnings.warn( - "You should use `playlist_change_details(playlist_id, ...)` instead", - DeprecationWarning, - ) - - return self.playlist_change_details(playlist_id, name, public, - collaborative, description) - - def user_playlist_unfollow(self, user, playlist_id): - """ This function is no longer in use, please use the recommended function in the warning! - - Unfollows (deletes) a playlist for a user - - Parameters: - - user - the id of the user - - name - the name of the playlist - """ - warnings.warn( - "You should use `current_user_unfollow_playlist(playlist_id)` instead", - DeprecationWarning, - ) - return self.current_user_unfollow_playlist(playlist_id) - - def user_playlist_add_tracks( - self, user, playlist_id, tracks, position=None - ): - """ This function is no longer in use, please use the recommended function in the warning! - - Adds tracks to a playlist - - Parameters: - - user - the id of the user - - playlist_id - the id of the playlist - - tracks - a list of track URIs, URLs or IDs - - position - the position to add the tracks - """ - warnings.warn( - "You should use `playlist_add_items(playlist_id, tracks)` instead", - DeprecationWarning, - ) - - tracks = [self._get_uri("track", tid) for tid in tracks] - return self.playlist_add_items(playlist_id, tracks, position) - - def user_playlist_add_episodes( - self, user, playlist_id, episodes, position=None - ): - """ This function is no longer in use, please use the recommended function in the warning! - - Adds episodes to a playlist - - Parameters: - - user - the id of the user - - playlist_id - the id of the playlist - - episodes - a list of track URIs, URLs or IDs - - position - the position to add the episodes - """ - warnings.warn( - "You should use `playlist_add_items(playlist_id, episodes)` instead", - DeprecationWarning, - ) - - episodes = [self._get_uri("episode", tid) for tid in episodes] - return self.playlist_add_items(playlist_id, episodes, position) - - def user_playlist_replace_tracks(self, user, playlist_id, tracks): - """ This function is no longer in use, please use the recommended function in the warning! - - Replace all tracks in a playlist for a user - - Parameters: - - user - the id of the user - - playlist_id - the id of the playlist - - tracks - the list of track ids to add to the playlist - """ - warnings.warn( - "You should use `playlist_replace_items(playlist_id, tracks)` instead", - DeprecationWarning, - ) - return self.playlist_replace_items(playlist_id, tracks) - - def user_playlist_reorder_tracks( - self, - user, - playlist_id, - range_start, - insert_before, - range_length=1, - snapshot_id=None, - ): - """ This function is no longer in use, please use the recommended function in the warning! - - Reorder tracks in a playlist from a user - - Parameters: - - user - the id of the user - - playlist_id - the id of the playlist - - range_start - the position of the first track to be reordered - - range_length - optional the number of tracks to be reordered - (default: 1) - - insert_before - the position where the tracks should be - inserted - - snapshot_id - optional playlist's snapshot ID - """ - warnings.warn( - "You should use `playlist_reorder_items(playlist_id, ...)` instead", - DeprecationWarning, - ) - return self.playlist_reorder_items(playlist_id, range_start, - insert_before, range_length, - snapshot_id) - - def user_playlist_remove_all_occurrences_of_tracks( - self, user, playlist_id, tracks, snapshot_id=None - ): - """ This function is no longer in use, please use the recommended function in the warning! - - Removes all occurrences of the given tracks from the given playlist - - Parameters: - - user - the id of the user - - playlist_id - the id of the playlist - - tracks - the list of track ids to remove from the playlist - - snapshot_id - optional id of the playlist snapshot - """ - warnings.warn( - "You should use `playlist_remove_all_occurrences_of_items" - "(playlist_id, tracks)` instead", - DeprecationWarning, - ) - return self.playlist_remove_all_occurrences_of_items(playlist_id, - tracks, - snapshot_id) - - def user_playlist_remove_specific_occurrences_of_tracks( - self, user, playlist_id, tracks, snapshot_id=None - ): - """ This function is no longer in use, please use the recommended function in the warning! - - Removes all occurrences of the given tracks from the given playlist - - Parameters: - - user - the id of the user - - playlist_id - the id of the playlist - - tracks - an array of objects containing Spotify URIs of the - tracks to remove with their current positions in the - playlist. For example: - [ { "uri":"4iV5W9uYEdYUVa79Axb7Rh", "positions":[2] }, - { "uri":"1301WleyT98MSxVHPZCA6M", "positions":[7] } ] - - snapshot_id - optional id of the playlist snapshot - """ - warnings.warn( - "You should use `playlist_remove_specific_occurrences_of_items" - "(playlist_id, tracks)` instead", - DeprecationWarning, - ) - plid = self._get_id("playlist", playlist_id) - ftracks = [] - for tr in tracks: - ftracks.append( - { - "uri": self._get_uri("track", tr["uri"]), - "positions": tr["positions"], - } - ) - payload = {"tracks": ftracks} - if snapshot_id: - payload["snapshot_id"] = snapshot_id - return self._delete( - f"users/{user}/playlists/{plid}/tracks", payload=payload - ) - - def user_playlist_follow_playlist(self, playlist_owner_id, playlist_id): - """ This function is no longer in use, please use the recommended function in the warning! - - Add the current authenticated user as a follower of a playlist. - - Parameters: - - playlist_owner_id - the user id of the playlist owner - - playlist_id - the id of the playlist - """ - warnings.warn( - "You should use `current_user_follow_playlist(playlist_id)` instead", - DeprecationWarning, - ) - return self.current_user_follow_playlist(playlist_id) - - def user_playlist_is_following( - self, playlist_owner_id, playlist_id, user_ids - ): - """ This function is no longer in use, please use the recommended function in the warning! - - Check to see if the given users are following the given playlist - - Parameters: - - playlist_owner_id - the user id of the playlist owner - - playlist_id - the id of the playlist - - user_ids - the ids of the users that you want to check to see - if they follow the playlist. Maximum: 5 ids. - """ - warnings.warn( - "You should use `playlist_is_following(playlist_id, user_ids)` instead", - DeprecationWarning, - ) - return self.playlist_is_following(playlist_id, user_ids) - def playlist_change_details( self, playlist_id, diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index 828d4f0..8937a3a 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -3,7 +3,6 @@ __all__ = [ "SpotifyOAuth", "SpotifyOauthError", "SpotifyStateError", - "SpotifyImplicitGrant", "SpotifyPKCE" ] @@ -11,7 +10,6 @@ import base64 import logging import os import time -import warnings import webbrowser import requests @@ -82,10 +80,7 @@ class SpotifyAuthBase: @staticmethod def _get_user_input(prompt): - try: - return raw_input(prompt) - except NameError: - return input(prompt) + return input(prompt) @staticmethod def is_token_expired(token_info): @@ -133,10 +128,10 @@ class SpotifyClientCredentials(SpotifyAuthBase): self, client_id=None, client_secret=None, + cache_handler=None, proxies=None, requests_session=True, - requests_timeout=None, - cache_handler=None + requests_timeout=None ): """ Creates a Client Credentials Flow Manager. @@ -154,14 +149,16 @@ class SpotifyClientCredentials(SpotifyAuthBase): Parameters: * client_id: Must be supplied or set as environment variable * client_secret: Must be supplied or set as environment variable - * proxies: Optional, proxy for the requests library to route through - * requests_session: A Requests session - * requests_timeout: Optional, tell Requests to stop waiting for a response after - a given number of seconds * cache_handler: An instance of the `CacheHandler` class to handle getting and saving cached authorization tokens. Optional, will otherwise use `CacheFileHandler`. - (takes precedence over `cache_path` and `username`) + * proxies: Optional, proxy for the requests library to route through + * requests_session: A Requests session object or a true value to create one. + A false value disables sessions. + It should generally be a good idea to keep sessions enabled + for performance reasons (connection pooling). + * requests_timeout: Optional, tell Requests to stop waiting for a response after + a given number of seconds """ @@ -179,35 +176,25 @@ class SpotifyClientCredentials(SpotifyAuthBase): else: self.cache_handler = CacheFileHandler() - def get_access_token(self, as_dict=True, check_cache=True): + def get_access_token(self, check_cache=True): """ If a valid access token is in memory, returns it Else fetches a new token and returns it Parameters: - - as_dict - a boolean indicating if returning the access token - as a token_info dictionary, otherwise it will be returned - as a string. + - check_cache - if true, checks for a locally stored token + before requesting a new token. """ - if as_dict: - warnings.warn( - "You're using 'as_dict = True'." - "get_access_token will return the token string directly in future " - "versions. Please adjust your code accordingly, or use " - "get_cached_token instead.", - DeprecationWarning, - stacklevel=2, - ) if check_cache: token_info = self.cache_handler.get_cached_token() if token_info and not self.is_token_expired(token_info): - return token_info if as_dict else token_info["access_token"] + return token_info["access_token"] token_info = self._request_access_token() token_info = self._add_custom_values_to_token_info(token_info) self.cache_handler.save_token_to_cache(token_info) - return token_info if as_dict else token_info["access_token"] + return token_info["access_token"] def _request_access_token(self): """Gets client credentials access token """ @@ -260,14 +247,12 @@ class SpotifyOAuth(SpotifyAuthBase): redirect_uri=None, state=None, scope=None, - cache_path=None, - username=None, + cache_handler=None, proxies=None, show_dialog=False, requests_session=True, requests_timeout=None, - open_browser=True, - cache_handler=None + open_browser=True ): """ Creates a SpotifyOAuth object @@ -280,24 +265,19 @@ class SpotifyOAuth(SpotifyAuthBase): * scope: Optional, either a string of scopes, or an iterable with elements of type `Scope` or `str`. E.g., {Scope.user_modify_playback_state, Scope.user_library_read} - - iterable of scopes or comma separated string of scopes. - e.g, "playlist-read-private,playlist-read-collaborative" - * cache_path: (deprecated) Optional, will otherwise be generated - (takes precedence over `username`) - * username: (deprecated) Optional or set as environment variable - (will set `cache_path` to `.cache-{username}`) + * cache_handler: An instance of the `CacheHandler` class to handle + getting and saving cached authorization tokens. + Optional, will otherwise use `CacheFileHandler`. * proxies: Optional, proxy for the requests library to route through * show_dialog: Optional, interpreted as boolean - * requests_session: A Requests session + * requests_session: A Requests session object or a true value to create one. + A false value disables sessions. + It should generally be a good idea to keep sessions enabled + for performance reasons (connection pooling). * requests_timeout: Optional, tell Requests to stop waiting for a response after a given number of seconds * open_browser: Optional, whether the web browser should be opened to authorize a user - * cache_handler: An instance of the `CacheHandler` class to handle - getting and saving cached authorization tokens. - Optional, will otherwise use `CacheFileHandler`. - (takes precedence over `cache_path` and `username`) """ super().__init__(requests_session) @@ -307,33 +287,14 @@ class SpotifyOAuth(SpotifyAuthBase): self.redirect_uri = redirect_uri self.state = state self.scope = self._normalize_scope(scope) - if username or cache_path: - warnings.warn("Specifying cache_path or username as arguments to SpotifyOAuth " + - "will be deprecated. Instead, please create a CacheFileHandler " + - "instance with the desired cache_path and username and pass it " + - "to SpotifyOAuth as the cache_handler. For example:\n\n" + - "\tfrom spotipy.oauth2 import CacheFileHandler\n" + - "\thandler = CacheFileHandler(cache_path=cache_path, " + - "username=username)\n" + - "\tsp = spotipy.SpotifyOAuth(client_id, client_secret, " + - "redirect_uri," + - " cache_handler=handler)", - DeprecationWarning - ) - if cache_handler: - warnings.warn("A cache_handler has been specified along with a cache_path or " + - "username. The cache_path and username arguments will be ignored.") + if cache_handler: assert issubclass(cache_handler.__class__, CacheHandler), \ "cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \ + " != " + str(CacheHandler) self.cache_handler = cache_handler else: - username = (username or os.getenv(CLIENT_CREDS_ENV_VARS["client_username"])) - self.cache_handler = CacheFileHandler( - username=username, - cache_path=cache_path - ) + self.cache_handler = CacheFileHandler() self.proxies = proxies self.requests_timeout = requests_timeout self.show_dialog = show_dialog @@ -475,24 +436,14 @@ class SpotifyOAuth(SpotifyAuthBase): return self.parse_response_code(response) return self.get_auth_response() - def get_access_token(self, code=None, as_dict=True, check_cache=True): + def get_access_token(self, code=None, check_cache=True): """ Gets the access token for the app given the code Parameters: - code - the response code - - as_dict - a boolean indicating if returning the access token - as a token_info dictionary, otherwise it will be returned - as a string. + - check_cache - if true, checks for a locally stored token + before requesting a new token """ - if as_dict: - warnings.warn( - "You're using 'as_dict = True'." - "get_access_token will return the token string directly in future " - "versions. Please adjust your code accordingly, or use " - "get_cached_token instead.", - DeprecationWarning, - stacklevel=2, - ) if check_cache: token_info = self.validate_token(self.cache_handler.get_cached_token()) if token_info is not None: @@ -500,7 +451,7 @@ class SpotifyOAuth(SpotifyAuthBase): token_info = self.refresh_access_token( token_info["refresh_token"] ) - return token_info if as_dict else token_info["access_token"] + return token_info["access_token"] payload = { "redirect_uri": self.redirect_uri, @@ -532,7 +483,7 @@ class SpotifyOAuth(SpotifyAuthBase): token_info = response.json() token_info = self._add_custom_values_to_token_info(token_info) self.cache_handler.save_token_to_cache(token_info) - return token_info if as_dict else token_info["access_token"] + return token_info["access_token"] except requests.exceptions.HTTPError as http_error: self._handle_oauth_error(http_error) @@ -576,26 +527,6 @@ class SpotifyOAuth(SpotifyAuthBase): token_info["scope"] = self.scope return token_info - def get_cached_token(self): - warnings.warn("Calling get_cached_token directly on the SpotifyOAuth object will be " + - "deprecated. Instead, please specify a CacheFileHandler instance as " + - "the cache_handler in SpotifyOAuth and use the CacheFileHandler's " + - "get_cached_token method. You can replace:\n\tsp.get_cached_token()" + - "\n\nWith:\n\tsp.validate_token(sp.cache_handler.get_cached_token())", - DeprecationWarning - ) - return self.validate_token(self.cache_handler.get_cached_token()) - - def _save_token_info(self, token_info): - warnings.warn("Calling _save_token_info directly on the SpotifyOAuth object will be " + - "deprecated. Instead, please specify a CacheFileHandler instance as " + - "the cache_handler in SpotifyOAuth and use the CacheFileHandler's " + - "save_token_to_cache method.", - DeprecationWarning - ) - self.cache_handler.save_token_to_cache(token_info) - return None - class SpotifyPKCE(SpotifyAuthBase): """ Implements PKCE Authorization Flow for client apps @@ -612,18 +543,18 @@ class SpotifyPKCE(SpotifyAuthBase): OAUTH_AUTHORIZE_URL = "https://accounts.spotify.com/authorize" OAUTH_TOKEN_URL = "https://accounts.spotify.com/api/token" - def __init__(self, - client_id=None, - redirect_uri=None, - state=None, - scope=None, - cache_path=None, - username=None, - proxies=None, - requests_timeout=None, - requests_session=True, - open_browser=True, - cache_handler=None): + def __init__( + self, + client_id=None, + redirect_uri=None, + state=None, + scope=None, + cache_handler=None, + proxies=None, + requests_timeout=None, + requests_session=True, + open_browser=True + ): """ Creates Auth Manager with the PKCE Auth flow. @@ -631,22 +562,21 @@ class SpotifyPKCE(SpotifyAuthBase): * client_id: Must be supplied or set as environment variable * redirect_uri: Must be supplied or set as environment variable * state: Optional, no verification is performed - * scope: Optional, either a list of scopes or comma separated string of scopes. - e.g, "playlist-read-private,playlist-read-collaborative" - * cache_path: (deprecated) Optional, will otherwise be generated - (takes precedence over `username`) - * username: (deprecated) Optional or set as environment variable - (will set `cache_path` to `.cache-{username}`) - * proxies: Optional, proxy for the requests library to route through - * requests_timeout: Optional, tell Requests to stop waiting for a response after - a given number of seconds - * requests_session: A Requests session - * open_browser: Optional, whether the web browser should be opened to - authorize a user + * scope: Optional, either a string of scopes, or an iterable with elements of type + `Scope` or `str`. E.g., + {Scope.user_modify_playback_state, Scope.user_library_read} * cache_handler: An instance of the `CacheHandler` class to handle getting and saving cached authorization tokens. Optional, will otherwise use `CacheFileHandler`. - (takes precedence over `cache_path` and `username`) + * proxies: Optional, proxy for the requests library to route through + * requests_timeout: Optional, tell Requests to stop waiting for a response after + a given number of seconds + * requests_session: A Requests session object or a true value to create one. + A false value disables sessions. + It should generally be a good idea to keep sessions enabled + for performance reasons (connection pooling). + * open_browser: Optional, thether or not the web browser should be opened to + authorize a user """ super().__init__(requests_session) @@ -654,31 +584,12 @@ class SpotifyPKCE(SpotifyAuthBase): self.redirect_uri = redirect_uri self.state = state self.scope = self._normalize_scope(scope) - if username or cache_path: - warnings.warn("Specifying cache_path or username as arguments to SpotifyPKCE " + - "will be deprecated. Instead, please create a CacheFileHandler " + - "instance with the desired cache_path and username and pass it " + - "to SpotifyPKCE as the cache_handler. For example:\n\n" + - "\tfrom spotipy.oauth2 import CacheFileHandler\n" + - "\thandler = CacheFileHandler(cache_path=cache_path, " + - "username=username)\n" + - "\tsp = spotipy.SpotifyImplicitGrant(client_id, client_secret, " + - "redirect_uri, cache_handler=handler)", - DeprecationWarning - ) - if cache_handler: - warnings.warn("A cache_handler has been specified along with a cache_path or " + - "username. The cache_path and username arguments will be ignored.") if cache_handler: assert issubclass(type(cache_handler), CacheHandler), \ "type(cache_handler): " + str(type(cache_handler)) + " != " + str(CacheHandler) self.cache_handler = cache_handler else: - username = (username or os.getenv(CLIENT_CREDS_ENV_VARS["client_username"])) - self.cache_handler = CacheFileHandler( - username=username, - cache_path=cache_path - ) + self.cache_handler = CacheFileHandler() self.proxies = proxies self.requests_timeout = requests_timeout @@ -941,285 +852,6 @@ class SpotifyPKCE(SpotifyAuthBase): def parse_auth_response_url(url): return SpotifyOAuth.parse_auth_response_url(url) - def get_cached_token(self): - warnings.warn("Calling get_cached_token directly on the SpotifyPKCE object will be " + - "deprecated. Instead, please specify a CacheFileHandler instance as " + - "the cache_handler in SpotifyOAuth and use the CacheFileHandler's " + - "get_cached_token method. You can replace:\n\tsp.get_cached_token()" + - "\n\nWith:\n\tsp.validate_token(sp.cache_handler.get_cached_token())", - DeprecationWarning - ) - return self.validate_token(self.cache_handler.get_cached_token()) - - def _save_token_info(self, token_info): - warnings.warn("Calling _save_token_info directly on the SpotifyOAuth object will be " + - "deprecated. Instead, please specify a CacheFileHandler instance as " + - "the cache_handler in SpotifyOAuth and use the CacheFileHandler's " + - "save_token_to_cache method.", - DeprecationWarning - ) - self.cache_handler.save_token_to_cache(token_info) - return None - - -class SpotifyImplicitGrant(SpotifyAuthBase): - """ Implements Implicit Grant Flow for client apps - - This auth manager enables *user and non-user* endpoints with only - a client secret, redirect uri, and username. The user will need to - copy and paste a URI from the browser every hour. - - Security Warning - ----------------- - The OAuth standard no longer recommends the Implicit Grant Flow for - client-side code. Spotify has implemented the OAuth-suggested PKCE - extension that removes the need for a client secret in the - Authentication Code flow. Use the SpotifyPKCE auth manager instead - of SpotifyImplicitGrant. - - SpotifyPKCE contains all the functionality of - SpotifyImplicitGrant, plus automatic response retrieval and - refreshable tokens. Only a few replacements need to be made: - - * get_auth_response()['access_token'] -> - get_access_token(get_authorization_code()) - * get_auth_response() -> - get_access_token(get_authorization_code()); get_cached_token() - * parse_response_token(url)['access_token'] -> - get_access_token(parse_response_code(url)) - * parse_response_token(url) -> - get_access_token(parse_response_code(url)); get_cached_token() - - The security concern in the Implicit Grant flow is that the token is - returned in the URL and can be intercepted through the browser. A - request with an authorization code and proof of origin could not be - easily intercepted without a compromised network. - """ - OAUTH_AUTHORIZE_URL = "https://accounts.spotify.com/authorize" - - def __init__(self, - client_id=None, - redirect_uri=None, - state=None, - scope=None, - cache_path=None, - username=None, - show_dialog=False, - cache_handler=None): - """ Creates Auth Manager using the Implicit Grant flow - - **See help(SpotifyImplicitGrant) for full Security Warning** - - Parameters - ---------- - * client_id: Must be supplied or set as environment variable - * redirect_uri: Must be supplied or set as environment variable - * state: May be supplied, no verification is performed - * scope: Optional, either a string of scopes, or an iterable with elements of type - `Scope` or `str`. E.g., - {Scope.user_modify_playback_state, Scope.user_library_read} - * cache_handler: An instance of the `CacheHandler` class to handle - getting and saving cached authorization tokens. - May be supplied, will otherwise use `CacheFileHandler`. - (takes precedence over `cache_path` and `username`) - * cache_path: (deprecated) May be supplied, will otherwise be generated - (takes precedence over `username`) - * username: (deprecated) May be supplied or set as environment variable - (will set `cache_path` to `.cache-{username}`) - * show_dialog: Interpreted as boolean - """ - logger.warning("The OAuth standard no longer recommends the Implicit " - "Grant Flow for client-side code. Use the SpotifyPKCE " - "auth manager instead of SpotifyImplicitGrant. For " - "more details and a guide to switching, see " - "help(SpotifyImplicitGrant).") - - self.client_id = client_id - self.redirect_uri = redirect_uri - self.state = state - if username or cache_path: - warnings.warn("Specifying cache_path or username as arguments to " + - "SpotifyImplicitGrant will be deprecated. Instead, please create " + - "a CacheFileHandler instance with the desired cache_path and " + - "username and pass it to SpotifyImplicitGrant as the " + - "cache_handler. For example:\n\n" + - "\tfrom spotipy.oauth2 import CacheFileHandler\n" + - "\thandler = CacheFileHandler(cache_path=cache_path, " + - "username=username)\n" + - "\tsp = spotipy.SpotifyImplicitGrant(client_id, client_secret, " + - "redirect_uri, cache_handler=handler)", - DeprecationWarning - ) - if cache_handler: - warnings.warn("A cache_handler has been specified along with a cache_path or " + - "username. The cache_path and username arguments will be ignored.") - if cache_handler: - assert issubclass(type(cache_handler), CacheHandler), \ - "type(cache_handler): " + str(type(cache_handler)) + " != " + str(CacheHandler) - self.cache_handler = cache_handler - else: - username = (username or os.getenv(CLIENT_CREDS_ENV_VARS["client_username"])) - self.cache_handler = CacheFileHandler( - username=username, - cache_path=cache_path - ) - self.scope = self._normalize_scope(scope) - self.show_dialog = show_dialog - self._session = None # As to not break inherited __del__ - - def validate_token(self, token_info): - if token_info is None: - return None - - # if scopes don't match, then bail - if "scope" not in token_info or not self._is_scope_subset( - self.scope, token_info["scope"] - ): - return None - - if self.is_token_expired(token_info): - return None - - return token_info - - def get_access_token(self, - state=None, - response=None, - check_cache=True): - """ Gets Auth Token from cache (preferred) or user interaction - - Parameters - ---------- - * state: May be given, overrides (without changing) self.state - * response: URI with token, can break expiration checks - * check_cache: Interpreted as boolean - """ - if check_cache: - token_info = self.validate_token(self.cache_handler.get_cached_token()) - if not (token_info is None or self.is_token_expired(token_info)): - return token_info["access_token"] - - if response: - token_info = self.parse_response_token(response) - else: - token_info = self.get_auth_response(state) - token_info = self._add_custom_values_to_token_info(token_info) - self.cache_handler.save_token_to_cache(token_info) - - return token_info["access_token"] - - def get_authorize_url(self, state=None): - """ Gets the URL to use to authorize this app """ - payload = { - "client_id": self.client_id, - "response_type": "token", - "redirect_uri": self.redirect_uri, - } - if self.scope: - payload["scope"] = self.scope - if state is None: - state = self.state - if state is not None: - payload["state"] = state - if self.show_dialog: - payload["show_dialog"] = True - - urlparams = urllibparse.urlencode(payload) - - return f"{self.OAUTH_AUTHORIZE_URL}?{urlparams}" - - def parse_response_token(self, url, state=None): - """ Parse the response code in the given response url """ - remote_state, token, t_type, exp_in = self.parse_auth_response_url(url) - if state is None: - state = self.state - if state is not None and remote_state != state: - raise SpotifyStateError(state, remote_state) - return {"access_token": token, "token_type": t_type, - "expires_in": exp_in, "state": state} - - @staticmethod - def parse_auth_response_url(url): - url_components = urlparse(url) - fragment_s = url_components.fragment - query_s = url_components.query - form = dict(i.split('=') for i - in (fragment_s or query_s or url).split('&')) - if "error" in form: - raise SpotifyOauthError(f"Received error from auth server: {form['error']}", - state=form["state"]) - if "expires_in" in form: - form["expires_in"] = int(form["expires_in"]) - return tuple(form.get(param) for param in ["state", "access_token", - "token_type", "expires_in"]) - - def _open_auth_url(self, state=None): - auth_url = self.get_authorize_url(state) - try: - webbrowser.open(auth_url) - logger.info("Opened %s in your browser", auth_url) - except webbrowser.Error: - logger.error("Please navigate here: %s", auth_url) - - def get_auth_response(self, state=None): - """ Gets a new auth **token** with user interaction """ - logger.info('User authentication requires interaction with your ' - 'web browser. Once you enter your credentials and ' - 'give authorization, you will be redirected to ' - 'a url. Paste that url you were directed to to ' - 'complete the authorization.') - - redirect_info = urlparse(self.redirect_uri) - redirect_host, redirect_port = get_host_port(redirect_info.netloc) - # Implicit Grant tokens are returned in a hash fragment - # which is only available to the browser. Therefore, interactive - # URL retrieval is required. - if (redirect_host in ("127.0.0.1", "localhost") - and redirect_info.scheme == "http" and redirect_port): - logger.warning('Using a local redirect URI with a ' - 'port, likely expecting automatic ' - 'retrieval. Due to technical limitations, ' - 'the authentication token cannot be ' - 'automatically retrieved and must be ' - 'copied and pasted.') - - self._open_auth_url(state) - logger.info('Paste that url you were directed to in order to ' - 'complete the authorization') - response = SpotifyImplicitGrant._get_user_input("Enter the URL you " - "were redirected to: ") - return self.parse_response_token(response, state) - - def _add_custom_values_to_token_info(self, token_info): - """ - Store some values that aren't directly provided by a Web API - response. - """ - token_info["expires_at"] = int(time.time()) + token_info["expires_in"] - token_info["scope"] = self.scope - return token_info - - def get_cached_token(self): - warnings.warn("Calling get_cached_token directly on the SpotifyImplicitGrant " + - "object will be deprecated. Instead, please specify a " + - "CacheFileHandler instance as the cache_handler in SpotifyOAuth " + - "and use the CacheFileHandler's get_cached_token method. " + - "You can replace:\n\tsp.get_cached_token()" + - "\n\nWith:\n\tsp.validate_token(sp.cache_handler.get_cached_token())", - DeprecationWarning - ) - return self.validate_token(self.cache_handler.get_cached_token()) - - def _save_token_info(self, token_info): - warnings.warn("Calling _save_token_info directly on the SpotifyImplicitGrant " + - "object will be deprecated. Instead, please specify a " + - "CacheFileHandler instance as the cache_handler in SpotifyOAuth " + - "and use the CacheFileHandler's save_token_to_cache method.", - DeprecationWarning - ) - self.cache_handler.save_token_to_cache(token_info) - return None - class RequestHandler(BaseHTTPRequestHandler): def do_GET(self): diff --git a/spotipy/util.py b/spotipy/util.py index 2d9e012..19b73db 100644 --- a/spotipy/util.py +++ b/spotipy/util.py @@ -2,7 +2,7 @@ from __future__ import annotations """ Shows a user's playlists. This needs to be authenticated via OAuth. """ -__all__ = ["CLIENT_CREDS_ENV_VARS", "prompt_for_user_token"] +__all__ = ["CLIENT_CREDS_ENV_VARS"] import logging import os @@ -23,91 +23,6 @@ CLIENT_CREDS_ENV_VARS = { } -def prompt_for_user_token( - username=None, - scope=None, - client_id=None, - client_secret=None, - redirect_uri=None, - cache_path=None, - oauth_manager=None, - show_dialog=False -): - warnings.warn( - "'prompt_for_user_token' is deprecated." - "Use the following instead: " - " auth_manager=SpotifyOAuth(scope=scope)" - " spotipy.Spotify(auth_manager=auth_manager)", - DeprecationWarning - ) - """Prompt the user to login if necessary and returns a user token - suitable for use with the spotipy.Spotify constructor. - - Parameters: - - username - the Spotify username. (optional) - - scope - the desired scope of the request. (optional) - - client_id - the client ID of your app. (required) - - client_secret - the client secret of your app. (required) - - redirect_uri - the redirect URI of your app. (required) - - cache_path - path to location to save tokens. (required) - - oauth_manager - OAuth manager object. (optional) - - show_dialog - If True, a login prompt always shows or defaults to False. (optional) - """ - if not oauth_manager: - if not client_id: - client_id = os.getenv("SPOTIPY_CLIENT_ID") - - if not client_secret: - client_secret = os.getenv("SPOTIPY_CLIENT_SECRET") - - if not redirect_uri: - redirect_uri = os.getenv("SPOTIPY_REDIRECT_URI") - - if not client_id: - LOGGER.warning( - """ - You need to set your Spotify API credentials. - You can do this by setting environment variables like so: - - export SPOTIPY_CLIENT_ID='your-spotify-client-id' - export SPOTIPY_CLIENT_SECRET='your-spotify-client-secret' - export SPOTIPY_REDIRECT_URI='your-app-redirect-url' - - Get your credentials at - https://developer.spotify.com/my-applications - """ - ) - raise spotipy.SpotifyException(550, -1, "no credentials set") - - sp_oauth = oauth_manager or spotipy.SpotifyOAuth( - client_id, - client_secret, - redirect_uri, - scope=scope, - cache_path=cache_path, - username=username, - show_dialog=show_dialog - ) - - # try to get a valid token for this user, from the cache, - # if not in the cache, then create a new (this will send - # the user to a web page where they can authorize this app) - - token_info = sp_oauth.validate_token(sp_oauth.cache_handler.get_cached_token()) - - if not token_info: - code = sp_oauth.get_auth_response() - token = sp_oauth.get_access_token(code, as_dict=False) - else: - return token_info["access_token"] - - # Auth'ed API request - if token: - return token - else: - return None - - def get_host_port(netloc): """ Split the network location string into host and port and returns a tuple where the host is a string and the the port is an integer. @@ -123,53 +38,3 @@ def get_host_port(netloc): port = None return host, port - - -def normalize_scope(scope): - """Normalize the scope to verify that it is a list or tuple. A string - input will split the string by commas to create a list of scopes. - A list or tuple input is used directly. - - Parameters: - - scope - a string representing scopes separated by commas, - or a list/tuple of scopes. - """ - if scope: - if isinstance(scope, str): - scopes = scope.split(',') - elif isinstance(scope, list) or isinstance(scope, tuple): - scopes = scope - else: - raise Exception( - "Unsupported scope value, please either provide a list of scopes, " - "or a string of scopes separated by commas." - ) - return " ".join(sorted(scopes)) - else: - return None - - -class Retry(urllib3.Retry): - """ - Custom class for printing a warning when a rate/request limit is reached. - """ - def increment( - self, - method: str | None = None, - url: str | None = None, - response: urllib3.BaseHTTPResponse | None = None, - error: Exception | None = None, - _pool: urllib3.connectionpool.ConnectionPool | None = None, - _stacktrace: TracebackType | None = None, - ) -> urllib3.Retry: - if response: - retry_header = response.headers.get("Retry-After") - if self.is_retry(method, response.status, bool(retry_header)): - logging.warning("Your application has reached a rate/request limit. " - f"Retry will occur after: {retry_header}") - return super().increment(method, - url, - response=response, - error=error, - _pool=_pool, - _stacktrace=_stacktrace) diff --git a/tests/integration/non_user_endpoints/test.py b/tests/integration/non_user_endpoints/test.py index 9da476a..ad8b219 100644 --- a/tests/integration/non_user_endpoints/test.py +++ b/tests/integration/non_user_endpoints/test.py @@ -70,7 +70,8 @@ class AuthTestSpotipy(unittest.TestCase): @classmethod def setUpClass(self): self.spotify = Spotify( - client_credentials_manager=SpotifyClientCredentials()) + auth_manager=SpotifyClientCredentials() + ) self.spotify.trace = False def test_audio_analysis(self): @@ -341,9 +342,9 @@ class AuthTestSpotipy(unittest.TestCase): self.assertTrue(find_album()) def test_search_timeout(self): - client_credentials_manager = SpotifyClientCredentials() + auth_manager = SpotifyClientCredentials() sp = spotipy.Spotify(requests_timeout=0.01, - client_credentials_manager=client_credentials_manager) + auth_manager=auth_manager) # depending on the timing or bandwidth, this raises a timeout or connection error self.assertRaises((requests.exceptions.Timeout, requests.exceptions.ConnectionError), @@ -352,17 +353,15 @@ class AuthTestSpotipy(unittest.TestCase): @unittest.skip("flaky test, need a better method to test retries") def test_max_retries_reached_get(self): spotify_no_retry = Spotify( - client_credentials_manager=SpotifyClientCredentials(), + auth_manager=SpotifyClientCredentials(), retries=0) - i = 0 - while i < 100: + for i in range(100): try: spotify_no_retry.search(q='foo') except SpotifyException as e: self.assertIsInstance(e, SpotifyException) self.assertEqual(e.http_status, 429) return - i += 1 self.fail() def test_album_search(self): @@ -460,7 +459,7 @@ class AuthTestSpotipy(unittest.TestCase): sess = requests.Session() sess.headers["user-agent"] = "spotipy-test" with_custom_session = spotipy.Spotify( - client_credentials_manager=SpotifyClientCredentials(), + auth_manager=SpotifyClientCredentials(), requests_session=sess) self.assertTrue( with_custom_session.user( @@ -469,7 +468,7 @@ class AuthTestSpotipy(unittest.TestCase): def test_force_no_requests_session(self): with_no_session = spotipy.Spotify( - client_credentials_manager=SpotifyClientCredentials(), + auth_manager=SpotifyClientCredentials(), requests_session=False) self.assertNotIsInstance(with_no_session._session, requests.Session) user = with_no_session.user(user="akx") diff --git a/tests/integration/user_endpoints/test.py b/tests/integration/user_endpoints/test.py index fb1fbb2..a909907 100644 --- a/tests/integration/user_endpoints/test.py +++ b/tests/integration/user_endpoints/test.py @@ -2,15 +2,30 @@ import os from spotipy import ( CLIENT_CREDS_ENV_VARS as CCEV, - prompt_for_user_token, Spotify, SpotifyException, - SpotifyImplicitGrant, - SpotifyPKCE + SpotifyOAuth, + SpotifyPKCE, + CacheFileHandler ) import unittest from tests import helpers +def _make_spotify(scopes=None, retries=None): + + retries = retries or Spotify.max_retries + + auth_manager = SpotifyOAuth( + scope=scopes, + cache_handler=CacheFileHandler() + ) + + spotify = Spotify( + auth_manager=auth_manager, + retries=retries + ) + + return spotify class SpotipyPlaylistApiTest(unittest.TestCase): @classmethod @@ -48,10 +63,10 @@ class SpotipyPlaylistApiTest(unittest.TestCase): 'user-read-playback-state' ) - token = prompt_for_user_token(cls.username, scope=scope) + cls.spotify = _make_spotify(scopes=scope) + cls.spotify_no_retry = _make_spotify(scopes=scope, retries=0) + - cls.spotify = Spotify(auth=token) - cls.spotify_no_retry = Spotify(auth=token, retries=0) cls.new_playlist_name = 'spotipy-playlist-test' cls.new_playlist = helpers.get_spotify_playlist( cls.spotify, cls.new_playlist_name, cls.username) or \ @@ -201,17 +216,6 @@ class SpotipyPlaylistApiTest(unittest.TestCase): return self.fail() - 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 @@ -240,10 +244,7 @@ class SpotipyLibraryApiTests(unittest.TestCase): 'ugc-image-upload ' 'user-read-playback-state' ) - - token = prompt_for_user_token(cls.username, scope=scope) - - cls.spotify = Spotify(auth=token) + cls.spotify = _make_spotify(scopes=scope) def test_track_bad_id(self): with self.assertRaises(SpotifyException): @@ -330,9 +331,7 @@ class SpotipyUserApiTests(unittest.TestCase): 'user-read-playback-state' ) - token = prompt_for_user_token(cls.username, scope=scope) - - cls.spotify = Spotify(auth=token) + cls.spotify = _make_spotify(scopes=scope) def test_basic_user_profile(self): user = self.spotify.user(self.username) @@ -360,9 +359,7 @@ class SpotipyUserApiTests(unittest.TestCase): 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) + cls.spotify = _make_spotify() def test_category(self): rock_cat_id = '0JQ5DAqbMKFDXXwE9BDJAr' @@ -455,9 +452,7 @@ class SpotipyFollowApiTests(unittest.TestCase): 'user-read-playback-state' ) - token = prompt_for_user_token(cls.username, scope=scope) - - cls.spotify = Spotify(auth=token) + cls.spotify = _make_spotify(scopes=scope) def test_current_user_follows(self): response = self.spotify.current_user_followed_artists() @@ -510,9 +505,7 @@ class SpotipyPlayerApiTests(unittest.TestCase): 'user-read-playback-state' ) - token = prompt_for_user_token(cls.username, scope=scope) - - cls.spotify = Spotify(auth=token) + cls.spotify = _make_spotify(scopes=scope) def test_devices(self): # No devices playing by default @@ -526,23 +519,6 @@ class SpotipyPlayerApiTests(unittest.TestCase): # not much more to test if account is inactive and has no recently played tracks -class SpotipyImplicitGrantTests(unittest.TestCase): - @classmethod - def setUpClass(cls): - scope = ( - 'user-follow-read ' - 'user-follow-modify ' - ) - auth_manager = SpotifyImplicitGrant(scope=scope, - cache_path=".cache-implicittest") - cls.spotify = Spotify(auth_manager=auth_manager) - - def test_current_user(self): - c_user = self.spotify.current_user() - user = self.spotify.user(c_user['id']) - self.assertEqual(c_user['display_name'], user['display_name']) - - class SpotifyPKCETests(unittest.TestCase): @classmethod @@ -551,7 +527,8 @@ class SpotifyPKCETests(unittest.TestCase): 'user-follow-read ' 'user-follow-modify ' ) - auth_manager = SpotifyPKCE(scope=scope, cache_path=".cache-pkcetest") + cache_handler = CacheFileHandler(cache_path=".cache-pkcetest") + auth_manager = SpotifyPKCE(scope=scope, cache_handler=cache_handler) cls.spotify = Spotify(auth_manager=auth_manager) def test_current_user(self): diff --git a/tests/unit/test_oauth.py b/tests/unit/test_oauth.py index 10e1062..ce21efc 100644 --- a/tests/unit/test_oauth.py +++ b/tests/unit/test_oauth.py @@ -5,10 +5,11 @@ import unittest import unittest.mock as mock import urllib.parse as urllibparse -from spotipy import SpotifyOAuth, SpotifyImplicitGrant, SpotifyPKCE -from spotipy.cache_handler import MemoryCacheHandler +from spotipy import SpotifyOAuth, SpotifyPKCE +from spotipy.cache_handler import CacheHandler from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOauthError from spotipy.oauth2 import SpotifyStateError +from spotipy import CacheFileHandler patch = mock.patch DEFAULT = mock.DEFAULT @@ -38,10 +39,6 @@ def _make_oauth(*args, **kwargs): return SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", *args, **kwargs) -def _make_implicitgrantauth(*args, **kwargs): - return SpotifyImplicitGrant("CLID", "REDIR", "STATE", *args, **kwargs) - - def _make_pkceauth(*args, **kwargs): return SpotifyPKCE("CLID", "REDIR", "STATE", *args, **kwargs) @@ -60,13 +57,12 @@ class OAuthCacheTest(unittest.TestCase): opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False)) is_token_expired.return_value = False - spot = _make_oauth(scope, path) + cache_handler = CacheFileHandler(cache_path=path) + spot = _make_oauth(scope, cache_handler=cache_handler) cached_tok = spot.validate_token(spot.cache_handler.get_cached_token()) - cached_tok_legacy = spot.get_cached_token() opener.assert_called_with(path) self.assertIsNotNone(cached_tok) - self.assertIsNotNone(cached_tok_legacy) self.assertEqual(refresh_access_token.call_count, 0) @patch.multiple(SpotifyOAuth, @@ -83,7 +79,8 @@ class OAuthCacheTest(unittest.TestCase): opener.return_value = token_file refresh_access_token.return_value = fresh_tok - spot = _make_oauth(scope, path) + cache_handler = CacheFileHandler(cache_path=path) + spot = _make_oauth(scope, cache_handler=cache_handler) spot.validate_token(spot.cache_handler.get_cached_token()) is_token_expired.assert_called_with(expired_tok) @@ -103,7 +100,9 @@ class OAuthCacheTest(unittest.TestCase): opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False)) is_token_expired.return_value = False - spot = _make_oauth(requested_scope, path) + cache_handler = CacheFileHandler(cache_path=path) + + spot = _make_oauth(requested_scope, cache_handler=cache_handler) cached_tok = spot.validate_token(spot.cache_handler.get_cached_token()) opener.assert_called_with(path) @@ -119,27 +118,13 @@ class OAuthCacheTest(unittest.TestCase): fi = _fake_file() opener.return_value = fi - spot = SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", scope, path) + cache_handler = CacheFileHandler(cache_path=path) + spot = SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", scope, cache_handler=cache_handler) spot.cache_handler.save_token_to_cache(tok) opener.assert_called_with(path, 'w') self.assertTrue(fi.write.called) - @patch('spotipy.cache_handler.open', create=True) - def test_saves_to_cache_path_legacy(self, opener): - scope = "playlist-modify-private" - path = ".cache-username" - tok = _make_fake_token(1, 1, scope) - - fi = _fake_file() - opener.return_value = fi - - spot = SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", scope, path) - spot._save_token_info(tok) - - opener.assert_called_with(path, 'w') - self.assertTrue(fi.write.called) - def test_cache_handler(self): scope = "playlist-modify-private" tok = _make_fake_token(1, 1, scope) @@ -243,142 +228,6 @@ class TestSpotifyClientCredentials(unittest.TestCase): oauth.get_access_token(check_cache=False) self.assertEqual(error.exception.error, 'invalid_client') - -class ImplicitGrantCacheTest(unittest.TestCase): - - @patch.object(SpotifyImplicitGrant, "is_token_expired", DEFAULT) - @patch('spotipy.cache_handler.open', create=True) - def test_gets_from_cache_path(self, opener, is_token_expired): - scope = "playlist-modify-private" - path = ".cache-username" - tok = _make_fake_token(1, 1, scope) - - opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False)) - is_token_expired.return_value = False - - spot = _make_implicitgrantauth(scope, path) - cached_tok = spot.cache_handler.get_cached_token() - cached_tok_legacy = spot.get_cached_token() - - opener.assert_called_with(path) - self.assertIsNotNone(cached_tok) - self.assertIsNotNone(cached_tok_legacy) - - @patch.object(SpotifyImplicitGrant, "is_token_expired", DEFAULT) - @patch('spotipy.cache_handler.open', create=True) - def test_expired_token_returns_none(self, opener, is_token_expired): - scope = "playlist-modify-private" - path = ".cache-username" - expired_tok = _make_fake_token(0, None, scope) - - token_file = _token_file(json.dumps(expired_tok, ensure_ascii=False)) - opener.return_value = token_file - - spot = _make_implicitgrantauth(scope, path) - cached_tok = spot.validate_token(spot.cache_handler.get_cached_token()) - - is_token_expired.assert_called_with(expired_tok) - opener.assert_any_call(path) - self.assertIsNone(cached_tok) - - @patch.object(SpotifyImplicitGrant, "is_token_expired", DEFAULT) - @patch('spotipy.cache_handler.open', create=True) - def test_badly_scoped_token_bails(self, opener, is_token_expired): - token_scope = "playlist-modify-public" - requested_scope = "playlist-modify-private" - path = ".cache-username" - tok = _make_fake_token(1, 1, token_scope) - - opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False)) - is_token_expired.return_value = False - - spot = _make_implicitgrantauth(requested_scope, path) - cached_tok = spot.validate_token(spot.cache_handler.get_cached_token()) - - opener.assert_called_with(path) - self.assertIsNone(cached_tok) - - @patch('spotipy.cache_handler.open', create=True) - def test_saves_to_cache_path(self, opener): - scope = "playlist-modify-private" - path = ".cache-username" - tok = _make_fake_token(1, 1, scope) - - fi = _fake_file() - opener.return_value = fi - - spot = SpotifyImplicitGrant("CLID", "REDIR", "STATE", scope, path) - spot.cache_handler.save_token_to_cache(tok) - - opener.assert_called_with(path, 'w') - self.assertTrue(fi.write.called) - - @patch('spotipy.cache_handler.open', create=True) - def test_saves_to_cache_path_legacy(self, opener): - scope = "playlist-modify-private" - path = ".cache-username" - tok = _make_fake_token(1, 1, scope) - - fi = _fake_file() - opener.return_value = fi - - spot = SpotifyImplicitGrant("CLID", "REDIR", "STATE", scope, path) - spot._save_token_info(tok) - - opener.assert_called_with(path, 'w') - self.assertTrue(fi.write.called) - - -class TestSpotifyImplicitGrant(unittest.TestCase): - - def test_get_authorize_url_doesnt_pass_state_by_default(self): - auth = SpotifyImplicitGrant("CLID", "REDIR") - - url = auth.get_authorize_url() - - parsed_url = urllibparse.urlparse(url) - parsed_qs = urllibparse.parse_qs(parsed_url.query) - self.assertNotIn('state', parsed_qs) - - def test_get_authorize_url_passes_state_from_constructor(self): - state = "STATE" - auth = SpotifyImplicitGrant("CLID", "REDIR", state) - - url = auth.get_authorize_url() - - parsed_url = urllibparse.urlparse(url) - parsed_qs = urllibparse.parse_qs(parsed_url.query) - self.assertEqual(parsed_qs['state'][0], state) - - def test_get_authorize_url_passes_state_from_func_call(self): - state = "STATE" - auth = SpotifyImplicitGrant("CLID", "REDIR", "NOT STATE") - - url = auth.get_authorize_url(state=state) - - parsed_url = urllibparse.urlparse(url) - parsed_qs = urllibparse.parse_qs(parsed_url.query) - self.assertEqual(parsed_qs['state'][0], state) - - def test_get_authorize_url_does_not_show_dialog_by_default(self): - auth = SpotifyImplicitGrant("CLID", "REDIR") - - url = auth.get_authorize_url() - - parsed_url = urllibparse.urlparse(url) - parsed_qs = urllibparse.parse_qs(parsed_url.query) - self.assertNotIn('show_dialog', parsed_qs) - - def test_get_authorize_url_shows_dialog_when_requested(self): - auth = SpotifyImplicitGrant("CLID", "REDIR", show_dialog=True) - - url = auth.get_authorize_url() - - parsed_url = urllibparse.urlparse(url) - parsed_qs = urllibparse.parse_qs(parsed_url.query) - self.assertTrue(parsed_qs['show_dialog']) - - class SpotifyPKCECacheTest(unittest.TestCase): @patch.multiple(SpotifyPKCE, @@ -393,13 +242,12 @@ class SpotifyPKCECacheTest(unittest.TestCase): opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False)) is_token_expired.return_value = False - spot = _make_pkceauth(scope, path) + cache_handler = CacheFileHandler(cache_path=path) + spot = _make_pkceauth(scope, cache_handler=cache_handler) cached_tok = spot.cache_handler.get_cached_token() - cached_tok_legacy = spot.get_cached_token() opener.assert_called_with(path) self.assertIsNotNone(cached_tok) - self.assertIsNotNone(cached_tok_legacy) self.assertEqual(refresh_access_token.call_count, 0) @patch.multiple(SpotifyPKCE, @@ -416,7 +264,8 @@ class SpotifyPKCECacheTest(unittest.TestCase): opener.return_value = token_file refresh_access_token.return_value = fresh_tok - spot = _make_pkceauth(scope, path) + cache_handler = CacheFileHandler(cache_path=path) + spot = _make_pkceauth(scope, cache_handler=cache_handler) spot.validate_token(spot.cache_handler.get_cached_token()) is_token_expired.assert_called_with(expired_tok) @@ -436,7 +285,8 @@ class SpotifyPKCECacheTest(unittest.TestCase): opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False)) is_token_expired.return_value = False - spot = _make_pkceauth(requested_scope, path) + cache_handler = CacheFileHandler(cache_path=path) + spot = _make_pkceauth(requested_scope, cache_handler=cache_handler) cached_tok = spot.validate_token(spot.cache_handler.get_cached_token()) opener.assert_called_with(path) @@ -452,27 +302,13 @@ class SpotifyPKCECacheTest(unittest.TestCase): fi = _fake_file() opener.return_value = fi - spot = SpotifyPKCE("CLID", "REDIR", "STATE", scope, path) + cache_handler = CacheFileHandler(cache_path=path) + spot = SpotifyPKCE("CLID", "REDIR", "STATE", scope, cache_handler=cache_handler) spot.cache_handler.save_token_to_cache(tok) opener.assert_called_with(path, 'w') self.assertTrue(fi.write.called) - @patch('spotipy.cache_handler.open', create=True) - def test_saves_to_cache_path_legacy(self, opener): - scope = "playlist-modify-private" - path = ".cache-username" - tok = _make_fake_token(1, 1, scope) - - fi = _fake_file() - opener.return_value = fi - - spot = SpotifyPKCE("CLID", "REDIR", "STATE", scope, path) - spot._save_token_info(tok) - - opener.assert_called_with(path, 'w') - self.assertTrue(fi.write.called) - class TestSpotifyPKCE(unittest.TestCase): diff --git a/tests/unit/test_scopes.py b/tests/unit/test_scopes.py index dcaefe8..441c793 100644 --- a/tests/unit/test_scopes.py +++ b/tests/unit/test_scopes.py @@ -88,3 +88,28 @@ class SpotipyScopeTest(TestCase): self.assertEqual(normalized_scope_string_2, "") self.assertIsNone(self.normalize_scope(None)) + + def test_all_scopes(self): + expected_scopes = { + Scope.user_read_currently_playing, + Scope.playlist_read_collaborative, + Scope.playlist_modify_private, + Scope.user_read_playback_position, + Scope.user_library_modify, + Scope.user_top_read, + Scope.user_read_playback_state, + Scope.user_read_email, + Scope.ugc_image_upload, + Scope.user_read_private, + Scope.playlist_modify_public, + Scope.user_library_read, + Scope.streaming, + Scope.user_read_recently_played, + Scope.user_follow_read, + Scope.user_follow_modify, + Scope.app_remote_control, + Scope.playlist_read_private, + Scope.user_modify_playback_state, + } + + self.assertEqual(expected_scopes, Scope.all())