Merge pull request #671 from Peter-Schorn/v3

V3
This commit is contained in:
Stéphane Bruckert 2021-04-14 20:57:00 +01:00 committed by GitHub
commit d7e8dd1e74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 318 additions and 1173 deletions

View File

@ -7,8 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased [3.0.0-alpha] ## Unreleased [3.0.0-alpha]
While this is unreleased, please only add v3 features here. While this is unreleased, please only add v3 features here. Rebasing master onto v3 doesn't require a changelog update.
Rebasing master onto v3 doesn't require a changelog update.
### Added ### Added
@ -17,16 +16,50 @@ Rebasing master onto v3 doesn't require a changelog update.
### Changed ### Changed
* Made `CacheHandler` an abstract base class * 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. * 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 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 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 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] ## Unreleased [2.x.x]
### Added ### 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 - Enabled using both short and long IDs for playlist_change_details
- Added a cache handler to `SpotifyClientCredentials` - Added a cache handler to `SpotifyClientCredentials`
- Added the following endpoints - Added the following endpoints
@ -43,13 +76,9 @@ Rebasing master onto v3 doesn't require a changelog update.
### Fixed ### 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 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. * 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__`. * Cleaned up the documentation for `SpotifyClientCredentials.__init__`, `SpotifyOAuth.__init__`, and `SpotifyPKCE.__init__`.
## [2.17.1] - 2021-02-28 ## [2.17.1] - 2021-02-28
### Fixed ### Fixed
@ -104,7 +133,7 @@ Rebasing master onto v3 doesn't require a changelog update.
### Added ### Added
- `SpotifyPKCE.parse_auth_response_url`, mirroring that method in - `SpotifyPKCE.parse_auth_response_url`, mirroring that method in
`SpotifyOAuth` `SpotifyOAuth`
### Changed ### Changed
@ -113,7 +142,7 @@ Rebasing master onto v3 doesn't require a changelog update.
### Fixed ### Fixed
- Using `SpotifyPKCE.get_authorization_url` will now generate a code - Using `SpotifyPKCE.get_authorization_url` will now generate a code
challenge if needed challenge if needed
## [2.14.0] - 2020-08-29 ## [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. - (experimental) Support to search multiple/all markets at once.
- Support to test whether the current user is following certain - Support to test whether the current user is following certain
users or artists users or artists
- Proper replacements for all deprecated playlist endpoints - 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. - 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 - Reason for 403 error in SpotifyException
- Support for the PKCE Auth Flow - 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_replace_tracks` in favor of `playlist_replace_items`
- `user_playlist_reorder_tracks` in favor of `playlist_reorder_items` - `user_playlist_reorder_tracks` in favor of `playlist_reorder_items`
- `user_playlist_remove_all_occurrences_of_tracks` in favor of - `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 - `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 - `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` - `user_playlist_is_following` in favor of `playlist_is_following`
- `playlist_tracks` in favor of `playlist_items` - `playlist_tracks` in favor of `playlist_items`
### Fixed ### Fixed
- fixed issue where episode URIs were being converted to track URIs in playlist calls - fixed issue where episode URIs were being converted to track URIs in playlist calls
## [2.13.0] - 2020-06-25 ## [2.13.0] - 2020-06-25
@ -157,12 +187,12 @@ Rebasing master onto v3 doesn't require a changelog update.
### Added ### Added
- Added `SpotifyImplicitGrant` as an auth manager option. It provides - Added `SpotifyImplicitGrant` as an auth manager option. It provides
user authentication without a client secret but sacrifices the ability user authentication without a client secret but sacrifices the ability
to refresh the token without user input. (However, read the class to refresh the token without user input. (However, read the class
docstring for security advisory.) docstring for security advisory.)
- Added built-in verification of the `state` query parameter - Added built-in verification of the `state` query parameter
- Added two new attributes: error and error_description to `SpotifyOauthError` exception class to show - 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` - Added `SpotifyStateError` subclass of `SpotifyOauthError`
- Allow extending `SpotifyClientCredentials` and `SpotifyOAuth` - Allow extending `SpotifyClientCredentials` and `SpotifyOAuth`
- Added the market paramter to `album_tracks` - Added the market paramter to `album_tracks`
@ -186,10 +216,10 @@ Rebasing master onto v3 doesn't require a changelog update.
### Changed ### Changed
- Updated the documentation to give more details on the authorization process and reflect - 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, - 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 ### Fixed
@ -258,6 +288,7 @@ Rebasing master onto v3 doesn't require a changelog update.
- Optional `show_dialog` parameter to be passed to `SpotifyOAuth` - Optional `show_dialog` parameter to be passed to `SpotifyOAuth`
### Changed ### Changed
- Both `SpotifyClientCredentials` and `SpotifyOAuth` inherit from a common `SpotifyAuthBase` which handles common parameters and logics. - Both `SpotifyClientCredentials` and `SpotifyOAuth` inherit from a common `SpotifyAuthBase` which handles common parameters and logics.
## [2.7.1] - 2020-01-20 ## [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 ## [2.6.1] - 2020-01-13
### Fixed ### Fixed
- Fixed inconsistent behaviour with some API methods when - 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 - Fixed invalid calls to logging warn method
### Removed ### Removed
- `mock` no longer needed for install. Only used in `tox`. - `mock` no longer needed for install. Only used in `tox`.
## [2.6.0] - 2020-01-12 ## [2.6.0] - 2020-01-12
### Added ### Added
- Support for `playlist` to get a playlist without specifying a user - Support for `playlist` to get a playlist without specifying a user
- Support for `current_user_saved_albums_delete` - Support for `current_user_saved_albums_delete`
- Support for `current_user_saved_albums_contains` - 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 - Lint with flake8 using Github action
### Changed ### Changed
- Fix typos in doc - Fix typos in doc
- Start following [SemVer](https://semver.org) properly - Start following [SemVer](https://semver.org) properly
## [2.5.0] - 2020-01-11 ## [2.5.0] - 2020-01-11
Added follow and player endpoints Added follow and player endpoints
## [2.4.4] - 2017-01-04 ## [2.4.4] - 2017-01-04
Python 3 fix Python 3 fix
## [2.4.3] - 2017-01-02 ## [2.4.3] - 2017-01-02
Fixed proxy issue in standard auth flow Fixed proxy issue in standard auth flow
## [2.4.2] - 2017-01-02 ## [2.4.2] - 2017-01-02
Support getting audio features for a single track Support getting audio features for a single track
## [2.4.1] - 2017-01-02 ## [2.4.1] - 2017-01-02
Incorporated proxy support Incorporated proxy support
## [2.4.0] - 2016-12-31 ## [2.4.0] - 2016-12-31
Incorporated a number of PRs Incorporated a number of PRs
## [2.3.8] - 2016-03-31 ## [2.3.8] - 2016-03-31
Added recs, audio features, user top lists Added recs, audio features, user top lists
## [2.3.7] - 2015-08-10 ## [2.3.7] - 2015-08-10
Added current_user_followed_artists Added current_user_followed_artists
## [2.3.6] - 2015-06-03 ## [2.3.6] - 2015-06-03
Support for offset/limit with album_tracks API Support for offset/limit with album_tracks API
## [2.3.5] - 2015-04-28 ## [2.3.5] - 2015-04-28
Fixed bug in auto retry logic Fixed bug in auto retry logic
## [2.3.3] - 2015-04-01 ## [2.3.3] - 2015-04-01
Aadded client credential flow Aadded client credential flow
## [2.3.2] - 2015-03-31 ## [2.3.2] - 2015-03-31
Added auto retry logic Added auto retry logic
## [2.3.0] - 2015-01-05 ## [2.3.0] - 2015-01-05
Added session support added by akx. Added session support added by akx.
## [2.2.0] - 2014-11-15 ## [2.2.0] - 2014-11-15
Added support for user_playlist_tracks Added support for user_playlist_tracks
## [2.1.0] - 2014-10-25 ## [2.1.0] - 2014-10-25
Added support for new_releases and featured_playlists Added support for new_releases and featured_playlists
## [2.0.2] - 2014-08-25 ## [2.0.2] - 2014-08-25
Moved to spotipy at pypi Moved to spotipy at pypi
## [1.2.0] - 2014-08-22 ## [1.2.0] - 2014-08-22
Upgraded APIs and docs to make it be a real library Upgraded APIs and docs to make it be a real library
## [1.310.0] - 2014-08-20 ## [1.310.0] - 2014-08-20
Added playlist replace and remove methods. Added auth tests. Improved API docs Added playlist replace and remove methods. Added auth tests. Improved API docs
## [1.301.0] - 2014-08-19 ## [1.301.0] - 2014-08-19
Upgraded version number to take precedence over previously botched release (sigh) Upgraded version number to take precedence over previously botched release (sigh)
## [1.50.0] - 2014-08-14 ## [1.50.0] - 2014-08-14
Refactored util out of examples and into the main package Refactored util out of examples and into the main package
## [1.49.0] - 2014-07-23 ## [1.49.0] - 2014-07-23
Support for "Your Music" tracks (add, delete, get), with examples Support for "Your Music" tracks (add, delete, get), with examples
## [1.45.0] - 2014-07-07 ## [1.45.0] - 2014-07-07
Support for related artists endpoint. Don't use cache auth codes when scope changes Support for related artists endpoint. Don't use cache auth codes when scope changes
## [1.44.0] - 2014-07-03 ## [1.44.0] - 2014-07-03
Added show tracks.py example Added show tracks.py example
## [1.43.0] - 2014-06-27 ## [1.43.0] - 2014-06-27
Fixed JSON handling issue Fixed JSON handling issue
## [1.42.0] - 2014-06-19 ## [1.42.0] - 2014-06-19
Removed dependency on simplejson Removed dependency on simplejson
## [1.40.0] - 2014-06-12 ## [1.40.0] - 2014-06-12
Initial public release. Initial public release.
## [1.4.2] - 2014-06-21 ## [1.4.2] - 2014-06-21
Added support for retrieving starred playlists Added support for retrieving starred playlists
## [1.1.0] - 2014-06-17 ## [1.1.0] - 2014-06-17
Updates to match released API Updates to match released API
## [1.1.0] - 2014-05-18 ## [1.1.0] - 2014-05-18
Repackaged for saner imports Repackaged for saner imports
## [1.0.0] - 2017-04-05 ## [1.0.0] - 2017-04-05
Initial release Initial release

View File

@ -7,7 +7,7 @@ import spotipy
logger = logging.getLogger('examples.artist_albums') logger = logging.getLogger('examples.artist_albums')
logging.basicConfig(level='INFO') logging.basicConfig(level='INFO')
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
def get_args(): def get_args():

View File

@ -68,6 +68,6 @@ def main():
if __name__ == '__main__': if __name__ == '__main__':
client_credentials_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) sp = spotipy.Spotify(auth_manager=auth_manager)
main() main()

