mirror of
https://github.com/spotipy-dev/spotipy.git
synced 2026-06-19 09:13:53 +00:00
Merge branch 'v3' into master
This commit is contained in:
commit
aa9652f1fd
2
.github/workflows/pythonapp.yml
vendored
2
.github/workflows/pythonapp.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
|
||||
python-version: [3.5, 3.6, 3.7, 3.8, 3.9]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
83
CHANGELOG.md
83
CHANGELOG.md
@ -5,7 +5,24 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased
|
||||
## Unreleased [3.0.0-alpha]
|
||||
|
||||
While this is unreleased, please only add v3 features here. Rebasing master onto v3 doesn't require a changelog update.
|
||||
|
||||
### Added
|
||||
|
||||
* `Scope` - An enum which contains all of the authorization scopes (see [here](https://github.com/plamere/spotipy/issues/652#issuecomment-797461311)).
|
||||
|
||||
### Changed
|
||||
|
||||
* Made `CacheHandler` an abstract base class
|
||||
|
||||
* Modified the return structure of the `audio_features` function (wrapping the [Get Audio Features for Several Tracks](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-audio-features) API) to conform to the return structure of the similar methods listed below. The functions wrapping these APIs do not unwrap the single key JSON response, and this is currently the only function that does this.
|
||||
* [Get Several Tracks](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-tracks)
|
||||
* [Get Multiple Artists](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-artists)
|
||||
* [Get Multiple Albums](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-albums)
|
||||
|
||||
## Unreleased [2.x.x]
|
||||
|
||||
### Added
|
||||
|
||||
@ -92,7 +109,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
|
||||
- `SpotifyPKCE.parse_auth_response_url`, mirroring that method in
|
||||
`SpotifyOAuth`
|
||||
`SpotifyOAuth`
|
||||
|
||||
### Changed
|
||||
|
||||
@ -101,7 +118,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Fixed
|
||||
|
||||
- Using `SpotifyPKCE.get_authorization_url` will now generate a code
|
||||
challenge if needed
|
||||
challenge if needed
|
||||
|
||||
## [2.14.0] - 2020-08-29
|
||||
|
||||
@ -109,9 +126,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- (experimental) Support to search multiple/all markets at once.
|
||||
- Support to test whether the current user is following certain
|
||||
users or artists
|
||||
users or artists
|
||||
- Proper replacements for all deprecated playlist endpoints
|
||||
(See https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ and below)
|
||||
(See https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ and below)
|
||||
- Allow for OAuth 2.0 authorization by instructing the user to open the URL in a browser instead of opening the browser.
|
||||
- Reason for 403 error in SpotifyException
|
||||
- Support for the PKCE Auth Flow
|
||||
@ -129,15 +146,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- `user_playlist_replace_tracks` in favor of `playlist_replace_items`
|
||||
- `user_playlist_reorder_tracks` in favor of `playlist_reorder_items`
|
||||
- `user_playlist_remove_all_occurrences_of_tracks` in favor of
|
||||
`playlist_remove_all_occurrences_of_items`
|
||||
`playlist_remove_all_occurrences_of_items`
|
||||
- `user_playlist_remove_specific_occurrences_of_tracks` in favor of
|
||||
`playlist_remove_specific_occurrences_of_items`
|
||||
`playlist_remove_specific_occurrences_of_items`
|
||||
- `user_playlist_follow_playlist` in favor of
|
||||
`current_user_follow_playlist`
|
||||
`current_user_follow_playlist`
|
||||
- `user_playlist_is_following` in favor of `playlist_is_following`
|
||||
- `playlist_tracks` in favor of `playlist_items`
|
||||
|
||||
### Fixed
|
||||
|
||||
- fixed issue where episode URIs were being converted to track URIs in playlist calls
|
||||
|
||||
## [2.13.0] - 2020-06-25
|
||||
@ -145,12 +163,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
|
||||
- Added `SpotifyImplicitGrant` as an auth manager option. It provides
|
||||
user authentication without a client secret but sacrifices the ability
|
||||
to refresh the token without user input. (However, read the class
|
||||
docstring for security advisory.)
|
||||
user authentication without a client secret but sacrifices the ability
|
||||
to refresh the token without user input. (However, read the class
|
||||
docstring for security advisory.)
|
||||
- Added built-in verification of the `state` query parameter
|
||||
- Added two new attributes: error and error_description to `SpotifyOauthError` exception class to show
|
||||
authorization/authentication web api errors details.
|
||||
authorization/authentication web api errors details.
|
||||
- Added `SpotifyStateError` subclass of `SpotifyOauthError`
|
||||
- Allow extending `SpotifyClientCredentials` and `SpotifyOAuth`
|
||||
- Added the market paramter to `album_tracks`
|
||||
@ -174,10 +192,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Changed
|
||||
|
||||
- Updated the documentation to give more details on the authorization process and reflect
|
||||
2020 Spotify Application jargon and practices.
|
||||
2020 Spotify Application jargon and practices.
|
||||
|
||||
- The local webserver is only started for localhost redirect_uri which specify a port,
|
||||
i.e. it is started for `http://localhost:8080` or `http://127.0.0.1:8080`, not for `http://localhost`.
|
||||
i.e. it is started for `http://localhost:8080` or `http://127.0.0.1:8080`, not for `http://localhost`.
|
||||
|
||||
### Fixed
|
||||
|
||||
@ -246,6 +264,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Optional `show_dialog` parameter to be passed to `SpotifyOAuth`
|
||||
|
||||
### Changed
|
||||
|
||||
- Both `SpotifyClientCredentials` and `SpotifyOAuth` inherit from a common `SpotifyAuthBase` which handles common parameters and logics.
|
||||
|
||||
## [2.7.1] - 2020-01-20
|
||||
@ -289,16 +308,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [2.6.1] - 2020-01-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed inconsistent behaviour with some API methods when
|
||||
a full HTTP URL is passed.
|
||||
a full HTTP URL is passed.
|
||||
- Fixed invalid calls to logging warn method
|
||||
|
||||
### Removed
|
||||
|
||||
- `mock` no longer needed for install. Only used in `tox`.
|
||||
|
||||
## [2.6.0] - 2020-01-12
|
||||
|
||||
### Added
|
||||
|
||||
- Support for `playlist` to get a playlist without specifying a user
|
||||
- Support for `current_user_saved_albums_delete`
|
||||
- Support for `current_user_saved_albums_contains`
|
||||
@ -307,95 +329,126 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Lint with flake8 using Github action
|
||||
|
||||
### Changed
|
||||
|
||||
- Fix typos in doc
|
||||
- Start following [SemVer](https://semver.org) properly
|
||||
|
||||
## [2.5.0] - 2020-01-11
|
||||
|
||||
Added follow and player endpoints
|
||||
|
||||
## [2.4.4] - 2017-01-04
|
||||
|
||||
Python 3 fix
|
||||
|
||||
## [2.4.3] - 2017-01-02
|
||||
|
||||
Fixed proxy issue in standard auth flow
|
||||
|
||||
## [2.4.2] - 2017-01-02
|
||||
|
||||
Support getting audio features for a single track
|
||||
|
||||
## [2.4.1] - 2017-01-02
|
||||
|
||||
Incorporated proxy support
|
||||
|
||||
## [2.4.0] - 2016-12-31
|
||||
|
||||
Incorporated a number of PRs
|
||||
|
||||
## [2.3.8] - 2016-03-31
|
||||
|
||||
Added recs, audio features, user top lists
|
||||
|
||||
## [2.3.7] - 2015-08-10
|
||||
|
||||
Added current_user_followed_artists
|
||||
|
||||
## [2.3.6] - 2015-06-03
|
||||
|
||||
Support for offset/limit with album_tracks API
|
||||
|
||||
## [2.3.5] - 2015-04-28
|
||||
|
||||
Fixed bug in auto retry logic
|
||||
|
||||
## [2.3.3] - 2015-04-01
|
||||
|
||||
Aadded client credential flow
|
||||
|
||||
## [2.3.2] - 2015-03-31
|
||||
|
||||
Added auto retry logic
|
||||
|
||||
## [2.3.0] - 2015-01-05
|
||||
|
||||
Added session support added by akx.
|
||||
|
||||
## [2.2.0] - 2014-11-15
|
||||
|
||||
Added support for user_playlist_tracks
|
||||
|
||||
## [2.1.0] - 2014-10-25
|
||||
|
||||
Added support for new_releases and featured_playlists
|
||||
|
||||
## [2.0.2] - 2014-08-25
|
||||
|
||||
Moved to spotipy at pypi
|
||||
|
||||
## [1.2.0] - 2014-08-22
|
||||
|
||||
Upgraded APIs and docs to make it be a real library
|
||||
|
||||
## [1.310.0] - 2014-08-20
|
||||
|
||||
Added playlist replace and remove methods. Added auth tests. Improved API docs
|
||||
|
||||
## [1.301.0] - 2014-08-19
|
||||
|
||||
Upgraded version number to take precedence over previously botched release (sigh)
|
||||
|
||||
## [1.50.0] - 2014-08-14
|
||||
|
||||
Refactored util out of examples and into the main package
|
||||
|
||||
## [1.49.0] - 2014-07-23
|
||||
|
||||
Support for "Your Music" tracks (add, delete, get), with examples
|
||||
|
||||
## [1.45.0] - 2014-07-07
|
||||
|
||||
Support for related artists endpoint. Don't use cache auth codes when scope changes
|
||||
|
||||
## [1.44.0] - 2014-07-03
|
||||
|
||||
Added show tracks.py example
|
||||
|
||||
## [1.43.0] - 2014-06-27
|
||||
|
||||
Fixed JSON handling issue
|
||||
|
||||
## [1.42.0] - 2014-06-19
|
||||
|
||||
Removed dependency on simplejson
|
||||
|
||||
## [1.40.0] - 2014-06-12
|
||||
|
||||
Initial public release.
|
||||
|
||||
## [1.4.2] - 2014-06-21
|
||||
|
||||
Added support for retrieving starred playlists
|
||||
|
||||
## [1.1.0] - 2014-06-17
|
||||
|
||||
Updates to match released API
|
||||
|
||||
## [1.1.0] - 2014-05-18
|
||||
|
||||
Repackaged for saner imports
|
||||
|
||||
## [1.0.0] - 2017-04-05
|
||||
|
||||
Initial release
|
||||
2
setup.py
2
setup.py
@ -18,7 +18,7 @@ extra_reqs = {
|
||||
|
||||
setup(
|
||||
name='spotipy',
|
||||
version='2.18.0',
|
||||
version='3.0.0-alpha',
|
||||
description='A light weight Python library for the Spotify Web API',
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
|
||||
@ -3,3 +3,4 @@ from .client import * # noqa
|
||||
from .exceptions import * # noqa
|
||||
from .oauth2 import * # noqa
|
||||
from .util import * # noqa
|
||||
from .scope import * # noqa
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__all__ = ['CacheHandler', 'CacheFileHandler', 'MemoryCacheHandler']
|
||||
|
||||
import errno
|
||||
@ -5,33 +7,32 @@ import json
|
||||
import logging
|
||||
import os
|
||||
from spotipy.util import CLIENT_CREDS_ENV_VARS
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CacheHandler():
|
||||
class CacheHandler(ABC):
|
||||
"""
|
||||
An abstraction layer for handling the caching and retrieval of
|
||||
authorization tokens.
|
||||
|
||||
Custom extensions of this class must implement get_cached_token
|
||||
and save_token_to_cache methods with the same input and output
|
||||
structure as the CacheHandler class.
|
||||
Clients are expected to subclass this class and override the
|
||||
get_cached_token and save_token_to_cache methods with the same
|
||||
type signatures of this class.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_cached_token(self):
|
||||
"""
|
||||
Get and return a token_info dictionary object.
|
||||
"""
|
||||
# return token_info
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def save_token_to_cache(self, token_info):
|
||||
"""
|
||||
Save a token_info dictionary object to the cache and return None.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
return None
|
||||
|
||||
|
||||
class CacheFileHandler(CacheHandler):
|
||||
|
||||
@ -1221,7 +1221,6 @@ class Spotify(object):
|
||||
- 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
|
||||
|
||||
"""
|
||||
return self._get("me/tracks", limit=limit, offset=offset, market=market)
|
||||
|
||||
@ -1347,6 +1346,7 @@ class Spotify(object):
|
||||
Parameters:
|
||||
- shows - a list of show URIs, URLs or IDs
|
||||
"""
|
||||
|
||||
slist = [self._get_id("show", s) for s in shows]
|
||||
return self._get("me/shows/contains?ids=" + ",".join(slist))
|
||||
|
||||
@ -1675,12 +1675,7 @@ class Spotify(object):
|
||||
else:
|
||||
tlist = [self._get_id("track", t) for t in tracks]
|
||||
results = self._get("audio-features/?ids=" + ",".join(tlist))
|
||||
# the response has changed, look for the new style first, and if
|
||||
# its not there, fallback on the old style
|
||||
if "audio_features" in results:
|
||||
return results["audio_features"]
|
||||
else:
|
||||
return results
|
||||
return results
|
||||
|
||||
def devices(self):
|
||||
""" Get a list of user's available devices.
|
||||
|
||||
@ -24,7 +24,10 @@ from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
from six.moves.urllib_parse import parse_qsl, urlparse
|
||||
|
||||
from spotipy.cache_handler import CacheFileHandler, CacheHandler
|
||||
from spotipy.util import CLIENT_CREDS_ENV_VARS, get_host_port, normalize_scope
|
||||
from spotipy.util import CLIENT_CREDS_ENV_VARS, get_host_port
|
||||
from spotipy.scope import Scope
|
||||
from typing import Iterable
|
||||
import re
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -72,6 +75,7 @@ def _ensure_value(value, env_key):
|
||||
|
||||
|
||||
class SpotifyAuthBase(object):
|
||||
|
||||
def __init__(self, requests_session):
|
||||
if isinstance(requests_session, requests.Session):
|
||||
self._session = requests_session
|
||||
@ -83,7 +87,40 @@ class SpotifyAuthBase(object):
|
||||
self._session = api
|
||||
|
||||
def _normalize_scope(self, scope):
|
||||
return normalize_scope(scope)
|
||||
"""
|
||||
Accepts a string of scopes, or an iterable with elements of type
|
||||
`Scope` or `str` and returns a space-separated string of scopes.
|
||||
Returns `None` if the argument is `None`.
|
||||
"""
|
||||
|
||||
# TODO: do we need to sort the scopes?
|
||||
|
||||
if isinstance(scope, str):
|
||||
# allow for any separator(s) between the scopes other than a word
|
||||
# character or a hyphen
|
||||
scopes = re.split(pattern=r"[^\w-]+", string=scope)
|
||||
return " ".join(sorted(scopes))
|
||||
|
||||
if isinstance(scope, Iterable):
|
||||
|
||||
# Assume all of the iterable's elements are of the same type.
|
||||
# If the iterable is empty, then return None.
|
||||
first_element = next(iter(scope), None)
|
||||
|
||||
if isinstance(first_element, str):
|
||||
return " ".join(sorted(scope))
|
||||
if isinstance(first_element, Scope):
|
||||
return Scope.make_string(scope)
|
||||
if first_element is None:
|
||||
return ""
|
||||
|
||||
elif scope is None:
|
||||
return None
|
||||
|
||||
raise TypeError(
|
||||
"Unsupported type for scopes: %s. Expected either a string of scopes, or "
|
||||
"an Iterable with elements of type `Scope` or `str`." % type(scope)
|
||||
)
|
||||
|
||||
@property
|
||||
def client_id(self):
|
||||
@ -289,7 +326,11 @@ class SpotifyOAuth(SpotifyAuthBase):
|
||||
* client_secret: Must be supplied or set as environment variable
|
||||
* redirect_uri: Must be supplied or set as environment variable
|
||||
* state: Optional, no verification is performed
|
||||
* scope: Optional, either a list of scopes or comma separated string of scopes.
|
||||
* 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}
|
||||
|
||||
iterable of scopes or comma separated string of scopes.
|
||||
e.g, "playlist-read-private,playlist-read-collaborative"
|
||||
* cache_path: (deprecated) Optional, will otherwise be generated
|
||||
(takes precedence over `username`)
|
||||
@ -650,8 +691,9 @@ class SpotifyPKCE(SpotifyAuthBase):
|
||||
* 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 list of scopes or comma separated string of scopes.
|
||||
e.g, "playlist-read-private,playlist-read-collaborative"
|
||||
* 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
|
||||
@ -1050,8 +1092,9 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
|
||||
* client_id: Must be supplied or set as environment variable
|
||||
* redirect_uri: Must be supplied or set as environment variable
|
||||
* state: May be supplied, no verification is performed
|
||||
* scope: Optional, either a list of scopes or comma separated string of scopes.
|
||||
e.g, "playlist-read-private,playlist-read-collaborative"
|
||||
* 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.
|
||||
May be supplied, will otherwise use `CacheFileHandler`.
|
||||
|
||||
85
spotipy/scope.py
Normal file
85
spotipy/scope.py
Normal file
@ -0,0 +1,85 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__all__ = ["Scope"]
|
||||
|
||||
from enum import Enum
|
||||
import re
|
||||
from typing import Iterable, Set
|
||||
|
||||
|
||||
class Scope(Enum):
|
||||
"""
|
||||
The Spotify authorization scopes
|
||||
|
||||
Create a Scope from a string:
|
||||
|
||||
scope = Scope("playlist-modify-private")
|
||||
|
||||
Create a set of scopes:
|
||||
|
||||
scopes = {
|
||||
Scope.user_read_currently_playing,
|
||||
Scope.playlist_read_collaborative,
|
||||
Scope.playlist_modify_public
|
||||
}
|
||||
"""
|
||||
|
||||
user_read_currently_playing = "user-read-currently-playing"
|
||||
playlist_read_collaborative = "playlist-read-collaborative"
|
||||
playlist_modify_private = "playlist-modify-private"
|
||||
user_read_playback_position = "user-read-playback-position"
|
||||
user_library_modify = "user-library-modify"
|
||||
user_top_read = "user-top-read"
|
||||
user_read_playback_state = "user-read-playback-state"
|
||||
user_read_email = "user-read-email"
|
||||
ugc_image_upload = "ugc-image-upload"
|
||||
user_read_private = "user-read-private"
|
||||
playlist_modify_public = "playlist-modify-public"
|
||||
user_library_read = "user-library-read"
|
||||
streaming = "streaming"
|
||||
user_read_recently_played = "user-read-recently-played"
|
||||
user_follow_read = "user-follow-read"
|
||||
user_follow_modify = "user-follow-modify"
|
||||
app_remote_control = "app-remote-control"
|
||||
playlist_read_private = "playlist-read-private"
|
||||
user_modify_playback_state = "user-modify-playback-state"
|
||||
|
||||
@staticmethod
|
||||
def all() -> Set['Scope']:
|
||||
"""Returns all of the authorization scopes"""
|
||||
|
||||
return set(Scope)
|
||||
|
||||
@staticmethod
|
||||
def make_string(scopes: Iterable['Scope']) -> str:
|
||||
"""
|
||||
Converts an iterable of scopes to a space-separated string.
|
||||
|
||||
* scopes: An iterable of scopes.
|
||||
|
||||
returns: a space-separated string of scopes
|
||||
"""
|
||||
return " ".join([scope.value for scope in scopes])
|
||||
|
||||
@staticmethod
|
||||
def from_string(scope_string: str) -> Set['Scope']:
|
||||
"""
|
||||
Converts a string of (usuallly space-separated) scopes into a
|
||||
set of scopes
|
||||
|
||||
Any scope-strings that do not match any of the known scopes are
|
||||
ignored.
|
||||
|
||||
* scope_string: a string of scopes
|
||||
|
||||
returns: a set of scopes.
|
||||
"""
|
||||
scope_string_list = re.split(pattern=r"[^\w-]+", string=scope_string)
|
||||
scopes = set()
|
||||
for scope_string in sorted(scope_string_list):
|
||||
try:
|
||||
scope = Scope(scope_string)
|
||||
scopes.add(scope)
|
||||
except ValueError:
|
||||
pass
|
||||
return scopes
|
||||
@ -7,7 +7,6 @@ __all__ = ["CLIENT_CREDS_ENV_VARS", "prompt_for_user_token"]
|
||||
import logging
|
||||
import os
|
||||
import warnings
|
||||
|
||||
import spotipy
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
@ -117,19 +116,3 @@ def get_host_port(netloc):
|
||||
port = None
|
||||
|
||||
return host, port
|
||||
|
||||
|
||||
def normalize_scope(scope):
|
||||
if scope:
|
||||
if isinstance(scope, str):
|
||||
scopes = scope.split(',')
|
||||
elif isinstance(scope, list) or isinstance(scope, tuple):
|
||||
scopes = scope
|
||||
else:
|
||||
raise Exception(
|
||||
"Unsupported scope value, please either provide a list of scopes, "
|
||||
"or a string of scopes separated by commas"
|
||||
)
|
||||
return " ".join(sorted(scopes))
|
||||
else:
|
||||
return None
|
||||
|
||||
@ -67,19 +67,19 @@ class AuthTestSpotipy(unittest.TestCase):
|
||||
|
||||
def test_audio_features(self):
|
||||
results = self.spotify.audio_features(self.four_tracks)
|
||||
self.assertTrue(len(results) == len(self.four_tracks))
|
||||
for track in results:
|
||||
self.assertTrue(len(results['audio_features']) == len(self.four_tracks))
|
||||
for track in results['audio_features']:
|
||||
assert('speechiness' in track)
|
||||
|
||||
def test_audio_features_with_bad_track(self):
|
||||
bad_tracks = ['spotify:track:bad']
|
||||
input = self.four_tracks + bad_tracks
|
||||
results = self.spotify.audio_features(input)
|
||||
self.assertTrue(len(results) == len(input))
|
||||
for track in results[:-1]:
|
||||
self.assertTrue(len(results['audio_features']) == len(input))
|
||||
for track in results['audio_features'][:-1]:
|
||||
if track is not None:
|
||||
assert('speechiness' in track)
|
||||
self.assertTrue(results[-1] is None)
|
||||
self.assertTrue(results['audio_features'][-1] is None)
|
||||
|
||||
def test_recommendations(self):
|
||||
results = self.spotify.recommendations(
|
||||
|
||||
90
tests/unit/test_scopes.py
Normal file
90
tests/unit/test_scopes.py
Normal file
@ -0,0 +1,90 @@
|
||||
from unittest import TestCase
|
||||
from spotipy.scope import Scope
|
||||
from spotipy.oauth2 import SpotifyAuthBase
|
||||
|
||||
|
||||
class SpotipyScopeTest(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.auth_manager = SpotifyAuthBase(requests_session=True)
|
||||
|
||||
def normalize_scope(self, scope):
|
||||
return self.auth_manager._normalize_scope(scope)
|
||||
|
||||
def test_empty_scope(self):
|
||||
scopes = set()
|
||||
scope_string = Scope.make_string(scopes)
|
||||
|
||||
normalized_scope_string = self.normalize_scope(scopes)
|
||||
normalized_scope_string_2 = self.normalize_scope(scope_string)
|
||||
|
||||
self.assertEqual(scope_string, "")
|
||||
self.assertEqual(normalized_scope_string, "")
|
||||
self.assertEqual(normalized_scope_string_2, "")
|
||||
|
||||
converted_scopes = Scope.from_string(scope_string)
|
||||
self.assertEqual(converted_scopes, set())
|
||||
|
||||
def test_scopes(self):
|
||||
scopes = {
|
||||
Scope.playlist_modify_public,
|
||||
Scope.playlist_read_collaborative,
|
||||
Scope.user_read_playback_state,
|
||||
Scope.ugc_image_upload
|
||||
}
|
||||
normalized_scope_string = self.normalize_scope(scopes)
|
||||
scope_string = Scope.make_string(scopes)
|
||||
self.assertEqual(scope_string, normalized_scope_string)
|
||||
|
||||
normalized_scope_string_2 = self.normalize_scope(scope_string)
|
||||
|
||||
converted_scopes = Scope.from_string(scope_string)
|
||||
normalized_converted_scope = Scope.from_string(normalized_scope_string)
|
||||
normalized_converted_scope_2 = Scope.from_string(normalized_scope_string_2)
|
||||
self.assertEqual(scopes, converted_scopes)
|
||||
self.assertEqual(scopes, normalized_converted_scope)
|
||||
self.assertEqual(scopes, normalized_converted_scope_2)
|
||||
|
||||
def test_single_scope(self):
|
||||
scope_string = "user-modify-playback-state"
|
||||
scope = Scope(scope_string)
|
||||
self.assertEqual(scope, Scope.user_modify_playback_state)
|
||||
self.assertEqual(scope_string, scope.value)
|
||||
|
||||
def test_scope_string(self):
|
||||
scope_string = (
|
||||
"user-read-currently-playing playlist-read-collaborative,user-library-read "
|
||||
"playlist-read-private user-read-email"
|
||||
)
|
||||
expected_scopes = {
|
||||
Scope.user_read_currently_playing,
|
||||
Scope.playlist_read_collaborative,
|
||||
Scope.user_library_read,
|
||||
Scope.playlist_read_private,
|
||||
Scope.user_read_email
|
||||
}
|
||||
parsed_scopes = Scope.from_string(scope_string)
|
||||
normalized_scope_string = self.normalize_scope(scope_string)
|
||||
normalized_parsed_scopes = Scope.from_string(normalized_scope_string)
|
||||
self.assertEqual(parsed_scopes, expected_scopes)
|
||||
self.assertEqual(normalized_parsed_scopes, expected_scopes)
|
||||
|
||||
def test_invalid_types(self):
|
||||
|
||||
numbers = [1, 2, 3]
|
||||
with self.assertRaises(TypeError):
|
||||
self.normalize_scope(numbers)
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
self.normalize_scope(True)
|
||||
|
||||
def test_normalize_scope(self):
|
||||
|
||||
normalized_scope_string = self.normalize_scope([])
|
||||
self.assertEqual(normalized_scope_string, "")
|
||||
|
||||
normalized_scope_string_2 = self.normalize_scope(())
|
||||
self.assertEqual(normalized_scope_string_2, "")
|
||||
|
||||
self.assertIsNone(self.normalize_scope(None))
|
||||
Loading…
Reference in New Issue
Block a user