diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cab961..b02c3ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased [3.0.0-alpha] -While this is unreleased, please only add v3 features here. -Rebasing master onto v3 doesn't require a changelog update. +While this is unreleased, please only add v3 features here. Rebasing master onto v3 doesn't require a changelog update. ### Added @@ -17,16 +16,50 @@ Rebasing master onto v3 doesn't require a changelog update. ### Changed * Made `CacheHandler` an abstract base class - * Modified the return structure of the `audio_features` function (wrapping the [Get Audio Features for Several Tracks](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-audio-features) API) to conform to the return structure of the similar methods listed below. The functions wrapping these APIs do not unwrap the single key JSON response, and this is currently the only function that does this. - * [Get Several Tracks](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-tracks) - * [Get Multiple Artists](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-artists) - * [Get Multiple Albums](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-albums) + * [Get Several Tracks](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-tracks) + * [Get Multiple Artists](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-artists) + * [Get Multiple Albums](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-albums) +* 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 + +* 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`. ## Unreleased [2.x.x] ### Added +* Added `MemoryCacheHandler`, a cache handler that simply stores the token info in memory as an instance attribute of this class. + +### Fixed + +* Fixed a bug in `CacheFileHandler.__init__`: The documentation says that the username will be retrieved from the environment, but it wasn't. + +## [2.18.0] - 2021-04-13 + +### Added + - Enabled using both short and long IDs for playlist_change_details - Added a cache handler to `SpotifyClientCredentials` - Added the following endpoints @@ -43,13 +76,9 @@ Rebasing master onto v3 doesn't require a changelog update. ### Fixed * Fixed the bugs in `SpotifyOAuth.refresh_access_token` and `SpotifyPKCE.refresh_access_token` which raised the incorrect exception upon receiving an error response from the server. This addresses #645. - * Fixed a bug in `RequestHandler.do_GET` in which the non-existent `state` attribute of `SpotifyOauthError` is accessed. This bug occurs when the user clicks "cancel" in the permissions dialog that opens in the browser. - * Cleaned up the documentation for `SpotifyClientCredentials.__init__`, `SpotifyOAuth.__init__`, and `SpotifyPKCE.__init__`. - - ## [2.17.1] - 2021-02-28 ### Fixed @@ -104,7 +133,7 @@ Rebasing master onto v3 doesn't require a changelog update. ### Added - `SpotifyPKCE.parse_auth_response_url`, mirroring that method in - `SpotifyOAuth` + `SpotifyOAuth` ### Changed @@ -113,7 +142,7 @@ Rebasing master onto v3 doesn't require a changelog update. ### Fixed - Using `SpotifyPKCE.get_authorization_url` will now generate a code - challenge if needed + challenge if needed ## [2.14.0] - 2020-08-29 @@ -121,9 +150,9 @@ Rebasing master onto v3 doesn't require a changelog update. - (experimental) Support to search multiple/all markets at once. - Support to test whether the current user is following certain - users or artists + users or artists - Proper replacements for all deprecated playlist endpoints - (See https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ and below) + (See https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ and below) - Allow for OAuth 2.0 authorization by instructing the user to open the URL in a browser instead of opening the browser. - Reason for 403 error in SpotifyException - Support for the PKCE Auth Flow @@ -141,15 +170,16 @@ Rebasing master onto v3 doesn't require a changelog update. - `user_playlist_replace_tracks` in favor of `playlist_replace_items` - `user_playlist_reorder_tracks` in favor of `playlist_reorder_items` - `user_playlist_remove_all_occurrences_of_tracks` in favor of - `playlist_remove_all_occurrences_of_items` + `playlist_remove_all_occurrences_of_items` - `user_playlist_remove_specific_occurrences_of_tracks` in favor of - `playlist_remove_specific_occurrences_of_items` + `playlist_remove_specific_occurrences_of_items` - `user_playlist_follow_playlist` in favor of - `current_user_follow_playlist` + `current_user_follow_playlist` - `user_playlist_is_following` in favor of `playlist_is_following` - `playlist_tracks` in favor of `playlist_items` ### Fixed + - fixed issue where episode URIs were being converted to track URIs in playlist calls ## [2.13.0] - 2020-06-25 @@ -157,12 +187,12 @@ Rebasing master onto v3 doesn't require a changelog update. ### Added - Added `SpotifyImplicitGrant` as an auth manager option. It provides - user authentication without a client secret but sacrifices the ability - to refresh the token without user input. (However, read the class - docstring for security advisory.) + user authentication without a client secret but sacrifices the ability + to refresh the token without user input. (However, read the class + docstring for security advisory.) - Added built-in verification of the `state` query parameter - Added two new attributes: error and error_description to `SpotifyOauthError` exception class to show - authorization/authentication web api errors details. + authorization/authentication web api errors details. - Added `SpotifyStateError` subclass of `SpotifyOauthError` - Allow extending `SpotifyClientCredentials` and `SpotifyOAuth` - Added the market paramter to `album_tracks` @@ -186,10 +216,10 @@ Rebasing master onto v3 doesn't require a changelog update. ### Changed - Updated the documentation to give more details on the authorization process and reflect - 2020 Spotify Application jargon and practices. + 2020 Spotify Application jargon and practices. - The local webserver is only started for localhost redirect_uri which specify a port, - i.e. it is started for `http://localhost:8080` or `http://127.0.0.1:8080`, not for `http://localhost`. + i.e. it is started for `http://localhost:8080` or `http://127.0.0.1:8080`, not for `http://localhost`. ### Fixed @@ -258,6 +288,7 @@ Rebasing master onto v3 doesn't require a changelog update. - Optional `show_dialog` parameter to be passed to `SpotifyOAuth` ### Changed + - Both `SpotifyClientCredentials` and `SpotifyOAuth` inherit from a common `SpotifyAuthBase` which handles common parameters and logics. ## [2.7.1] - 2020-01-20 @@ -301,16 +332,19 @@ Rebasing master onto v3 doesn't require a changelog update. ## [2.6.1] - 2020-01-13 ### Fixed + - Fixed inconsistent behaviour with some API methods when - a full HTTP URL is passed. + a full HTTP URL is passed. - Fixed invalid calls to logging warn method ### Removed + - `mock` no longer needed for install. Only used in `tox`. ## [2.6.0] - 2020-01-12 ### Added + - Support for `playlist` to get a playlist without specifying a user - Support for `current_user_saved_albums_delete` - Support for `current_user_saved_albums_contains` @@ -319,95 +353,126 @@ Rebasing master onto v3 doesn't require a changelog update. - Lint with flake8 using Github action ### Changed + - Fix typos in doc - Start following [SemVer](https://semver.org) properly ## [2.5.0] - 2020-01-11 + Added follow and player endpoints ## [2.4.4] - 2017-01-04 + Python 3 fix ## [2.4.3] - 2017-01-02 + Fixed proxy issue in standard auth flow ## [2.4.2] - 2017-01-02 + Support getting audio features for a single track ## [2.4.1] - 2017-01-02 + Incorporated proxy support ## [2.4.0] - 2016-12-31 + Incorporated a number of PRs ## [2.3.8] - 2016-03-31 + Added recs, audio features, user top lists ## [2.3.7] - 2015-08-10 + Added current_user_followed_artists ## [2.3.6] - 2015-06-03 + Support for offset/limit with album_tracks API ## [2.3.5] - 2015-04-28 + Fixed bug in auto retry logic ## [2.3.3] - 2015-04-01 + Aadded client credential flow ## [2.3.2] - 2015-03-31 + Added auto retry logic ## [2.3.0] - 2015-01-05 + Added session support added by akx. ## [2.2.0] - 2014-11-15 + Added support for user_playlist_tracks ## [2.1.0] - 2014-10-25 + Added support for new_releases and featured_playlists ## [2.0.2] - 2014-08-25 + Moved to spotipy at pypi ## [1.2.0] - 2014-08-22 + Upgraded APIs and docs to make it be a real library ## [1.310.0] - 2014-08-20 + Added playlist replace and remove methods. Added auth tests. Improved API docs ## [1.301.0] - 2014-08-19 + Upgraded version number to take precedence over previously botched release (sigh) ## [1.50.0] - 2014-08-14 + Refactored util out of examples and into the main package ## [1.49.0] - 2014-07-23 + Support for "Your Music" tracks (add, delete, get), with examples ## [1.45.0] - 2014-07-07 + Support for related artists endpoint. Don't use cache auth codes when scope changes ## [1.44.0] - 2014-07-03 + Added show tracks.py example ## [1.43.0] - 2014-06-27 + Fixed JSON handling issue ## [1.42.0] - 2014-06-19 + Removed dependency on simplejson ## [1.40.0] - 2014-06-12 + Initial public release. ## [1.4.2] - 2014-06-21 + Added support for retrieving starred playlists ## [1.1.0] - 2014-06-17 + Updates to match released API ## [1.1.0] - 2014-05-18 + Repackaged for saner imports ## [1.0.0] - 2017-04-05 -Initial release + +Initial release \ No newline at end of file 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 6095a36..a9a16e7 100644 --- a/examples/artist_discography.py +++ b/examples/artist_discography.py @@ -68,6 +68,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 1f728a5..44e7180 100644 --- a/examples/audio_analysis_for_track.py +++ b/examples/audio_analysis_for_track.py @@ -8,8 +8,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 30caddb..fda6ce6 100644 --- a/examples/audio_features.py +++ b/examples/audio_features.py @@ -9,8 +9,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 9e156d3..b4f1938 100644 --- a/examples/audio_features_for_track.py +++ b/examples/audio_features_for_track.py @@ -10,8 +10,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 92347d3..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()) \ No newline at end of file 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 f6d2b18..9a5d5f5 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 @@ -18,4 +18,4 @@ while True: pprint(response['items']) offset = offset + len(response['items']) - print(offset, "/", response['total']) \ No newline at end of file + print(offset, "/", response['total']) 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 248e305..96d907f 100644 --- a/examples/show_album.py +++ b/examples/show_album.py @@ -11,6 +11,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 857bfb8..d5dbbfb 100644 --- a/examples/show_artist_top_tracks.py +++ b/examples/show_artist_top_tracks.py @@ -9,7 +9,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 6fed03f..4656eaa 100644 --- a/examples/show_related.py +++ b/examples/show_related.py @@ -10,8 +10,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/simple0.py b/examples/simple0.py index a89ca42..e0b0e1e 100644 --- a/examples/simple0.py +++ b/examples/simple0.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/simple1.py b/examples/simple1.py index c4cc562..8300215 100644 --- a/examples/simple1.py +++ b/examples/simple1.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/simple2.py b/examples/simple2.py index 1a207b2..3186c0d 100644 --- a/examples/simple2.py +++ b/examples/simple2.py @@ -4,8 +4,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/simple3.py b/examples/simple3.py index 86ca6d5..5d11f63 100644 --- a/examples/simple3.py +++ b/examples/simple3.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 f3bc321..0bdc401 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 = set(['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/cache_handler.py b/spotipy/cache_handler.py index 91d18cd..0344a27 100644 --- a/spotipy/cache_handler.py +++ b/spotipy/cache_handler.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- -__all__ = ['CacheHandler', 'CacheFileHandler'] +__all__ = ['CacheHandler', 'CacheFileHandler', 'MemoryCacheHandler'] import errno import json import logging +import os +from spotipy.util import CLIENT_CREDS_ENV_VARS from abc import ABC, abstractmethod logger = logging.getLogger(__name__) @@ -54,6 +56,7 @@ class CacheFileHandler(CacheHandler): self.cache_path = cache_path else: cache_path = ".cache" + username = (username or os.getenv(CLIENT_CREDS_ENV_VARS["client_username"])) if username: cache_path += "-" + str(username) self.cache_path = cache_path @@ -83,3 +86,24 @@ class CacheFileHandler(CacheHandler): except IOError: logger.warning('Couldn\'t write token to cache at: %s', self.cache_path) + + +class MemoryCacheHandler(CacheHandler): + """ + A cache handler that simply stores the token info in memory as an + instance attribute of this class. The token info will be lost when this + instance is freed. + """ + + def __init__(self, token_info=None): + """ + Parameters: + * token_info: The token info to store in memory. Can be None. + """ + self.token_info = token_info + + def get_cached_token(self): + return self.token_info + + def save_token_to_cache(self, token_info): + self.token_info = token_info diff --git a/spotipy/client.py b/spotipy/client.py index 16df251..eeccd22 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -98,11 +98,9 @@ class Spotify(object): 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, @@ -114,19 +112,16 @@ class Spotify(object): """ 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 @@ -146,17 +141,23 @@ class Spotify(object): The language parameter advertises what language the user prefers to see. See ISO-639 language code: https://www.loc.gov/standards/iso639-2/php/code_list.php """ + + 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): @@ -167,22 +168,6 @@ class Spotify(object): 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""" if isinstance(self._session, requests.Session): @@ -204,12 +189,12 @@ class Spotify(object): self._session.mount('https://', adapter) def _auth_headers(self): - if self._auth: - return {"Authorization": "Bearer {0}".format(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": "Bearer {0}".format(token)} @@ -615,34 +600,6 @@ class Spotify(object): 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 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. - - 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, @@ -697,55 +654,6 @@ class Spotify(object): 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 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("users/%s/starred" % user) - 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 @@ -777,197 +685,6 @@ class Spotify(object): return self._post("users/%s/playlists" % (user,), payload=data) - def user_playlist_change_details( - self, - user, - playlist_id, - name=None, - public=None, - collaborative=None, - description=None, - ): - warnings.warn( - "You should use `playlist_change_details(playlist_id, ...)` instead", - DeprecationWarning, - ) - """ 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 - """ - - return self.playlist_change_details(playlist_id, name, public, - collaborative, description) - - def user_playlist_unfollow(self, user, playlist_id): - """ 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 - ): - warnings.warn( - "You should use `playlist_add_items(playlist_id, tracks)` instead", - DeprecationWarning, - ) - """ 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 - """ - return self.playlist_add_items(playlist_id, tracks, position) - - def user_playlist_replace_tracks(self, user, playlist_id, tracks): - """ Replace all tracks in a playlist - - 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, - ): - """ Reorder tracks in a playlist - - 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 - ): - """ 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 - ): - """ 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( - "users/%s/playlists/%s/tracks" % (user, plid), payload=payload - ) - - def user_playlist_follow_playlist(self, playlist_owner_id, playlist_id): - """ - 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 - ): - """ - 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 e4bfde0..74292fc 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -5,7 +5,6 @@ __all__ = [ "SpotifyOAuth", "SpotifyOauthError", "SpotifyStateError", - "SpotifyImplicitGrant", "SpotifyPKCE" ] @@ -13,7 +12,6 @@ import base64 import logging import os import time -import warnings import webbrowser import requests @@ -179,10 +177,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. @@ -200,14 +198,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 """ @@ -225,35 +225,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 feches 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 """ @@ -309,14 +299,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 @@ -329,24 +317,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 or not 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(SpotifyOAuth, self).__init__(requests_session) @@ -356,34 +339,13 @@ class SpotifyOAuth(SpotifyAuthBase): self.redirect_uri = redirect_uri self.state = state self.scope = self._normalize_scope(scope) - username = (username or os.getenv(CLIENT_CREDS_ENV_VARS["client_username"])) - 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: - - 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 @@ -527,24 +489,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: @@ -552,7 +504,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, @@ -589,7 +541,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"] def refresh_access_token(self, refresh_token): payload = { @@ -636,26 +588,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 @@ -672,18 +604,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. @@ -694,20 +626,18 @@ class SpotifyPKCE(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} - * 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, thether or not 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`) + * 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(SpotifyPKCE, self).__init__(requests_session) @@ -715,31 +645,13 @@ class SpotifyPKCE(SpotifyAuthBase): self.redirect_uri = redirect_uri self.state = state self.scope = self._normalize_scope(scope) - username = (username or os.getenv(CLIENT_CREDS_ENV_VARS["client_username"])) - 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) + 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: - self.cache_handler = CacheFileHandler( - username=username, - cache_path=cache_path - ) + self.cache_handler = CacheFileHandler() self.proxies = proxies self.requests_timeout = requests_timeout @@ -1018,286 +930,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 of 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 Implict 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(SpotifyImplictGrant) 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(SpotifyImplictGrant).") - - self.client_id = client_id - self.redirect_uri = redirect_uri - self.state = state - username = (username or os.getenv(CLIENT_CREDS_ENV_VARS["client_username"])) - 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: - 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 "%s?%s" % (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("Received error from auth server: " - "{}".format(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 8ed6f8f..eec44ae 100644 --- a/spotipy/util.py +++ b/spotipy/util.py @@ -2,12 +2,9 @@ """ Shows a user's playlists (need to be authenticated via oauth) """ -__all__ = ["CLIENT_CREDS_ENV_VARS", "prompt_for_user_token"] +__all__ = ["CLIENT_CREDS_ENV_VARS"] import logging -import os -import warnings -import spotipy LOGGER = logging.getLogger(__name__) @@ -19,94 +16,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 - ) - """ prompts the user to login if necessary and returns - the 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 (optional) - - oauth_manager - Oauth manager object (optional) - - show_dialog - If true, a login prompt always shows (optional, defaults to False) - - """ - 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, the 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): if ":" in netloc: host, port = netloc.split(":", 1) diff --git a/tests/integration/test_non_user_endpoints.py b/tests/integration/test_non_user_endpoints.py index f72a3a0..39a2b08 100644 --- a/tests/integration/test_non_user_endpoints.py +++ b/tests/integration/test_non_user_endpoints.py @@ -58,7 +58,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): @@ -232,9 +233,9 @@ class AuthTestSpotipy(unittest.TestCase): self.assertTrue(found) 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), @@ -242,17 +243,15 @@ class AuthTestSpotipy(unittest.TestCase): 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): @@ -350,7 +349,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( @@ -359,7 +358,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/test_user_endpoints.py b/tests/integration/test_user_endpoints.py index 206b419..76d8889 100644 --- a/tests/integration/test_user_endpoints.py +++ b/tests/integration/test_user_endpoints.py @@ -2,16 +2,33 @@ 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 def setUpClass(cls): @@ -48,10 +65,9 @@ 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 \ @@ -119,8 +135,7 @@ class SpotipyPlaylistApiTest(unittest.TestCase): self.assertEqual(pl["tracks"]["total"], 0) def test_max_retries_reached_post(self): - i = 0 - while i < 500: + for i in range(500): try: self.spotify_no_retry.playlist_change_details( self.new_playlist['id'], description="test") @@ -128,7 +143,6 @@ class SpotipyPlaylistApiTest(unittest.TestCase): self.assertIsInstance(e, SpotifyException) self.assertEqual(e.http_status, 429) return - i += 1 self.fail() def test_playlist_add_items(self): @@ -188,17 +202,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 @@ -227,10 +230,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): @@ -305,9 +305,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) @@ -335,9 +333,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): response = self.spotify.category('rock') @@ -383,9 +379,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() @@ -438,9 +432,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 @@ -454,39 +446,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_user_follows_and_unfollows_artist(self): - # Initially follows 1 artist - current_user_followed_artists = self.spotify.current_user_followed_artists()[ - 'artists']['total'] - - # Follow 2 more artists - artists = ["6DPYiyq5kWVQS4RGwxzPC7", "0NbfKEOTQCcwd6o7wSDOHI"] - self.spotify.user_follow_artists(artists) - res = self.spotify.current_user_followed_artists() - self.assertEqual(res['artists']['total'], current_user_followed_artists + len(artists)) - - # Unfollow these 2 artists - self.spotify.user_unfollow_artists(artists) - res = self.spotify.current_user_followed_artists() - self.assertEqual(res['artists']['total'], current_user_followed_artists) - - 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 @@ -495,7 +454,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_user_follows_and_unfollows_artist(self): diff --git a/tests/unit/test_oauth.py b/tests/unit/test_oauth.py index 9acdcc9..3017767 100644 --- a/tests/unit/test_oauth.py +++ b/tests/unit/test_oauth.py @@ -5,10 +5,10 @@ import unittest import six.moves.urllib.parse as urllibparse -from spotipy import SpotifyOAuth, SpotifyImplicitGrant, SpotifyPKCE -from spotipy.cache_handler import CacheHandler +from spotipy import SpotifyOAuth, SpotifyPKCE from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOauthError from spotipy.oauth2 import SpotifyStateError +from spotipy import MemoryCacheHandler, CacheFileHandler try: import unittest.mock as mock @@ -43,26 +43,10 @@ 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) -class MemoryCache(CacheHandler): - def __init__(self, token_info=None): - self.token_info = token_info - - def get_cached_token(self): - return self.token_info - - def save_token_to_cache(self, token_info): - self.token_info = token_info - return None - - class OAuthCacheTest(unittest.TestCase): @patch.multiple(SpotifyOAuth, @@ -77,13 +61,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, @@ -100,7 +83,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) @@ -120,7 +104,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) @@ -136,32 +122,18 @@ 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) - spot = _make_oauth(scope, cache_handler=MemoryCache()) + spot = _make_oauth(scope, cache_handler=MemoryCacheHandler()) spot.cache_handler.save_token_to_cache(tok) cached_tok = spot.cache_handler.get_cached_token() @@ -261,141 +233,6 @@ class TestSpotifyClientCredentials(unittest.TestCase): 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, @@ -410,13 +247,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, @@ -433,7 +269,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) @@ -453,7 +290,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) @@ -469,27 +307,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())