View File

@ -8,8 +8,8 @@ from spotipy.oauth2 import SpotifyClientCredentials
logger = logging.getLogger('examples.artist_recommendations') logger = logging.getLogger('examples.artist_recommendations')
logging.basicConfig(level='INFO') logging.basicConfig(level='INFO')
client_credentials_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) sp = spotipy.Spotify(auth_manager=auth_manager)
def get_args(): def get_args():

View File

@ -8,8 +8,8 @@ import time
import sys import sys
client_credentials_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) sp = spotipy.Spotify(auth_manager=auth_manager)
if len(sys.argv) > 1: if len(sys.argv) > 1:
tid = sys.argv[1] tid = sys.argv[1]

View File

@ -9,8 +9,8 @@ import time
import sys import sys
client_credentials_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) sp = spotipy.Spotify(auth_manager=auth_manager)
sp.trace = False sp.trace = False
if len(sys.argv) > 1: if len(sys.argv) > 1:

View File

@ -10,8 +10,8 @@ import time
import sys import sys
client_credentials_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) sp = spotipy.Spotify(auth_manager=auth_manager)
sp.trace = True sp.trace = True
if len(sys.argv) > 1: if len(sys.argv) > 1:

View File

@ -2,8 +2,8 @@ from spotipy.oauth2 import SpotifyClientCredentials
import spotipy import spotipy
from pprint import pprint from pprint import pprint
client_credentials_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) sp = spotipy.Spotify(auth_manager=auth_manager)
search_str = 'Muse' search_str = 'Muse'
result = sp.search(search_str) result = sp.search(search_str)

