mirror of
https://github.com/spotipy-dev/spotipy.git
synced 2026-10-12 01:10:45 +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:
1 parent
958ff6ad2b
commit
85c9d74dc1
29 files changed
+85
-125
No files matched your search
@@ -80,7 +80,7 @@ class CacheFileHandler(CacheHandler):
|
||||
f.close()
|
||||
token_info = json.loads(token_info_string)
|
||||
|
||||
except IOError as error:
|
||||
except OSError as error:
|
||||
if error.errno == errno.ENOENT:
|
||||
logger.debug("cache does not exist at: %s", self.cache_path)
|
||||
else:
|
||||
@@ -93,7 +93,7 @@ class CacheFileHandler(CacheHandler):
|
||||
f = open(self.cache_path, "w")
|
||||
f.write(json.dumps(token_info, cls=self.encoder_cls))
|
||||
f.close()
|
||||
except IOError:
|
||||
except OSError:
|
||||
logger.warning('Couldn\'t write token to cache at: %s',
|
||||
self.cache_path)
|
||||
|
||||
|
||||
+31
-34
@@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" A simple and thin Python library for the Spotify Web API """
|
||||
|
||||
__all__ = ["Spotify", "SpotifyException"]
|
||||
@@ -10,7 +8,6 @@ import re
|
||||
import warnings
|
||||
|
||||
import requests
|
||||
import six
|
||||
import urllib3
|
||||
|
||||
from spotipy.exceptions import SpotifyException
|
||||
@@ -20,7 +17,7 @@ from collections import defaultdict
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Spotify(object):
|
||||
class Spotify:
|
||||
"""
|
||||
Example usage::
|
||||
|
||||
@@ -234,14 +231,14 @@ class Spotify(object):
|
||||
|
||||
def _auth_headers(self):
|
||||
if self._auth:
|
||||
return {"Authorization": "Bearer {0}".format(self._auth)}
|
||||
return {"Authorization": f"Bearer {self._auth}"}
|
||||
if not self.auth_manager:
|
||||
return {}
|
||||
try:
|
||||
token = self.auth_manager.get_access_token(as_dict=False)
|
||||
except TypeError:
|
||||
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):
|
||||
args = dict(params=params)
|
||||
@@ -296,7 +293,7 @@ class Spotify(object):
|
||||
raise SpotifyException(
|
||||
response.status_code,
|
||||
-1,
|
||||
"%s:\n %s" % (response.url, msg),
|
||||
f"{response.url}:\n {msg}",
|
||||
reason=reason,
|
||||
headers=response.headers,
|
||||
)
|
||||
@@ -310,7 +307,7 @@ class Spotify(object):
|
||||
raise SpotifyException(
|
||||
429,
|
||||
-1,
|
||||
"%s:\n %s" % (request.path_url, "Max Retries"),
|
||||
f"{request.path_url}:\n Max Retries",
|
||||
reason=reason
|
||||
)
|
||||
except ValueError:
|
||||
@@ -663,7 +660,7 @@ class Spotify(object):
|
||||
"""
|
||||
plid = self._get_id("playlist", playlist_id)
|
||||
return self._get(
|
||||
"playlists/%s" % (plid),
|
||||
f"playlists/{plid}",
|
||||
fields=fields,
|
||||
market=market,
|
||||
additional_types=",".join(additional_types),
|
||||
@@ -719,7 +716,7 @@ class Spotify(object):
|
||||
"""
|
||||
plid = self._get_id("playlist", playlist_id)
|
||||
return self._get(
|
||||
"playlists/%s/tracks" % (plid),
|
||||
f"playlists/{plid}/tracks",
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
fields=fields,
|
||||
@@ -734,7 +731,7 @@ class Spotify(object):
|
||||
- playlist_id - the playlist ID, URI or URL
|
||||
"""
|
||||
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):
|
||||
""" Replace the image used to represent a specific playlist
|
||||
@@ -746,7 +743,7 @@ class Spotify(object):
|
||||
"""
|
||||
plid = self._get_id("playlist", playlist_id)
|
||||
return self._put(
|
||||
"playlists/{}/images".format(plid),
|
||||
f"playlists/{plid}/images",
|
||||
payload=image_b64,
|
||||
content_type="image/jpeg",
|
||||
)
|
||||
@@ -765,7 +762,7 @@ class Spotify(object):
|
||||
- fields - which fields to return
|
||||
"""
|
||||
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)
|
||||
|
||||
def user_playlist_tracks(
|
||||
@@ -809,7 +806,7 @@ class Spotify(object):
|
||||
- offset - the index of the first item to return
|
||||
"""
|
||||
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=""):
|
||||
@@ -829,7 +826,7 @@ class Spotify(object):
|
||||
"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(
|
||||
self,
|
||||
@@ -1004,7 +1001,7 @@ class Spotify(object):
|
||||
if snapshot_id:
|
||||
payload["snapshot_id"] = snapshot_id
|
||||
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):
|
||||
@@ -1061,16 +1058,16 @@ class Spotify(object):
|
||||
"""
|
||||
|
||||
data = {}
|
||||
if isinstance(name, six.string_types):
|
||||
if isinstance(name, str):
|
||||
data["name"] = name
|
||||
if isinstance(public, bool):
|
||||
data["public"] = public
|
||||
if isinstance(collaborative, bool):
|
||||
data["collaborative"] = collaborative
|
||||
if isinstance(description, six.string_types):
|
||||
if isinstance(description, str):
|
||||
data["description"] = description
|
||||
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):
|
||||
@@ -1081,7 +1078,7 @@ class Spotify(object):
|
||||
- name - the name of the playlist
|
||||
"""
|
||||
return self._delete(
|
||||
"playlists/%s/followers" % (playlist_id)
|
||||
f"playlists/{playlist_id}/followers"
|
||||
)
|
||||
|
||||
def playlist_add_items(
|
||||
@@ -1097,7 +1094,7 @@ class Spotify(object):
|
||||
plid = self._get_id("playlist", playlist_id)
|
||||
ftracks = [self._get_uri("track", tid) for tid in items]
|
||||
return self._post(
|
||||
"playlists/%s/tracks" % (plid),
|
||||
f"playlists/{plid}/tracks",
|
||||
payload=ftracks,
|
||||
position=position,
|
||||
)
|
||||
@@ -1113,7 +1110,7 @@ class Spotify(object):
|
||||
ftracks = [self._get_uri("track", tid) for tid in items]
|
||||
payload = {"uris": ftracks}
|
||||
return self._put(
|
||||
"playlists/%s/tracks" % (plid), payload=payload
|
||||
f"playlists/{plid}/tracks", payload=payload
|
||||
)
|
||||
|
||||
def playlist_reorder_items(
|
||||
@@ -1144,7 +1141,7 @@ class Spotify(object):
|
||||
if snapshot_id:
|
||||
payload["snapshot_id"] = snapshot_id
|
||||
return self._put(
|
||||
"playlists/%s/tracks" % (plid), payload=payload
|
||||
f"playlists/{plid}/tracks", payload=payload
|
||||
)
|
||||
|
||||
def playlist_remove_all_occurrences_of_items(
|
||||
@@ -1165,7 +1162,7 @@ class Spotify(object):
|
||||
if snapshot_id:
|
||||
payload["snapshot_id"] = snapshot_id
|
||||
return self._delete(
|
||||
"playlists/%s/tracks" % (plid), payload=payload
|
||||
f"playlists/{plid}/tracks", payload=payload
|
||||
)
|
||||
|
||||
def playlist_remove_specific_occurrences_of_items(
|
||||
@@ -1196,7 +1193,7 @@ class Spotify(object):
|
||||
if snapshot_id:
|
||||
payload["snapshot_id"] = snapshot_id
|
||||
return self._delete(
|
||||
"playlists/%s/tracks" % (plid), payload=payload
|
||||
f"playlists/{plid}/tracks", payload=payload
|
||||
)
|
||||
|
||||
def current_user_follow_playlist(self, playlist_id):
|
||||
@@ -1208,7 +1205,7 @@ class Spotify(object):
|
||||
|
||||
"""
|
||||
return self._put(
|
||||
"playlists/{}/followers".format(playlist_id)
|
||||
f"playlists/{playlist_id}/followers"
|
||||
)
|
||||
|
||||
def playlist_is_following(
|
||||
@@ -1874,7 +1871,7 @@ class Spotify(object):
|
||||
return
|
||||
return self._put(
|
||||
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
|
||||
self._put(
|
||||
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
|
||||
self._put(
|
||||
self._append_device_id(
|
||||
"me/player/volume?volume_percent=%s" % volume_percent,
|
||||
f"me/player/volume?volume_percent={volume_percent}",
|
||||
device_id,
|
||||
)
|
||||
)
|
||||
@@ -1927,7 +1924,7 @@ class Spotify(object):
|
||||
state = str(state).lower()
|
||||
self._put(
|
||||
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)
|
||||
|
||||
endpoint = "me/player/queue?uri=%s" % uri
|
||||
endpoint = f"me/player/queue?uri={uri}"
|
||||
|
||||
if device_id is not None:
|
||||
endpoint += "&device_id=%s" % device_id
|
||||
endpoint += f"&device_id={device_id}"
|
||||
|
||||
return self._post(endpoint)
|
||||
|
||||
@@ -1974,9 +1971,9 @@ class Spotify(object):
|
||||
"""
|
||||
if device_id:
|
||||
if "?" in path:
|
||||
path += "&device_id=%s" % device_id
|
||||
path += f"&device_id={device_id}"
|
||||
else:
|
||||
path += "?device_id=%s" % device_id
|
||||
path += f"?device_id={device_id}"
|
||||
return path
|
||||
|
||||
def _get_id(self, type, id):
|
||||
|
||||
@@ -12,5 +12,5 @@ class SpotifyException(Exception):
|
||||
self.headers = headers
|
||||
|
||||
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)
|
||||
+21
-39
@@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__all__ = [
|
||||
"SpotifyClientCredentials",
|
||||
"SpotifyOAuth",
|
||||
@@ -17,11 +15,9 @@ import warnings
|
||||
import webbrowser
|
||||
|
||||
import requests
|
||||
# Workaround to support both python 2 & 3
|
||||
import six
|
||||
import six.moves.urllib.parse as urllibparse
|
||||
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
from six.moves.urllib_parse import parse_qsl, urlparse
|
||||
import urllib.parse as urllibparse
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from 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
|
||||
@@ -36,7 +32,7 @@ class SpotifyOauthError(Exception):
|
||||
self.error = error
|
||||
self.error_description = error_description
|
||||
self.__dict__.update(kwargs)
|
||||
super(SpotifyOauthError, self).__init__(message, *args, **kwargs)
|
||||
super().__init__(message, *args, **kwargs)
|
||||
|
||||
|
||||
class SpotifyStateError(SpotifyOauthError):
|
||||
@@ -54,24 +50,21 @@ class SpotifyStateError(SpotifyOauthError):
|
||||
|
||||
def _make_authorization_headers(client_id, client_secret):
|
||||
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):
|
||||
env_val = CLIENT_CREDS_ENV_VARS[env_key]
|
||||
_val = value or os.getenv(env_val)
|
||||
if _val is None:
|
||||
msg = "No %s. Pass it or set a %s environment variable." % (
|
||||
env_key,
|
||||
env_val,
|
||||
)
|
||||
msg = f"No {env_key}. Pass it or set a {env_val} environment variable."
|
||||
raise SpotifyOauthError(msg)
|
||||
return _val
|
||||
|
||||
|
||||
class SpotifyAuthBase(object):
|
||||
class SpotifyAuthBase:
|
||||
def __init__(self, requests_session):
|
||||
if isinstance(requests_session, requests.Session):
|
||||
self._session = requests_session
|
||||
@@ -144,9 +137,7 @@ class SpotifyAuthBase(object):
|
||||
error_description = None
|
||||
|
||||
raise SpotifyOauthError(
|
||||
'error: {0}, error_description: {1}'.format(
|
||||
error, error_description
|
||||
),
|
||||
f'error: {error}, error_description: {error_description}',
|
||||
error=error,
|
||||
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_secret = client_secret
|
||||
@@ -327,7 +318,7 @@ class SpotifyOAuth(SpotifyAuthBase):
|
||||
(takes precedence over `cache_path` and `username`)
|
||||
"""
|
||||
|
||||
super(SpotifyOAuth, self).__init__(requests_session)
|
||||
super().__init__(requests_session)
|
||||
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
@@ -402,7 +393,7 @@ class SpotifyOAuth(SpotifyAuthBase):
|
||||
|
||||
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):
|
||||
""" Parse the response code in the given response url
|
||||
@@ -421,8 +412,7 @@ class SpotifyOAuth(SpotifyAuthBase):
|
||||
query_s = urlparse(url).query
|
||||
form = dict(parse_qsl(query_s))
|
||||
if "error" in form:
|
||||
raise SpotifyOauthError("Received error from auth server: "
|
||||
"{}".format(form["error"]),
|
||||
raise SpotifyOauthError(f"Received error from auth server: {form['error']}",
|
||||
error=form["error"])
|
||||
return tuple(form.get(param) for param in ["state", "code"])
|
||||
|
||||
@@ -677,7 +667,7 @@ class SpotifyPKCE(SpotifyAuthBase):
|
||||
(takes precedence over `cache_path` and `username`)
|
||||
"""
|
||||
|
||||
super(SpotifyPKCE, self).__init__(requests_session)
|
||||
super().__init__(requests_session)
|
||||
self.client_id = client_id
|
||||
self.redirect_uri = redirect_uri
|
||||
self.state = state
|
||||
@@ -727,15 +717,8 @@ class SpotifyPKCE(SpotifyAuthBase):
|
||||
length = random.randint(33, 96)
|
||||
|
||||
# The seeded length generates between a 44 and 128 base64 characters encoded string
|
||||
try:
|
||||
import secrets
|
||||
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
|
||||
import secrets
|
||||
return secrets.token_urlsafe(length)
|
||||
|
||||
def _get_code_challenge(self):
|
||||
""" Spotify PCKE code challenge - See step 1 of the reference guide below
|
||||
@@ -766,7 +749,7 @@ class SpotifyPKCE(SpotifyAuthBase):
|
||||
if state is not None:
|
||||
payload["state"] = state
|
||||
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):
|
||||
auth_url = self.get_authorize_url(state)
|
||||
@@ -817,7 +800,7 @@ class SpotifyPKCE(SpotifyAuthBase):
|
||||
if server.auth_code is not None:
|
||||
return server.auth_code
|
||||
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:
|
||||
raise SpotifyOauthError("Server listening on localhost has not been accessed")
|
||||
|
||||
@@ -1160,7 +1143,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
|
||||
|
||||
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):
|
||||
""" Parse the response code in the given response url """
|
||||
@@ -1180,8 +1163,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
|
||||
form = dict(i.split('=') for i
|
||||
in (fragment_s or query_s or url).split('&'))
|
||||
if "error" in form:
|
||||
raise SpotifyOauthError("Received error from auth server: "
|
||||
"{}".format(form["error"]),
|
||||
raise SpotifyOauthError(f"Received error from auth server: {form['error']}",
|
||||
state=form["state"])
|
||||
if "expires_in" in form:
|
||||
form["expires_in"] = int(form["expires_in"])
|
||||
@@ -1273,7 +1255,7 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
if self.server.auth_code:
|
||||
status = "successful"
|
||||
elif self.server.error:
|
||||
status = "failed ({})".format(self.server.error)
|
||||
status = f"failed ({self.server.error})"
|
||||
else:
|
||||
self._write("<html><body><h1>Invalid request</h1></body></html>")
|
||||
return
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Shows a user's playlists (need to be authenticated via oauth) """
|
||||
|
||||
__all__ = ["CLIENT_CREDS_ENV_VARS", "prompt_for_user_token"]
|
||||
|
||||
Reference in new issue
Block a user