From f312d7a772215ddd3a143ace334d07ab57093efa Mon Sep 17 00:00:00 2001 From: Niko Date: Sat, 25 Jan 2025 00:07:11 +0100 Subject: [PATCH] convert docstings to reStructuredText format for cache_handler.py, exceptions.py, oauth2.py, scope.py, util.py --- spotipy/cache_handler.py | 121 +++++++++++--- spotipy/exceptions.py | 27 +++- spotipy/oauth2.py | 338 ++++++++++++++++++++++++++++----------- spotipy/scope.py | 21 ++- spotipy/util.py | 18 ++- 5 files changed, 387 insertions(+), 138 deletions(-) diff --git a/spotipy/cache_handler.py b/spotipy/cache_handler.py index 0bbc800d..97dfbe5b 100644 --- a/spotipy/cache_handler.py +++ b/spotipy/cache_handler.py @@ -48,15 +48,25 @@ class CacheHandler(ABC): @abstractmethod def get_cached_token(self) -> TokenInfo | None: - """Get and return a token_info dictionary object.""" + """ + Get and return a token_info dictionary object. + + :return: A token_info dictionary object or None if no token is cached. + """ @abstractmethod def save_token_to_cache(self, token_info: TokenInfo) -> None: - """Save a token_info dictionary object to the cache and return None.""" + """ + Save a token_info dictionary object to the cache. + + :param token_info: A token_info dictionary object to be cached. + """ class CacheFileHandler(CacheHandler): - """Read and write cached Spotify authorization tokens as json files on disk.""" + """ + Read and write cached Spotify authorization tokens as json files on disk. + """ def __init__( self, @@ -82,7 +92,11 @@ def __init__( self.cache_path = cache_path def get_cached_token(self) -> TokenInfo | None: - """Get cached token from file.""" + """ + Get cached token from file. + + :return: A token_info dictionary object or None if no token is cached. + """ token_info: TokenInfo | None = None try: @@ -100,7 +114,11 @@ def get_cached_token(self) -> TokenInfo | None: return token_info def save_token_to_cache(self, token_info: TokenInfo) -> None: - """Save token cache to file.""" + """ + Save token cache to file. + + :param token_info: A token_info dictionary object to be cached. + """ try: f = open(self.cache_path, "w") f.write(json.dumps(token_info, cls=self.encoder_cls)) @@ -110,22 +128,32 @@ def save_token_to_cache(self, token_info: TokenInfo) -> None: class MemoryCacheHandler(CacheHandler): - """Cache handler that stores the token non-persistently as an instance attribute.""" + """ + Cache handler that stores the token non-persistently as an instance attribute. + """ def __init__(self, token_info: TokenInfo | None = None) -> None: """ Initialize MemoryCacheHandler instance. - :param token_info: Optional initial cached token + :param token_info: Optional initial cached token. """ self.token_info = token_info def get_cached_token(self) -> TokenInfo | None: - """Retrieve the cached token from the instance.""" + """ + Retrieve the cached token from the instance. + + :return: A token_info dictionary object or None if no token is cached. + """ return self.token_info def save_token_to_cache(self, token_info: TokenInfo) -> None: - """Cache the token in this instance.""" + """ + Cache the token in this instance. + + :param token_info: A token_info dictionary object to be cached. + """ self.token_info = token_info @@ -139,13 +167,18 @@ class DjangoSessionCacheHandler(CacheHandler): def __init__(self, request): """ - Parameters: - * request: HttpRequest object provided by Django for every - incoming request + Initialize DjangoSessionCacheHandler instance. + + :param request: HttpRequest object provided by Django for every incoming request. """ self.request = request def get_cached_token(self): + """ + Retrieve the cached token from the Django session. + + :return: A token_info dictionary object or None if no token is cached. + """ token_info = None try: token_info = self.request.session["token_info"] @@ -155,6 +188,11 @@ def get_cached_token(self): return token_info def save_token_to_cache(self, token_info): + """ + Cache the token in the Django session. + + :param token_info: A token_info dictionary object to be cached. + """ try: self.request.session["token_info"] = token_info except Exception as e: @@ -164,13 +202,23 @@ def save_token_to_cache(self, token_info): class FlaskSessionCacheHandler(CacheHandler): """ A cache handler that stores the token info in the session framework - provided by flask. + provided by Flask. """ def __init__(self, session): + """ + Initialize FlaskSessionCacheHandler instance. + + :param session: Flask session object. + """ self.session = session def get_cached_token(self): + """ + Retrieve the cached token from the Flask session. + + :return: A token_info dictionary object or None if no token is cached. + """ token_info = None try: token_info = self.session["token_info"] @@ -180,6 +228,11 @@ def get_cached_token(self): return token_info def save_token_to_cache(self, token_info): + """ + Cache the token in the Flask session. + + :param token_info: A token_info dictionary object to be cached. + """ try: self.session["token_info"] = token_info except Exception as e: @@ -187,20 +240,26 @@ def save_token_to_cache(self, token_info): class RedisCacheHandler(CacheHandler): - """A cache handler that stores the token info in the Redis.""" + """ + A cache handler that stores the token info in Redis. + """ def __init__(self, redis_obj: redis.client.Redis, key: str | None = None) -> None: """ Initialize RedisCacheHandler instance. - :param redis: The Redis object to function as the cache - :param key: (Optional) The key to used to store the token in the cache + :param redis_obj: The Redis object to function as the cache. + :param key: (Optional) The key to use to store the token in the cache. """ self.redis = redis_obj self.key = key or "token_info" def get_cached_token(self) -> TokenInfo | None: - """Fetch cache token from the Redis.""" + """ + Fetch cached token from Redis. + + :return: A token_info dictionary object or None if no token is cached. + """ token_info = None try: token_info = self.redis.get(self.key) @@ -212,7 +271,11 @@ def get_cached_token(self) -> TokenInfo | None: return token_info def save_token_to_cache(self, token_info: TokenInfo) -> None: - """Cache token in the Redis.""" + """ + Cache token in Redis. + + :param token_info: A token_info dictionary object to be cached. + """ try: self.redis.set(self.key, json.dumps(token_info)) except RedisError as e: @@ -220,21 +283,26 @@ def save_token_to_cache(self, token_info: TokenInfo) -> None: class MemcacheCacheHandler(CacheHandler): - """A Cache handler that stores the token info in Memcache using the pymemcache client + """ + A cache handler that stores the token info in Memcache using the pymemcache client. """ def __init__(self, memcache, key=None) -> None: """ - Parameters: - * memcache: memcache client object provided by pymemcache - (https://pymemcache.readthedocs.io/en/latest/getting_started.html) - * key: May be supplied, will otherwise be generated - (takes precedence over `token_info`) + Initialize MemcacheCacheHandler instance. + + :param memcache: Memcache client object provided by pymemcache. + :param key: (Optional) The key to use to store the token in the cache. """ self.memcache = memcache self.key = key or "token_info" def get_cached_token(self): + """ + Fetch cached token from Memcache. + + :return: A token_info dictionary object or None if no token is cached. + """ from pymemcache import MemcacheError try: @@ -245,6 +313,11 @@ def get_cached_token(self): logger.warning(f"Error getting token to cache: {e}") def save_token_to_cache(self, token_info): + """ + Cache token in Memcache. + + :param token_info: A token_info dictionary object to be cached. + """ from pymemcache import MemcacheError try: diff --git a/spotipy/exceptions.py b/spotipy/exceptions.py index cacce9fd..0a611339 100644 --- a/spotipy/exceptions.py +++ b/spotipy/exceptions.py @@ -3,6 +3,15 @@ class SpotifyBaseException(Exception): class SpotifyException(SpotifyBaseException): + """ + Exception raised for Spotify API errors. + + :param http_status: The HTTP status code returned by the API. + :param code: The specific error code returned by the API. + :param msg: The error message returned by the API. + :param reason: (Optional) The reason for the error. + :param headers: (Optional) The headers returned by the API. + """ def __init__(self, http_status, code, msg, reason=None, headers=None): self.http_status = http_status @@ -22,7 +31,13 @@ def __str__(self): class SpotifyOauthError(SpotifyBaseException): - """ Error during Auth Code or Implicit Grant flow """ + """ + Exception raised for errors during Auth Code or Implicit Grant flow. + + :param message: The error message. + :param error: (Optional) The specific error code. + :param error_description: (Optional) A description of the error. + """ def __init__(self, message, error=None, error_description=None, *args, **kwargs): self.error = error @@ -32,7 +47,15 @@ def __init__(self, message, error=None, error_description=None, *args, **kwargs) class SpotifyStateError(SpotifyOauthError): - """ The state sent and state received were different """ + """ + Exception raised when the state sent and state received are different. + + :param local_state: The state sent. + :param remote_state: The state received. + :param message: (Optional) The error message. + :param error: (Optional) The specific error code. + :param error_description: (Optional) A description of the error. + """ def __init__(self, local_state=None, remote_state=None, message=None, error=None, error_description=None, *args, **kwargs): diff --git a/spotipy/oauth2.py b/spotipy/oauth2.py index 4f1b4d5d..c1318e14 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -28,6 +28,13 @@ def _make_authorization_headers(client_id, client_secret): + """ + Create authorization headers for Spotify API requests. + + :param client_id: The client ID provided by Spotify. + :param client_secret: The client secret provided by Spotify. + :return: A dictionary containing the authorization headers. + """ auth_header = base64.b64encode( f"{client_id}:{client_secret}".encode("ascii") ) @@ -35,6 +42,14 @@ def _make_authorization_headers(client_id, client_secret): def _ensure_value(value, env_key): + """ + Ensure that a value is provided, either directly or via an environment variable. + + :param value: The value to check. + :param env_key: The key for the environment variable to check if the value is not provided. + :return: The value or the value from the environment variable. + :raises SpotifyOauthError: If neither the value nor the environment variable is set. + """ env_val = CLIENT_CREDS_ENV_VARS[env_key] _val = value or os.getenv(env_val) if _val is None: @@ -44,6 +59,11 @@ def _ensure_value(value, env_key): class SpotifyAuthBase: + """ + Base class for Spotify authentication. + + :param requests_session: A Requests session object or a boolean value to create one. + """ def __init__(self, requests_session): if isinstance(requests_session, requests.Session): @@ -59,6 +79,10 @@ def _normalize_scope(self, scope): Accepts a string of scopes, or an iterable with elements of type `Scope` or `str` and returns a space-separated string of scopes. Returns `None` if the argument is `None`. + + :param scope: A string or iterable of scopes. + :return: A space-separated string of scopes or `None`. + :raises TypeError: If the scope is not a string or iterable. """ # TODO: do we need to sort the scopes? @@ -71,7 +95,7 @@ def _normalize_scope(self, scope): if isinstance(scope, Iterable): - # Assume all of the iterable's elements are of the same type. + # Assume all the iterable's elements are of the same type. # If the iterable is empty, then return None. first_element = next(iter(scope), None) @@ -120,11 +144,24 @@ def _get_user_input(prompt): @staticmethod def is_token_expired(token_info): + """ + Check if the token is expired. + + :param token_info: The token information. + :return: `True` if the token is expired, `False` otherwise. + """ now = int(time.time()) return token_info["expires_at"] - now < 60 @staticmethod def _is_scope_subset(needle_scope, haystack_scope): + """ + Check if one scope is a subset of another. + + :param needle_scope: The scope to check. + :param haystack_scope: The scope to check against. + :return: `True` if `needle_scope` is a subset of `haystack_scope`, `False` otherwise. + """ needle_scope = set(needle_scope.split()) if needle_scope else set() haystack_scope = ( set(haystack_scope.split()) if haystack_scope else set() @@ -132,6 +169,12 @@ def _is_scope_subset(needle_scope, haystack_scope): return needle_scope <= haystack_scope def _handle_oauth_error(self, http_error): + """ + Handle OAuth errors. + + :param http_error: The HTTP error. + :raises SpotifyOauthError: If an OAuth error occurs. + """ response = http_error.response try: error_payload = response.json() @@ -158,6 +201,9 @@ def __del__(self): class SpotifyClientCredentials(SpotifyAuthBase): + """ + Implements Client Credentials Flow for Spotify's OAuth implementation. + """ OAUTH_TOKEN_URL = "https://accounts.spotify.com/api/token" def __init__( @@ -176,26 +222,20 @@ def __init__( 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 + 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 - * cache_handler: An instance of the `CacheHandler` class to handle + :param client_id: Must be supplied or set as environment variable. + :param client_secret: Must be supplied or set as environment variable. + :param cache_handler: An instance of the `CacheHandler` class to handle getting and saving cached authorization tokens. Optional, will otherwise use `CacheFileHandler`. - * proxies: Optional, proxy for the requests library to route through - * requests_session: A Requests session object or a true value to create one. + :param proxies: Optional, proxy for the requests library to route through. + :param 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 - + :param requests_timeout: Optional, tell Requests to stop waiting for a response after + a given number of seconds. """ super().__init__(requests_session) @@ -215,12 +255,12 @@ def __init__( def get_access_token(self, check_cache=True): """ - If a valid access token is in memory, returns it - Else fetches a new token and returns it + If a valid access token is in memory, returns it. + Else fetches a new token and returns it. - Parameters: - - check_cache - if true, checks for a locally stored token + :param check_cache: If true, checks for a locally stored token before requesting a new token. + :return: The access token. """ if check_cache: @@ -234,7 +274,12 @@ def get_access_token(self, check_cache=True): return token_info["access_token"] def _request_access_token(self): - """Gets client credentials access token """ + """ + Gets client credentials access token. + + :return: The token information. + :raises SpotifyOauthError: If an OAuth error occurs. + """ payload = {"grant_type": "client_credentials"} headers = _make_authorization_headers( @@ -261,8 +306,10 @@ def _request_access_token(self): def _add_custom_values_to_token_info(self, token_info): """ - Store some values that aren't directly provided by a Web API - response. + Store some values that aren't directly provided by a Web API response. + + :param token_info: The token information. + :return: The token information with additional values. """ token_info["expires_at"] = int(time.time()) + token_info["expires_in"] return token_info @@ -290,29 +337,27 @@ def __init__( open_browser=True ): """ - Creates a SpotifyOAuth object - - 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 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 + Creates a SpotifyOAuth object. + + :param client_id: Must be supplied or set as environment variable. + :param client_secret: Must be supplied or set as environment variable. + :param redirect_uri: Must be supplied or set as environment variable. + :param state: Optional, no verification is performed. + :param 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}. + :param cache_handler: An instance of the `CacheHandler` class to handle getting and saving cached authorization tokens. Optional, will otherwise use `CacheFileHandler`. - * proxies: Optional, proxy for the requests library to route through - * show_dialog: Optional, interpreted as boolean - * requests_session: A Requests session object or a true value to create one. + :param proxies: Optional, proxy for the requests library to route through. + :param show_dialog: Optional, interpreted as boolean. + :param requests_session: A Requests session object or a true value to create one. A false value disables sessions. It should generally be a good idea to keep sessions enabled for performance reasons (connection pooling). - * requests_timeout: Optional, tell Requests to stop waiting for a response after - a given number of seconds - * open_browser: Optional, whether the web browser should be opened to - authorize a user + :param requests_timeout: Optional, tell Requests to stop waiting for a response after + a given number of seconds. + :param open_browser: Optional, whether the web browser should be opened to + authorize a user. """ super().__init__(requests_session) @@ -338,6 +383,12 @@ def __init__( self.open_browser = open_browser def validate_token(self, token_info): + """ + Validate the token information. + + :param token_info: The token information. + :return: The validated token information or None if invalid. + """ if token_info is None: return None @@ -355,7 +406,11 @@ def validate_token(self, token_info): return token_info def get_authorize_url(self, state=None): - """ Gets the URL to use to authorize this app + """ + Get the URL to use to authorize this app. + + :param state: Optional, the state parameter. + :return: The authorization URL. """ payload = { "client_id": self.client_id, @@ -376,16 +431,24 @@ def get_authorize_url(self, state=None): return f"{self.OAUTH_AUTHORIZE_URL}?{urlparams}" def parse_response_code(self, url): - """ Parse the response code in the given response url + """ + Parse the response code in the given response URL. - Parameters: - - url - the response url + :param url: The response URL. + :return: The response code. """ _, code = self.parse_auth_response_url(url) return url if code is None else code @staticmethod def parse_auth_response_url(url): + """ + Parse the authorization response URL. + + :param url: The response URL. + :return: A tuple containing the state and code. + :raises SpotifyOauthError: If an error occurs. + """ query_s = urlparse(url).query form = dict(parse_qsl(query_s)) if "error" in form: @@ -407,6 +470,12 @@ def _open_auth_url(self): logger.error(f"Please navigate here: {auth_url}") def _get_auth_response_interactive(self, open_browser=False): + """ + Get the authorization response interactively. + + :param open_browser: Whether to open the browser. + :return: The authorization code. + """ if open_browser: self._open_auth_url() prompt = "Enter the URL you were redirected to: " @@ -423,6 +492,13 @@ def _get_auth_response_interactive(self, open_browser=False): return code def _get_auth_response_local_server(self, redirect_port): + """ + Get the authorization response using a local server. + + :param redirect_port: The port on which to start the server. + :return: The authorization code. + :raises SpotifyOauthError: If an error occurs. + """ server = start_local_http_server(redirect_port) self._open_auth_url() server.handle_request() @@ -437,6 +513,12 @@ def _get_auth_response_local_server(self, redirect_port): raise SpotifyOauthError("Server listening on localhost has not been accessed") def get_auth_response(self, open_browser=None): + """ + Get the authorization response. + + :param open_browser: Whether to open the browser. + :return: The authorization code. + """ logger.info('User authentication requires interaction with your ' 'web browser. Once you enter your credentials and ' 'give authorization, you will be redirected to ' @@ -467,17 +549,24 @@ def get_auth_response(self, open_browser=None): return self._get_auth_response_interactive(open_browser=open_browser) def get_authorization_code(self, response=None): + """ + Get the authorization code. + + :param response: The response URL. + :return: The authorization code. + """ if response: return self.parse_response_code(response) return self.get_auth_response() def get_access_token(self, code=None, check_cache=True): - """ Gets the access token for the app given the code + """ + Get the access token for the app given the code. - Parameters: - - code - the response code - - check_cache - if true, checks for a locally stored token - before requesting a new token + :param code: The response code. + :param check_cache: If true, checks for a locally stored token + before requesting a new token. + :return: The access token. """ if check_cache: token_info = self.validate_token(self.cache_handler.get_cached_token()) @@ -521,6 +610,13 @@ def get_access_token(self, code=None, check_cache=True): self._handle_oauth_error(http_error) def refresh_access_token(self, refresh_token): + """ + Refresh the access token. + + :param refresh_token: The refresh token. + :return: The new token information. + :raises SpotifyOauthError: If an error occurs. + """ payload = { "refresh_token": refresh_token, "grant_type": "refresh_token", @@ -551,8 +647,10 @@ def refresh_access_token(self, refresh_token): def _add_custom_values_to_token_info(self, token_info): """ - Store some values that aren't directly provided by a Web API - response. + Store some values that aren't directly provided by a Web API response. + + :param token_info: The token information. + :return: The token information with additional values. """ token_info["expires_at"] = int(time.time()) + token_info["expires_in"] token_info["scope"] = self.scope @@ -560,7 +658,8 @@ def _add_custom_values_to_token_info(self, token_info): class SpotifyPKCE(SpotifyAuthBase): - """ Implements PKCE Authorization Flow for client apps + """ + Implements PKCE Authorization Flow for client apps. This auth manager enables *user and non-user* endpoints with only a client ID, redirect URI, and username. When the app requests @@ -568,7 +667,6 @@ class SpotifyPKCE(SpotifyAuthBase): authorize the new client app. After authorizing the app, the client app is then given both access and refresh tokens. This is the preferred way of authorizing a mobile/desktop client. - """ OAUTH_AUTHORIZE_URL = "https://accounts.spotify.com/authorize" @@ -589,35 +687,23 @@ def __init__( """ Creates Auth Manager with the PKCE Auth flow. - Parameters: - * client_id: 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 string of scopes, or an iterable with elements of type - `Scope` or `str`. E.g., - {Scope.user_modify_playback_state, Scope.user_library_read} - * cache_path: (deprecated) Optional, will otherwise be generated - (takes precedence over `username`) - * username: (deprecated) Optional or set as environment variable - (will set `cache_path` to `.cache-{username}`) - * proxies: Optional, proxy for the requests library to route through - * requests_timeout: Optional, tell Requests to stop waiting for a response after - a given number of seconds - * requests_session: A Requests session - * open_browser: Optional, whether the web browser should be opened to - authorize a user - * cache_handler: An instance of the `CacheHandler` class to handle + :param client_id: Must be supplied or set as environment variable. + :param redirect_uri: Must be supplied or set as environment variable. + :param state: Optional, no verification is performed. + :param 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}. + :param cache_handler: An instance of the `CacheHandler` class to handle getting and saving cached authorization tokens. Optional, will otherwise use `CacheFileHandler`. - * proxies: Optional, proxy for the requests library to route through - * 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. + :param proxies: Optional, proxy for the requests library to route through. + :param requests_timeout: Optional, tell Requests to stop waiting for a response after + a given number of seconds. + :param 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, whether the web browser should be opened to - authorize a user + :param open_browser: Optional, whether the web browser should be opened to + authorize a user. """ super().__init__(requests_session) @@ -644,9 +730,12 @@ def __init__( self.open_browser = open_browser def _get_code_verifier(self): - """ Spotify PCKE code verifier - See step 1 of the reference guide below + """ + Spotify PCKE code verifier - See step 1 of the reference guide below Reference: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow-with-proof-key-for-code-exchange-pkce + + :return: A code verifier string. """ # Range (33,96) is used to select between 44-128 base64 characters for the # next operation. The range looks weird because base64 is 6 bytes @@ -658,9 +747,12 @@ def _get_code_verifier(self): return secrets.token_urlsafe(length) def _get_code_challenge(self): - """ Spotify PCKE code challenge - See step 1 of the reference guide below + """ + Spotify PCKE code challenge - See step 1 of the reference guide below Reference: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow-with-proof-key-for-code-exchange-pkce + + :return: A code challenge string. """ import base64 import hashlib @@ -669,7 +761,12 @@ def _get_code_challenge(self): return code_challenge.replace('=', '') def get_authorize_url(self, state=None): - """ Gets the URL to use to authorize this app """ + """ + Get the URL to use to authorize this app. + + :param state: Optional, the state parameter. + :return: The authorization URL. + """ if not self.code_challenge: self.get_pkce_handshake_parameters() payload = { @@ -755,11 +852,23 @@ def _get_auth_response_interactive(self, open_browser=False): return code def get_authorization_code(self, response=None): + """ + Get the authorization code. + + :param response: The response URL. + :return: The authorization code. + """ if response: return self.parse_response_code(response) return self._get_auth_response() def validate_token(self, token_info): + """ + Validate the token information. + + :param token_info: The token information. + :return: The validated token information or None if invalid. + """ if token_info is None: return None @@ -778,27 +887,33 @@ def validate_token(self, 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. + Store some values that aren't directly provided by a Web API response. + + :param token_info: The token information. + :return: The token information with additional values. """ token_info["expires_at"] = int(time.time()) + token_info["expires_in"] return token_info def get_pkce_handshake_parameters(self): + """ + Generate PKCE handshake parameters. + """ self.code_verifier = self._get_code_verifier() self.code_challenge = self._get_code_challenge() def get_access_token(self, code=None, check_cache=True): - """ Gets the access token for the app + """ + Get the access token for the app. - If the code is not given and no cached token is used, an - authentication window will be shown to the user to get a new - code. + If the code is not given and no cached token is used, an + authentication window will be shown to the user to get a new + code. - Parameters: - - code - the response code from authentication - - check_cache - if true, checks for a locally stored token - before requesting a new token + :param code: The response code from authentication. + :param check_cache: If true, checks for a locally stored token + before requesting a new token. + :return: The access token. """ if check_cache: @@ -844,6 +959,13 @@ def get_access_token(self, code=None, check_cache=True): self._handle_oauth_error(http_error) def refresh_access_token(self, refresh_token): + """ + Refresh the access token. + + :param refresh_token: The refresh token. + :return: The new token information. + :raises SpotifyOauthError: If an error occurs. + """ payload = { "refresh_token": refresh_token, "grant_type": "refresh_token", @@ -874,21 +996,44 @@ def refresh_access_token(self, refresh_token): self._handle_oauth_error(http_error) def parse_response_code(self, url): - """ Parse the response code in the given response url + """ + Parse the response code in the given response URL. - Parameters: - - url - the response url + :param url: The response URL. + :return: The response code. """ _, code = self.parse_auth_response_url(url) return url if code is None else code @staticmethod def parse_auth_response_url(url): + """ + Parse the authorization response URL. + + :param url: The response URL. + :return: A tuple containing the state and code. + :raises SpotifyOauthError: If an error occurs. + """ return SpotifyOAuth.parse_auth_response_url(url) class RequestHandler(BaseHTTPRequestHandler): + """ + Handles HTTP GET requests for the local server used in OAuth authentication. + + This handler processes the OAuth redirect response, extracting the authorization + code or error from the URL and sending an appropriate HTML response back to the client. + """ + def do_GET(self): + """ + Handle GET requests. + + Parses the URL to extract the state and authorization code, or an error if present. + Sends an HTML response indicating the authentication status. + + :raises SpotifyOauthError: If an error occurs during URL parsing. + """ self.server.auth_code = self.server.error = None try: state, auth_code = SpotifyOAuth.parse_auth_response_url(self.path) @@ -931,6 +1076,13 @@ def log_message(self, format, *args): def start_local_http_server(port, handler=RequestHandler): + """ + Start a local HTTP server to handle OAuth redirects. + + :param port: The port on which to start the server. + :param handler: The request handler class to use. + :return: An instance of the HTTPServer. + """ server = HTTPServer(("127.0.0.1", port), handler) server.allow_reuse_address = True server.auth_code = None diff --git a/spotipy/scope.py b/spotipy/scope.py index b5b4bf7b..dabc1aaf 100644 --- a/spotipy/scope.py +++ b/spotipy/scope.py @@ -46,8 +46,11 @@ class Scope(Enum): @staticmethod def all() -> Set['Scope']: - """Returns all of the authorization scopes""" + """ + Returns all the authorization scopes. + :return: A set of all scopes. + """ return set(Scope) @staticmethod @@ -55,24 +58,20 @@ def make_string(scopes: Iterable['Scope']) -> str: """ Converts an iterable of scopes to a space-separated string. - * scopes: An iterable of scopes. - - returns: a space-separated string of scopes + :param scopes: An iterable of scopes. + :return: A space-separated string of scopes. """ return " ".join([scope.value for scope in scopes]) @staticmethod def from_string(scope_string: str) -> Set['Scope']: """ - Converts a string of (usuallly space-separated) scopes into a - set of scopes - - Any scope-strings that do not match any of the known scopes are - ignored. + Converts a string of (usually space-separated) scopes into a set of scopes. - * scope_string: a string of scopes + Any scope-strings that do not match any of the known scopes are ignored. - returns: a set of scopes. + :param scope_string: A string of scopes. + :return: A set of scopes. """ scope_string_list = re.split(pattern=r"[^\w-]+", string=scope_string) scopes = set() diff --git a/spotipy/util.py b/spotipy/util.py index 8400a548..7f3e91f7 100644 --- a/spotipy/util.py +++ b/spotipy/util.py @@ -21,11 +21,12 @@ def get_host_port(netloc): - """ Split the network location string into host and port and returns a tuple - where the host is a string and the the port is an integer. + """ + Split the network location string into host and port and return a tuple + where the host is a string and the port is an integer. - Parameters: - - netloc - a string representing the network location. + :param netloc: A string representing the network location. + :return: A tuple containing the host and port. """ if ":" in netloc: host, port = netloc.split(":", 1) @@ -38,13 +39,14 @@ def get_host_port(netloc): def normalize_scope(scope): - """Normalize the scope to verify that it is a list or tuple. A string + """ + Normalize the scope to verify that it is a list or tuple. A string input will split the string by commas to create a list of scopes. A list or tuple input is used directly. - Parameters: - - scope - a string representing scopes separated by commas, - or a list/tuple of scopes. + :param scope: A string representing scopes separated by commas, or a list/tuple of scopes. + :return: A space-separated string of scopes. + :raises TypeError: If the scope is not a string, list, or tuple. """ if scope: if isinstance(scope, str):