View File

@ -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())

View File

@ -4,7 +4,7 @@ from pprint import pprint
from time import sleep from time import sleep
scope = "user-read-playback-state,user-modify-playback-state" 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 # Shows playing devices
res = sp.devices() res = sp.devices()

View File

@ -6,7 +6,7 @@ import spotipy
PlaylistExample = '37i9dQZEVXbMDoHDwVN2tF' PlaylistExample = '37i9dQZEVXbMDoHDwVN2tF'
# create spotipy client # create spotipy client
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
# load the first 100 songs # load the first 100 songs
tracks = [] tracks = []

View File

@ -2,7 +2,7 @@ from spotipy.oauth2 import SpotifyClientCredentials
import spotipy import spotipy
from pprint import pprint from pprint import pprint
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
pl_id = 'spotify:playlist:5RIbzhG2QqdkaP24iXLnZX' pl_id = 'spotify:playlist:5RIbzhG2QqdkaP24iXLnZX'
offset = 0 offset = 0

View File

@ -2,8 +2,8 @@ from spotipy.oauth2 import SpotifyClientCredentials
import spotipy import spotipy
import json import json
client_credentials_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) sp = spotipy.Spotify(auth_manager=auth_manager)
playlist_id = 'spotify:user:spotifycharts:playlist:37i9dQZEVXbJiZcmkrIHGU' playlist_id = 'spotify:user:spotifycharts:playlist:37i9dQZEVXbJiZcmkrIHGU'
results = sp.playlist(playlist_id) results = sp.playlist(playlist_id)

View File

@ -10,6 +10,6 @@ if len(sys.argv) > 1:
else: else:
search_str = 'Radiohead' search_str = 'Radiohead'
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
result = sp.search(search_str) result = sp.search(search_str)
pprint.pprint(result) pprint.pprint(result)

View File

@ -11,6 +11,6 @@ if len(sys.argv) > 1:
else: else:
urn = 'spotify:album:5yTx83u3qerZF7GRJu7eFk' urn = 'spotify:album:5yTx83u3qerZF7GRJu7eFk'
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
album = sp.album(urn) album = sp.album(urn)
pprint(album) pprint(album)

View File

@ -10,7 +10,7 @@ if len(sys.argv) > 1:
else: else:
urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu' urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu'
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
artist = sp.artist(urn) artist = sp.artist(urn)
pprint(artist) pprint(artist)

View File

@ -9,7 +9,7 @@ if len(sys.argv) > 1:
else: else:
urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu' urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu'
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
response = sp.artist_top_tracks(urn) response = sp.artist_top_tracks(urn)
for track in response['tracks']: for track in response['tracks']:

View File

@ -10,8 +10,8 @@ if len(sys.argv) > 1:
else: else:
artist_name = 'weezer' artist_name = 'weezer'
client_credentials_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) sp = spotipy.Spotify(auth_manager=auth_manager)
result = sp.search(q='artist:' + artist_name, type='artist') result = sp.search(q='artist:' + artist_name, type='artist')
try: try:
name = result['artists']['items'][0]['name'] name = result['artists']['items'][0]['name']

View File

@ -10,7 +10,7 @@ if len(sys.argv) > 1:
else: else:
urn = 'spotify:track:0Svkvt5I79wficMFgaqEQJ' urn = 'spotify:track:0Svkvt5I79wficMFgaqEQJ'
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
track = sp.track(urn) track = sp.track(urn)
pprint(track) pprint(track)

View File

@ -15,8 +15,8 @@ if __name__ == '__main__':
file = sys.stdin file = sys.stdin
tids = file.read().split() tids = file.read().split()
client_credentials_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) sp = spotipy.Spotify(auth_manager=auth_manager)
for start in range(0, len(tids), max_tracks_per_call): for start in range(0, len(tids), max_tracks_per_call):
results = sp.tracks(tids[start: start + max_tracks_per_call]) results = sp.tracks(tids[start: start + max_tracks_per_call])
for track in results['tracks']: for track in results['tracks']:

View File

@ -10,8 +10,8 @@ if len(sys.argv) > 1:
else: else:
username = 'plamere' username = 'plamere'
client_credentials_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) sp = spotipy.Spotify(auth_manager=auth_manager)
sp.trace = True sp.trace = True
user = sp.user(username) user = sp.user(username)
pprint.pprint(user) pprint.pprint(user)

View File

@ -1,8 +1,8 @@
from spotipy.oauth2 import SpotifyClientCredentials from spotipy.oauth2 import SpotifyClientCredentials
import spotipy import spotipy
client_credentials_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) sp = spotipy.Spotify(auth_manager=auth_manager)
results = sp.search(q='weezer', limit=20) results = sp.search(q='weezer', limit=20)
for i, t in enumerate(results['tracks']['items']): for i, t in enumerate(results['tracks']['items']):

