mirror of
https://github.com/spotipy-dev/spotipy.git
synced 2026-06-19 09:13:53 +00:00
Drop support for EOL Python 3.7 (#1065)
* Add python_requires to help pip * Update supported versions in tox.ini * Upgrade Python syntax with pyupgrade --py37-plus * Bump GitHub Actions * Add Python 3.11 and 3.12 to CI * Remove six dependency * Remove redundant dependencies * Remove redudant Python 3.5 code * Drop support for EOL Python 3.7 * Upgrade Python syntax with pyupgrade --py38-plus * Update CHANGELOG * More f-strings --------- Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
This commit is contained in:
parent
958ff6ad2b
commit
85c9d74dc1
4
.github/workflows/integration_tests.yml
vendored
4
.github/workflows/integration_tests.yml
vendored
@ -11,9 +11,9 @@ jobs:
|
|||||||
SPOTIPY_CLIENT_SECRET: ${{ secrets.SPOTIPY_CLIENT_SECRET }}
|
SPOTIPY_CLIENT_SECRET: ${{ secrets.SPOTIPY_CLIENT_SECRET }}
|
||||||
PYTHON_VERSION: "3.10"
|
PYTHON_VERSION: "3.10"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
6
.github/workflows/publish.yml
vendored
6
.github/workflows/publish.yml
vendored
@ -13,9 +13,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
- name: Install pypa/build
|
- name: Install pypa/build
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
--outdir dist/
|
--outdir dist/
|
||||||
.
|
.
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "2.x"
|
python-version: "2.x"
|
||||||
- name: Install pypa/build
|
- name: Install pypa/build
|
||||||
|
|||||||
4
.github/workflows/pull_request.yml
vendored
4
.github/workflows/pull_request.yml
vendored
@ -8,8 +8,8 @@ jobs:
|
|||||||
changelog:
|
changelog:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v4
|
||||||
- uses: dangoslen/changelog-enforcer@v1.1.1
|
- uses: dangoslen/changelog-enforcer@v3.5.1
|
||||||
with:
|
with:
|
||||||
changeLogPath: 'CHANGELOG.md'
|
changeLogPath: 'CHANGELOG.md'
|
||||||
skipLabel: 'skip-changelog'
|
skipLabel: 'skip-changelog'
|
||||||
6
.github/workflows/pythonapp.yml
vendored
6
.github/workflows/pythonapp.yml
vendored
@ -8,11 +8,11 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@ -30,6 +30,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Fixed
|
### Fixed
|
||||||
- Fixed unused description parameter in playlist creation example
|
- Fixed unused description parameter in playlist creation example
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Drop support for EOL Python 3.7.
|
||||||
|
|
||||||
|
|
||||||
## [2.23.0] - 2023-04-07
|
## [2.23.0] - 2023-04-07
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# spotipy documentation build configuration file, created by
|
# spotipy documentation build configuration file, created by
|
||||||
# sphinx-quickstart on Thu Aug 21 11:04:39 2014.
|
# sphinx-quickstart on Thu Aug 21 11:04:39 2014.
|
||||||
|
|||||||
@ -333,7 +333,7 @@ Export the needed Environment variables:::
|
|||||||
export SPOTIPY_REDIRECT_URI=http://localhost:8080 # Make url is set in app you created to get your ID and SECRET
|
export SPOTIPY_REDIRECT_URI=http://localhost:8080 # Make url is set in app you created to get your ID and SECRET
|
||||||
|
|
||||||
Create virtual environment, install dependencies, run tests:::
|
Create virtual environment, install dependencies, run tests:::
|
||||||
$ virtualenv --python=python3.7 env
|
$ virtualenv --python=python3.12 env
|
||||||
(env) $ pip install --user -e .
|
(env) $ pip install --user -e .
|
||||||
(env) $ python -m unittest discover -v tests
|
(env) $ python -m unittest discover -v tests
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
# shows audio analysis for the given track
|
# shows audio analysis for the given track
|
||||||
|
|
||||||
from __future__ import print_function # (at top of module)
|
|
||||||
from spotipy.oauth2 import SpotifyClientCredentials
|
from spotipy.oauth2 import SpotifyClientCredentials
|
||||||
import json
|
import json
|
||||||
import spotipy
|
import spotipy
|
||||||
@ -20,4 +19,4 @@ start = time.time()
|
|||||||
analysis = sp.audio_analysis(tid)
|
analysis = sp.audio_analysis(tid)
|
||||||
delta = time.time() - start
|
delta = time.time() - start
|
||||||
print(json.dumps(analysis, indent=4))
|
print(json.dumps(analysis, indent=4))
|
||||||
print("analysis retrieved in %.2f seconds" % (delta,))
|
print(f"analysis retrieved in {delta:.2f} seconds")
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
# shows acoustic features for tracks for the given artist
|
# shows acoustic features for tracks for the given artist
|
||||||
|
|
||||||
from __future__ import print_function # (at top of module)
|
|
||||||
from spotipy.oauth2 import SpotifyClientCredentials
|
from spotipy.oauth2 import SpotifyClientCredentials
|
||||||
import json
|
import json
|
||||||
import spotipy
|
import spotipy
|
||||||
@ -33,4 +31,4 @@ for feature in features:
|
|||||||
analysis = sp._get(feature['analysis_url'])
|
analysis = sp._get(feature['analysis_url'])
|
||||||
print(json.dumps(analysis, indent=4))
|
print(json.dumps(analysis, indent=4))
|
||||||
print()
|
print()
|
||||||
print("features retrieved in %.2f seconds" % (delta,))
|
print(f"features retrieved in {delta:.2f} seconds")
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
# shows acoustic features for tracks for the given artist
|
# shows acoustic features for tracks for the given artist
|
||||||
|
|
||||||
from __future__ import print_function # (at top of module)
|
|
||||||
from spotipy.oauth2 import SpotifyClientCredentials
|
from spotipy.oauth2 import SpotifyClientCredentials
|
||||||
import json
|
import json
|
||||||
import spotipy
|
import spotipy
|
||||||
@ -22,4 +19,4 @@ if len(sys.argv) > 1:
|
|||||||
features = sp.audio_features(tids)
|
features = sp.audio_features(tids)
|
||||||
delta = time.time() - start
|
delta = time.time() - start
|
||||||
print(json.dumps(features, indent=4))
|
print(json.dumps(features, indent=4))
|
||||||
print("features retrieved in %.2f seconds" % (delta,))
|
print(f"features retrieved in {delta:.2f} seconds")
|
||||||
|
|||||||
@ -11,7 +11,7 @@ scope = 'user-library-read'
|
|||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
tid = sys.argv[1]
|
tid = sys.argv[1]
|
||||||
else:
|
else:
|
||||||
print("Usage: %s track-id ..." % (sys.argv[0],))
|
print(f"Usage: {sys.argv[0]} track-id ...")
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
|
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
|
||||||
|
|||||||
@ -11,7 +11,7 @@ scope = 'user-library-modify'
|
|||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
tid = sys.argv[1]
|
tid = sys.argv[1]
|
||||||
else:
|
else:
|
||||||
print("Usage: %s track-id ..." % (sys.argv[0],))
|
print(f"Usage: {sys.argv[0]} track-id ...")
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
|
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
|
||||||
|
|||||||
@ -15,8 +15,7 @@ if len(sys.argv) > 2:
|
|||||||
track_ids.append({"uri": tid, "positions": [int(pos)]})
|
track_ids.append({"uri": tid, "positions": [int(pos)]})
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
"Usage: %s playlist_id track_id,pos track_id,pos ..." %
|
f"Usage: {sys.argv[0]} playlist_id track_id,pos track_id,pos ...")
|
||||||
(sys.argv[0],))
|
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
scope = 'playlist-modify-public'
|
scope = 'playlist-modify-public'
|
||||||
|
|||||||
@ -10,7 +10,7 @@ if len(sys.argv) > 2:
|
|||||||
playlist_id = sys.argv[2]
|
playlist_id = sys.argv[2]
|
||||||
track_ids = sys.argv[3:]
|
track_ids = sys.argv[3:]
|
||||||
else:
|
else:
|
||||||
print("Usage: %s playlist_id track_id ..." % (sys.argv[0]))
|
print(f"Usage: {sys.argv[0]} playlist_id track_id ...")
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
scope = 'playlist-modify-public'
|
scope = 'playlist-modify-public'
|
||||||
|
|||||||
@ -10,7 +10,7 @@ if len(sys.argv) > 3:
|
|||||||
playlist_id = sys.argv[1]
|
playlist_id = sys.argv[1]
|
||||||
track_ids = sys.argv[2:]
|
track_ids = sys.argv[2:]
|
||||||
else:
|
else:
|
||||||
print("Usage: %s playlist_id track_id ..." % (sys.argv[0],))
|
print(f"Usage: {sys.argv[0]} playlist_id track_id ...")
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
scope = 'playlist-modify-public'
|
scope = 'playlist-modify-public'
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# shows album info for a URN or URL
|
# shows album info for a URN or URL
|
||||||
|
|
||||||
from spotipy.oauth2 import SpotifyClientCredentials
|
from spotipy.oauth2 import SpotifyClientCredentials
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# shows related artists for the given seed artist
|
# shows related artists for the given seed artist
|
||||||
|
|
||||||
from spotipy.oauth2 import SpotifyClientCredentials
|
from spotipy.oauth2 import SpotifyClientCredentials
|
||||||
|
|||||||
@ -13,4 +13,4 @@ while results['next']:
|
|||||||
albums.extend(results['items'])
|
albums.extend(results['items'])
|
||||||
|
|
||||||
for album in albums:
|
for album in albums:
|
||||||
print((album['name']))
|
print(album['name'])
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
from spotipy.oauth2 import SpotifyClientCredentials
|
from spotipy.oauth2 import SpotifyClientCredentials
|
||||||
import spotipy
|
import spotipy
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ client_credentials_manager = SpotifyClientCredentials()
|
|||||||
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
||||||
|
|
||||||
|
|
||||||
skiplist = set(['dm', 'remix'])
|
skiplist = {'dm', 'remix'}
|
||||||
max_offset = 500
|
max_offset = 500
|
||||||
seen = set()
|
seen = set()
|
||||||
|
|
||||||
|
|||||||
5
setup.py
5
setup.py
@ -1,6 +1,6 @@
|
|||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
with open("README.md", "r") as f:
|
with open("README.md") as f:
|
||||||
long_description = f.read()
|
long_description = f.read()
|
||||||
|
|
||||||
test_reqs = [
|
test_reqs = [
|
||||||
@ -28,11 +28,10 @@ setup(
|
|||||||
project_urls={
|
project_urls={
|
||||||
'Source': 'https://github.com/plamere/spotipy',
|
'Source': 'https://github.com/plamere/spotipy',
|
||||||
},
|
},
|
||||||
|
python_requires='>3.8',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"redis>=3.5.3",
|
"redis>=3.5.3",
|
||||||
"redis<4.0.0;python_version<'3.4'",
|
|
||||||
"requests>=2.25.0",
|
"requests>=2.25.0",
|
||||||
"six>=1.15.0",
|
|
||||||
"urllib3>=1.26.0"
|
"urllib3>=1.26.0"
|
||||||
],
|
],
|
||||||
tests_require=test_reqs,
|
tests_require=test_reqs,
|
||||||
|
|||||||
@ -80,7 +80,7 @@ class CacheFileHandler(CacheHandler):
|
|||||||
f.close()
|
f.close()
|
||||||
token_info = json.loads(token_info_string)
|
token_info = json.loads(token_info_string)
|
||||||
|
|
||||||
except IOError as error:
|
except OSError as error:
|
||||||
if error.errno == errno.ENOENT:
|
if error.errno == errno.ENOENT:
|
||||||
logger.debug("cache does not exist at: %s", self.cache_path)
|
logger.debug("cache does not exist at: %s", self.cache_path)
|
||||||
else:
|
else:
|
||||||
@ -93,7 +93,7 @@ class CacheFileHandler(CacheHandler):
|
|||||||
f = open(self.cache_path, "w")
|
f = open(self.cache_path, "w")
|
||||||
f.write(json.dumps(token_info, cls=self.encoder_cls))
|
f.write(json.dumps(token_info, cls=self.encoder_cls))
|
||||||
f.close()
|
f.close()
|
||||||
except IOError:
|
except OSError:
|
||||||
logger.warning('Couldn\'t write token to cache at: %s',
|
logger.warning('Couldn\'t write token to cache at: %s',
|
||||||
self.cache_path)
|
self.cache_path)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
""" A simple and thin Python library for the Spotify Web API """
|
""" A simple and thin Python library for the Spotify Web API """
|
||||||
|
|
||||||
__all__ = ["Spotify", "SpotifyException"]
|
__all__ = ["Spotify", "SpotifyException"]
|
||||||
@ -10,7 +8,6 @@ import re
|
|||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import six
|
|
||||||
import urllib3
|
import urllib3
|
||||||
|
|
||||||
from spotipy.exceptions import SpotifyException
|
from spotipy.exceptions import SpotifyException
|
||||||
@ -20,7 +17,7 @@ from collections import defaultdict
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Spotify(object):
|
class Spotify:
|
||||||
"""
|
"""
|
||||||
Example usage::
|
Example usage::
|
||||||
|
|
||||||
@ -234,14 +231,14 @@ class Spotify(object):
|
|||||||
|
|
||||||
def _auth_headers(self):
|
def _auth_headers(self):
|
||||||
if self._auth:
|
if self._auth:
|
||||||
return {"Authorization": "Bearer {0}".format(self._auth)}
|
return {"Authorization": f"Bearer {self._auth}"}
|
||||||
if not self.auth_manager:
|
if not self.auth_manager:
|
||||||
return {}
|
return {}
|
||||||
try:
|
try:
|
||||||
token = self.auth_manager.get_access_token(as_dict=False)
|
token = self.auth_manager.get_access_token(as_dict=False)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
token = self.auth_manager.get_access_token()
|
token = self.auth_manager.get_access_token()
|
||||||
return {"Authorization": "Bearer {0}".format(token)}
|
return {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
def _internal_call(self, method, url, payload, params):
|
def _internal_call(self, method, url, payload, params):
|
||||||
args = dict(params=params)
|
args = dict(params=params)
|
||||||
@ -296,7 +293,7 @@ class Spotify(object):
|
|||||||
raise SpotifyException(
|
raise SpotifyException(
|
||||||
response.status_code,
|
response.status_code,
|
||||||
-1,
|
-1,
|
||||||
"%s:\n %s" % (response.url, msg),
|
f"{response.url}:\n {msg}",
|
||||||
reason=reason,
|
reason=reason,
|
||||||
headers=response.headers,
|
headers=response.headers,
|
||||||
)
|
)
|
||||||
@ -310,7 +307,7 @@ class Spotify(object):
|
|||||||
raise SpotifyException(
|
raise SpotifyException(
|
||||||
429,
|
429,
|
||||||
-1,
|
-1,
|
||||||
"%s:\n %s" % (request.path_url, "Max Retries"),
|
f"{request.path_url}:\n Max Retries",
|
||||||
reason=reason
|
reason=reason
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -663,7 +660,7 @@ class Spotify(object):
|
|||||||
"""
|
"""
|
||||||
plid = self._get_id("playlist", playlist_id)
|
plid = self._get_id("playlist", playlist_id)
|
||||||
return self._get(
|
return self._get(
|
||||||
"playlists/%s" % (plid),
|
f"playlists/{plid}",
|
||||||
fields=fields,
|
fields=fields,
|
||||||
market=market,
|
market=market,
|
||||||
additional_types=",".join(additional_types),
|
additional_types=",".join(additional_types),
|
||||||
@ -719,7 +716,7 @@ class Spotify(object):
|
|||||||
"""
|
"""
|
||||||
plid = self._get_id("playlist", playlist_id)
|
plid = self._get_id("playlist", playlist_id)
|
||||||
return self._get(
|
return self._get(
|
||||||
"playlists/%s/tracks" % (plid),
|
f"playlists/{plid}/tracks",
|
||||||
limit=limit,
|
limit=limit,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
fields=fields,
|
fields=fields,
|
||||||
@ -734,7 +731,7 @@ class Spotify(object):
|
|||||||
- playlist_id - the playlist ID, URI or URL
|
- playlist_id - the playlist ID, URI or URL
|
||||||
"""
|
"""
|
||||||
plid = self._get_id("playlist", playlist_id)
|
plid = self._get_id("playlist", playlist_id)
|
||||||
return self._get("playlists/%s/images" % (plid))
|
return self._get(f"playlists/{plid}/images")
|
||||||
|
|
||||||
def playlist_upload_cover_image(self, playlist_id, image_b64):
|
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
|
||||||
@ -746,7 +743,7 @@ class Spotify(object):
|
|||||||
"""
|
"""
|
||||||
plid = self._get_id("playlist", playlist_id)
|
plid = self._get_id("playlist", playlist_id)
|
||||||
return self._put(
|
return self._put(
|
||||||
"playlists/{}/images".format(plid),
|
f"playlists/{plid}/images",
|
||||||
payload=image_b64,
|
payload=image_b64,
|
||||||
content_type="image/jpeg",
|
content_type="image/jpeg",
|
||||||
)
|
)
|
||||||
@ -765,7 +762,7 @@ class Spotify(object):
|
|||||||
- fields - which fields to return
|
- fields - which fields to return
|
||||||
"""
|
"""
|
||||||
if playlist_id is None:
|
if playlist_id is None:
|
||||||
return self._get("users/%s/starred" % user)
|
return self._get(f"users/{user}/starred")
|
||||||
return self.playlist(playlist_id, fields=fields, market=market)
|
return self.playlist(playlist_id, fields=fields, market=market)
|
||||||
|
|
||||||
def user_playlist_tracks(
|
def user_playlist_tracks(
|
||||||
@ -809,7 +806,7 @@ class Spotify(object):
|
|||||||
- offset - the index of the first item to return
|
- offset - the index of the first item to return
|
||||||
"""
|
"""
|
||||||
return self._get(
|
return self._get(
|
||||||
"users/%s/playlists" % user, limit=limit, offset=offset
|
f"users/{user}/playlists", limit=limit, offset=offset
|
||||||
)
|
)
|
||||||
|
|
||||||
def user_playlist_create(self, user, name, public=True, collaborative=False, description=""):
|
def user_playlist_create(self, user, name, public=True, collaborative=False, description=""):
|
||||||
@ -829,7 +826,7 @@ class Spotify(object):
|
|||||||
"description": description
|
"description": description
|
||||||
}
|
}
|
||||||
|
|
||||||
return self._post("users/%s/playlists" % (user,), payload=data)
|
return self._post(f"users/{user}/playlists", payload=data)
|
||||||
|
|
||||||
def user_playlist_change_details(
|
def user_playlist_change_details(
|
||||||
self,
|
self,
|
||||||
@ -1004,7 +1001,7 @@ class Spotify(object):
|
|||||||
if snapshot_id:
|
if snapshot_id:
|
||||||
payload["snapshot_id"] = snapshot_id
|
payload["snapshot_id"] = snapshot_id
|
||||||
return self._delete(
|
return self._delete(
|
||||||
"users/%s/playlists/%s/tracks" % (user, plid), payload=payload
|
f"users/{user}/playlists/{plid}/tracks", payload=payload
|
||||||
)
|
)
|
||||||
|
|
||||||
def user_playlist_follow_playlist(self, playlist_owner_id, playlist_id):
|
def user_playlist_follow_playlist(self, playlist_owner_id, playlist_id):
|
||||||
@ -1061,16 +1058,16 @@ class Spotify(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
if isinstance(name, six.string_types):
|
if isinstance(name, str):
|
||||||
data["name"] = name
|
data["name"] = name
|
||||||
if isinstance(public, bool):
|
if isinstance(public, bool):
|
||||||
data["public"] = public
|
data["public"] = public
|
||||||
if isinstance(collaborative, bool):
|
if isinstance(collaborative, bool):
|
||||||
data["collaborative"] = collaborative
|
data["collaborative"] = collaborative
|
||||||
if isinstance(description, six.string_types):
|
if isinstance(description, str):
|
||||||
data["description"] = description
|
data["description"] = description
|
||||||
return self._put(
|
return self._put(
|
||||||
"playlists/%s" % (self._get_id("playlist", playlist_id)), payload=data
|
f"playlists/{self._get_id('playlist', playlist_id)}", payload=data
|
||||||
)
|
)
|
||||||
|
|
||||||
def current_user_unfollow_playlist(self, playlist_id):
|
def current_user_unfollow_playlist(self, playlist_id):
|
||||||
@ -1081,7 +1078,7 @@ class Spotify(object):
|
|||||||
- name - the name of the playlist
|
- name - the name of the playlist
|
||||||
"""
|
"""
|
||||||
return self._delete(
|
return self._delete(
|
||||||
"playlists/%s/followers" % (playlist_id)
|
f"playlists/{playlist_id}/followers"
|
||||||
)
|
)
|
||||||
|
|
||||||
def playlist_add_items(
|
def playlist_add_items(
|
||||||
@ -1097,7 +1094,7 @@ class Spotify(object):
|
|||||||
plid = self._get_id("playlist", playlist_id)
|
plid = self._get_id("playlist", playlist_id)
|
||||||
ftracks = [self._get_uri("track", tid) for tid in items]
|
ftracks = [self._get_uri("track", tid) for tid in items]
|
||||||
return self._post(
|
return self._post(
|
||||||
"playlists/%s/tracks" % (plid),
|
f"playlists/{plid}/tracks",
|
||||||
payload=ftracks,
|
payload=ftracks,
|
||||||
position=position,
|
position=position,
|
||||||
)
|
)
|
||||||
@ -1113,7 +1110,7 @@ class Spotify(object):
|
|||||||
ftracks = [self._get_uri("track", tid) for tid in items]
|
ftracks = [self._get_uri("track", tid) for tid in items]
|
||||||
payload = {"uris": ftracks}
|
payload = {"uris": ftracks}
|
||||||
return self._put(
|
return self._put(
|
||||||
"playlists/%s/tracks" % (plid), payload=payload
|
f"playlists/{plid}/tracks", payload=payload
|
||||||
)
|
)
|
||||||
|
|
||||||
def playlist_reorder_items(
|
def playlist_reorder_items(
|
||||||
@ -1144,7 +1141,7 @@ class Spotify(object):
|
|||||||
if snapshot_id:
|
if snapshot_id:
|
||||||
payload["snapshot_id"] = snapshot_id
|
payload["snapshot_id"] = snapshot_id
|
||||||
return self._put(
|
return self._put(
|
||||||
"playlists/%s/tracks" % (plid), payload=payload
|
f"playlists/{plid}/tracks", payload=payload
|
||||||
)
|
)
|
||||||
|
|
||||||
def playlist_remove_all_occurrences_of_items(
|
def playlist_remove_all_occurrences_of_items(
|
||||||
@ -1165,7 +1162,7 @@ class Spotify(object):
|
|||||||
if snapshot_id:
|
if snapshot_id:
|
||||||
payload["snapshot_id"] = snapshot_id
|
payload["snapshot_id"] = snapshot_id
|
||||||
return self._delete(
|
return self._delete(
|
||||||
"playlists/%s/tracks" % (plid), payload=payload
|
f"playlists/{plid}/tracks", payload=payload
|
||||||
)
|
)
|
||||||
|
|
||||||
def playlist_remove_specific_occurrences_of_items(
|
def playlist_remove_specific_occurrences_of_items(
|
||||||
@ -1196,7 +1193,7 @@ class Spotify(object):
|
|||||||
if snapshot_id:
|
if snapshot_id:
|
||||||
payload["snapshot_id"] = snapshot_id
|
payload["snapshot_id"] = snapshot_id
|
||||||
return self._delete(
|
return self._delete(
|
||||||
"playlists/%s/tracks" % (plid), payload=payload
|
f"playlists/{plid}/tracks", payload=payload
|
||||||
)
|
)
|
||||||
|
|
||||||
def current_user_follow_playlist(self, playlist_id):
|
def current_user_follow_playlist(self, playlist_id):
|
||||||
@ -1208,7 +1205,7 @@ class Spotify(object):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
return self._put(
|
return self._put(
|
||||||
"playlists/{}/followers".format(playlist_id)
|
f"playlists/{playlist_id}/followers"
|
||||||
)
|
)
|
||||||
|
|
||||||
def playlist_is_following(
|
def playlist_is_following(
|
||||||
@ -1874,7 +1871,7 @@ class Spotify(object):
|
|||||||
return
|
return
|
||||||
return self._put(
|
return self._put(
|
||||||
self._append_device_id(
|
self._append_device_id(
|
||||||
"me/player/seek?position_ms=%s" % position_ms, device_id
|
f"me/player/seek?position_ms={position_ms}", device_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1890,7 +1887,7 @@ class Spotify(object):
|
|||||||
return
|
return
|
||||||
self._put(
|
self._put(
|
||||||
self._append_device_id(
|
self._append_device_id(
|
||||||
"me/player/repeat?state=%s" % state, device_id
|
f"me/player/repeat?state={state}", device_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1909,7 +1906,7 @@ class Spotify(object):
|
|||||||
return
|
return
|
||||||
self._put(
|
self._put(
|
||||||
self._append_device_id(
|
self._append_device_id(
|
||||||
"me/player/volume?volume_percent=%s" % volume_percent,
|
f"me/player/volume?volume_percent={volume_percent}",
|
||||||
device_id,
|
device_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -1927,7 +1924,7 @@ class Spotify(object):
|
|||||||
state = str(state).lower()
|
state = str(state).lower()
|
||||||
self._put(
|
self._put(
|
||||||
self._append_device_id(
|
self._append_device_id(
|
||||||
"me/player/shuffle?state=%s" % state, device_id
|
f"me/player/shuffle?state={state}", device_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1952,10 +1949,10 @@ class Spotify(object):
|
|||||||
|
|
||||||
uri = self._get_uri("track", uri)
|
uri = self._get_uri("track", uri)
|
||||||
|
|
||||||
endpoint = "me/player/queue?uri=%s" % uri
|
endpoint = f"me/player/queue?uri={uri}"
|
||||||
|
|
||||||
if device_id is not None:
|
if device_id is not None:
|
||||||
endpoint += "&device_id=%s" % device_id
|
endpoint += f"&device_id={device_id}"
|
||||||
|
|
||||||
return self._post(endpoint)
|
return self._post(endpoint)
|
||||||
|
|
||||||
@ -1974,9 +1971,9 @@ class Spotify(object):
|
|||||||
"""
|
"""
|
||||||
if device_id:
|
if device_id:
|
||||||
if "?" in path:
|
if "?" in path:
|
||||||
path += "&device_id=%s" % device_id
|
path += f"&device_id={device_id}"
|
||||||
else:
|
else:
|
||||||
path += "?device_id=%s" % device_id
|
path += f"?device_id={device_id}"
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def _get_id(self, type, id):
|
def _get_id(self, type, id):
|
||||||
|
|||||||
@ -12,5 +12,5 @@ class SpotifyException(Exception):
|
|||||||
self.headers = headers
|
self.headers = headers
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return 'http status: {0}, code:{1} - {2}, reason: {3}'.format(
|
return 'http status: {}, code:{} - {}, reason: {}'.format(
|
||||||
self.http_status, self.code, self.msg, self.reason)
|
self.http_status, self.code, self.msg, self.reason)
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"SpotifyClientCredentials",
|
"SpotifyClientCredentials",
|
||||||
"SpotifyOAuth",
|
"SpotifyOAuth",
|
||||||
@ -17,11 +15,9 @@ import warnings
|
|||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
# Workaround to support both python 2 & 3
|
import urllib.parse as urllibparse
|
||||||
import six
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
import six.moves.urllib.parse as urllibparse
|
from urllib.parse import parse_qsl, urlparse
|
||||||
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.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, normalize_scope
|
||||||
@ -36,7 +32,7 @@ class SpotifyOauthError(Exception):
|
|||||||
self.error = error
|
self.error = error
|
||||||
self.error_description = error_description
|
self.error_description = error_description
|
||||||
self.__dict__.update(kwargs)
|
self.__dict__.update(kwargs)
|
||||||
super(SpotifyOauthError, self).__init__(message, *args, **kwargs)
|
super().__init__(message, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class SpotifyStateError(SpotifyOauthError):
|
class SpotifyStateError(SpotifyOauthError):
|
||||||
@ -54,24 +50,21 @@ class SpotifyStateError(SpotifyOauthError):
|
|||||||
|
|
||||||
def _make_authorization_headers(client_id, client_secret):
|
def _make_authorization_headers(client_id, client_secret):
|
||||||
auth_header = base64.b64encode(
|
auth_header = base64.b64encode(
|
||||||
six.text_type(client_id + ":" + client_secret).encode("ascii")
|
str(client_id + ":" + client_secret).encode("ascii")
|
||||||
)
|
)
|
||||||
return {"Authorization": "Basic %s" % auth_header.decode("ascii")}
|
return {"Authorization": f"Basic {auth_header.decode('ascii')}"}
|
||||||
|
|
||||||
|
|
||||||
def _ensure_value(value, env_key):
|
def _ensure_value(value, env_key):
|
||||||
env_val = CLIENT_CREDS_ENV_VARS[env_key]
|
env_val = CLIENT_CREDS_ENV_VARS[env_key]
|
||||||
_val = value or os.getenv(env_val)
|
_val = value or os.getenv(env_val)
|
||||||
if _val is None:
|
if _val is None:
|
||||||
msg = "No %s. Pass it or set a %s environment variable." % (
|
msg = f"No {env_key}. Pass it or set a {env_val} environment variable."
|
||||||
env_key,
|
|
||||||
env_val,
|
|
||||||
)
|
|
||||||
raise SpotifyOauthError(msg)
|
raise SpotifyOauthError(msg)
|
||||||
return _val
|
return _val
|
||||||
|
|
||||||
|
|
||||||
class SpotifyAuthBase(object):
|
class SpotifyAuthBase:
|
||||||
def __init__(self, requests_session):
|
def __init__(self, requests_session):
|
||||||
if isinstance(requests_session, requests.Session):
|
if isinstance(requests_session, requests.Session):
|
||||||
self._session = requests_session
|
self._session = requests_session
|
||||||
@ -144,9 +137,7 @@ class SpotifyAuthBase(object):
|
|||||||
error_description = None
|
error_description = None
|
||||||
|
|
||||||
raise SpotifyOauthError(
|
raise SpotifyOauthError(
|
||||||
'error: {0}, error_description: {1}'.format(
|
f'error: {error}, error_description: {error_description}',
|
||||||
error, error_description
|
|
||||||
),
|
|
||||||
error=error,
|
error=error,
|
||||||
error_description=error_description
|
error_description=error_description
|
||||||
)
|
)
|
||||||
@ -196,7 +187,7 @@ class SpotifyClientCredentials(SpotifyAuthBase):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super(SpotifyClientCredentials, self).__init__(requests_session)
|
super().__init__(requests_session)
|
||||||
|
|
||||||
self.client_id = client_id
|
self.client_id = client_id
|
||||||
self.client_secret = client_secret
|
self.client_secret = client_secret
|
||||||
@ -327,7 +318,7 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
(takes precedence over `cache_path` and `username`)
|
(takes precedence over `cache_path` and `username`)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super(SpotifyOAuth, self).__init__(requests_session)
|
super().__init__(requests_session)
|
||||||
|
|
||||||
self.client_id = client_id
|
self.client_id = client_id
|
||||||
self.client_secret = client_secret
|
self.client_secret = client_secret
|
||||||
@ -402,7 +393,7 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
|
|
||||||
urlparams = urllibparse.urlencode(payload)
|
urlparams = urllibparse.urlencode(payload)
|
||||||
|
|
||||||
return "%s?%s" % (self.OAUTH_AUTHORIZE_URL, urlparams)
|
return f"{self.OAUTH_AUTHORIZE_URL}?{urlparams}"
|
||||||
|
|
||||||
def parse_response_code(self, url):
|
def parse_response_code(self, url):
|
||||||
""" Parse the response code in the given response url
|
""" Parse the response code in the given response url
|
||||||
@ -421,8 +412,7 @@ class SpotifyOAuth(SpotifyAuthBase):
|
|||||||
query_s = urlparse(url).query
|
query_s = urlparse(url).query
|
||||||
form = dict(parse_qsl(query_s))
|
form = dict(parse_qsl(query_s))
|
||||||
if "error" in form:
|
if "error" in form:
|
||||||
raise SpotifyOauthError("Received error from auth server: "
|
raise SpotifyOauthError(f"Received error from auth server: {form['error']}",
|
||||||
"{}".format(form["error"]),
|
|
||||||
error=form["error"])
|
error=form["error"])
|
||||||
return tuple(form.get(param) for param in ["state", "code"])
|
return tuple(form.get(param) for param in ["state", "code"])
|
||||||
|
|
||||||
@ -677,7 +667,7 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
(takes precedence over `cache_path` and `username`)
|
(takes precedence over `cache_path` and `username`)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
super(SpotifyPKCE, self).__init__(requests_session)
|
super().__init__(requests_session)
|
||||||
self.client_id = client_id
|
self.client_id = client_id
|
||||||
self.redirect_uri = redirect_uri
|
self.redirect_uri = redirect_uri
|
||||||
self.state = state
|
self.state = state
|
||||||
@ -727,15 +717,8 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
length = random.randint(33, 96)
|
length = random.randint(33, 96)
|
||||||
|
|
||||||
# The seeded length generates between a 44 and 128 base64 characters encoded string
|
# The seeded length generates between a 44 and 128 base64 characters encoded string
|
||||||
try:
|
import secrets
|
||||||
import secrets
|
return secrets.token_urlsafe(length)
|
||||||
verifier = secrets.token_urlsafe(length)
|
|
||||||
except ImportError: # For python 3.5 support
|
|
||||||
import base64
|
|
||||||
import os
|
|
||||||
rand_bytes = os.urandom(length)
|
|
||||||
verifier = base64.urlsafe_b64encode(rand_bytes).decode('utf-8').replace('=', '')
|
|
||||||
return verifier
|
|
||||||
|
|
||||||
def _get_code_challenge(self):
|
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
|
||||||
@ -766,7 +749,7 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
if state is not None:
|
if state is not None:
|
||||||
payload["state"] = state
|
payload["state"] = state
|
||||||
urlparams = urllibparse.urlencode(payload)
|
urlparams = urllibparse.urlencode(payload)
|
||||||
return "%s?%s" % (self.OAUTH_AUTHORIZE_URL, urlparams)
|
return f"{self.OAUTH_AUTHORIZE_URL}?{urlparams}"
|
||||||
|
|
||||||
def _open_auth_url(self, state=None):
|
def _open_auth_url(self, state=None):
|
||||||
auth_url = self.get_authorize_url(state)
|
auth_url = self.get_authorize_url(state)
|
||||||
@ -817,7 +800,7 @@ class SpotifyPKCE(SpotifyAuthBase):
|
|||||||
if server.auth_code is not None:
|
if server.auth_code is not None:
|
||||||
return server.auth_code
|
return server.auth_code
|
||||||
elif server.error is not None:
|
elif server.error is not None:
|
||||||
raise SpotifyOauthError("Received error from OAuth server: {}".format(server.error))
|
raise SpotifyOauthError(f"Received error from OAuth server: {server.error}")
|
||||||
else:
|
else:
|
||||||
raise SpotifyOauthError("Server listening on localhost has not been accessed")
|
raise SpotifyOauthError("Server listening on localhost has not been accessed")
|
||||||
|
|
||||||
@ -1160,7 +1143,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
|
|||||||
|
|
||||||
urlparams = urllibparse.urlencode(payload)
|
urlparams = urllibparse.urlencode(payload)
|
||||||
|
|
||||||
return "%s?%s" % (self.OAUTH_AUTHORIZE_URL, urlparams)
|
return f"{self.OAUTH_AUTHORIZE_URL}?{urlparams}"
|
||||||
|
|
||||||
def parse_response_token(self, url, state=None):
|
def parse_response_token(self, url, state=None):
|
||||||
""" Parse the response code in the given response url """
|
""" Parse the response code in the given response url """
|
||||||
@ -1180,8 +1163,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
|
|||||||
form = dict(i.split('=') for i
|
form = dict(i.split('=') for i
|
||||||
in (fragment_s or query_s or url).split('&'))
|
in (fragment_s or query_s or url).split('&'))
|
||||||
if "error" in form:
|
if "error" in form:
|
||||||
raise SpotifyOauthError("Received error from auth server: "
|
raise SpotifyOauthError(f"Received error from auth server: {form['error']}",
|
||||||
"{}".format(form["error"]),
|
|
||||||
state=form["state"])
|
state=form["state"])
|
||||||
if "expires_in" in form:
|
if "expires_in" in form:
|
||||||
form["expires_in"] = int(form["expires_in"])
|
form["expires_in"] = int(form["expires_in"])
|
||||||
@ -1273,7 +1255,7 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|||||||
if self.server.auth_code:
|
if self.server.auth_code:
|
||||||
status = "successful"
|
status = "successful"
|
||||||
elif self.server.error:
|
elif self.server.error:
|
||||||
status = "failed ({})".format(self.server.error)
|
status = f"failed ({self.server.error})"
|
||||||
else:
|
else:
|
||||||
self._write("<html><body><h1>Invalid request</h1></body></html>")
|
self._write("<html><body><h1>Invalid request</h1></body></html>")
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
""" Shows a user's playlists (need to be authenticated via oauth) """
|
""" Shows a user's playlists (need to be authenticated via oauth) """
|
||||||
|
|
||||||
__all__ = ["CLIENT_CREDS_ENV_VARS", "prompt_for_user_token"]
|
__all__ = ["CLIENT_CREDS_ENV_VARS", "prompt_for_user_token"]
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from spotipy import (
|
from spotipy import (
|
||||||
Spotify,
|
Spotify,
|
||||||
SpotifyClientCredentials,
|
SpotifyClientCredentials,
|
||||||
|
|||||||
@ -1,20 +1,15 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import six.moves.urllib.parse as urllibparse
|
import unittest.mock as mock
|
||||||
|
import urllib.parse as urllibparse
|
||||||
|
|
||||||
from spotipy import SpotifyOAuth, SpotifyImplicitGrant, SpotifyPKCE
|
from spotipy import SpotifyOAuth, SpotifyImplicitGrant, SpotifyPKCE
|
||||||
from spotipy.cache_handler import MemoryCacheHandler
|
from spotipy.cache_handler import MemoryCacheHandler
|
||||||
from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOauthError
|
from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOauthError
|
||||||
from spotipy.oauth2 import SpotifyStateError
|
from spotipy.oauth2 import SpotifyStateError
|
||||||
|
|
||||||
try:
|
|
||||||
import unittest.mock as mock
|
|
||||||
except ImportError:
|
|
||||||
import mock
|
|
||||||
|
|
||||||
patch = mock.patch
|
patch = mock.patch
|
||||||
DEFAULT = mock.DEFAULT
|
DEFAULT = mock.DEFAULT
|
||||||
|
|
||||||
|
|||||||
4
tox.ini
4
tox.ini
@ -1,10 +1,8 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = py27,py34
|
envlist = py3{8,9,10,11,12}
|
||||||
[testenv]
|
[testenv]
|
||||||
deps=
|
deps=
|
||||||
requests
|
requests
|
||||||
six
|
|
||||||
py27: mock
|
|
||||||
commands=python -m unittest discover -v tests
|
commands=python -m unittest discover -v tests
|
||||||
[flake8]
|
[flake8]
|
||||||
max-line-length = 99
|
max-line-length = 99
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user