From e13b10885a36795f8b5cb13c9a0fb9032f4fbe5e Mon Sep 17 00:00:00 2001 From: Niko Date: Sun, 2 Feb 2025 09:20:42 +0100 Subject: [PATCH] Convert docstrings to reStructuredText (#1185) * convert docstings to reStructuredText format for client.py * convert docstings to reStructuredText format for cache_handler.py, exceptions.py, oauth2.py, scope.py, util.py * fix formatting and inconsistencies * add changelog --- CHANGELOG.md | 1 + spotipy/cache_handler.py | 129 +++- spotipy/client.py | 1220 +++++++++++++++++++------------------- spotipy/exceptions.py | 27 +- spotipy/oauth2.py | 400 +++++++++---- spotipy/scope.py | 21 +- spotipy/util.py | 18 +- 7 files changed, 1036 insertions(+), 780 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c487ae9..d20c47b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Rebasing master onto v3 doesn't require a changelog update. - Removed the `client_credentials_manager` and `oauth_manager` parameters because they are redundant. - Replaced the `set_auth` and `auth_manager` properties with standard attributes. - Replaced string concatenations and `str.format()` with f-strings +- Modified docstrings to use the reStructuredText format ### Fixed diff --git a/spotipy/cache_handler.py b/spotipy/cache_handler.py index 0bbc800..37729d6 100644 --- a/spotipy/cache_handler.py +++ b/spotipy/cache_handler.py @@ -48,21 +48,31 @@ 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, - cache_path: str | None = None, - username: str | None = None, - encoder_cls: type[JSONEncoder] | None = None, + self, + cache_path: str | None = None, + username: str | None = None, + encoder_cls: type[JSONEncoder] | None = None, ) -> None: """ Initialize CacheFileHandler instance. @@ -82,7 +92,11 @@ class CacheFileHandler(CacheHandler): 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 @@ class CacheFileHandler(CacheHandler): 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 @@ class CacheFileHandler(CacheHandler): 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 @@ class DjangoSessionCacheHandler(CacheHandler): 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 @@ class DjangoSessionCacheHandler(CacheHandler): 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 @@ class FlaskSessionCacheHandler(CacheHandler): 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 @@ class FlaskSessionCacheHandler(CacheHandler): 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 used 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 @@ class RedisCacheHandler(CacheHandler): 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 @@ class RedisCacheHandler(CacheHandler): 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 used 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 @@ class MemcacheCacheHandler(CacheHandler): 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/client.py b/spotipy/client.py index f7c368e..8f9fe59 100644 --- a/spotipy/client.py +++ b/spotipy/client.py @@ -18,18 +18,18 @@ logger = logging.getLogger(__name__) class Spotify: """ - Example usage:: + Example usage:: - import spotipy + import spotipy - urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu' - sp = spotipy.Spotify() + urn = 'spotify:artist:3jOstUTkEu2JkjvRdBA5Gu' + sp = spotipy.Spotify() - artist = sp.artist(urn) - print(artist) + artist = sp.artist(urn) + print(artist) - user = sp.user('plamere') - print(user) + user = sp.user('plamere') + print(user) """ max_retries = 3 default_retry_codes = (429, 500, 502, 503, 504) @@ -123,17 +123,17 @@ class Spotify: _regex_base62 = r'^[0-9A-Za-z]+$' def __init__( - self, - access_token=None, - auth_manager=None, - requests_session=True, - proxies=None, - requests_timeout=5, - status_forcelist=None, - retries=max_retries, - status_retries=max_retries, - backoff_factor=0.3, - language=None, + self, + access_token=None, + auth_manager=None, + requests_session=True, + proxies=None, + requests_timeout=5, + status_forcelist=None, + retries=max_retries, + status_retries=max_retries, + backoff_factor=0.3, + language=None, ): """ Creates a Spotify API client. @@ -141,33 +141,22 @@ class Spotify: :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: - 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). - :param proxies: - Definition of proxies (optional). + :param auth_manager: SpotifyOauth, SpotifyClientCredentials, or SpotifyPKCE object + :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). + :param proxies: Definition of proxies (optional). See Requests doc https://2.python-requests.org/en/master/user/advanced/#proxies - :param requests_timeout: - Tell Requests to stop waiting for a response after a given + :param requests_timeout: Tell Requests to stop waiting for a response after a given number of seconds - :param status_forcelist: - Tell requests what type of status codes retries should occur on - :param retries: - Total number of retries to allow - :param status_retries: - Number of times to retry on bad status codes - :param backoff_factor: - A backoff factor to apply between attempts after the second try + :param status_forcelist: Tell requests what type of status codes retries should occur on + :param retries: Total number of retries to allow + :param status_retries: Number of times to retry on bad status codes + :param backoff_factor: A backoff factor to apply between attempts after the second try See urllib3 https://urllib3.readthedocs.io/en/latest/reference/urllib3.util.html - :param language: - The language parameter advertises what language the user prefers to see. + :param language: The language parameter advertises what language the user prefers to see. See ISO-639-1 language code: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes """ - if access_token is not None and auth_manager is not None: warnings.warn( "Either `access_token` or `auth_manager` should be provided, " @@ -323,76 +312,85 @@ class Spotify: return self._internal_call("PUT", url, payload, kwargs) def next(self, result): - """ returns the next result given a paged result + """ + Returns the next result given a paged result. - Parameters: - - result - a previously returned paged result + :param result: A previously returned paged result. + :return: The next result. """ return self._get(result["next"]) if result["next"] else None def previous(self, result): - """ returns the previous result given a paged result + """ + Returns the previous result given a paged result. - Parameters: - - result - a previously returned paged result + :param result: A previously returned paged result. + :return: The previous result. """ return self._get(result["previous"]) if result["previous"] else None def track(self, track_id, market=None): - """ returns a single track given the track's ID, URI or URL + """ + Returns a single track given the track's ID, URI or URL. - Parameters: - - track_id - a spotify URI, URL or ID - - market - an ISO 3166-1 alpha-2 country code. + :param track_id: A Spotify URI, URL or ID. + :param market: An ISO 3166-1 alpha-2 country code. + :return: The track information. """ trid = self._get_id("track", track_id) return self._get(f"tracks/{trid}", market=market) def tracks(self, tracks, market=None): - """ returns a list of tracks given a list of track IDs, URIs, or URLs + """ + Returns a list of tracks given a list of track IDs, URIs, or URLs. - Parameters: - - tracks - a list of spotify URIs, URLs or IDs. Maximum: 50 IDs. - - market - an ISO 3166-1 alpha-2 country code. + :param tracks: A list of Spotify URIs, URLs or IDs. Maximum: 50 IDs. + :param market: An ISO 3166-1 alpha-2 country code. + :return: The list of tracks information. """ tlist = [self._get_id("track", t) for t in tracks] return self._get(f"tracks/?ids={','.join(tlist)}", market=market) def artist(self, artist_id): - """ returns a single artist given the artist's ID, URI or URL + """ + Returns a single artist given the artist's ID, URI or URL. - Parameters: - - artist_id - an artist ID, URI or URL + :param artist_id: An artist ID, URI or URL. + :return: The artist information. """ trid = self._get_id("artist", artist_id) return self._get(f"artists/{trid}") def artists(self, artists): - """ returns a list of artists given the artist IDs, URIs, or URLs + """ + Returns a list of artists given the artist IDs, URIs, or URLs. - Parameters: - - artists - a list of artist IDs, URIs or URLs + :param artists: A list of artist IDs, URIs or URLs. + :return: The list of artists information. """ tlist = [self._get_id("artist", a) for a in artists] return self._get(f"artists/?ids={','.join(tlist)}") def artist_albums( - self, artist_id, album_type=None, include_groups=None, country=None, limit=20, offset=0 + self, artist_id, album_type=None, include_groups=None, country=None, limit=20, offset=0 ): - """ Get Spotify catalog information about an artist's albums + """ + Get Spotify catalog information about an artist's albums. - Parameters: - - artist_id - the artist ID, URI or URL - - include_groups - the types of items to return. One or more of 'album', 'single', - 'appears_on', 'compilation'. If multiple types are desired, - pass in a comma separated string; e.g., 'album,single'. - - country - limit the response to one particular country. - - limit - the number of albums to return - - offset - the index of the first album to return + :param artist_id: An artist ID, URI or URL. + :param album_type: The type of album. + :param include_groups: The types of items to return. One or more of 'album', 'single', + 'appears_on', 'compilation'. If multiple types are desired, + pass in a comma separated string; e.g., 'album,single'. + :param country: An ISO 3166-1 alpha-2 country code. + Limit the response to one particular country. + :param limit: The number of items to return. + :param offset: The index of the first item to return. + :return: The artist's albums information. """ if album_type: @@ -415,24 +413,23 @@ class Spotify: ) def artist_top_tracks(self, artist_id, country="US"): - """ Get Spotify catalog information about an artist's top 10 tracks - by country. + """ + Get Spotify catalog information about an artist's top 10 tracks by country. - Parameters: - - artist_id - the artist ID, URI or URL - - country - limit the response to one particular country. + :param artist_id: The artist ID, URI or URL. + :param country: Limit the response to one particular country. + :return: The artist's top tracks information. """ trid = self._get_id("artist", artist_id) return self._get(f"artists/{trid}/top-tracks", country=country) def artist_related_artists(self, artist_id): - """ Get Spotify catalog information about artists similar to an - identified artist. Similarity is based on analysis of the - Spotify community's listening history. + """ + Get Spotify catalog information about artists similar to an identified artist. - Parameters: - - artist_id - the artist ID, URI or URL + :param artist_id: The artist ID, URI or URL. + :return: The related artists' information. """ warnings.warn( "You're using `artist_related_artists(...)`, " @@ -443,11 +440,12 @@ class Spotify: return self._get(f"artists/{trid}/related-artists") def album(self, album_id, market=None): - """ returns a single album given the album's ID, URIs or URL + """ + Returns a single album given the album's ID, URI or URL. - Parameters: - - album_id - the album ID, URI or URL - - market - an ISO 3166-1 alpha-2 country code + :param album_id: The album ID, URI or URL. + :param market: An ISO 3166-1 alpha-2 country code. + :return: The album information. """ trid = self._get_id("album", album_id) @@ -457,14 +455,14 @@ class Spotify: return self._get(f"albums/{trid}") def album_tracks(self, album_id, limit=50, offset=0, market=None): - """ Get Spotify catalog information about an album's tracks - - Parameters: - - album_id - the album ID, URI or URL - - limit - the number of items to return - - offset - the index of the first item to return - - market - an ISO 3166-1 alpha-2 country code. + """ + Get Spotify catalog information about an album's tracks. + :param album_id: The album ID, URI or URL. + :param limit: The number of items to return. + :param offset: The index of the first item to return. + :param market: An ISO 3166-1 alpha-2 country code. + :return: The album's tracks information. """ trid = self._get_id("album", album_id) @@ -473,11 +471,12 @@ class Spotify: ) def albums(self, albums, market=None): - """ returns a list of albums given the album IDs, URIs, or URLs + """ + Returns a list of albums given the album IDs, URIs, or URLs. - Parameters: - - albums - a list of album IDs, URIs or URLs - - market - an ISO 3166-1 alpha-2 country code + :param albums: A list of album IDs, URIs or URLs. + :param market: An ISO 3166-1 alpha-2 country code. + :return: The list of albums information. """ tlist = [self._get_id("album", a) for a in albums] @@ -487,47 +486,50 @@ class Spotify: return self._get(f"albums/?ids={','.join(tlist)}") def show(self, show_id, market=None): - """ returns a single show given the show's ID, URIs or URL + """ + Returns a single show given the show's ID, URI or URL. - Parameters: - - show_id - the show ID, URI or URL - - market - an ISO 3166-1 alpha-2 country code. - The show must be available in the given market. - If user-based authorization is in use, the user's country - takes precedence. If neither market nor user country are - provided, the content is considered unavailable for the client. + :param show_id: The show ID, URI or URL. + :param market: An ISO 3166-1 alpha-2 country code. + The show must be available in the given market. + If user-based authorization is in use, the user's country takes precedence. + If neither market nor user country are provided, + the content is considered unavailable for the client. + :return: The show information. """ trid = self._get_id("show", show_id) return self._get(f"shows/{trid}", market=market) def shows(self, shows, market=None): - """ returns a list of shows given the show IDs, URIs, or URLs + """ + Returns a list of shows given the show IDs, URIs, or URLs. - Parameters: - - shows - a list of show IDs, URIs or URLs - - market - an ISO 3166-1 alpha-2 country code. - Only shows available in the given market will be returned. - If user-based authorization is in use, the user's country - takes precedence. If neither market nor user country are - provided, the content is considered unavailable for the client. + :param shows: A list of show IDs, URIs or URLs. + :param market: An ISO 3166-1 alpha-2 country code. + The shows must be available in the given market. + If user-based authorization is in use, the user's country takes precedence. + If neither market nor user country are provided, + the content is considered unavailable for the client. + :return: The list of shows information. """ tlist = [self._get_id("show", s) for s in shows] return self._get(f"shows/?ids={','.join(tlist)}", market=market) def show_episodes(self, show_id, limit=50, offset=0, market=None): - """ Get Spotify catalog information about a show's episodes + """ + Get Spotify catalog information about a show's episodes. - Parameters: - - show_id - the show ID, URI or URL - - limit - the number of items to return - - offset - the index of the first item to return - - market - an ISO 3166-1 alpha-2 country code. - Only episodes available in the given market will be returned. - If user-based authorization is in use, the user's country - takes precedence. If neither market nor user country are - provided, the content is considered unavailable for the client. + :param show_id: The show ID, URI or URL. + :param limit: The number of items to return. + :param offset: The index of the first item to return. + :param market: An ISO 3166-1 alpha-2 country code. + The episodes must be available in the given market. + If user-based authorization is in use, the user's country takes precedence. + If neither market nor user country are provided, + the content is considered unavailable for the client. + :return: The show's episodes information. """ trid = self._get_id("show", show_id) @@ -536,68 +538,79 @@ class Spotify: ) def episode(self, episode_id, market=None): - """ returns a single episode given the episode's ID, URIs or URL + """ + Returns a single episode given the episode's ID, URI or URL. - Parameters: - - episode_id - the episode ID, URI or URL - - market - an ISO 3166-1 alpha-2 country code. - The episode must be available in the given market. - If user-based authorization is in use, the user's country - takes precedence. If neither market nor user country are - provided, the content is considered unavailable for the client. + :param episode_id: The episode ID, URI or URL. + :param market: An ISO 3166-1 alpha-2 country code. + The episode must be available in the given market. + If user-based authorization is in use, the user's country takes precedence. + If neither market nor user country are provided, + the content is considered unavailable for the client. + :return: The episode information. """ trid = self._get_id("episode", episode_id) return self._get(f"episodes/{trid}", market=market) def episodes(self, episodes, market=None): - """ returns a list of episodes given the episode IDs, URIs, or URLs + """ + Returns a list of episodes given the episode IDs, URIs, or URLs. - Parameters: - - episodes - a list of episode IDs, URIs or URLs - - market - an ISO 3166-1 alpha-2 country code. - Only episodes available in the given market will be returned. - If user-based authorization is in use, the user's country - takes precedence. If neither market nor user country are - provided, the content is considered unavailable for the client. + :param episodes: A list of episode IDs, URIs or URLs. + :param market: An ISO 3166-1 alpha-2 country code. + The episodes must be available in the given market. + If user-based authorization is in use, the user's country takes precedence. + If neither market nor user country are provided, + the content is considered unavailable for the client. + :return: The list of episodes information. """ tlist = [self._get_id("episode", e) for e in episodes] return self._get(f"episodes/?ids={','.join(tlist)}", market=market) def search(self, q, limit=10, offset=0, type="track", market=None): - """ searches for an item + """ + Searches for an item. - Parameters: - - q - the search query (see how to write a query in the - official documentation https://developer.spotify.com/documentation/web-api/reference/search/) # noqa - - 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 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. + :param q: The search query. (see how to write a query in the official documentation: + https://developer.spotify.com/documentation/web-api/reference/search/) + :param limit: The number of items to return (min = 1, default = 10, max = 50). + :param offset: The index of the first item to return. + The limit is applied within each type, not on the total response. + :param type: The type of item to search for. 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'. + :param market: An ISO 3166-1 alpha-2 country code. + The item must be available in the given market. + If user-based authorization is in use, the user's country takes precedence. + If neither market nor user country are provided, + the content is considered unavailable for the client. + :return: The search results. """ return self._get( "search", q=q, limit=limit, offset=offset, type=type, market=market ) def search_markets(self, q, limit=10, offset=0, type="track", markets=None, total=None): - """ (experimental) Searches multiple markets for an item + """ + (Experimental) Searches multiple markets for an item. - Parameters: - - q - the search query (see how to write a query in the - official documentation https://developer.spotify.com/documentation/web-api/reference/search/) # noqa - - 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). - If multiple types are specified, this applies to each type. - - offset - the index of the first item to return - - 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 across multiple markets and types. + :param q: The search query. (see how to write a query in the official documentation: + https://developer.spotify.com/documentation/web-api/reference/search/) + :param limit: The number of items to return. + :param offset: The index of the first item to return (min = 1, default = 10, max = 50). + The limit is applied within each type, not on the total response. + :param type: The type of item to search for. 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'. + :param markets: A list of ISO 3166-1 alpha-2 country codes. + The item must be available in the given markets. + If user-based authorization is in use, the user's country takes precedence. + If neither market nor user country are provided, + the content is considered unavailable for the client. + :param total: The total number of items to return across all markets. + :return: The search results. """ warnings.warn( "Searching multiple markets is an experimental feature. " @@ -617,31 +630,33 @@ class Spotify: return self._search_multiple_markets(q, limit, offset, type, markets, total) def user(self, user): - """ Gets basic profile information about a Spotify User + """ + Gets basic profile information about a Spotify User. - Parameters: - - user - the id of the usr + :param user: The ID of the user. + :return: The user's profile information. """ return self._get(f"users/{user}") def current_user_playlists(self, limit=50, offset=0): - """ Get current user playlists without required getting his profile - Parameters: - - limit - the number of items to return - - offset - the index of the first item to return + """ + Get current user playlists without requiring getting their profile. + + :param limit: The number of items to return. + :param offset: The index of the first item to return. + :return: The current user's playlists. """ return self._get("me/playlists", limit=limit, offset=offset) def playlist(self, playlist_id, fields=None, market=None, additional_types=("track",)): - """ Gets playlist by id. + """ + Gets a playlist by ID. - Parameters: - - playlist - the id of the playlist - - fields - which fields to return - - market - An ISO 3166-1 alpha-2 country code or the - string from_token. - - additional_types - list of item types to return. - valid types are: track and episode + :param playlist_id: The ID of the playlist. + :param fields: Which fields to return. + :param market: An ISO 3166-1 alpha-2 country code or the string from_token. + :param additional_types: List of item types to return. Valid types are: track and episode. + :return: The playlist information. """ plid = self._get_id("playlist", playlist_id) return self._get( @@ -652,24 +667,24 @@ class Spotify: ) def playlist_items( - self, - playlist_id, - fields=None, - limit=100, - offset=0, - market=None, - additional_types=("track", "episode") + self, + playlist_id, + fields=None, + limit=100, + offset=0, + market=None, + additional_types=("track", "episode") ): - """ Get full details of the tracks and episodes of a playlist. + """ + Get full details of the tracks and episodes of a playlist. - Parameters: - - playlist_id - the playlist ID, URI or URL - - 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 + :param playlist_id: The playlist ID, URI or URL. + :param fields: Which fields to return. + :param limit: The maximum number of tracks to return. + :param offset: The index of the first track to return. + :param market: An ISO 3166-1 alpha-2 country code. + :param additional_types: List of item types to return. Valid types are: track and episode. + :return: The playlist's items information. """ plid = self._get_id("playlist", playlist_id) return self._get( @@ -682,21 +697,22 @@ class Spotify: ) def playlist_cover_image(self, playlist_id): - """ Get cover image of a playlist. + """ + Get cover image of a playlist. - Parameters: - - playlist_id - the playlist ID, URI or URL + :param playlist_id: The playlist ID, URI or URL. + :return: The playlist's cover image. """ plid = self._get_id("playlist", playlist_id) return self._get(f"playlists/{plid}/images") def playlist_upload_cover_image(self, playlist_id, image_b64): - """ Replace the image used to represent a specific playlist + """ + Replace the image used to represent a specific playlist. - Parameters: - - playlist_id - the id of the playlist - - image_b64 - image data as a Base64 encoded JPEG image string - (maximum payload size is 256 KB) + :param playlist_id: The playlist ID, URI or URL. + :param image_b64: Base64 encoded JPEG image data, maximum payload size is 256 KB. + :return: The response from the API. """ plid = self._get_id("playlist", playlist_id) return self._put( @@ -706,24 +722,26 @@ class Spotify: ) def user_playlists(self, user, limit=50, offset=0): - """ Gets playlists of a user + """ + Gets playlists of a user. - Parameters: - - user - the id of the usr - - limit - the number of items to return - - offset - the index of the first item to return + :param user: The ID of the user. + :param limit: The number of items to return. + :param offset: The index of the first item to return. + :return: The user's playlists. """ return self._get(f"users/{user}/playlists", limit=limit, offset=offset) def user_playlist_create(self, user, name, public=True, collaborative=False, description=""): - """ Creates a playlist for a user + """ + Creates a playlist for a user. - Parameters: - - user - the id of the user - - name - the name of the playlist - - public - is the created playlist public - - collaborative - is the created playlist collaborative - - description - the description of the playlist + :param user: The ID of the user. + :param name: The name of the playlist. + :param public: Whether the playlist is public. + :param collaborative: Whether the playlist is collaborative. + :param description: The description of the playlist. + :return: The created playlist information. """ data = { "name": name, @@ -735,22 +753,22 @@ class Spotify: return self._post(f"users/{user}/playlists", payload=data) def playlist_change_details( - self, - playlist_id, - name=None, - public=None, - collaborative=None, - description=None, + self, + playlist_id, + name=None, + public=None, + collaborative=None, + description=None, ): - """ Changes a playlist's name and/or public/private state, - collaborative state, and/or description + """ + Change a playlist's details. - Parameters: - - 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 + :param playlist_id: The ID of the playlist. + :param name: The new name of the playlist. + :param public: Whether the playlist is public. + :param collaborative: Whether the playlist is collaborative. + :param description: The new description of the playlist. + :return: The response from the API. """ data = {} @@ -767,25 +785,27 @@ class Spotify: ) def current_user_unfollow_playlist(self, playlist_id): - """ Unfollows (deletes) a playlist for the current authenticated - user + """ + Unfollows (deletes) a playlist for the current authenticated user. - Parameters: - - playlist_id - the id of the playlist + :param playlist_id: The ID of the playlist. + :return: The response from the API. """ return self._delete( f"playlists/{self._get_id('playlist', playlist_id)}/followers" ) def playlist_add_items( - self, playlist_id, items, position=None + self, playlist_id, items, position=None ): - """ Adds tracks/episodes to a playlist + """ + Add one or more items to a user's playlist. - Parameters: - - playlist_id - the id of the playlist - - items - a list of track/episode URIs or URLs - - position - the position to add the tracks + :param playlist_id: The playlist ID, URI or URL. + :param items: A list of item URIs, URLs or IDs. + :param position: The position to insert the items, a zero-based index. + If omitted, the items will be appended to the playlist. + :return: The response from the API. """ for item in items: if not self._is_uri(item) and not self._is_url(item): @@ -799,11 +819,12 @@ class Spotify: ) def playlist_replace_items(self, playlist_id, items): - """ Replace all tracks/episodes in a playlist + """ + Replace all items in a playlist. - Parameters: - - playlist_id - the id of the playlist - - items - list of track/episode ids to comprise playlist + :param playlist_id: The playlist ID, URI or URL. + :param items: A list of item URIs, URLs or IDs. + :return: The response from the API. """ plid = self._get_id("playlist", playlist_id) ftracks = [self._get_uri("track", tid) for tid in items] @@ -811,23 +832,22 @@ class Spotify: return self._put(f"playlists/{plid}/tracks", payload=payload) def playlist_reorder_items( - self, - playlist_id, - range_start, - insert_before, - range_length=1, - snapshot_id=None, + self, + playlist_id, + range_start, + insert_before, + range_length=1, + snapshot_id=None, ): - """ Reorder tracks in a playlist + """ + Reorder a playlist's items. - Parameters: - - 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 + :param playlist_id: The playlist ID, URI or URL. + :param range_start: The position of the first item to be reordered. + :param insert_before: The position where the items should be inserted. + :param range_length: The number of items to be reordered. + :param snapshot_id: The playlist's snapshot ID. + :return: The response from the API. """ plid = self._get_id("playlist", playlist_id) payload = { @@ -840,15 +860,15 @@ class Spotify: return self._put(f"playlists/{plid}/tracks", payload=payload) def playlist_remove_all_occurrences_of_items( - self, playlist_id, items, snapshot_id=None + self, playlist_id, items, snapshot_id=None ): - """ Removes all occurrences of the given tracks/episodes from the given playlist - - Parameters: - - playlist_id - the id of the playlist - - items - list of track/episode ids to remove from the playlist - - snapshot_id - optional id of the playlist snapshot + """ + Remove all occurrences of specific items from a playlist. + :param playlist_id: The playlist ID, URI or URL. + :param items: A list of item URIs, URLs or IDs. + :param snapshot_id: The playlist's snapshot ID. + :return: The response from the API. """ plid = self._get_id("playlist", playlist_id) @@ -859,18 +879,15 @@ class Spotify: return self._delete(f"playlists/{plid}/tracks", payload=payload) def playlist_remove_specific_occurrences_of_items( - self, playlist_id, items, snapshot_id=None + self, playlist_id, items, snapshot_id=None ): - """ Removes all occurrences of the given tracks from the given playlist + """ + Remove specific occurrences of items from a playlist. - Parameters: - - playlist_id - the id of the playlist - - items - an array of objects containing Spotify URIs of the - tracks/episodes 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 + :param playlist_id: The playlist ID, URI or URL. + :param items: A list of dictionaries containing item URIs, URLs or IDs and their positions. + :param snapshot_id: The playlist's snapshot ID. + :return: The response from the API. """ plid = self._get_id("playlist", playlist_id) @@ -890,9 +907,9 @@ class Spotify: """ Add the current authenticated user as a follower of a playlist. - Parameters: - - playlist_id - the id of the playlist - + :param playlist_id: The ID of the playlist. + :param public: Whether the playlist should be followed publicly. + :return: The response from the API. """ return self._put( f"playlists/{playlist_id}/followers", @@ -900,55 +917,60 @@ class Spotify: ) def playlist_is_following( - self, playlist_id, user_ids + self, playlist_id, user_ids ): """ - Check to see if the given users are following the given playlist - - Parameters: - - 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. + Check if one or more users are following a playlist. + :param playlist_id: The ID of the playlist. + :param user_ids: A list of user IDs. + :return: A list of booleans indicating whether each user is following the playlist. """ return self._get( f"playlists/{playlist_id}/followers/contains?ids={','.join(user_ids)}" ) def me(self): - """ Get detailed profile information about the current user. - An alias for the 'current_user' method. + """ + Get detailed profile information about the current user. + + :return: The current user's profile information. """ return self._get("me/") def current_user(self): - """ Get detailed profile information about the current user. - An alias for the 'me' method. + """ + Get detailed profile information about the current user. + + :return: The current user's profile information. """ return self.me() def current_user_playing_track(self): - """ Get information about the current users currently playing track. + """ + Get information about the current user's currently playing track. + + :return: The currently playing track information. """ return self._get("me/player/currently-playing") def current_user_saved_albums(self, limit=20, offset=0, market=None): - """ Gets a list of the albums saved in the current authorized user's - "Your Music" library - - Parameters: - - limit - the number of albums to return (MAX_LIMIT=50) - - offset - the index of the first album to return - - market - an ISO 3166-1 alpha-2 country code. + """ + Get a list of the albums saved in the current authorized user's library. + :param limit: The number of items to return. + :param offset: The index of the first item to return. + :param market: An ISO 3166-1 alpha-2 country code. + :return: The list of saved albums. """ return self._get("me/albums", limit=limit, offset=offset, market=market) def current_user_saved_albums_add(self, albums=None): - """ Add one or more albums to the current user's - "Your Music" library. - Parameters: - - albums - a list of album URIs, URLs or IDs + """ + Add one or more albums to the current user's library. + + :param albums: A list of album IDs, URIs or URLs. + :return: The response from the API. """ if albums is None: @@ -957,11 +979,11 @@ class Spotify: return self._put(f"me/albums?ids={','.join(alist)}") def current_user_saved_albums_delete(self, albums=None): - """ Remove one or more albums from the current user's - "Your Music" library. + """ + Remove one or more albums from the current user's library. - Parameters: - - albums - a list of album URIs, URLs or IDs + :param albums: A list of album IDs, URIs or URLs. + :return: The response from the API. """ if albums is None: albums = [] @@ -969,11 +991,11 @@ class Spotify: return self._delete(f"me/albums/?ids={','.join(alist)}") def current_user_saved_albums_contains(self, albums=None): - """ Check if one or more albums is already saved in - the current Spotify user’s “Your Music” library. + """ + Check if one or more albums are already saved in the current user's library. - Parameters: - - albums - a list of album URIs, URLs or IDs + :param albums: A list of album IDs, URIs or URLs. + :return: A list of booleans indicating whether each album is saved. """ if albums is None: albums = [] @@ -981,64 +1003,63 @@ class Spotify: return self._get(f"me/albums/contains?ids={','.join(alist)}") def current_user_saved_tracks(self, limit=20, offset=0, market=None): - """ Gets a list of the tracks saved in the current authorized user's - "Your Music" library + """ + Get a list of the tracks saved in the current authorized user's library. - Parameters: - - limit - the number of tracks to return - - offset - the index of the first track to return - - market - an ISO 3166-1 alpha-2 country code + :param limit: The number of items to return. + :param offset: The index of the first item to return. + :param market: An ISO 3166-1 alpha-2 country code. + :return: The list of saved tracks. """ return self._get("me/tracks", limit=limit, offset=offset, market=market) def current_user_saved_tracks_add(self, tracks=None): - """ Add one or more tracks to the current user's - "Your Music" library. + """ + Add one or more tracks to the current user's library. - Parameters: - - tracks - a list of track URIs, URLs or IDs + :param tracks: A list of track IDs, URIs or URLs. + :return: The response from the API. """ tlist = [] if tracks is None else [self._get_id("track", t) for t in tracks] return self._put(f"me/tracks/?ids={','.join(tlist)}") def current_user_saved_tracks_delete(self, tracks=None): - """ Remove one or more tracks from the current user's - "Your Music" library. + """ + Remove one or more tracks from the current user's library. - Parameters: - - tracks - a list of track URIs, URLs or IDs + :param tracks: A list of track IDs, URIs or URLs. + :return: The response from the API. """ tlist = [] if tracks is None else [self._get_id("track", t) for t in tracks] return self._delete(f"me/tracks/?ids={','.join(tlist)}") def current_user_saved_tracks_contains(self, tracks=None): - """ Check if one or more tracks is already saved in - the current Spotify user’s “Your Music” library. + """ + Check if one or more tracks are already saved in the current user's library. - Parameters: - - tracks - a list of track URIs, URLs or IDs + :param tracks: A list of track IDs, URIs or URLs. + :return: A list of booleans indicating whether each track is saved. """ tlist = [] if tracks is None else [self._get_id("track", t) for t in tracks] return self._get(f"me/tracks/contains?ids={','.join(tlist)}") def current_user_saved_episodes(self, limit=20, offset=0, market=None): - """ Gets a list of the episodes saved in the current authorized user's - "Your Music" library - - Parameters: - - limit - the number of episodes to return - - offset - the index of the first episode to return - - market - an ISO 3166-1 alpha-2 country code + """ + Get a list of the episodes saved in the current authorized user's library. + :param limit: The number of items to return. + :param offset: The index of the first item to return. + :param market: An ISO 3166-1 alpha-2 country code. + :return: The list of saved episodes. """ return self._get("me/episodes", limit=limit, offset=offset, market=market) def current_user_saved_episodes_add(self, episodes=None): - """ Add one or more episodes to the current user's - "Your Music" library. + """ + Add one or more episodes to the current user's library. - Parameters: - - episodes - a list of episode URIs, URLs or IDs + :param episodes: A list of episode IDs, URIs or URLs. + :return: The response from the API. """ elist = [] if episodes is not None: @@ -1046,11 +1067,11 @@ class Spotify: return self._put(f"me/episodes/?ids={','.join(elist)}") def current_user_saved_episodes_delete(self, episodes=None): - """ Remove one or more episodes from the current user's - "Your Music" library. + """ + Remove one or more episodes from the current user's library. - Parameters: - - episodes - a list of episode URIs, URLs or IDs + :param episodes: A list of episode IDs, URIs or URLs. + :return: The response from the API. """ elist = [] if episodes is not None: @@ -1058,11 +1079,11 @@ class Spotify: return self._delete(f"me/episodes/?ids={','.join(elist)}") def current_user_saved_episodes_contains(self, episodes=None): - """ Check if one or more episodes is already saved in - the current Spotify user’s “Your Music” library. + """ + Check if one or more episodes are already saved in the current user's library. - Parameters: - - episodes - a list of episode URIs, URLs or IDs + :param episodes: A list of episode IDs, URIs or URLs. + :return: A list of booleans indicating whether each episode is saved. """ elist = [] if episodes is not None: @@ -1070,22 +1091,22 @@ class Spotify: return self._get(f"me/episodes/contains?ids={','.join(elist)}") def current_user_saved_shows(self, limit=20, offset=0, market=None): - """ Gets a list of the shows saved in the current authorized user's - "Your Music" library - - Parameters: - - limit - the number of shows to return - - offset - the index of the first show to return - - market - an ISO 3166-1 alpha-2 country code + """ + Get a list of the shows saved in the current authorized user's library. + :param limit: The number of items to return. + :param offset: The index of the first item to return. + :param market: An ISO 3166-1 alpha-2 country code. + :return: The list of saved shows. """ return self._get("me/shows", limit=limit, offset=offset, market=market) def current_user_saved_shows_add(self, shows=None): - """ Add one or more albums to the current user's - "Your Music" library. - Parameters: - - shows - a list of show URIs, URLs or IDs + """ + Add one or more shows to the current user's library. + + :param shows: A list of show IDs, URIs or URLs. + :return: The response from the API. """ if shows is None: shows = [] @@ -1093,11 +1114,11 @@ class Spotify: return self._put(f"me/shows?ids={','.join(slist)}") def current_user_saved_shows_delete(self, shows=None): - """ Remove one or more shows from the current user's - "Your Music" library. + """ + Remove one or more shows from the current user's library. - Parameters: - - shows - a list of show URIs, URLs or IDs + :param shows: A list of show IDs, URIs or URLs. + :return: The response from the API. """ if shows is None: shows = [] @@ -1105,11 +1126,11 @@ class Spotify: return self._delete(f"me/shows/?ids={','.join(slist)}") def current_user_saved_shows_contains(self, shows=None): - """ Check if one or more shows is already saved in - the current Spotify user’s “Your Music” library. + """ + Check if one or more shows are already saved in the current user's library. - Parameters: - - shows - a list of show URIs, URLs or IDs + :param shows: A list of show IDs, URIs or URLs. + :return: A list of booleans indicating whether each show is saved. """ if shows is None: @@ -1118,23 +1139,21 @@ class Spotify: return self._get(f"me/shows/contains?ids={','.join(slist)}") def current_user_followed_artists(self, limit=20, after=None): - """ Gets a list of the artists followed by the current authorized user - - Parameters: - - limit - the number of artists to return - - after - the last artist ID retrieved from the previous - request + """ + Get a list of the artists followed by the current authorized user. + :param limit: The number of artists to return. + :param after: The last artist ID retrieved from the previous request. + :return: The list of followed artists. """ return self._get(f"me/following?type=artist&limit={limit}&after={after}") def current_user_following_artists(self, ids=None): - """ Check if the current user is following certain artists + """ + Check if the current user is following certain artists. - Returns list of booleans respective to ids - - Parameters: - - ids - a list of artist URIs, URLs or IDs + :param ids: A list of artist URIs, URLs or IDs. + :return: A list of booleans indicating whether each artist is followed. """ idlist = [self._get_id("artist", i) for i in ids] if ids is not None else [] return self._get( @@ -1142,12 +1161,11 @@ class Spotify: ) def current_user_following_users(self, ids=None): - """ Check if the current user is following certain users + """ + Check if the current user is following certain users. - Returns list of booleans respective to ids - - Parameters: - - ids - a list of user URIs, URLs or IDs + :param ids: A list of user URIs, URLs or IDs. + :return: A list of booleans indicating whether each user is followed. """ idlist = [self._get_id("user", i) for i in ids] if ids is not None else [] return self._get( @@ -1155,46 +1173,47 @@ class Spotify: ) def current_user_top_artists( - self, limit=20, offset=0, time_range="medium_term" + self, limit=20, offset=0, time_range="medium_term" ): - """ Get the current user's top artists + """ + Get the current user's top artists. - Parameters: - - limit - the number of entities to return (max 50) - - offset - the index of the first entity to return - - time_range - Over what time frame are the affinities computed - Valid-values: short_term, medium_term, long_term + :param limit: The number of entities to return. + :param offset: The index of the first entity to return. + :param time_range: Over what time frame are the affinities computed. + Valid values: short_term, medium_term, long_term. + :return: The list of top artists. """ return self._get( "me/top/artists", time_range=time_range, limit=limit, offset=offset ) def current_user_top_tracks( - self, limit=20, offset=0, time_range="medium_term" + self, limit=20, offset=0, time_range="medium_term" ): - """ Get the current user's top tracks + """ + Get the current user's top tracks. - Parameters: - - limit - the number of entities to return - - offset - the index of the first entity to return - - time_range - Over what time frame are the affinities computed - Valid-values: short_term, medium_term, long_term + :param limit: The number of entities to return. + :param offset: The index of the first entity to return. + :param time_range: Over what time frame are the affinities computed. + Valid values: short_term, medium_term, long_term. + :return: The list of top tracks. """ return self._get( "me/top/tracks", time_range=time_range, limit=limit, offset=offset ) def current_user_recently_played(self, limit=50, after=None, before=None): - """ Get the current user's recently played tracks + """ + Get the current user's recently played tracks. - Parameters: - - limit - the number of entities to return - - after - unix timestamp in milliseconds. Returns all items - after (but not including) this cursor position. - Cannot be used if before is specified. - - before - unix timestamp in milliseconds. Returns all items - before (but not including) this cursor position. - Cannot be used if after is specified + :param limit: The number of entities to return. + :param after: Unix timestamp in milliseconds. Returns all items after (but not including) + this cursor position. Cannot be used if before is specified. + :param before: Unix timestamp in milliseconds. Returns all items before (but not including) + this cursor position. Cannot be used if after is specified. + :return: The list of recently played tracks. """ return self._get( "me/player/recently-played", @@ -1204,65 +1223,63 @@ class Spotify: ) def user_follow_artists(self, ids=None): - """ Follow one or more artists - Parameters: - - ids - a list of artist IDs + """ + Follow one or more artists. + + :param ids: A list of artist IDs. + :return: The response from the API. """ if ids is None: ids = [] return self._put(f"me/following?type=artist&ids={','.join(ids)}") def user_follow_users(self, ids=None): - """ Follow one or more users - Parameters: - - ids - a list of user IDs + """ + Follow one or more users. + + :param ids: A list of user IDs. + :return: The response from the API. """ if ids is None: ids = [] return self._put(f"me/following?type=user&ids={','.join(ids)}") def user_unfollow_artists(self, ids=None): - """ Unfollow one or more artists - Parameters: - - ids - a list of artist IDs + """ + Unfollow one or more artists. + + :param ids: A list of artist IDs. + :return: The response from the API. """ if ids is None: ids = [] return self._delete(f"me/following?type=artist&ids={','.join(ids)}") def user_unfollow_users(self, ids=None): - """ Unfollow one or more users - Parameters: - - ids - a list of user IDs + """ + Unfollow one or more users. + + :param ids: A list of user IDs. + :return: The response from the API. """ if ids is None: ids = [] return self._delete(f"me/following?type=user&ids={','.join(ids)}") def featured_playlists( - self, locale=None, country=None, timestamp=None, limit=20, offset=0 + self, locale=None, country=None, timestamp=None, limit=20, offset=0 ): - """ Get a list of Spotify featured playlists - - Parameters: - - locale - The desired language, consisting of a lowercase ISO - 639-1 alpha-2 language code and an uppercase ISO 3166-1 alpha-2 - country code, joined by an underscore. - - - country - An ISO 3166-1 alpha-2 country code. - - - timestamp - A timestamp in ISO 8601 format: - yyyy-MM-ddTHH:mm:ss. Use this parameter to specify the user's - local time to get results tailored for that specific date and - time in the day - - - limit - The maximum number of items to return. Default: 20. - Minimum: 1. Maximum: 50 - - - offset - The index of the first item to return. Default: 0 - (the first object). Use with limit to get the next set of - items. """ + Get a list of Spotify featured playlists. + + :param locale: The desired language, consisting of an ISO 639-1 language code + and an ISO 3166-1 alpha-2 country code, joined by an underscore. + :param country: An ISO 3166-1 alpha-2 country code. + :param timestamp: A timestamp in ISO 8601 format. + :param limit: The number of items to return. + :param offset: The index of the first item to return. + :return: The list of featured playlists. + """ warnings.warn( "You're using `featured_playlists(...)`, " "which is marked as deprecated by Spotify.", @@ -1278,52 +1295,44 @@ class Spotify: ) def new_releases(self, country=None, limit=20, offset=0): - """ Get a list of new album releases featured in Spotify + """ + Get a list of new album releases featured in Spotify. - Parameters: - - country - An ISO 3166-1 alpha-2 country code. - - - limit - The maximum number of items to return. Default: 20. - Minimum: 1. Maximum: 50 - - - offset - The index of the first item to return. Default: 0 - (the first object). Use with limit to get the next set of - items. + :param country: An ISO 3166-1 alpha-2 country code. + :param limit: The number of items to return. + :param offset: The index of the first item to return. + :return: The list of new releases. """ return self._get( "browse/new-releases", country=country, limit=limit, offset=offset ) def category(self, category_id, country=None, locale=None): - """ Get info about a category + """ + Get a single category used to tag items in Spotify + (on, for example, the Spotify player’s “Browse” tab). - Parameters: - - category_id - The Spotify category ID for the category. - - - country - An ISO 3166-1 alpha-2 country code. - - locale - The desired language, consisting of an ISO 639-1 alpha-2 - language code and an ISO 3166-1 alpha-2 country code, joined - by an underscore. + :param category_id: The Spotify category ID. + :param country: An ISO 3166-1 alpha-2 country code. + :param locale: The desired language, consisting of an ISO 639-1 language code + and an ISO 3166-1 alpha-2 country code, joined by an underscore. + :return: The category information. """ return self._get( f"browse/categories/{category_id}", country=country, locale=locale ) def categories(self, country=None, locale=None, limit=20, offset=0): - """ Get a list of categories + """ + Get a list of categories used to tag items in Spotify + (on, for example, the Spotify player’s “Browse” tab). - Parameters: - - country - An ISO 3166-1 alpha-2 country code. - - locale - The desired language, consisting of an ISO 639-1 alpha-2 - language code and an ISO 3166-1 alpha-2 country code, joined - by an underscore. - - - limit - The maximum number of items to return. Default: 20. - Minimum: 1. Maximum: 50 - - - offset - The index of the first item to return. Default: 0 - (the first object). Use with limit to get the next set of - items. + :param country: An ISO 3166-1 alpha-2 country code. + :param locale: The desired language, consisting of an ISO 639-1 language code + and an ISO 3166-1 alpha-2 country code, joined by an underscore. + :param limit: The number of items to return. + :param offset: The index of the first item to return. + :return: The list of categories. """ return self._get( "browse/categories", @@ -1334,21 +1343,16 @@ class Spotify: ) def category_playlists( - self, category_id=None, country=None, limit=20, offset=0 + self, category_id=None, country=None, limit=20, offset=0 ): - """ Get a list of playlists for a specific Spotify category + """ + Get a list of Spotify playlists tagged with a particular category. - Parameters: - - category_id - The Spotify category ID for the category. - - - country - An ISO 3166-1 alpha-2 country code. - - - limit - The maximum number of items to return. Default: 20. - Minimum: 1. Maximum: 50 - - - offset - The index of the first item to return. Default: 0 - (the first object). Use with limit to get the next set of - items. + :param category_id: The Spotify category ID. + :param country: An ISO 3166-1 alpha-2 country code. + :param limit: The number of items to return. + :param offset: The index of the first item to return. + :return: The list of playlists. """ warnings.warn( "You're using `category_playlists(...)`, " @@ -1363,34 +1367,24 @@ class Spotify: ) def recommendations( - self, - seed_artists=None, - seed_genres=None, - seed_tracks=None, - limit=20, - country=None, - **kwargs + self, + seed_artists=None, + seed_genres=None, + seed_tracks=None, + limit=20, + country=None, + **kwargs ): - """ Get a list of recommended tracks for one to five seeds. - (at least one of `seed_artists`, `seed_tracks` and `seed_genres` - are needed) + """ + Get a list of recommended tracks based on the given seed artists, genres, and tracks. - Parameters: - - seed_artists - a list of artist IDs, URIs or URLs - - seed_tracks - a list of track IDs, URIs or URLs - - seed_genres - a list of genre names. Available genres for - recommendations can be found by calling - recommendation_genre_seeds - - - country - An ISO 3166-1 alpha-2 country code. If provided, - all results will be playable in this country. - - - limit - The maximum number of items to return. Default: 20. - Minimum: 1. Maximum: 100 - - - min/max/target_ - For the tuneable track - attributes listed in the documentation, these values - provide filters and targeting on results. + :param seed_artists: A list of Spotify IDs for seed artists. + :param seed_genres: A list of seed genres. + :param seed_tracks: A list of Spotify IDs for seed tracks. + :param limit: The number of items to return. + :param country: An ISO 3166-1 alpha-2 country code. + :param kwargs: Additional parameters for recommendations. + :return: The list of recommended tracks. """ warnings.warn( "You're using `recommendations(...)`, " @@ -1435,6 +1429,11 @@ class Spotify: return self._get("recommendations", **params) def recommendation_genre_seeds(self): + """ + Get a list of available genres seed parameter values for recommendations. + + :return: The list of available genre seeds. + """ warnings.warn( "You're using `recommendation_genre_seeds(...)`, " "which is marked as deprecated by Spotify.", @@ -1445,9 +1444,11 @@ class Spotify: return self._get("recommendations/available-genre-seeds") def audio_analysis(self, track_id): - """ Get audio analysis for a track based upon its Spotify ID - Parameters: - - track_id - a track URI, URL or ID + """ + Get audio analysis for a track based upon its Spotify ID. + + :param track_id: A Spotify URI, URL or ID. + :return: The audio analysis information. """ warnings.warn( "You're using `audio_analysis(...)`, " @@ -1458,9 +1459,11 @@ class Spotify: return self._get(f"audio-analysis/{trid}") def audio_features(self, tracks=None): - """ Get audio features for one or multiple tracks based upon their Spotify IDs - Parameters: - - tracks - a list of track URIs, URLs or IDs, maximum: 100 ids + """ + Get audio features for one or multiple tracks based upon their Spotify IDs. + + :param tracks: A list of track IDs, URIs or URLs. + :return: The audio features information. """ warnings.warn( "You're using `audio_features(...)`, " @@ -1485,65 +1488,59 @@ class Spotify: return results def devices(self): - """ Get a list of user's available devices. + """ + Get a list of user's available devices. + + :return: The list of available devices. """ return self._get("me/player/devices") def current_playback(self, market=None, additional_types=None): - """ Get information about user's current playback. + """ + Get information about user's current playback. - Parameters: - - market - an ISO 3166-1 alpha-2 country code. - - additional_types - `episode` to get podcast track information + :param market: An ISO 3166-1 alpha-2 country code. + :param additional_types: List of item types to return. Valid types are: track and episode. + :return: The current playback information. """ return self._get("me/player", market=market, additional_types=additional_types) def currently_playing(self, market=None, additional_types=None): - """ Get user's currently playing track. + """ + Get user's currently playing track. - Parameters: - - market - an ISO 3166-1 alpha-2 country code. - - additional_types - `episode` to get podcast track information + :param market: An ISO 3166-1 alpha-2 country code. + :param additional_types: List of item types to return. Valid types are: track and episode. + :return: The currently playing track information. """ return self._get("me/player/currently-playing", market=market, additional_types=additional_types) def transfer_playback(self, device_id, force_play=True): - """ Transfer playback to another device. - Note that the API accepts a list of device ids, but only - actually supports one. + """ + Transfer playback to another device. - Parameters: - - device_id - transfer playback to this device - - force_play - true: after transfer, play. false: - keep current state. + :param device_id: The ID of the device. + :param force_play: Whether to start playback on the new device. Default: True. + :return: The response from the API. """ data = {"device_ids": [device_id], "play": force_play} return self._put("me/player", payload=data) def start_playback( - self, device_id=None, context_uri=None, uris=None, offset=None, position_ms=None + self, device_id=None, context_uri=None, uris=None, offset=None, position_ms=None ): - """ Start or resume user's playback. + """ + Start or resume user's playback. - Provide a `context_uri` to start playback of an album, - artist, or playlist. - - Provide a `uris` list to start playback of one or more - tracks. - - Provide `offset` as {"position": } or {"uri": ""} - to start playback at a particular offset. - - Parameters: - - device_id - device target for playback - - context_uri - spotify context uri to play - - uris - spotify track uris - - offset - offset into context by index or track - - position_ms - (optional) indicates from what position to start playback. - Must be a positive number. Passing in a position that is - greater than the length of the track will cause the player to - start playing the next song. + :param device_id: The ID of the device. + :param context_uri: Spotify context URI to play. + :param uris: List of Spotify track URIs. + :param offset: Offset into context by index or track. + :param position_ms: Indicates from what position to start playback. + Must be a positive number. Passing in a position that is greater than the length + of the track will cause the player to start playing the next song. + :return: The response from the API. """ if context_uri is not None and uris is not None: logger.warning("Specify either context uri or uris, not both") @@ -1565,37 +1562,41 @@ class Spotify: ) def pause_playback(self, device_id=None): - """ Pause user's playback. + """ + Pause user's playback. - Parameters: - - device_id - device target for playback + :param device_id: The ID of the device. + :return: The response from the API. """ return self._put(self._append_device_id("me/player/pause", device_id)) def next_track(self, device_id=None): - """ Skip user's playback to next track. + """ + Skip user's playback to next track. - Parameters: - - device_id - device target for playback + :param device_id: The ID of the device. + :return: The response from the API. """ return self._post(self._append_device_id("me/player/next", device_id)) def previous_track(self, device_id=None): - """ Skip user's playback to previous track. + """ + Skip user's playback to previous track. - Parameters: - - device_id - device target for playback + :param device_id: The ID of the device. + :return: The response from the API. """ return self._post( self._append_device_id("me/player/previous", device_id) ) def seek_track(self, position_ms, device_id=None): - """ Seek to position in current track. + """ + Seek to position in current track. - Parameters: - - position_ms - position in milliseconds to seek to - - device_id - device target for playback + :param position_ms: Position in milliseconds to seek to. + :param device_id: The ID of the device. + :return: The response from the API. """ if not isinstance(position_ms, int): logger.warning("Position_ms must be an integer") @@ -1607,11 +1608,12 @@ class Spotify: ) def repeat(self, state, device_id=None): - """ Set repeat mode for playback. + """ + Set repeat mode for playback. - Parameters: - - state - `track`, `context`, or `off` - - device_id - device target for playback + :param state: `track`, `context`, or `off`. + :param device_id: The ID of the device. + :return: The response from the API. """ if state not in ["track", "context", "off"]: logger.warning("Invalid state") @@ -1619,11 +1621,12 @@ class Spotify: self._put(self._append_device_id(f"me/player/repeat?state={state}", device_id)) def volume(self, volume_percent, device_id=None): - """ Set playback volume. + """ + Set playback volume. - Parameters: - - volume_percent - volume between 0 and 100 - - device_id - device target for playback + :param volume_percent: Volume between 0 and 100. + :param device_id: The ID of the device. + :return: The response from the API. """ if not isinstance(volume_percent, int): logger.warning("Volume must be an integer") @@ -1638,11 +1641,12 @@ class Spotify: ) def shuffle(self, state, device_id=None): - """ Toggle playback shuffling. + """ + Toggle playback shuffling. - Parameters: - - state - true or false - - device_id - device target for playback + :param state: A boolean indicating whether to enable or disable shuffling. + :param device_id: The ID of the device. + :return: The response from the API. """ if not isinstance(state, bool): logger.warning("state must be a boolean") @@ -1653,22 +1657,25 @@ class Spotify: ) def queue(self): - """ Gets the current user's queue """ + """ + Get the current user's queue. + + :return: The current user's queue. + """ return self._get("me/player/queue") def add_to_queue(self, uri, device_id=None): - """ Adds a song to the end of a user's queue + """ + Add a song to the end of a user's queue. - If device A is currently playing music, and you try to add to the queue - and pass in the id for device B, you will get a - 'Player command failed: Restriction violated' error - I therefore recommend leaving device_id as None so that the active device is targeted - - :param uri: song uri, id, or url - :param device_id: - the id of a Spotify device. - If None, then the active device is used. + If device A is currently playing music, and you try to add to the queue + and pass in the ID for device B, + you will get a 'Player command failed: Restriction violated' error. + It is recommended to leave device_id as None so that the active device is targeted. + :param uri: The song URI, ID, or URL. + :param device_id: The ID of a Spotify device. If None, the active device is used. + :return: The response from the API. """ uri = self._get_uri("track", uri) @@ -1681,18 +1688,17 @@ class Spotify: return self._post(endpoint) def available_markets(self): - """ Get the list of markets where Spotify is available. - Returns a list of the countries in which Spotify is available, identified by their - ISO 3166-1 alpha-2 country code with additional country codes for special territories. + """ + Get the list of markets where Spotify is available. + + Returns a list of the countries in which Spotify is available, identified by their + ISO 3166-1 alpha-2 country code with additional country codes for special territories. + + :return: The list of available markets. """ return self._get("markets") def _append_device_id(self, path, device_id): - """ Append device ID to API path. - - Parameters: - - device_id - device id to append - """ if device_id: path += f"&device_id={device_id}" if "?" in path else f"?device_id={device_id}" return path @@ -1769,12 +1775,12 @@ class Spotify: return results def get_audiobook(self, id, market=None): - """ Get Spotify catalog information for a single audiobook identified by its unique - Spotify ID. + """ + Get Spotify catalog information for a single audiobook identified by its unique Spotify ID. - Parameters: - - id - the Spotify ID for the audiobook - - market - an ISO 3166-1 alpha-2 country code. + :param id: The Spotify ID for the audiobook. + :param market: An ISO 3166-1 alpha-2 country code. + :return: The audiobook information. """ audiobook_id = self._get_id("audiobook", id) endpoint = f"audiobooks/{audiobook_id}" @@ -1785,11 +1791,12 @@ class Spotify: return self._get(endpoint) def get_audiobooks(self, ids, market=None): - """ Get Spotify catalog information for multiple audiobooks based on their Spotify IDs. + """ + Get Spotify catalog information for multiple audiobooks based on their Spotify IDs. - Parameters: - - ids - a list of Spotify IDs for the audiobooks - - market - an ISO 3166-1 alpha-2 country code. + :param ids: A list of Spotify IDs for the audiobooks. + :param market: An ISO 3166-1 alpha-2 country code. + :return: The audiobooks information. """ audiobook_ids = [self._get_id("audiobook", id) for id in ids] endpoint = f"audiobooks?ids={','.join(audiobook_ids)}" @@ -1800,13 +1807,14 @@ class Spotify: return self._get(endpoint) def get_audiobook_chapters(self, id, market=None, limit=20, offset=0): - """ Get Spotify catalog information about an audiobook’s chapters. + """ + Get Spotify catalog information about an audiobook’s chapters. - Parameters: - - id - the Spotify ID for the audiobook - - market - an ISO 3166-1 alpha-2 country code. - - limit - the maximum number of items to return - - offset - the index of the first item to return + :param id: The Spotify ID for the audiobook. + :param market: An ISO 3166-1 alpha-2 country code. + :param limit: The maximum number of items to return. + :param offset: The index of the first item to return. + :return: The audiobook's chapters information. """ audiobook_id = self._get_id("audiobook", id) endpoint = f"audiobooks/{audiobook_id}/chapters?limit={limit}&offset={offset}" diff --git a/spotipy/exceptions.py b/spotipy/exceptions.py index cacce9f..0a61133 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 @@ class SpotifyException(SpotifyBaseException): 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 @@ class SpotifyOauthError(SpotifyBaseException): 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 4f1b4d5..3f15a41 100644 --- a/spotipy/oauth2.py +++ b/spotipy/oauth2.py @@ -28,6 +28,13 @@ logger = logging.getLogger(__name__) 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 @@ class SpotifyAuthBase: 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 @@ class SpotifyAuthBase: 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 @@ class SpotifyAuthBase: @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 @@ class SpotifyAuthBase: 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,16 +201,19 @@ class SpotifyAuthBase: class SpotifyClientCredentials(SpotifyAuthBase): + """ + Implements Client Credentials Flow for Spotify's OAuth implementation. + """ OAUTH_TOKEN_URL = "https://accounts.spotify.com/api/token" def __init__( - self, - client_id=None, - client_secret=None, - cache_handler=None, - proxies=None, - requests_session=True, - requests_timeout=None + self, + client_id=None, + client_secret=None, + cache_handler=None, + proxies=None, + requests_session=True, + requests_timeout=None ): """ Creates a Client Credentials Flow Manager. @@ -176,26 +222,20 @@ class SpotifyClientCredentials(SpotifyAuthBase): 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 - * 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. - 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 + user interaction. + :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`. + :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). + :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 @@ class SpotifyClientCredentials(SpotifyAuthBase): 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 - before requesting a new 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 @@ class SpotifyClientCredentials(SpotifyAuthBase): 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 @@ class SpotifyClientCredentials(SpotifyAuthBase): 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 @@ class SpotifyOAuth(SpotifyAuthBase): open_browser=True ): """ - Creates a SpotifyOAuth object + 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 - 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. - 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 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`. + :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). + :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 @@ class SpotifyOAuth(SpotifyAuthBase): 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 @@ class SpotifyOAuth(SpotifyAuthBase): 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 @@ class SpotifyOAuth(SpotifyAuthBase): 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 @@ class SpotifyOAuth(SpotifyAuthBase): 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 @@ class SpotifyOAuth(SpotifyAuthBase): 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 @@ class SpotifyOAuth(SpotifyAuthBase): 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 @@ class SpotifyOAuth(SpotifyAuthBase): 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 @@ class SpotifyOAuth(SpotifyAuthBase): 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 @@ class SpotifyOAuth(SpotifyAuthBase): 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 @@ class SpotifyOAuth(SpotifyAuthBase): 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,56 +667,43 @@ 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" OAUTH_TOKEN_URL = "https://accounts.spotify.com/api/token" def __init__( - self, - client_id=None, - redirect_uri=None, - state=None, - scope=None, - cache_handler=None, - proxies=None, - requests_timeout=None, - requests_session=True, - open_browser=True + self, + client_id=None, + redirect_uri=None, + state=None, + scope=None, + cache_handler=None, + proxies=None, + requests_timeout=None, + requests_session=True, + open_browser=True ): """ 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 - 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. - 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 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`. + :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). + :param open_browser: Optional, whether the web browser should be opened to + authorize a user. """ super().__init__(requests_session) @@ -644,9 +730,12 @@ class SpotifyPKCE(SpotifyAuthBase): 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 @@ class SpotifyPKCE(SpotifyAuthBase): 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 @@ class SpotifyPKCE(SpotifyAuthBase): 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 @@ class SpotifyPKCE(SpotifyAuthBase): 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 @@ class SpotifyPKCE(SpotifyAuthBase): 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 @@ class SpotifyPKCE(SpotifyAuthBase): 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,42 @@ class SpotifyPKCE(SpotifyAuthBase): 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. + """ self.server.auth_code = self.server.error = None try: state, auth_code = SpotifyOAuth.parse_auth_response_url(self.path) @@ -931,6 +1074,13 @@ window.close() 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 b5b4bf7..dabc1aa 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 @@ class Scope(Enum): """ 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 + Converts a string of (usually space-separated) scopes into a set of scopes. - Any scope-strings that do not match any of the known scopes are - ignored. + Any scope-strings that do not match any of the known scopes are ignored. - * scope_string: a string of scopes - - 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 8400a54..7f3e91f 100644 --- a/spotipy/util.py +++ b/spotipy/util.py @@ -21,11 +21,12 @@ CLIENT_CREDS_ENV_VARS = { 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):