View File

@ -3,8 +3,8 @@ import spotipy
birdy_uri = 'spotify:artist:2WX2uTcsvV5OnS0inACecP' birdy_uri = 'spotify:artist:2WX2uTcsvV5OnS0inACecP'
client_credentials_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) sp = spotipy.Spotify(auth_manager=auth_manager)
results = sp.artist_albums(birdy_uri, album_type='album') results = sp.artist_albums(birdy_uri, album_type='album')
albums = results['items'] albums = results['items']

View File

@ -4,8 +4,8 @@ import spotipy
lz_uri = 'spotify:artist:36QJpDe2go2KgaRleHCDTp' lz_uri = 'spotify:artist:36QJpDe2go2KgaRleHCDTp'
client_credentials_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) sp = spotipy.Spotify(auth_manager=auth_manager)
results = sp.artist_top_tracks(lz_uri) results = sp.artist_top_tracks(lz_uri)

View File

@ -4,7 +4,7 @@ import sys
from spotipy.oauth2 import SpotifyClientCredentials from spotipy.oauth2 import SpotifyClientCredentials
import spotipy import spotipy
sp = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials()) sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials())
if len(sys.argv) > 1: if len(sys.argv) > 1:
name = ' '.join(sys.argv[1:]) name = ' '.join(sys.argv[1:])

View File

@ -9,8 +9,8 @@ import random
usage: python title_chain.py [song name] usage: python title_chain.py [song name]
''' '''
client_credentials_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) sp = spotipy.Spotify(auth_manager=auth_manager)
skiplist = set(['dm', 'remix']) skiplist = set(['dm', 'remix'])

View File

@ -6,8 +6,8 @@ from spotipy.oauth2 import SpotifyClientCredentials
import spotipy import spotipy
import sys import sys
client_credentials_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) sp = spotipy.Spotify(auth_manager=auth_manager)
if len(sys.argv) > 1: if len(sys.argv) > 1:
artist_name = ' '.join(sys.argv[1:]) artist_name = ' '.join(sys.argv[1:])

View File

@ -6,8 +6,8 @@ import sys
import spotipy import spotipy
from spotipy.oauth2 import SpotifyClientCredentials from spotipy.oauth2 import SpotifyClientCredentials
client_credentials_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) sp = spotipy.Spotify(auth_manager=auth_manager)
user = 'spotify' user = 'spotify'

