mirror of
https://github.com/spotipy-dev/spotipy.git
synced 2026-06-19 01:03:53 +00:00
Added cache handler to SpotifyClientCredentials and fixed a bug in refresh tokens methods that raised the wrong exception (#655)
* 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`. * 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. * Updated CHANGELOG.md * Removed `FileNotFoundError` because it does not exist in python 2.7 (*sigh*) and replaced it with a call to `os.path.exists`. * Replaced ` os.path.exists` with `error.errno == errno.ENOENT` to supress errors when the cache file does not exist. * 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. * Removed duplicate word "multiple" from CHANGELOG * * 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__`. * Removed unneeded import Co-authored-by: Stéphane Bruckert <stephane.bruckert@gmail.com>
This commit is contained in:
parent
cb5c61881e
commit
dc89a00113
14
CHANGELOG.md
14
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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Added
|
### 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`
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Add support for a list of scopes rather than just a comma separated string of scopes
|
- 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
|
## [2.17.1] - 2021-02-28
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@ -24,7 +24,6 @@ from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
|||||||
from six.moves.urllib_parse import parse_qsl, urlparse
|
from six.moves.urllib_parse import parse_qsl, urlparse
|
||||||
|
|
||||||
from spotipy.cache_handler import CacheFileHandler, CacheHandler
|
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
|
from spotipy.util import CLIENT_CREDS_ENV_VARS, get_host_port, normalize_scope
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -139,27 +138,57 @@ class SpotifyAuthBase(object):
|
|||||||
class SpotifyClientCredentials(SpotifyAuthBase):
|
class SpotifyClientCredentials(SpotifyAuthBase):
|
||||||
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,
|
||||||
client_secret=None,
|
client_id=None,
|
||||||
proxies=None,
|
client_secret=None,
|
||||||
requests_session=True,
|
proxies=None,
|
||||||
requests_timeout=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
|
You can either provide a client_id and client_secret to the
|
||||||
constructor or set SPOTIPY_CLIENT_ID and SPOTIPY_CLIENT_SECRET
|
constructor or set SPOTIPY_CLIENT_ID and SPOTIPY_CLIENT_SECRET
|
||||||
environment variables
|
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)
|
super(SpotifyClientCredentials, self).__init__(requests_session)
|
||||||
|
|
||||||
self.client_id = client_id
|
self.client_id = client_id
|
||||||
self.client_secret = client_secret
|
self.client_secret = client_secret
|
||||||
self.token_info = None
|
|
||||||
self.proxies = proxies
|
self.proxies = proxies
|
||||||
self.requests_timeout = requests_timeout
|
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
|
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
|
||||||
@ -179,13 +208,15 @@ class SpotifyClientCredentials(SpotifyAuthBase):
|
|||||||
stacklevel=2,
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.token_info and not self.is_token_expired(self.token_info):
|
if check_cache:
|
||||||
return self.token_info if as_dict else self.token_info["access_token"]
|
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._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.token_info = token_info
|
self.cache_handler.save_token_to_cache(token_info)
|
||||||
return self.token_info["access_token"]
|
return token_info if as_dict else token_info["access_token"]
|
||||||
|
|
||||||
def _request_access_token(self):
|
def _request_access_token(self):
|
||||||
"""Gets client credentials access token """
|
"""Gets client credentials access token """
|
||||||
@ -260,20 +291,21 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
* state: Optional, no verification is performed
|
* state: Optional, no verification is performed
|
||||||
* scope: Optional, either a list of scopes or comma separated string of scopes.
|
* scope: Optional, either a list of scopes or comma separated string of scopes.
|
||||||
e.g, "playlist-read-private,playlist-read-collaborative"
|
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
|
* cache_path: (deprecated) Optional, will otherwise be generated
|
||||||
(takes precedence over `username`)
|
(takes precedence over `username`)
|
||||||
* username: (deprecated) Optional or set as environment variable
|
* username: (deprecated) Optional or set as environment variable
|
||||||
(will set `cache_path` to `.cache-{username}`)
|
(will set `cache_path` to `.cache-{username}`)
|
||||||
* show_dialog: Optional, interpreted as boolean
|
|
||||||
* 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
|
||||||
|
* requests_session: A Requests session
|
||||||
* 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)
|
||||||
@ -414,7 +446,7 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
if server.auth_code is not None:
|
if server.auth_code is not None:
|
||||||
return server.auth_code
|
return server.auth_code
|
||||||
elif server.error is not None:
|
elif server.error is not None:
|
||||||
raise SpotifyOauthError("Received error from OAuth server: {}".format(server.error))
|
raise server.error
|
||||||
else:
|
else:
|
||||||
raise SpotifyOauthError("Server listening on localhost has not been accessed")
|
raise SpotifyOauthError("Server listening on localhost has not been accessed")
|
||||||
|
|
||||||
@ -432,7 +464,7 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
open_browser = self.open_browser
|
open_browser = self.open_browser
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(open_browser or self.open_browser)
|
open_browser
|
||||||
and redirect_host in ("127.0.0.1", "localhost")
|
and redirect_host in ("127.0.0.1", "localhost")
|
||||||
and redirect_info.scheme == "http"
|
and redirect_info.scheme == "http"
|
||||||
):
|
):
|
||||||
@ -539,20 +571,13 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
timeout=self.requests_timeout,
|
timeout=self.requests_timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
if response.status_code != 200:
|
||||||
response.raise_for_status()
|
error_payload = response.json()
|
||||||
except BaseException:
|
raise SpotifyOauthError(
|
||||||
logger.error('Couldn\'t refresh token. Response Status Code: %s '
|
'error: {0}, error_description: {1}'.format(
|
||||||
'Reason: %s', response.status_code, response.reason)
|
error_payload['error'], error_payload['error_description']),
|
||||||
|
error=error_payload['error'],
|
||||||
message = "Couldn't refresh token: code:%d reason:%s" % (
|
error_description=error_payload['error_description'])
|
||||||
response.status_code,
|
|
||||||
response.reason,
|
|
||||||
)
|
|
||||||
raise SpotifyException(response.status_code,
|
|
||||||
-1,
|
|
||||||
message,
|
|
||||||
headers)
|
|
||||||
|
|
||||||
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)
|
||||||
@ -623,25 +648,24 @@ class SpotifyPKCE(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
|
|
||||||
* redirect_uri: Must be supplied or set as environment variable
|
* redirect_uri: Must be supplied or set as environment variable
|
||||||
* state: Optional, no verification is performed
|
* state: Optional, no verification is performed
|
||||||
* scope: Optional, either a list of scopes or comma separated string of scopes.
|
* scope: Optional, either a list of scopes or comma separated string of scopes.
|
||||||
e.g, "playlist-read-private,playlist-read-collaborative"
|
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
|
* cache_path: (deprecated) Optional, will otherwise be generated
|
||||||
(takes precedence over `username`)
|
(takes precedence over `username`)
|
||||||
* username: (deprecated) Optional or set as environment variable
|
* username: (deprecated) Optional or set as environment variable
|
||||||
(will set `cache_path` to `.cache-{username}`)
|
(will set `cache_path` to `.cache-{username}`)
|
||||||
* show_dialog: Optional, interpreted as boolean
|
|
||||||
* proxies: Optional, proxy for the requests library to route through
|
* proxies: Optional, proxy for the requests library to route through
|
||||||
* 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
|
||||||
|
* requests_session: A Requests session
|
||||||
* open_browser: Optional, thether or not the web browser should be opened to
|
* open_browser: Optional, thether 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(SpotifyPKCE, self).__init__(requests_session)
|
super(SpotifyPKCE, self).__init__(requests_session)
|
||||||
@ -921,20 +945,13 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
timeout=self.requests_timeout,
|
timeout=self.requests_timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
if response.status_code != 200:
|
||||||
response.raise_for_status()
|
error_payload = response.json()
|
||||||
except BaseException:
|
raise SpotifyOauthError(
|
||||||
logger.error('Couldn\'t refresh token. Response Status Code: %s '
|
'error: {0}, error_description: {1}'.format(
|
||||||
'Reason: %s', response.status_code, response.reason)
|
error_payload['error'], error_payload['error_description']),
|
||||||
|
error=error_payload['error'],
|
||||||
message = "Couldn't refresh token: code:%d reason:%s" % (
|
error_description=error_payload['error_description'])
|
||||||
response.status_code,
|
|
||||||
response.reason,
|
|
||||||
)
|
|
||||||
raise SpotifyException(response.status_code,
|
|
||||||
-1,
|
|
||||||
message,
|
|
||||||
headers)
|
|
||||||
|
|
||||||
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)
|
||||||
@ -1246,9 +1263,8 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|||||||
state, auth_code = SpotifyOAuth.parse_auth_response_url(self.path)
|
state, auth_code = SpotifyOAuth.parse_auth_response_url(self.path)
|
||||||
self.server.state = state
|
self.server.state = state
|
||||||
self.server.auth_code = auth_code
|
self.server.auth_code = auth_code
|
||||||
except SpotifyOauthError as err:
|
except SpotifyOauthError as error:
|
||||||
self.server.state = err.state
|
self.server.error = error
|
||||||
self.server.error = err.error
|
|
||||||
|
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-Type", "text/html")
|
self.send_header("Content-Type", "text/html")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user