From 8d4643c2e1d74c5ed816eb50f99c955e9a7cc5f0 Mon Sep 17 00:00:00 2001 From: Faruk Sahin Date: Wed, 1 Apr 2015 16:03:29 +0200 Subject: [PATCH] Add client credentials flow --- examples/client_credentials_flow.py | 10 ++++ spotipy/client.py | 9 +++- spotipy/oauth2.py | 79 ++++++++++++++++++++++++++--- tests/client_credentials_tests.py | 27 ++++++++++ 4 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 examples/client_credentials_flow.py create mode 100644 tests/client_credentials_tests.py diff --git a/examples/client_credentials_flow.py b/examples/client_credentials_flow.py new file mode 100644 index 0000000..ab1f315 --- /dev/null +++ b/examples/client_credentials_flow.py @@ -0,0 +1,10 @@ +from spotipy.oauth2 import SpotifyClientCredentials +import spotipy +import pprint + +client_credentials_manager = SpotifyClientCredentials() +sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) + +search_str = 'Muse' +result = sp.search(search_str) +pprint.pprint(result) diff --git a/spotipy/client.py b/spotipy/client.py index 685e15a..05ec005 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -40,7 +40,8 @@ class Spotify(object): trace = False # Enable tracing? - def __init__(self, auth=None, requests_session=True): + def __init__(self, auth=None, requests_session=True, + client_credentials_manager=None): ''' Create a Spotify API object. @@ -50,10 +51,13 @@ class Spotify(object): A falsy value disables sessions. It should generally be a good idea to keep sessions enabled for performance reasons (connection pooling). + :param client_credentials_manager: + SpotifyClientCredentials object ''' self.prefix = 'https://api.spotify.com/v1/' self._auth = auth + self.client_credentials_manager = client_credentials_manager if isinstance(requests_session, requests.Session): self._session = requests_session @@ -67,6 +71,9 @@ class Spotify(object): def _auth_headers(self): if self._auth: return {'Authorization': 'Bearer {0}'.format(self._auth)} + elif self.client_credentials_manager: + token = self.client_credentials_manager.get_access_token() + return {'Authorization': 'Bearer {0}'.format(token)} else: return {} diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index 7264bb6..aa8034e 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -7,9 +7,76 @@ import json import time import sys + class SpotifyOauthError(Exception): pass + +class SpotifyClientCredentials(object): + OAUTH_TOKEN_URL = 'https://accounts.spotify.com/api/token' + + def __init__(self, client_id=None, client_secret=None): + """ + You can either provid a client_id and client_secret to the + constructor or set SPOTIPY_CLIENT_ID and SPOTIPY_CLIENT_SECRET + environment variables + """ + if not client_id: + client_id = os.getenv('SPOTIPY_CLIENT_ID') + + if not client_secret: + client_secret = os.getenv('SPOTIPY_CLIENT_SECRET') + + if not client_id: + raise SpotifyOauthError('No client id') + + if not client_secret: + raise SpotifyOauthError('No client secret') + + self.client_id = client_id + self.client_secret = client_secret + self.token_info = None + + def get_access_token(self): + """ + If a valid access token is in memory, returns it + Else feches a new token and returns it + """ + if self.token_info and not self._is_token_expired(self.token_info): + return self.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'] + + def _request_access_token(self): + """Gets client credentials access token """ + payload = { 'grant_type': 'client_credentials'} + + auth_header = base64.b64encode(self.client_id + ':' + self.client_secret) + headers = {'Authorization': 'Basic %s' % auth_header} + + response = requests.post(self.OAUTH_TOKEN_URL, data=payload, + headers=headers, verify=True) + if response.status_code is not 200: + raise SpotifyOauthError(response.reason) + token_info = response.json() + return token_info + + def _is_token_expired(self, token_info): + now = int(time.time()) + return token_info['expires_at'] < now + + 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'] + return token_info + + class SpotifyOAuth(object): ''' Implements Authorization Code Flow for Spotify's OAuth implementation. @@ -18,7 +85,7 @@ class SpotifyOAuth(object): OAUTH_AUTHORIZE_URL = 'https://accounts.spotify.com/authorize' OAUTH_TOKEN_URL = 'https://accounts.spotify.com/api/token' - def __init__(self, client_id, client_secret, redirect_uri, + def __init__(self, client_id, client_secret, redirect_uri, state=None, scope=None, cache_path=None): ''' Creates a SpotifyOAuth object @@ -38,7 +105,7 @@ class SpotifyOAuth(object): self.state=state self.cache_path = cache_path self.scope=self._normalize_scope(scope) - + def get_cached_token(self): ''' Gets a cached auth token ''' @@ -75,7 +142,7 @@ class SpotifyOAuth(object): def _is_token_expired(self, token_info): now = int(time.time()) return token_info['expires_at'] < now - + def get_authorize_url(self): """ Gets the URL to use to authorize this app """ @@ -122,7 +189,7 @@ class SpotifyOAuth(object): headers = {'Authorization': 'Basic %s' % auth_header} - response = requests.post(self.OAUTH_TOKEN_URL, data=payload, + response = requests.post(self.OAUTH_TOKEN_URL, data=payload, headers=headers, verify=True) if response.status_code is not 200: raise SpotifyOauthError(response.reason) @@ -146,7 +213,7 @@ class SpotifyOAuth(object): auth_header = base64.b64encode(self.client_id + ':' + self.client_secret) headers = {'Authorization': 'Basic %s' % auth_header} - response = requests.post(self.OAUTH_TOKEN_URL, data=payload, + response = requests.post(self.OAUTH_TOKEN_URL, data=payload, headers=headers) if response.status_code != 200: if False: # debugging code @@ -163,7 +230,7 @@ class SpotifyOAuth(object): return token_info def _add_custom_values_to_token_info(self, token_info): - ''' + ''' Store some values that aren't directly provided by a Web API response. ''' diff --git a/tests/client_credentials_tests.py b/tests/client_credentials_tests.py new file mode 100644 index 0000000..767406b --- /dev/null +++ b/tests/client_credentials_tests.py @@ -0,0 +1,27 @@ +# -*- coding: latin-1 -*- + +import spotipy +from spotipy.oauth2 import SpotifyClientCredentials +import unittest + +''' + Client Credentials Requests Tests +''' + +class ClientCredentialsTestSpotipy(unittest.TestCase): + ''' + These tests require user authentication + ''' + + muse_urn = 'spotify:artist:12Chz98pHFMPJEknJQMWvI' + + def test_request_with_token(self): + artist = spotify.artist(self.muse_urn) + self.assertTrue(artist['name'] == u'Muse') + + +if __name__ == '__main__': + spotify_cc = SpotifyClientCredentials() + spotify = spotipy.Spotify(client_credentials_manager=spotify_cc) + spotify.trace = False + unittest.main()