From 6faf49cbe3cc417cec6671308c184c0542ecc0c5 Mon Sep 17 00:00:00 2001 From: Peter-Schorn Date: Wed, 11 Nov 2020 00:43:50 -0600 Subject: [PATCH 1/9] Added an exception clause that catches `FileNotFoundError` and logs a debug message in `SpotifyOAuth.get_cached_token`, `SpotifyPKCE.get_cached_token` and `SpotifyImplicitGrant.get_cached_token`. --- spotipy/oauth2.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index 802e58bc..d046e802 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -306,6 +306,8 @@ def get_cached_token(self): token_info["refresh_token"] ) + except FileNotFoundError: + logger.debug("cache does not exist at: %s", self.cache_path) except IOError: logger.warning("Couldn't read cache at: %s", self.cache_path) @@ -785,6 +787,8 @@ def get_cached_token(self): token_info["refresh_token"] ) + except FileNotFoundError: + logger.debug("cache does not exist at: %s", self.cache_path) except IOError: logger.warning("Couldn't read cache at: %s", self.cache_path) @@ -1030,6 +1034,8 @@ def get_cached_token(self): if self.is_token_expired(token_info): return None + except FileNotFoundError: + logger.debug("cache does not exist at: %s", self.cache_path) except IOError: logger.warning("Couldn't read cache at: %s", self.cache_path) From 8ae2cddcff67262b18ff770218f59b3341919dc4 Mon Sep 17 00:00:00 2001 From: Peter-Schorn Date: Wed, 11 Nov 2020 00:53:15 -0600 Subject: [PATCH 2/9] Changed docs for `auth` parameter of `Spotify.init` to `access token` instead of `authorization token`. In issue #599, a user confused the access token with the authorization code. --- spotipy/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spotipy/client.py b/spotipy/client.py index fbb719af..6b0d97ff 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -114,7 +114,7 @@ def __init__( """ Creates a Spotify API client. - :param auth: An authorization token (optional) + :param auth: An access token (optional) :param requests_session: A Requests session object or a truthy value to create one. A falsy value disables sessions. From 957a26af2b6fd831db9317768e8d41154d518ccc Mon Sep 17 00:00:00 2001 From: Peter-Schorn Date: Wed, 11 Nov 2020 01:03:28 -0600 Subject: [PATCH 3/9] Updated CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43e26bde..0559085c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -// Add your changes here and then delete this line +- A warning will no longer be emitted when the cache file does not exist at the specified path +- The docs for the `auth` parameter of `Spotify.init` use the term "access token" instead of "authorization token" ## [2.16.1] - 2020-10-24 From 04b2b02beebdecc8d29b44547cd749d41d8a7789 Mon Sep 17 00:00:00 2001 From: Peter-Schorn Date: Wed, 11 Nov 2020 01:29:07 -0600 Subject: [PATCH 4/9] Removed `FileNotFoundError` because it does not exist in python 2.7 (*sigh*) and replaced it with a call to `os.path.exists`. --- spotipy/oauth2.py | 104 +++++++++++++++++++++++----------------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index d046e802..319f23cd 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -289,27 +289,27 @@ def get_cached_token(self): """ token_info = None - try: - f = open(self.cache_path) - token_info_string = f.read() - f.close() - token_info = json.loads(token_info_string) - - # 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 os.path.exists(self.cache_path): + try: + f = open(self.cache_path) + token_info_string = f.read() + f.close() + token_info = json.loads(token_info_string) - if self.is_token_expired(token_info): - token_info = self.refresh_access_token( - token_info["refresh_token"] - ) + # 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 - except FileNotFoundError: + if self.is_token_expired(token_info): + token_info = self.refresh_access_token( + token_info["refresh_token"] + ) + except IOError: + logger.warning("Couldn't read cache at: %s", self.cache_path) + else: logger.debug("cache does not exist at: %s", self.cache_path) - except IOError: - logger.warning("Couldn't read cache at: %s", self.cache_path) return token_info @@ -770,27 +770,27 @@ def get_cached_token(self): """ token_info = None - try: - f = open(self.cache_path) - token_info_string = f.read() - f.close() - token_info = json.loads(token_info_string) - - # 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 os.path.exists(self.cache_path): + try: + f = open(self.cache_path) + token_info_string = f.read() + f.close() + token_info = json.loads(token_info_string) - if self.is_token_expired(token_info): - token_info = self.refresh_access_token( - token_info["refresh_token"] - ) + # 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 - except FileNotFoundError: + if self.is_token_expired(token_info): + token_info = self.refresh_access_token( + token_info["refresh_token"] + ) + except IOError: + logger.warning("Couldn't read cache at: %s", self.cache_path) + else: logger.debug("cache does not exist at: %s", self.cache_path) - except IOError: - logger.warning("Couldn't read cache at: %s", self.cache_path) return token_info @@ -1019,25 +1019,25 @@ def get_cached_token(self): """ token_info = None - try: - f = open(self.cache_path) - token_info_string = f.read() - f.close() - token_info = json.loads(token_info_string) - - # 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 os.path.exists(self.cache_path): + try: + f = open(self.cache_path) + token_info_string = f.read() + f.close() + token_info = json.loads(token_info_string) - if self.is_token_expired(token_info): - 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 - except FileNotFoundError: + if self.is_token_expired(token_info): + return None + except IOError: + logger.warning("Couldn't read cache at: %s", self.cache_path) + else: logger.debug("cache does not exist at: %s", self.cache_path) - except IOError: - logger.warning("Couldn't read cache at: %s", self.cache_path) return token_info From 77a16dcd110008a904e78fa326408b86671ddf91 Mon Sep 17 00:00:00 2001 From: Peter-Schorn Date: Wed, 11 Nov 2020 17:38:34 -0600 Subject: [PATCH 5/9] Replaced ` os.path.exists` with `error.errno == errno.ENOENT` to supress errors when the cache file does not exist. --- spotipy/oauth2.py | 117 +++++++++++++++++++++++----------------------- 1 file changed, 59 insertions(+), 58 deletions(-) diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index 319f23cd..c2eb8d66 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -11,6 +11,7 @@ ] import base64 +import errno import json import logging import os @@ -289,27 +290,27 @@ def get_cached_token(self): """ token_info = None - if os.path.exists(self.cache_path): - try: - f = open(self.cache_path) - token_info_string = f.read() - f.close() - token_info = json.loads(token_info_string) - - # 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): - token_info = self.refresh_access_token( - token_info["refresh_token"] - ) - except IOError: + try: + f = open(self.cache_path) + token_info_string = f.read() + f.close() + token_info = json.loads(token_info_string) + + # 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): + token_info = self.refresh_access_token( + token_info["refresh_token"] + ) + except IOError as error: + if error.errno == errno.ENOENT: + logger.debug("cache does not exist at: %s", self.cache_path) + else: logger.warning("Couldn't read cache at: %s", self.cache_path) - else: - logger.debug("cache does not exist at: %s", self.cache_path) return token_info @@ -770,27 +771,27 @@ def get_cached_token(self): """ token_info = None - if os.path.exists(self.cache_path): - try: - f = open(self.cache_path) - token_info_string = f.read() - f.close() - token_info = json.loads(token_info_string) - - # 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): - token_info = self.refresh_access_token( - token_info["refresh_token"] - ) - except IOError: + try: + f = open(self.cache_path) + token_info_string = f.read() + f.close() + token_info = json.loads(token_info_string) + + # 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): + token_info = self.refresh_access_token( + token_info["refresh_token"] + ) + except IOError as error: + if error.errno == errno.ENOENT: + logger.debug("cache does not exist at: %s", self.cache_path) + else: logger.warning("Couldn't read cache at: %s", self.cache_path) - else: - logger.debug("cache does not exist at: %s", self.cache_path) return token_info @@ -1019,25 +1020,25 @@ def get_cached_token(self): """ token_info = None - if os.path.exists(self.cache_path): - try: - f = open(self.cache_path) - token_info_string = f.read() - f.close() - token_info = json.loads(token_info_string) - - # 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 - except IOError: + try: + f = open(self.cache_path) + token_info_string = f.read() + f.close() + token_info = json.loads(token_info_string) + + # 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 + except IOError as error: + if error.errno == errno.ENOENT: + logger.debug("cache does not exist at: %s", self.cache_path) + else: logger.warning("Couldn't read cache at: %s", self.cache_path) - else: - logger.debug("cache does not exist at: %s", self.cache_path) return token_info From 1e6107e8b6098b8d84518e615deb5e95720155cb Mon Sep 17 00:00:00 2001 From: Peter-Schorn Date: Sat, 14 Nov 2020 13:59:36 -0600 Subject: [PATCH 6/9] Changed docs for `search` to mention that you can provide multiple multiple types to search for. The query parameters of requests are now logged. Added log messages for when the access token and refresh tokens are retrieved and when they are refreshed. Other small grammar fixes. --- CHANGELOG.md | 10 +++++++++- spotipy/client.py | 16 +++++++++------- spotipy/oauth2.py | 29 +++++++++++++++++++++++++++-- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af8ed08a..59b1bbca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - moved os.remove(session_cache_path()) inside try block to avoid TypeError on app.py example file - A warning will no longer be emitted when the cache file does not exist at the specified path -- The docs for the `auth` parameter of `Spotify.init` use the term "access token" instead of "authorization token" +- The docs for the `auth` parameter of `Spotify.init` use the term "access token" instead of "authorization token" +- Changed docs for `search` to mention that you can provide multiple multiple types to search for +- The query parameters of requests are now logged + +### Added + +- Added log messages for when the access token and refresh tokens are retrieved and when they are refreshed + + ## [2.16.1] - 2020-10-24 diff --git a/spotipy/client.py b/spotipy/client.py index 6b0d97ff..a2163ed2 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -233,8 +233,8 @@ def _internal_call(self, method, url, payload, params): if self.language is not None: headers["Accept-Language"] = self.language - logger.debug('Sending %s to %s with Headers: %s and Body: %r ', - method, url, headers, args.get('data')) + logger.debug('Sending %s to %s with Params: %s Headers: %s and Body: %r ', + method, url, args.get("params"), headers, args.get('data')) try: response = self._session.request( @@ -534,10 +534,12 @@ def search(self, q, limit=10, offset=0, type="track", market=None): Parameters: - q - the search query (see how to write a query in the official documentation https://developer.spotify.com/documentation/web-api/reference/search/search/) # noqa - - limit - the number of items to return (min = 1, default = 10, max = 50) + - limit - the number of items to return (min = 1, default = 10, max = 50). The limit is applied + within each type, not on the total response. - offset - the index of the first item to return - - type - the type of item to return. One of 'artist', 'album', - 'track', 'playlist', 'show', or 'episode' + - type - the types of items to return. One or more of 'artist', 'album', + 'track', 'playlist', 'show', and 'episode'. If multiple types are desired, + pass in a comma separated string; e.g., 'track,album,episode'. - market - An ISO 3166-1 alpha-2 country code or the string from_token. """ @@ -554,8 +556,8 @@ def search_markets(self, q, limit=10, offset=0, type="track", markets=None, tota - limit - the number of items to return (min = 1, default = 10, max = 50). If a search is to be done on multiple markets, then this limit is applied to each market. (e.g. search US, CA, MX each with a limit of 10). - offset - the index of the first item to return - - type - the type's of item's to return. One or more of 'artist', 'album', - 'track', 'playlist', 'show', or 'episode'. If multiple types are desired, pass in a comma separated list. + - type - the types of items to return. One or more of 'artist', 'album', + 'track', 'playlist', 'show', or 'episode'. If multiple types are desired, pass in a comma separated string. - markets - A list of ISO 3166-1 alpha-2 country codes. Search all country markets by default. - total - the total number of results to return if multiple markets are supplied in the search. If multiple types are specified, this only applies to the first type. diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index c2eb8d66..c0b41ce3 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -197,6 +197,11 @@ def _request_access_token(self): self.client_id, self.client_secret ) + logger.debug( + "sending POST request to %s with Headers: %s and Body: %r", + self.OAUTH_TOKEN_URL, headers, payload + ) + response = self._session.post( self.OAUTH_TOKEN_URL, data=payload, @@ -494,6 +499,11 @@ def get_access_token(self, code=None, as_dict=True, check_cache=True): headers = self._make_authorization_headers() + logger.debug( + "sending POST request to %s with Headers: %s and Body: %r", + self.OAUTH_TOKEN_URL, headers, payload + ) + response = self._session.post( self.OAUTH_TOKEN_URL, data=payload, @@ -529,6 +539,11 @@ def refresh_access_token(self, refresh_token): headers = self._make_authorization_headers() + logger.debug( + "sending POST request to %s with Headers: %s and Body: %r", + self.OAUTH_TOKEN_URL, headers, payload + ) + response = self._session.post( self.OAUTH_TOKEN_URL, data=payload, @@ -836,8 +851,8 @@ def get_access_token(self, code=None, check_cache=True): Parameters: - code - the response code from authentication - - check_cache - if true, checks for locally stored token - before requesting a new token if True + - check_cache - if true, checks for a locally stored token + before requesting a new token """ if check_cache: @@ -862,6 +877,11 @@ def get_access_token(self, code=None, check_cache=True): headers = {"Content-Type": "application/x-www-form-urlencoded"} + logger.debug( + "sending POST request to %s with Headers: %s and Body: %r", + self.OAUTH_TOKEN_URL, headers, payload + ) + response = self._session.post( self.OAUTH_TOKEN_URL, data=payload, @@ -892,6 +912,11 @@ def refresh_access_token(self, refresh_token): headers = {"Content-Type": "application/x-www-form-urlencoded"} + logger.debug( + "sending POST request to %s with Headers: %s and Body: %r", + self.OAUTH_TOKEN_URL, headers, payload + ) + response = self._session.post( self.OAUTH_TOKEN_URL, data=payload, From 5493ec68145a332f8fbe4a7aa245ebc806ba92ae Mon Sep 17 00:00:00 2001 From: Peter Schorn Date: Sat, 14 Nov 2020 14:09:16 -0600 Subject: [PATCH 7/9] Removed duplicate word "multiple" from CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59b1bbca..a18fda37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - moved os.remove(session_cache_path()) inside try block to avoid TypeError on app.py example file - A warning will no longer be emitted when the cache file does not exist at the specified path - The docs for the `auth` parameter of `Spotify.init` use the term "access token" instead of "authorization token" -- Changed docs for `search` to mention that you can provide multiple multiple types to search for +- Changed docs for `search` to mention that you can provide multiple types to search for - The query parameters of requests are now logged ### Added From bcf8a3f78b2708aad74bd3763b376967647933de Mon Sep 17 00:00:00 2001 From: Peter-Schorn Date: Fri, 12 Mar 2021 04:55:47 -0600 Subject: [PATCH 8/9] * Fixed the bugs in `SpotifyOAuth.refresh_access_token` and `SpotifyPKCE.refresh_access_token` which raised the incorrect exception upon receiving an error response from the server. This addresses #645. * Fixed a bug in `RequestHandler.do_GET` in which the non-existent `state` attribute of `SpotifyOauthError` is accessed. This bug occurs when the user clicks "cancel" in the permissions dialog that opens in the browser. * Cleaned up the documentation for `SpotifyClientCredentials.__init__`, `SpotifyOAuth.__init__`, and `SpotifyPKCE.__init__`. --- CHANGELOG.md | 14 ++++- spotipy/oauth2.py | 129 ++++++++++++++++++++++++++-------------------- 2 files changed, 86 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dbbf742..f5fd7b2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - ## Unreleased ### Added + - Enabled using both short and long IDs for playlist_change_details +- Added a cache handler to `SpotifyClientCredentials` ### Changed + - Add support for a list of scopes rather than just a comma separated string of scopes +### Fixed + +* Fixed the bugs in `SpotifyOAuth.refresh_access_token` and `SpotifyPKCE.refresh_access_token` which raised the incorrect exception upon receiving an error response from the server. This addresses #645. + +* Fixed a bug in `RequestHandler.do_GET` in which the non-existent `state` attribute of `SpotifyOauthError` is accessed. This bug occurs when the user clicks "cancel" in the permissions dialog that opens in the browser. + +* Cleaned up the documentation for `SpotifyClientCredentials.__init__`, `SpotifyOAuth.__init__`, and `SpotifyPKCE.__init__`. + + + ## [2.17.1] - 2021-02-28 ### Fixed diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index 4309eb24..8e71be59 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -139,27 +139,57 @@ def __del__(self): class SpotifyClientCredentials(SpotifyAuthBase): OAUTH_TOKEN_URL = "https://accounts.spotify.com/api/token" - def __init__(self, - client_id=None, - client_secret=None, - proxies=None, - requests_session=True, - requests_timeout=None): + def __init__( + self, + client_id=None, + client_secret=None, + proxies=None, + requests_session=True, + requests_timeout=None, + cache_handler=None + ): """ + Creates a Client Credentials Flow Manager. + + The Client Credentials flow is used in server-to-server authentication. + Only endpoints that do not access user information can be accessed. + This means that endpoints that require authorization scopes cannot be accessed. + The advantage, however, of this authorization flow is that it does not require any + user interaction + You can either provide a client_id and client_secret to the constructor or set SPOTIPY_CLIENT_ID and SPOTIPY_CLIENT_SECRET environment variables + + Parameters: + * client_id: Must be supplied or set as environment variable + * client_secret: Must be supplied or set as environment variable + * proxies: Optional, proxy for the requests library to route through + * requests_session: A Requests session + * requests_timeout: Optional, tell Requests to stop waiting for a response after + a given number of seconds + * cache_handler: An instance of the `CacheHandler` class to handle + getting and saving cached authorization tokens. + Optional, will otherwise use `CacheFileHandler`. + (takes precedence over `cache_path` and `username`) + """ super(SpotifyClientCredentials, self).__init__(requests_session) self.client_id = client_id self.client_secret = client_secret - self.token_info = None self.proxies = proxies self.requests_timeout = requests_timeout + if cache_handler: + assert issubclass(cache_handler.__class__, CacheHandler), \ + "cache_handler must be a subclass of CacheHandler: " + str(type(cache_handler)) \ + + " != " + str(CacheHandler) + self.cache_handler = cache_handler + else: + self.cache_handler = CacheFileHandler() - def get_access_token(self, as_dict=True): + def get_access_token(self, as_dict=True, check_cache=True): """ If a valid access token is in memory, returns it Else feches a new token and returns it @@ -179,13 +209,15 @@ def get_access_token(self, as_dict=True): stacklevel=2, ) - if self.token_info and not self.is_token_expired(self.token_info): - return self.token_info if as_dict else self.token_info["access_token"] + if check_cache: + token_info = self.cache_handler.get_cached_token() + if token_info and not self.is_token_expired(token_info): + return token_info if as_dict else token_info["access_token"] token_info = self._request_access_token() token_info = self._add_custom_values_to_token_info(token_info) - self.token_info = token_info - return self.token_info["access_token"] + self.cache_handler.save_token_to_cache(token_info) + return token_info if as_dict else token_info["access_token"] def _request_access_token(self): """Gets client credentials access token """ @@ -260,20 +292,21 @@ def __init__( * state: Optional, no verification is performed * scope: Optional, either a list of scopes or comma separated string of scopes. e.g, "playlist-read-private,playlist-read-collaborative" - * cache_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`) * 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}`) - * show_dialog: Optional, interpreted as boolean * proxies: Optional, proxy for the requests library to route through + * show_dialog: Optional, interpreted as boolean + * requests_session: A Requests session * requests_timeout: Optional, tell Requests to stop waiting for a response after a given number of seconds * open_browser: Optional, whether or not the web browser should be opened to authorize a user + * cache_handler: An instance of the `CacheHandler` class to handle + getting and saving cached authorization tokens. + Optional, will otherwise use `CacheFileHandler`. + (takes precedence over `cache_path` and `username`) """ super(SpotifyOAuth, self).__init__(requests_session) @@ -414,7 +447,7 @@ def _get_auth_response_local_server(self, redirect_port): if server.auth_code is not None: return server.auth_code elif server.error is not None: - raise SpotifyOauthError("Received error from OAuth server: {}".format(server.error)) + raise server.error else: raise SpotifyOauthError("Server listening on localhost has not been accessed") @@ -432,7 +465,7 @@ def get_auth_response(self, open_browser=None): open_browser = self.open_browser if ( - (open_browser or self.open_browser) + open_browser and redirect_host in ("127.0.0.1", "localhost") and redirect_info.scheme == "http" ): @@ -539,20 +572,13 @@ def refresh_access_token(self, refresh_token): timeout=self.requests_timeout, ) - try: - response.raise_for_status() - except BaseException: - logger.error('Couldn\'t refresh token. Response Status Code: %s ' - 'Reason: %s', response.status_code, response.reason) - - message = "Couldn't refresh token: code:%d reason:%s" % ( - response.status_code, - response.reason, - ) - raise SpotifyException(response.status_code, - -1, - message, - headers) + if response.status_code != 200: + error_payload = response.json() + raise SpotifyOauthError( + 'error: {0}, error_description: {1}'.format( + error_payload['error'], error_payload['error_description']), + error=error_payload['error'], + error_description=error_payload['error_description']) token_info = response.json() token_info = self._add_custom_values_to_token_info(token_info) @@ -623,25 +649,24 @@ def __init__(self, Parameters: * client_id: Must be supplied or set as environment variable - * client_secret: Must be supplied or set as environment variable * redirect_uri: Must be supplied or set as environment variable * state: Optional, no verification is performed * scope: Optional, either a list of scopes or comma separated string of scopes. e.g, "playlist-read-private,playlist-read-collaborative" - * cache_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`) * 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}`) - * show_dialog: Optional, interpreted as boolean * proxies: Optional, proxy for the requests library to route through * requests_timeout: Optional, tell Requests to stop waiting for a response after a given number of seconds + * requests_session: A Requests session * open_browser: Optional, thether or not the web browser should be opened to authorize a user + * cache_handler: An instance of the `CacheHandler` class to handle + getting and saving cached authorization tokens. + Optional, will otherwise use `CacheFileHandler`. + (takes precedence over `cache_path` and `username`) """ super(SpotifyPKCE, self).__init__(requests_session) @@ -921,20 +946,13 @@ def refresh_access_token(self, refresh_token): timeout=self.requests_timeout, ) - try: - response.raise_for_status() - except BaseException: - logger.error('Couldn\'t refresh token. Response Status Code: %s ' - 'Reason: %s', response.status_code, response.reason) - - message = "Couldn't refresh token: code:%d reason:%s" % ( - response.status_code, - response.reason, - ) - raise SpotifyException(response.status_code, - -1, - message, - headers) + if response.status_code != 200: + error_payload = response.json() + raise SpotifyOauthError( + 'error: {0}, error_description: {1}'.format( + error_payload['error'], error_payload['error_description']), + error=error_payload['error'], + error_description=error_payload['error_description']) token_info = response.json() token_info = self._add_custom_values_to_token_info(token_info) @@ -1246,9 +1264,8 @@ def do_GET(self): state, auth_code = SpotifyOAuth.parse_auth_response_url(self.path) self.server.state = state self.server.auth_code = auth_code - except SpotifyOauthError as err: - self.server.state = err.state - self.server.error = err.error + except SpotifyOauthError as error: + self.server.error = error self.send_response(200) self.send_header("Content-Type", "text/html") From 690f389782c0609dc1e3b9f33d828496f20390af Mon Sep 17 00:00:00 2001 From: Peter-Schorn Date: Fri, 12 Mar 2021 05:55:48 -0600 Subject: [PATCH 9/9] Removed unneeded import --- spotipy/oauth2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index 8e71be59..c711ce09 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -24,7 +24,6 @@ from six.moves.urllib_parse import parse_qsl, urlparse from spotipy.cache_handler import CacheFileHandler, CacheHandler -from spotipy.exceptions import SpotifyException from spotipy.util import CLIENT_CREDS_ENV_VARS, get_host_port, normalize_scope logger = logging.getLogger(__name__)