This commit is contained in:
Blake V. 2026-02-16 20:30:38 +01:00 committed by GitHub
commit 56e494cb82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 491 additions and 329 deletions

View File

@ -10,7 +10,7 @@ jobs:
SPOTIPY_CLIENT_SECRET: ${{ secrets.SPOTIPY_CLIENT_SECRET }}
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}

View File

@ -11,6 +11,8 @@ Add your changes below.
### Added
- Adds type hints to all function args
### Fixed
### Removed

View File

@ -31,7 +31,7 @@ setup(
project_urls={
'Source': 'https://github.com/plamere/spotipy',
},
python_requires='>3.8',
python_requires='>=3.10',
install_requires=[
"redis>=3.5.3", # TODO: Move to extras_require in v3
"requests>=2.25.0",

View File

@ -11,6 +11,8 @@ import errno
import json
import logging
import os
from json import JSONEncoder
from typing import Dict
from redis import RedisError
@ -36,7 +38,7 @@ class CacheHandler():
# return token_info
raise NotImplementedError()
def save_token_to_cache(self, token_info):
def save_token_to_cache(self, token_info: Dict):
"""
Save a token_info dictionary object to the cache and return None.
"""
@ -49,10 +51,12 @@ class CacheFileHandler(CacheHandler):
as json files on disk.
"""
def __init__(self,
cache_path=None,
username=None,
encoder_cls=None):
def __init__(
self,
cache_path: str | None = None,
username: str | None = None,
encoder_cls: JSONEncoder | None = None,
):
"""
Parameters:
* cache_path: May be supplied, will otherwise be generated
@ -90,7 +94,7 @@ class CacheFileHandler(CacheHandler):
return token_info
def save_token_to_cache(self, token_info):
def save_token_to_cache(self, token_info: Dict):
try:
with open(self.cache_path, "w", encoding='utf-8') as f:
f.write(json.dumps(token_info, cls=self.encoder_cls))
@ -109,7 +113,7 @@ class MemoryCacheHandler(CacheHandler):
instance is freed.
"""
def __init__(self, token_info=None):
def __init__(self, token_info: Dict | None = None):
"""
Parameters:
* token_info: The token info to store in memory. Can be None.
@ -119,7 +123,7 @@ class MemoryCacheHandler(CacheHandler):
def get_cached_token(self):
return self.token_info
def save_token_to_cache(self, token_info):
def save_token_to_cache(self, token_info: Dict):
self.token_info = token_info
@ -148,7 +152,7 @@ class DjangoSessionCacheHandler(CacheHandler):
return token_info
def save_token_to_cache(self, token_info):
def save_token_to_cache(self, token_info: Dict):
try:
self.request.session['token_info'] = token_info
except Exception as e:
@ -173,7 +177,7 @@ class FlaskSessionCacheHandler(CacheHandler):
return token_info
def save_token_to_cache(self, token_info):
def save_token_to_cache(self, token_info: Dict):
try:
self.session["token_info"] = token_info
except Exception as e:
@ -185,7 +189,7 @@ class RedisCacheHandler(CacheHandler):
A cache handler that stores the token info in the Redis.
"""
def __init__(self, redis, key=None):
def __init__(self, redis, key: str | None = None):
"""
Parameters:
* redis: Redis object provided by redis-py library
@ -194,7 +198,7 @@ class RedisCacheHandler(CacheHandler):
(takes precedence over `token_info`)
"""
self.redis = redis
self.key = key if key else 'token_info'
self.key: str = key if key else 'token_info'
def get_cached_token(self):
token_info = None
@ -207,7 +211,7 @@ class RedisCacheHandler(CacheHandler):
return token_info
def save_token_to_cache(self, token_info):
def save_token_to_cache(self, token_info: Dict):
try:
self.redis.set(self.key, json.dumps(token_info))
except RedisError as e:
@ -218,7 +222,7 @@ class MemcacheCacheHandler(CacheHandler):
"""A Cache handler that stores the token info in Memcache using the pymemcache client
"""
def __init__(self, memcache, key=None) -> None:
def __init__(self, memcache, key: str | None = None):
"""
Parameters:
* memcache: memcache client object provided by pymemcache
@ -227,7 +231,7 @@ class MemcacheCacheHandler(CacheHandler):
(takes precedence over `token_info`)
"""
self.memcache = memcache
self.key = key if key else 'token_info'
self.key: str = key if key else 'token_info'
def get_cached_token(self):
from pymemcache import MemcacheError
@ -238,7 +242,7 @@ class MemcacheCacheHandler(CacheHandler):
except MemcacheError as e:
logger.warning(f"Error getting token to cache: {e}")
def save_token_to_cache(self, token_info):
def save_token_to_cache(self, token_info: Dict):
from pymemcache import MemcacheError
try:
self.memcache.set(self.key, json.dumps(token_info))

File diff suppressed because it is too large Load Diff

View File

@ -34,8 +34,16 @@ class SpotifyOauthError(SpotifyBaseException):
class SpotifyStateError(SpotifyOauthError):
""" The state sent and state received were different """
def __init__(self, local_state=None, remote_state=None, message=None,
error=None, error_description=None, *args, **kwargs):
def __init__(
self,
local_state=None,
remote_state=None,
message=None,
error=None,
error_description=None,
*args,
**kwargs,
):
if not message:
message = ("Expected " + local_state + " but received "
+ remote_state)

View File

@ -16,6 +16,7 @@ import urllib.parse as urllibparse
import warnings
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import Any, Dict, Union
from urllib.parse import parse_qsl, urlparse
import requests
@ -23,19 +24,21 @@ import requests
from spotipy.cache_handler import CacheFileHandler, CacheHandler
from spotipy.exceptions import SpotifyOauthError, SpotifyStateError
from spotipy.util import (CLIENT_CREDS_ENV_VARS, REQUESTS_SESSION,
get_host_port, normalize_scope)
ScopeArgType, get_host_port, normalize_scope)
logger = logging.getLogger(__name__)
# TODO: improve the types that are "Any"
def _make_authorization_headers(client_id, client_secret):
def _make_authorization_headers(client_id: str, client_secret: str):
auth_header = base64.b64encode(
str(client_id + ":" + client_secret).encode("ascii")
)
return {"Authorization": f"Basic {auth_header.decode('ascii')}"}
def _ensure_value(value, env_key):
def _ensure_value(value: str | None, env_key: str) -> str:
env_val = CLIENT_CREDS_ENV_VARS[env_key]
_val = value or os.getenv(env_val)
if _val is None:
@ -45,7 +48,7 @@ def _ensure_value(value, env_key):
class SpotifyAuthBase:
def __init__(self, requests_session):
def __init__(self, requests_session: Union[requests.Session, bool] | None = None):
if isinstance(requests_session, requests.Session):
self._session = requests_session
else:
@ -55,54 +58,54 @@ class SpotifyAuthBase:
from requests import api
self._session = api
def _normalize_scope(self, scope):
def _normalize_scope(self, scope: ScopeArgType | None):
return normalize_scope(scope)
@property
def client_id(self):
def client_id(self) -> str:
return self._client_id
@client_id.setter
def client_id(self, val):
def client_id(self, val: str | None):
self._client_id = _ensure_value(val, "client_id")
@property
def client_secret(self):
def client_secret(self) -> str:
return self._client_secret
@client_secret.setter
def client_secret(self, val):
def client_secret(self, val: str | None):
self._client_secret = _ensure_value(val, "client_secret")
@property
def redirect_uri(self):
def redirect_uri(self) -> str:
return self._redirect_uri
@redirect_uri.setter
def redirect_uri(self, val):
def redirect_uri(self, val: str | None):
self._redirect_uri = _ensure_value(val, "redirect_uri")
@staticmethod
def _get_user_input(prompt):
def _get_user_input(prompt: Union[str, object]) -> str:
try:
return raw_input(prompt)
except NameError:
return input(prompt)
@staticmethod
def is_token_expired(token_info):
def is_token_expired(token_info: Dict):
now = int(time.time())
return token_info["expires_at"] - now < 60
@staticmethod
def _is_scope_subset(needle_scope, haystack_scope):
def _is_scope_subset(
needle_scope: str | None, haystack_scope: str | None
) -> bool:
needle_scope = set(needle_scope.split()) if needle_scope else set()
haystack_scope = (
set(haystack_scope.split()) if haystack_scope else set()
)
haystack_scope = set(haystack_scope.split()) if haystack_scope else set()
return needle_scope <= haystack_scope
def _handle_oauth_error(self, http_error):
def _handle_oauth_error(self, http_error: requests.exceptions.HTTPError):
response = http_error.response
try:
error_payload = response.json()
@ -133,12 +136,12 @@ class SpotifyClientCredentials(SpotifyAuthBase):
def __init__(
self,
client_id=None,
client_secret=None,
proxies=None,
requests_session=True,
requests_timeout=None,
cache_handler=None
client_id: str | None = None,
client_secret: str | None = None,
proxies: Any | None = None,
requests_session: Union[requests.Session, bool] = True,
requests_timeout: int | None = None,
cache_handler: CacheHandler | None = None,
):
"""
Creates a Client Credentials Flow Manager.
@ -181,7 +184,8 @@ class SpotifyClientCredentials(SpotifyAuthBase):
else:
self.cache_handler = CacheFileHandler()
def get_access_token(self, as_dict=True, check_cache=True):
# TODO: better return type based oninput type (overrides)
def get_access_token(self, as_dict: bool = True, check_cache: bool = True):
"""
If a valid access token is in memory, returns it
Else fetches a new token and returns it
@ -237,7 +241,7 @@ class SpotifyClientCredentials(SpotifyAuthBase):
except requests.exceptions.HTTPError as http_error:
self._handle_oauth_error(http_error)
def _add_custom_values_to_token_info(self, token_info):
def _add_custom_values_to_token_info(self, token_info: Dict):
"""
Store some values that aren't directly provided by a Web API
response.
@ -254,20 +258,20 @@ class SpotifyOAuth(SpotifyAuthBase):
OAUTH_TOKEN_URL = "https://accounts.spotify.com/api/token"
def __init__(
self,
client_id=None,
client_secret=None,
redirect_uri=None,
state=None,
scope=None,
cache_path=None,
username=None,
proxies=None,
show_dialog=False,
requests_session=True,
requests_timeout=None,
open_browser=True,
cache_handler=None
self,
client_id: str | None = None,
client_secret: str | None = None,
redirect_uri: str | None = None,
state: Any | None = None,
scope: ScopeArgType | None = None,
cache_path: str | None = None,
username: str | None = None,
proxies: Any | None = None,
show_dialog: bool = False,
requests_session: Union[requests.Session, bool] = True,
requests_timeout: int | None = None,
open_browser: bool = True,
cache_handler: CacheHandler | None = None,
):
"""
Creates a SpotifyOAuth object
@ -335,7 +339,7 @@ class SpotifyOAuth(SpotifyAuthBase):
self.show_dialog = show_dialog
self.open_browser = open_browser
def validate_token(self, token_info):
def validate_token(self, token_info: Dict | None):
if token_info is None:
return None
@ -352,7 +356,7 @@ class SpotifyOAuth(SpotifyAuthBase):
return token_info
def get_authorize_url(self, state=None):
def get_authorize_url(self, state: Any | None = None) -> str:
""" Gets the URL to use to authorize this app
"""
payload = {
@ -405,7 +409,7 @@ class SpotifyOAuth(SpotifyAuthBase):
except webbrowser.Error:
logger.error(f"Please navigate here: {auth_url}")
def _get_auth_response_interactive(self, open_browser=False):
def _get_auth_response_interactive(self, open_browser: bool = False):
if open_browser:
self._open_auth_url()
prompt = "Enter the URL you were redirected to: "
@ -421,7 +425,7 @@ class SpotifyOAuth(SpotifyAuthBase):
raise SpotifyStateError(self.state, state)
return code
def _get_auth_response_local_server(self, redirect_port):
def _get_auth_response_local_server(self, redirect_port: int):
server = start_local_http_server(redirect_port)
self._open_auth_url()
server.handle_request()
@ -435,7 +439,7 @@ class SpotifyOAuth(SpotifyAuthBase):
else:
raise SpotifyOauthError("Server listening on localhost has not been accessed")
def get_auth_response(self, open_browser=None):
def get_auth_response(self, open_browser: bool | None = None):
logger.info('User authentication requires interaction with your '
'web browser. Once you enter your credentials and '
'give authorization, you will be redirected to '
@ -476,12 +480,14 @@ class SpotifyOAuth(SpotifyAuthBase):
return self._get_auth_response_interactive(open_browser=open_browser)
def get_authorization_code(self, response=None):
def get_authorization_code(self, response: Any | None = None):
if response:
return self.parse_response_code(response)
return self.get_auth_response()
def get_access_token(self, code=None, as_dict=True, check_cache=True):
def get_access_token(
self, code: str | None = None, as_dict: bool = True, check_cache: bool = True
):
""" Gets the access token for the app given the code
Parameters:
@ -540,7 +546,7 @@ class SpotifyOAuth(SpotifyAuthBase):
except requests.exceptions.HTTPError as http_error:
self._handle_oauth_error(http_error)
def refresh_access_token(self, refresh_token):
def refresh_access_token(self, refresh_token: str):
payload = {
"refresh_token": refresh_token,
"grant_type": "refresh_token",
@ -569,7 +575,7 @@ class SpotifyOAuth(SpotifyAuthBase):
except requests.exceptions.HTTPError as http_error:
self._handle_oauth_error(http_error)
def _add_custom_values_to_token_info(self, token_info):
def _add_custom_values_to_token_info(self, token_info: Dict):
"""
Store some values that aren't directly provided by a Web API
response.
@ -593,7 +599,7 @@ class SpotifyOAuth(SpotifyAuthBase):
)
return self.validate_token(self.cache_handler.get_cached_token())
def _save_token_info(self, token_info):
def _save_token_info(self, token_info: Dict):
warnings.warn("Calling _save_token_info directly on the SpotifyOAuth object will be " +
"deprecated. Instead, please specify a CacheFileHandler instance as " +
"the cache_handler in SpotifyOAuth and use the CacheFileHandler's " +
@ -619,18 +625,20 @@ class SpotifyPKCE(SpotifyAuthBase):
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_path=None,
username=None,
proxies=None,
requests_timeout=None,
requests_session=True,
open_browser=True,
cache_handler=None):
def __init__(
self,
client_id: str | None = None,
redirect_uri: str | None = None,
state: Any | None = None,
scope: ScopeArgType | None = None,
cache_path: str | None = None,
username: str | None = None,
proxies: Any | None = None,
requests_timeout: int | None = None,
requests_session: Union[requests.Session, bool] = True,
open_browser: bool = True,
cache_handler: CacheHandler | None = None,
):
"""
Creates Auth Manager with the PKCE Auth flow.
@ -695,7 +703,7 @@ class SpotifyPKCE(SpotifyAuthBase):
self.authorization_code = None
self.open_browser = open_browser
def _get_code_verifier(self):
def _get_code_verifier(self) -> str:
""" 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
@ -709,7 +717,7 @@ class SpotifyPKCE(SpotifyAuthBase):
import secrets
return secrets.token_urlsafe(length)
def _get_code_challenge(self):
def _get_code_challenge(self) -> str:
""" 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
@ -720,7 +728,7 @@ class SpotifyPKCE(SpotifyAuthBase):
code_challenge = base64.urlsafe_b64encode(code_challenge_digest).decode('utf-8')
return code_challenge.replace('=', '')
def get_authorize_url(self, state=None):
def get_authorize_url(self, state: Any | None = None) -> str:
""" Gets the URL to use to authorize this app """
if not self.code_challenge:
self.get_pkce_handshake_parameters()
@ -740,7 +748,7 @@ class SpotifyPKCE(SpotifyAuthBase):
urlparams = urllibparse.urlencode(payload)
return f"{self.OAUTH_AUTHORIZE_URL}?{urlparams}"
def _open_auth_url(self, state=None):
def _open_auth_url(self, state: Any | None = None):
auth_url = self.get_authorize_url(state)
try:
webbrowser.open(auth_url)
@ -748,7 +756,7 @@ class SpotifyPKCE(SpotifyAuthBase):
except webbrowser.Error:
logger.error(f"Please navigate here: {auth_url}")
def _get_auth_response(self, open_browser=None):
def _get_auth_response(self, open_browser: bool | None = None):
logger.info('User authentication requires interaction with your '
'web browser. Once you enter your credentials and '
'give authorization, you will be redirected to '
@ -788,7 +796,7 @@ class SpotifyPKCE(SpotifyAuthBase):
'the URL your browser is redirected to.')
return self._get_auth_response_interactive(open_browser=open_browser)
def _get_auth_response_local_server(self, redirect_port):
def _get_auth_response_local_server(self, redirect_port: int):
server = start_local_http_server(redirect_port)
self._open_auth_url()
server.handle_request()
@ -803,7 +811,7 @@ class SpotifyPKCE(SpotifyAuthBase):
else:
raise SpotifyOauthError("Server listening on localhost has not been accessed")
def _get_auth_response_interactive(self, open_browser=False):
def _get_auth_response_interactive(self, open_browser: bool = False):
if open_browser or self.open_browser:
self._open_auth_url()
prompt = "Enter the URL you were redirected to: "
@ -817,7 +825,7 @@ class SpotifyPKCE(SpotifyAuthBase):
raise SpotifyStateError(self.state, state)
return code
def get_authorization_code(self, response=None):
def get_authorization_code(self, response: Any | None = None):
if response:
return self.parse_response_code(response)
return self._get_auth_response()
@ -851,7 +859,7 @@ class SpotifyPKCE(SpotifyAuthBase):
self.code_verifier = self._get_code_verifier()
self.code_challenge = self._get_code_challenge()
def get_access_token(self, code=None, check_cache=True):
def get_access_token(self, code: Any | None = None, check_cache: bool = True):
""" Gets the access token for the app
If the code is not given and no cached token is used, an
@ -906,7 +914,7 @@ class SpotifyPKCE(SpotifyAuthBase):
except requests.exceptions.HTTPError as http_error:
self._handle_oauth_error(http_error)
def refresh_access_token(self, refresh_token):
def refresh_access_token(self, refresh_token: str):
payload = {
"refresh_token": refresh_token,
"grant_type": "refresh_token",
@ -962,7 +970,7 @@ class SpotifyPKCE(SpotifyAuthBase):
)
return self.validate_token(self.cache_handler.get_cached_token())
def _save_token_info(self, token_info):
def _save_token_info(self, token_info: Dict):
warnings.warn("Calling _save_token_info directly on the SpotifyOAuth object will be " +
"deprecated. Instead, please specify a CacheFileHandler instance as " +
"the cache_handler in SpotifyOAuth and use the CacheFileHandler's " +
@ -1008,15 +1016,17 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
"""
OAUTH_AUTHORIZE_URL = "https://accounts.spotify.com/authorize"
def __init__(self,
client_id=None,
redirect_uri=None,
state=None,
scope=None,
cache_path=None,
username=None,
show_dialog=False,
cache_handler=None):
def __init__(
self,
client_id: str | None = None,
redirect_uri: str | None = None,
state: Any | None = None,
scope: ScopeArgType | None = None,
cache_path: str | None = None,
username: str | None = None,
show_dialog: bool = False,
cache_handler: CacheHandler | None = None,
):
""" Creates Auth Manager using the Implicit Grant flow
**See help(SpotifyImplicitGrant) for full Security Warning**
@ -1077,7 +1087,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
self.show_dialog = show_dialog
self._session = None # As to not break inherited __del__
def validate_token(self, token_info):
def validate_token(self, token_info: Dict | None):
if token_info is None:
return None
@ -1092,10 +1102,12 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
return token_info
def get_access_token(self,
state=None,
response=None,
check_cache=True):
def get_access_token(
self,
state: Any | None = None,
response: Any | None = None,
check_cache: bool = True,
):
""" Gets Auth Token from cache (preferred) or user interaction
Parameters
@ -1118,7 +1130,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
return token_info["access_token"]
def get_authorize_url(self, state=None):
def get_authorize_url(self, state: Any | None = None) -> str:
""" Gets the URL to use to authorize this app """
payload = {
"client_id": self.client_id,
@ -1138,7 +1150,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
return f"{self.OAUTH_AUTHORIZE_URL}?{urlparams}"
def parse_response_token(self, url, state=None):
def parse_response_token(self, url, state: Any | None = None):
""" Parse the response code in the given response url """
remote_state, token, t_type, exp_in = self.parse_auth_response_url(url)
if state is None:
@ -1163,7 +1175,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
return tuple(form.get(param) for param in ["state", "access_token",
"token_type", "expires_in"])
def _open_auth_url(self, state=None):
def _open_auth_url(self, state: Any | None = None):
auth_url = self.get_authorize_url(state)
try:
webbrowser.open(auth_url)
@ -1171,7 +1183,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
except webbrowser.Error:
logger.error(f"Please navigate here: {auth_url}")
def get_auth_response(self, state=None):
def get_auth_response(self, state: Any | None = None):
""" Gets a new auth **token** with user interaction """
logger.info('User authentication requires interaction with your '
'web browser. Once you enter your credentials and '
@ -1200,7 +1212,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
"were redirected to: ")
return self.parse_response_token(response, state)
def _add_custom_values_to_token_info(self, token_info):
def _add_custom_values_to_token_info(self, token_info: Dict):
"""
Store some values that aren't directly provided by a Web API
response.
@ -1225,7 +1237,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
)
return self.validate_token(self.cache_handler.get_cached_token())
def _save_token_info(self, token_info):
def _save_token_info(self, token_info: Dict):
warnings.warn("Calling _save_token_info directly on the SpotifyImplicitGrant " +
"object will be deprecated. Instead, please specify a " +
"CacheFileHandler instance as the cache_handler in SpotifyOAuth " +
@ -1274,14 +1286,14 @@ Close Window
</body>
</html>""")
def _write(self, text):
def _write(self, text: str):
return self.wfile.write(text.encode("utf-8"))
def log_message(self, format, *args):
return
def start_local_http_server(port, handler=RequestHandler):
def start_local_http_server(port: int, handler=RequestHandler):
server = HTTPServer(("127.0.0.1", port), handler)
server.allow_reuse_address = True
server.auth_code = None

View File

@ -8,6 +8,7 @@ import logging
import os
import warnings
from types import TracebackType
from typing import List, Tuple, Union
import requests
import urllib3
@ -26,17 +27,19 @@ CLIENT_CREDS_ENV_VARS = {
# workaround for garbage collection
REQUESTS_SESSION = requests.Session
StrListOrTuple = Union[List[str], Tuple[str, ...]]
def prompt_for_user_token(
username=None,
scope=None,
client_id=None,
client_secret=None,
redirect_uri=None,
cache_path=None,
oauth_manager=None,
show_dialog=False
):
username: str | None = None,
scope: str | None = None,
client_id: str | None = None,
client_secret: str | None = None,
redirect_uri: str | None = None,
cache_path: str | None = None,
oauth_manager: spotipy.SpotifyOAuth | None = None,
show_dialog: bool = False,
) -> str | None:
""" Prompt the user to login if necessary and returns a user token
suitable for use with the spotipy.Spotify constructor.
@ -116,7 +119,7 @@ def prompt_for_user_token(
return None
def get_host_port(netloc):
def get_host_port(netloc: str):
""" 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.
@ -133,7 +136,10 @@ def get_host_port(netloc):
return host, port
def normalize_scope(scope):
ScopeArgType = Union[str, StrListOrTuple]
def normalize_scope(scope: ScopeArgType | None) -> str | None:
"""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.
@ -163,13 +169,13 @@ class Retry(urllib3.Retry):
"""
def increment(
self,
method: str | None = None,
url: str | None = None,
response: urllib3.BaseHTTPResponse | None = None,
error: Exception | None = None,
_pool: urllib3.connectionpool.ConnectionPool | None = None,
_stacktrace: TracebackType | None = None,
self,
method: str | None = None,
url: str | None = None,
response: urllib3.BaseHTTPResponse | None = None,
error: Exception | None = None,
_pool: urllib3.connectionpool.ConnectionPool | None = None,
_stacktrace: TracebackType | None = None,
) -> urllib3.Retry:
if response:
retry_header = response.headers.get("Retry-After")

View File

@ -1,9 +1,12 @@
import base64
from typing import Union
import requests
from spotipy import Spotify
def get_spotify_playlist(spotify_object, playlist_name, username):
def get_spotify_playlist(spotify_object: Spotify, playlist_name: str, username: str):
playlists = spotify_object.user_playlists(username)
while playlists:
for item in playlists['items']:
@ -12,5 +15,5 @@ def get_spotify_playlist(spotify_object, playlist_name, username):
playlists = spotify_object.next(playlists)
def get_as_base64(url):
def get_as_base64(url: Union[str, bytes]) -> str:
return base64.b64encode(requests.get(url).content).decode("utf-8")

View File

@ -13,7 +13,7 @@ patch = mock.patch
DEFAULT = mock.DEFAULT
def _make_fake_token(expires_at, expires_in, scope):
def _make_fake_token(expires_at: int, expires_in: int, scope: str):
return dict(
expires_at=expires_at,
expires_in=expires_in,