View File

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
__all__ = ['CacheHandler', 'CacheFileHandler'] __all__ = ['CacheHandler', 'CacheFileHandler', 'MemoryCacheHandler']
import errno import errno
import json import json
import logging import logging
import os
from spotipy.util import CLIENT_CREDS_ENV_VARS
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -54,6 +56,7 @@ class CacheFileHandler(CacheHandler):
self.cache_path = cache_path self.cache_path = cache_path
else: else:
cache_path = ".cache" cache_path = ".cache"
username = (username or os.getenv(CLIENT_CREDS_ENV_VARS["client_username"]))
if username: if username:
cache_path += "-" + str(username) cache_path += "-" + str(username)
self.cache_path = cache_path self.cache_path = cache_path
@ -83,3 +86,24 @@ class CacheFileHandler(CacheHandler):
except IOError: except IOError:
logger.warning('Couldn\'t write token to cache at: %s', logger.warning('Couldn\'t write token to cache at: %s',
self.cache_path) 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

View File

@ -98,11 +98,9 @@ class Spotify(object):
def __init__( def __init__(
self, self,
auth=None, access_token=None,
requests_session=True,
client_credentials_manager=None,
oauth_manager=None,
auth_manager=None, auth_manager=None,
requests_session=True,
proxies=None, proxies=None,
requests_timeout=5, requests_timeout=5,
status_forcelist=None, status_forcelist=None,
@ -114,19 +112,16 @@ class Spotify(object):
""" """
Creates a Spotify API client. 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: :param requests_session:
A Requests session object or a truthy value to create one. A Requests session object or a true value to create one.
A falsy value disables sessions. A false value disables sessions.
It should generally be a good idea to keep sessions enabled It should generally be a good idea to keep sessions enabled
for performance reasons (connection pooling). 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: :param proxies:
Definition of proxies (optional). Definition of proxies (optional).
See Requests doc https://2.python-requests.org/en/master/user/advanced/#proxies 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. 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 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.prefix = "https://api.spotify.com/v1/"
self._auth = auth self.access_token = access_token
self.client_credentials_manager = client_credentials_manager
self.oauth_manager = oauth_manager
self.auth_manager = auth_manager self.auth_manager = auth_manager
self.proxies = proxies self.proxies = proxies
self.requests_timeout = requests_timeout self.requests_timeout = requests_timeout
self.status_forcelist = status_forcelist or self.default_retry_codes self.status_forcelist = status_forcelist or self.default_retry_codes
self.backoff_factor = backoff_factor
self.retries = retries self.retries = retries
self.status_retries = status_retries self.status_retries = status_retries
self.backoff_factor = backoff_factor
self.language = language self.language = language
if isinstance(requests_session, requests.Session): if isinstance(requests_session, requests.Session):
@ -167,22 +168,6 @@ class Spotify(object):
else: # Use the Requests API module as a "session". else: # Use the Requests API module as a "session".
self._session = requests.api 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): def __del__(self):
"""Make sure the connection (pool) gets closed""" """Make sure the connection (pool) gets closed"""
if isinstance(self._session, requests.Session): if isinstance(self._session, requests.Session):
@ -204,12 +189,12 @@ class Spotify(object):
self._session.mount('https://', adapter) self._session.mount('https://', adapter)
def _auth_headers(self): def _auth_headers(self):
if self._auth: if self.access_token:
return {"Authorization": "Bearer {0}".format(self._auth)} return {"Authorization": "Bearer {0}".format(self.access_token)}
if not self.auth_manager: if not self.auth_manager:
return {} return {}
try: try:
token = self.auth_manager.get_access_token(as_dict=False) token = self.auth_manager.get_access_token()
except TypeError: except TypeError:
token = self.auth_manager.get_access_token() token = self.auth_manager.get_access_token()
return {"Authorization": "Bearer {0}".format(token)} return {"Authorization": "Bearer {0}".format(token)}
@ -615,34 +600,6 @@ class Spotify(object):
additional_types=",".join(additional_types), 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( def playlist_items(
self, self,
playlist_id, playlist_id,
@ -697,55 +654,6 @@ class Spotify(object):
content_type="image/jpeg", 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): def user_playlists(self, user, limit=50, offset=0):
""" Gets playlists of a user """ Gets playlists of a user
@ -777,197 +685,6 @@ class Spotify(object):
return self._post("users/%s/playlists" % (user,), payload=data) 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( def playlist_change_details(
self, self,
playlist_id, playlist_id,

View File

@ -5,7 +5,6 @@ __all__ = [
"SpotifyOAuth", "SpotifyOAuth",
"SpotifyOauthError", "SpotifyOauthError",
"SpotifyStateError", "SpotifyStateError",
"SpotifyImplicitGrant",
"SpotifyPKCE" "SpotifyPKCE"
] ]
@ -13,7 +12,6 @@ import base64
import logging import logging
import os import os
import time import time
import warnings
import webbrowser import webbrowser
import requests import requests
@ -179,10 +177,10 @@ class SpotifyClientCredentials(SpotifyAuthBase):
self, self,
client_id=None, client_id=None,
client_secret=None, client_secret=None,
cache_handler=None,
proxies=None, proxies=None,
requests_session=True, requests_session=True,
requests_timeout=None, requests_timeout=None
cache_handler=None
): ):
""" """
Creates a Client Credentials Flow Manager. Creates a Client Credentials Flow Manager.
@ -200,14 +198,16 @@ class SpotifyClientCredentials(SpotifyAuthBase):
Parameters: Parameters:
* client_id: Must be supplied or set as environment variable * client_id: Must be supplied or set as environment variable
* client_secret: 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 * cache_handler: An instance of the `CacheHandler` class to handle
getting and saving cached authorization tokens. getting and saving cached authorization tokens.
Optional, will otherwise use `CacheFileHandler`. 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: else:
self.cache_handler = CacheFileHandler() 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 If a valid access token is in memory, returns it
Else feches a new token and returns it Else feches a new token and returns it
Parameters: Parameters:
- as_dict - a boolean indicating if returning the access token - check_cache - if true, checks for a locally stored token
as a token_info dictionary, otherwise it will be returned before requesting a new token.
as a string.
""" """
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: if check_cache:
token_info = self.cache_handler.get_cached_token() token_info = self.cache_handler.get_cached_token()
if token_info and not self.is_token_expired(token_info): 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._request_access_token()
token_info = self._add_custom_values_to_token_info(token_info) token_info = self._add_custom_values_to_token_info(token_info)
self.cache_handler.save_token_to_cache(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): def _request_access_token(self):
"""Gets client credentials access token """ """Gets client credentials access token """
@ -309,14 +299,12 @@ class SpotifyOAuth(SpotifyAuthBase):
redirect_uri=None, redirect_uri=None,
state=None, state=None,
scope=None, scope=None,
cache_path=None, cache_handler=None,
username=None,
proxies=None, proxies=None,
show_dialog=False, show_dialog=False,
requests_session=True, requests_session=True,
requests_timeout=None, requests_timeout=None,
open_browser=True, open_browser=True
cache_handler=None
): ):
""" """
Creates a SpotifyOAuth object 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: Optional, either a string of scopes, or an iterable with elements of type
`Scope` or `str`. E.g., `Scope` or `str`. E.g.,
{Scope.user_modify_playback_state, Scope.user_library_read} {Scope.user_modify_playback_state, Scope.user_library_read}
* cache_handler: An instance of the `CacheHandler` class to handle
iterable of scopes or comma separated string of scopes. getting and saving cached authorization tokens.
e.g, "playlist-read-private,playlist-read-collaborative" Optional, will otherwise use `CacheFileHandler`.
* 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 * proxies: Optional, proxy for the requests library to route through
* show_dialog: Optional, interpreted as boolean * 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 * requests_timeout: Optional, tell Requests to stop waiting for a response after
a given number of seconds a given number of seconds
* open_browser: Optional, whether or not the web browser should be opened to * open_browser: Optional, whether or not the web browser should be opened to
authorize a user 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) super(SpotifyOAuth, self).__init__(requests_session)
@ -356,34 +339,13 @@ class SpotifyOAuth(SpotifyAuthBase):
self.redirect_uri = redirect_uri self.redirect_uri = redirect_uri
self.state = state self.state = state
self.scope = self._normalize_scope(scope) 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: if cache_handler:
assert issubclass(cache_handler.__class__, CacheHandler), \ assert issubclass(cache_handler.__class__, CacheHandler), \
"cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \ "cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \
+ " != " + str(CacheHandler) + " != " + str(CacheHandler)
self.cache_handler = cache_handler self.cache_handler = cache_handler
else: else:
self.cache_handler = CacheFileHandler()
self.cache_handler = CacheFileHandler(
username=username,
cache_path=cache_path
)
self.proxies = proxies self.proxies = proxies
self.requests_timeout = requests_timeout self.requests_timeout = requests_timeout
self.show_dialog = show_dialog self.show_dialog = show_dialog
@ -527,24 +489,14 @@ class SpotifyOAuth(SpotifyAuthBase):
return self.parse_response_code(response) return self.parse_response_code(response)
return self.get_auth_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 """ Gets the access token for the app given the code
Parameters: Parameters:
- code - the response code - code - the response code
- as_dict - a boolean indicating if returning the access token - check_cache - if true, checks for a locally stored token
as a token_info dictionary, otherwise it will be returned before requesting a new token
as a string.
""" """
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: if check_cache:
token_info = self.validate_token(self.cache_handler.get_cached_token()) token_info = self.validate_token(self.cache_handler.get_cached_token())
if token_info is not None: if token_info is not None:
@ -552,7 +504,7 @@ class SpotifyOAuth(SpotifyAuthBase):
token_info = self.refresh_access_token( token_info = self.refresh_access_token(
token_info["refresh_token"] token_info["refresh_token"]
) )
return token_info if as_dict else token_info["access_token"] return token_info["access_token"]
payload = { payload = {
"redirect_uri": self.redirect_uri, "redirect_uri": self.redirect_uri,
@ -589,7 +541,7 @@ class SpotifyOAuth(SpotifyAuthBase):
token_info = response.json() token_info = response.json()
token_info = self._add_custom_values_to_token_info(token_info) token_info = self._add_custom_values_to_token_info(token_info)
self.cache_handler.save_token_to_cache(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): def refresh_access_token(self, refresh_token):
payload = { payload = {
@ -636,26 +588,6 @@ class SpotifyOAuth(SpotifyAuthBase):
token_info["scope"] = self.scope token_info["scope"] = self.scope
return token_info 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): class SpotifyPKCE(SpotifyAuthBase):
""" Implements PKCE Authorization Flow for client apps """ Implements PKCE Authorization Flow for client apps
@ -672,18 +604,18 @@ class SpotifyPKCE(SpotifyAuthBase):
OAUTH_AUTHORIZE_URL = "https://accounts.spotify.com/authorize" OAUTH_AUTHORIZE_URL = "https://accounts.spotify.com/authorize"
OAUTH_TOKEN_URL = "https://accounts.spotify.com/api/token" OAUTH_TOKEN_URL = "https://accounts.spotify.com/api/token"
def __init__(self, def __init__(
client_id=None, self,
redirect_uri=None, client_id=None,
state=None, redirect_uri=None,
scope=None, state=None,
cache_path=None, scope=None,
username=None, cache_handler=None,
proxies=None, proxies=None,
requests_timeout=None, requests_timeout=None,
requests_session=True, requests_session=True,
open_browser=True, open_browser=True
cache_handler=None): ):
""" """
Creates Auth Manager with the PKCE Auth flow. 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: Optional, either a string of scopes, or an iterable with elements of type
`Scope` or `str`. E.g., `Scope` or `str`. E.g.,
{Scope.user_modify_playback_state, Scope.user_library_read} {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 * cache_handler: An instance of the `CacheHandler` class to handle
getting and saving cached authorization tokens. getting and saving cached authorization tokens.
Optional, will otherwise use `CacheFileHandler`. 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) super(SpotifyPKCE, self).__init__(requests_session)
@ -715,31 +645,13 @@ class SpotifyPKCE(SpotifyAuthBase):
self.redirect_uri = redirect_uri self.redirect_uri = redirect_uri
self.state = state self.state = state
self.scope = self._normalize_scope(scope) 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: if cache_handler:
assert issubclass(type(cache_handler), CacheHandler), \ assert issubclass(cache_handler.__class__, CacheHandler), \
"type(cache_handler): " + str(type(cache_handler)) + " != " + str(CacheHandler) "cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \
+ " != " + str(CacheHandler)
self.cache_handler = cache_handler self.cache_handler = cache_handler
else: else:
self.cache_handler = CacheFileHandler( self.cache_handler = CacheFileHandler()
username=username,
cache_path=cache_path
)
self.proxies = proxies self.proxies = proxies
self.requests_timeout = requests_timeout self.requests_timeout = requests_timeout
@ -1018,286 +930,6 @@ class SpotifyPKCE(SpotifyAuthBase):
def parse_auth_response_url(url): def parse_auth_response_url(url):
return SpotifyOAuth.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): class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self): def do_GET(self):

View File

@ -2,12 +2,9 @@
""" Shows a user's playlists (need to be authenticated via oauth) """ """ 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 logging
import os
import warnings
import spotipy
LOGGER = logging.getLogger(__name__) 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): def get_host_port(netloc):
if ":" in netloc: if ":" in netloc:
host, port = netloc.split(":", 1) host, port = netloc.split(":", 1)

View File

@ -58,7 +58,8 @@ class AuthTestSpotipy(unittest.TestCase):
@classmethod @classmethod
def setUpClass(self): def setUpClass(self):
self.spotify = Spotify( self.spotify = Spotify(
client_credentials_manager=SpotifyClientCredentials()) auth_manager=SpotifyClientCredentials()
)
self.spotify.trace = False self.spotify.trace = False
def test_audio_analysis(self): def test_audio_analysis(self):
@ -232,9 +233,9 @@ class AuthTestSpotipy(unittest.TestCase):
self.assertTrue(found) self.assertTrue(found)
def test_search_timeout(self): def test_search_timeout(self):
client_credentials_manager = SpotifyClientCredentials() auth_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(requests_timeout=0.01, 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" # depending on the timing or bandwidth, this raises a timeout or connection error"
self.assertRaises((requests.exceptions.Timeout, requests.exceptions.ConnectionError), self.assertRaises((requests.exceptions.Timeout, requests.exceptions.ConnectionError),
@ -242,17 +243,15 @@ class AuthTestSpotipy(unittest.TestCase):
def test_max_retries_reached_get(self): def test_max_retries_reached_get(self):
spotify_no_retry = Spotify( spotify_no_retry = Spotify(
client_credentials_manager=SpotifyClientCredentials(), auth_manager=SpotifyClientCredentials(),
retries=0) retries=0)
i = 0 for i in range(100):
while i < 100:
try: try:
spotify_no_retry.search(q='foo') spotify_no_retry.search(q='foo')
except SpotifyException as e: except SpotifyException as e:
self.assertIsInstance(e, SpotifyException) self.assertIsInstance(e, SpotifyException)
self.assertEqual(e.http_status, 429) self.assertEqual(e.http_status, 429)
return return
i += 1
self.fail() self.fail()
def test_album_search(self): def test_album_search(self):
@ -350,7 +349,7 @@ class AuthTestSpotipy(unittest.TestCase):
sess = requests.Session() sess = requests.Session()
sess.headers["user-agent"] = "spotipy-test" sess.headers["user-agent"] = "spotipy-test"
with_custom_session = spotipy.Spotify( with_custom_session = spotipy.Spotify(
client_credentials_manager=SpotifyClientCredentials(), auth_manager=SpotifyClientCredentials(),
requests_session=sess) requests_session=sess)
self.assertTrue( self.assertTrue(
with_custom_session.user( with_custom_session.user(
@ -359,7 +358,7 @@ class AuthTestSpotipy(unittest.TestCase):
def test_force_no_requests_session(self): def test_force_no_requests_session(self):
with_no_session = spotipy.Spotify( with_no_session = spotipy.Spotify(
client_credentials_manager=SpotifyClientCredentials(), auth_manager=SpotifyClientCredentials(),
requests_session=False) requests_session=False)
self.assertNotIsInstance(with_no_session._session, requests.Session) self.assertNotIsInstance(with_no_session._session, requests.Session)
user = with_no_session.user(user="akx") user = with_no_session.user(user="akx")

View File

@ -2,16 +2,33 @@ import os
from spotipy import ( from spotipy import (
CLIENT_CREDS_ENV_VARS as CCEV, CLIENT_CREDS_ENV_VARS as CCEV,
prompt_for_user_token,
Spotify, Spotify,
SpotifyException, SpotifyException,
SpotifyImplicitGrant, SpotifyOAuth,
SpotifyPKCE SpotifyPKCE,
CacheFileHandler
) )
import unittest import unittest
from tests import helpers 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): class SpotipyPlaylistApiTest(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@ -48,10 +65,9 @@ class SpotipyPlaylistApiTest(unittest.TestCase):
'user-read-playback-state' '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_name = 'spotipy-playlist-test'
cls.new_playlist = helpers.get_spotify_playlist( cls.new_playlist = helpers.get_spotify_playlist(
cls.spotify, cls.new_playlist_name, cls.username) or \ cls.spotify, cls.new_playlist_name, cls.username) or \
@ -119,8 +135,7 @@ class SpotipyPlaylistApiTest(unittest.TestCase):
self.assertEqual(pl["tracks"]["total"], 0) self.assertEqual(pl["tracks"]["total"], 0)
def test_max_retries_reached_post(self): def test_max_retries_reached_post(self):
i = 0 for i in range(500):
while i < 500:
try: try:
self.spotify_no_retry.playlist_change_details( self.spotify_no_retry.playlist_change_details(
self.new_playlist['id'], description="test") self.new_playlist['id'], description="test")
@ -128,7 +143,6 @@ class SpotipyPlaylistApiTest(unittest.TestCase):
self.assertIsInstance(e, SpotifyException) self.assertIsInstance(e, SpotifyException)
self.assertEqual(e.http_status, 429) self.assertEqual(e.http_status, 429)
return return
i += 1
self.fail() self.fail()
def test_playlist_add_items(self): def test_playlist_add_items(self):
@ -188,17 +202,6 @@ class SpotipyPlaylistApiTest(unittest.TestCase):
return return
self.fail() 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): class SpotipyLibraryApiTests(unittest.TestCase):
@classmethod @classmethod
@ -227,10 +230,7 @@ class SpotipyLibraryApiTests(unittest.TestCase):
'ugc-image-upload ' 'ugc-image-upload '
'user-read-playback-state' 'user-read-playback-state'
) )
cls.spotify = _make_spotify(scopes=scope)
token = prompt_for_user_token(cls.username, scope=scope)
cls.spotify = Spotify(auth=token)
def test_track_bad_id(self): def test_track_bad_id(self):
with self.assertRaises(SpotifyException): with self.assertRaises(SpotifyException):
@ -305,9 +305,7 @@ class SpotipyUserApiTests(unittest.TestCase):
'user-read-playback-state' 'user-read-playback-state'
) )
token = prompt_for_user_token(cls.username, scope=scope) cls.spotify = _make_spotify(scopes=scope)
cls.spotify = Spotify(auth=token)
def test_basic_user_profile(self): def test_basic_user_profile(self):
user = self.spotify.user(self.username) user = self.spotify.user(self.username)
@ -335,9 +333,7 @@ class SpotipyUserApiTests(unittest.TestCase):
class SpotipyBrowseApiTests(unittest.TestCase): class SpotipyBrowseApiTests(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
username = os.getenv(CCEV['client_username']) cls.spotify = _make_spotify()
token = prompt_for_user_token(username)
cls.spotify = Spotify(auth=token)
def test_category(self): def test_category(self):
response = self.spotify.category('rock') response = self.spotify.category('rock')
@ -383,9 +379,7 @@ class SpotipyFollowApiTests(unittest.TestCase):
'user-read-playback-state' 'user-read-playback-state'
) )
token = prompt_for_user_token(cls.username, scope=scope) cls.spotify = _make_spotify(scopes=scope)
cls.spotify = Spotify(auth=token)
def test_current_user_follows(self): def test_current_user_follows(self):
response = self.spotify.current_user_followed_artists() response = self.spotify.current_user_followed_artists()
@ -438,9 +432,7 @@ class SpotipyPlayerApiTests(unittest.TestCase):
'user-read-playback-state' 'user-read-playback-state'
) )
token = prompt_for_user_token(cls.username, scope=scope) cls.spotify = _make_spotify(scopes=scope)
cls.spotify = Spotify(auth=token)
def test_devices(self): def test_devices(self):
# No devices playing by default # 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 # 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): class SpotifyPKCETests(unittest.TestCase):
@classmethod @classmethod
@ -495,7 +454,8 @@ class SpotifyPKCETests(unittest.TestCase):
'user-follow-read ' 'user-follow-read '
'user-follow-modify ' '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) cls.spotify = Spotify(auth_manager=auth_manager)
def test_user_follows_and_unfollows_artist(self): def test_user_follows_and_unfollows_artist(self):

View File

@ -5,10 +5,10 @@ import unittest
import six.moves.urllib.parse as urllibparse import six.moves.urllib.parse as urllibparse
from spotipy import SpotifyOAuth, SpotifyImplicitGrant, SpotifyPKCE from spotipy import SpotifyOAuth, SpotifyPKCE
from spotipy.cache_handler import CacheHandler
from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOauthError from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOauthError
from spotipy.oauth2 import SpotifyStateError from spotipy.oauth2 import SpotifyStateError
from spotipy import MemoryCacheHandler, CacheFileHandler
try: try:
import unittest.mock as mock import unittest.mock as mock
@ -43,26 +43,10 @@ def _make_oauth(*args, **kwargs):
return SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", *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): def _make_pkceauth(*args, **kwargs):
return SpotifyPKCE("CLID", "REDIR", "STATE", *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): class OAuthCacheTest(unittest.TestCase):
@patch.multiple(SpotifyOAuth, @patch.multiple(SpotifyOAuth,
@ -77,13 +61,12 @@ class OAuthCacheTest(unittest.TestCase):
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False)) opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
is_token_expired.return_value = 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 = spot.validate_token(spot.cache_handler.get_cached_token())
cached_tok_legacy = spot.get_cached_token()
opener.assert_called_with(path) opener.assert_called_with(path)
self.assertIsNotNone(cached_tok) self.assertIsNotNone(cached_tok)
self.assertIsNotNone(cached_tok_legacy)
self.assertEqual(refresh_access_token.call_count, 0) self.assertEqual(refresh_access_token.call_count, 0)
@patch.multiple(SpotifyOAuth, @patch.multiple(SpotifyOAuth,
@ -100,7 +83,8 @@ class OAuthCacheTest(unittest.TestCase):
opener.return_value = token_file opener.return_value = token_file
refresh_access_token.return_value = fresh_tok 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()) spot.validate_token(spot.cache_handler.get_cached_token())
is_token_expired.assert_called_with(expired_tok) 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)) opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
is_token_expired.return_value = 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()) cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
opener.assert_called_with(path) opener.assert_called_with(path)
@ -136,32 +122,18 @@ class OAuthCacheTest(unittest.TestCase):
fi = _fake_file() fi = _fake_file()
opener.return_value = fi 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) spot.cache_handler.save_token_to_cache(tok)
opener.assert_called_with(path, 'w') opener.assert_called_with(path, 'w')
self.assertTrue(fi.write.called) 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): def test_cache_handler(self):
scope = "playlist-modify-private" scope = "playlist-modify-private"
tok = _make_fake_token(1, 1, scope) 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) spot.cache_handler.save_token_to_cache(tok)
cached_tok = spot.cache_handler.get_cached_token() cached_tok = spot.cache_handler.get_cached_token()
@ -261,141 +233,6 @@ class TestSpotifyClientCredentials(unittest.TestCase):
self.assertEqual(error.exception.error, 'invalid_client') 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): class SpotifyPKCECacheTest(unittest.TestCase):
@patch.multiple(SpotifyPKCE, @patch.multiple(SpotifyPKCE,
@ -410,13 +247,12 @@ class SpotifyPKCECacheTest(unittest.TestCase):
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False)) opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
is_token_expired.return_value = 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 = spot.cache_handler.get_cached_token()
cached_tok_legacy = spot.get_cached_token()
opener.assert_called_with(path) opener.assert_called_with(path)
self.assertIsNotNone(cached_tok) self.assertIsNotNone(cached_tok)
self.assertIsNotNone(cached_tok_legacy)
self.assertEqual(refresh_access_token.call_count, 0) self.assertEqual(refresh_access_token.call_count, 0)
@patch.multiple(SpotifyPKCE, @patch.multiple(SpotifyPKCE,
@ -433,7 +269,8 @@ class SpotifyPKCECacheTest(unittest.TestCase):
opener.return_value = token_file opener.return_value = token_file
refresh_access_token.return_value = fresh_tok 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()) spot.validate_token(spot.cache_handler.get_cached_token())
is_token_expired.assert_called_with(expired_tok) 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)) opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False))
is_token_expired.return_value = 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()) cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
opener.assert_called_with(path) opener.assert_called_with(path)
@ -469,27 +307,13 @@ class SpotifyPKCECacheTest(unittest.TestCase):
fi = _fake_file() fi = _fake_file()
opener.return_value = fi 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) spot.cache_handler.save_token_to_cache(tok)
opener.assert_called_with(path, 'w') opener.assert_called_with(path, 'w')
self.assertTrue(fi.write.called) 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): class TestSpotifyPKCE(unittest.TestCase):

View File

@ -88,3 +88,28 @@ class SpotipyScopeTest(TestCase):
self.assertEqual(normalized_scope_string_2, "") self.assertEqual(normalized_scope_string_2, "")
self.assertIsNone(self.normalize_scope(None)) 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())