more resilient http error handling, added retry limit on refreshing token

This commit is contained in:
aj 2019-10-03 02:09:39 +01:00
parent af0abe0285
commit 0c6aeb5181
2 changed files with 115 additions and 60 deletions

View File

@ -25,8 +25,9 @@ class Network:
def __init__(self, user: NetworkUser): def __init__(self, user: NetworkUser):
self.user = user self.user = user
self.refresh_counter = 0
def make_get_request(self, method, url=None, params=None, headers=None, whole_url=None) -> Optional[dict]: def get_request(self, method, url=None, params=None, headers=None, whole_url=None) -> Optional[dict]:
if headers is None: if headers is None:
headers = dict() headers = dict()
@ -50,25 +51,39 @@ class Network:
if req.status_code == 429: if req.status_code == 429:
retry_after = req.headers.get('Retry-After', None) retry_after = req.headers.get('Retry-After', None)
if retry_after: if self.refresh_counter < 5:
logger.warning(f'{method} rate limit reached: retrying in {retry_after} seconds') self.refresh_counter += 1
time.sleep(int(retry_after) + 1) if retry_after:
return self.make_get_request(method, url, params, headers) logger.warning(f'{method} rate limit reached: retrying in {retry_after} seconds')
time.sleep(int(retry_after) + 1)
return self.get_request(method, url, params, headers)
else:
logger.error(f'{method} rate limit reached: cannot find Retry-After header')
else: else:
logger.error(f'{method} rate limit reached: cannot find Retry-After header') self.refresh_counter = 0
logger.critical(f'refresh token limit (5) reached')
elif req.status_code == 401: elif req.status_code == 401:
logger.warning(f'{method} access token expired, refreshing') logger.warning(f'{method} access token expired, refreshing')
self.user.refresh_token() self.user.refresh_token()
return self.make_get_request(method, url, params, headers) if self.refresh_counter < 5:
self.refresh_counter += 1
return self.get_request(method, url, params, headers)
else:
self.refresh_counter = 0
logger.critical(f'refresh token limit (5) reached')
else: else:
error_text = req.json()['error']['message'] error = req.json().get('error', None)
logger.error(f'{method} get {req.status_code} {error_text}') if error:
message = error.get('message', 'n/a')
logger.error(f'{method} {req.status_code} {message}')
else:
logger.error(f'{method} {req.status_code} no error object found')
return None return None
def make_post_request(self, method, url, params=None, json=None, headers=None) -> Optional[Response]: def post_request(self, method, url, params=None, json=None, headers=None) -> Optional[Response]:
if headers is None: if headers is None:
headers = dict() headers = dict()
@ -85,25 +100,39 @@ class Network:
if req.status_code == 429: if req.status_code == 429:
retry_after = req.headers.get('Retry-After', None) retry_after = req.headers.get('Retry-After', None)
if retry_after: if self.refresh_counter < 5:
logger.warning(f'{method} rate limit reached: retrying in {retry_after} seconds') self.refresh_counter += 1
time.sleep(int(retry_after) + 1) if retry_after:
return self.make_post_request(method, url, params, json, headers) logger.warning(f'{method} rate limit reached: retrying in {retry_after} seconds')
time.sleep(int(retry_after) + 1)
return self.post_request(method, url, params, json, headers)
else:
logger.error(f'{method} rate limit reached: cannot find Retry-After header')
else: else:
logger.error(f'{method} rate limit reached: cannot find Retry-After header') self.refresh_counter = 0
logger.critical(f'refresh token limit (5) reached')
elif req.status_code == 401: elif req.status_code == 401:
logger.warning(f'{method} access token expired, refreshing') logger.warning(f'{method} access token expired, refreshing')
self.user.refresh_token() self.user.refresh_token()
return self.make_post_request(method, url, params, json, headers) if self.refresh_counter < 5:
self.refresh_counter += 1
return self.post_request(method, url, params, json, headers)
else:
self.refresh_counter = 0
logger.critical(f'refresh token limit (5) reached')
else: else:
error_text = str(req.text) error = req.json().get('error', None)
logger.error(f'{method} post {req.status_code} {error_text}') if error:
message = error.get('message', 'n/a')
logger.error(f'{method} {req.status_code} {message}')
else:
logger.error(f'{method} {req.status_code} no error object found')
return None return None
def make_put_request(self, method, url, params=None, json=None, headers=None) -> Optional[Response]: def put_request(self, method, url, params=None, json=None, headers=None) -> Optional[Response]:
if headers is None: if headers is None:
headers = dict() headers = dict()
@ -120,21 +149,35 @@ class Network:
if req.status_code == 429: if req.status_code == 429:
retry_after = req.headers.get('Retry-After', None) retry_after = req.headers.get('Retry-After', None)
if retry_after: if self.refresh_counter < 5:
logger.warning(f'{method} rate limit reached: retrying in {retry_after} seconds') self.refresh_counter += 1
time.sleep(int(retry_after) + 1) if retry_after:
return self.make_put_request(method, url, params, json, headers) logger.warning(f'{method} rate limit reached: retrying in {retry_after} seconds')
time.sleep(int(retry_after) + 1)
return self.put_request(method, url, params, json, headers)
else:
logger.error(f'{method} rate limit reached: cannot find Retry-After header')
else: else:
logger.error(f'{method} rate limit reached: cannot find Retry-After header') self.refresh_counter = 0
logger.critical(f'refresh token limit (5) reached')
elif req.status_code == 401: elif req.status_code == 401:
logger.warning(f'{method} access token expired, refreshing') logger.warning(f'{method} access token expired, refreshing')
self.user.refresh_token() self.user.refresh_token()
return self.make_put_request(method, url, params, json, headers) if self.refresh_counter < 5:
self.refresh_counter += 1
return self.put_request(method, url, params, json, headers)
else:
self.refresh_counter = 0
logger.critical(f'refresh token limit (5) reached')
else: else:
error_text = str(req.text) error = req.json().get('error', None)
logger.error(f'{method} put {req.status_code} {error_text}') if error:
message = error.get('message', 'n/a')
logger.error(f'{method} {req.status_code} {message}')
else:
logger.error(f'{method} {req.status_code} no error object found')
return None return None
@ -168,7 +211,7 @@ class Network:
if description: if description:
json['description'] = description json['description'] = description
req = self.make_post_request('createPlaylist', f'users/{username}/playlists', json=json) req = self.post_request('createPlaylist', f'users/{username}/playlists', json=json)
if 200 <= req.status_code < 300: if 200 <= req.status_code < 300:
return self.parse_playlist(req.json()) return self.parse_playlist(req.json())
@ -250,7 +293,7 @@ class Network:
logger.info("retrieving") logger.info("retrieving")
resp = self.make_get_request('getAvailableDevices', 'me/player/devices') resp = self.get_request('getAvailableDevices', 'me/player/devices')
if resp: if resp:
return [self.parse_device(i) for i in resp['devices']] return [self.parse_device(i) for i in resp['devices']]
else: else:
@ -273,7 +316,7 @@ class Network:
if before: if before:
params['before'] = int(before.timestamp() * 1000) params['before'] = int(before.timestamp() * 1000)
resp = self.make_get_request('getRecentlyPlayedTracks', 'me/player/recently-played', params=params) resp = self.get_request('getRecentlyPlayedTracks', 'me/player/recently-played', params=params)
if resp: if resp:
pager = PageCollection(self, page=resp) pager = PageCollection(self, page=resp)
@ -293,7 +336,7 @@ class Network:
logger.info("retrieved") logger.info("retrieved")
resp = self.make_get_request('getPlayer', 'me/player') resp = self.get_request('getPlayer', 'me/player')
if resp: if resp:
return self.parse_currently_playing(resp) return self.parse_currently_playing(resp)
else: else:
@ -325,7 +368,7 @@ class Network:
'play': True 'play': True
} }
resp = self.make_put_request('changePlaybackDevice', 'me/player', json=json) resp = self.put_request('changePlaybackDevice', 'me/player', json=json)
if resp: if resp:
return True return True
else: else:
@ -351,7 +394,7 @@ class Network:
if uris: if uris:
payload['uris'] = [str(i) for i in uris[:200]] payload['uris'] = [str(i) for i in uris[:200]]
req = self.make_put_request('play', 'me/player/play', params=params, json=payload) req = self.put_request('play', 'me/player/play', params=params, json=payload)
if req: if req:
return req return req
else: else:
@ -367,7 +410,7 @@ class Network:
else: else:
params = None params = None
req = self.make_put_request('pause', 'me/player/pause', params=params) req = self.put_request('pause', 'me/player/pause', params=params)
if req: if req:
return req return req
else: else:
@ -383,7 +426,7 @@ class Network:
else: else:
params = None params = None
req = self.make_post_request('next', 'me/player/next', params=params) req = self.post_request('next', 'me/player/next', params=params)
if req: if req:
return req return req
else: else:
@ -399,7 +442,7 @@ class Network:
else: else:
params = None params = None
req = self.make_post_request('previous', 'me/player/previous', params=params) req = self.post_request('previous', 'me/player/previous', params=params)
if req: if req:
return req return req
else: else:
@ -414,7 +457,7 @@ class Network:
if deviceid is not None: if deviceid is not None:
params['device_id'] = deviceid params['device_id'] = deviceid
req = self.make_put_request('setShuffle', 'me/player/shuffle', params=params) req = self.put_request('setShuffle', 'me/player/shuffle', params=params)
if req: if req:
return req return req
else: else:
@ -431,7 +474,7 @@ class Network:
if deviceid is not None: if deviceid is not None:
params['device_id'] = deviceid params['device_id'] = deviceid
req = self.make_put_request('setVolume', 'me/player/volume', params=params) req = self.put_request('setVolume', 'me/player/volume', params=params)
if req: if req:
return req return req
else: else:
@ -450,8 +493,8 @@ class Network:
json = {"uris": [str(i) for i in uris[:100]]} json = {"uris": [str(i) for i in uris[:100]]}
req = self.make_put_request('replacePlaylistTracks', f'playlists/{uri.object_id}/tracks', req = self.put_request('replacePlaylistTracks', f'playlists/{uri.object_id}/tracks',
json=json, headers=headers) json=json, headers=headers)
if req is not None: if req is not None:
@ -491,8 +534,8 @@ class Network:
logger.warning('update dictionairy length 0') logger.warning('update dictionairy length 0')
return None return None
else: else:
req = self.make_put_request('changePlaylistDetails', f'playlists/{uri.object_id}', req = self.put_request('changePlaylistDetails', f'playlists/{uri.object_id}',
json=json, headers=headers) json=json, headers=headers)
if req: if req:
return req return req
else: else:
@ -507,8 +550,8 @@ class Network:
json = {"uris": [str(i) for i in uris[:100]]} json = {"uris": [str(i) for i in uris[:100]]}
req = self.make_post_request('addPlaylistTracks', f'playlists/{uri.object_id}/tracks', req = self.post_request('addPlaylistTracks', f'playlists/{uri.object_id}/tracks',
json=json, headers=headers) json=json, headers=headers)
if req is not None: if req is not None:
resp = req.json() resp = req.json()
@ -542,7 +585,7 @@ class Network:
logger.warning('update dictionairy length 0') logger.warning('update dictionairy length 0')
return None return None
else: else:
resp = self.make_get_request('getRecommendations', 'recommendations', params=params) resp = self.get_request('getRecommendations', 'recommendations', params=params)
if resp: if resp:
if 'tracks' in resp: if 'tracks' in resp:
return [self.parse_track(i) for i in resp['tracks']] return [self.parse_track(i) for i in resp['tracks']]
@ -600,7 +643,7 @@ class Network:
'range_length': range_length, 'range_length': range_length,
'insert_before': insert_before} 'insert_before': insert_before}
resp = self.make_put_request('reorderPlaylistTracks', f'playlists/{uri.object_id}/tracks', json=json) resp = self.put_request('reorderPlaylistTracks', f'playlists/{uri.object_id}/tracks', json=json)
if resp: if resp:
return resp return resp
@ -613,9 +656,9 @@ class Network:
audio_features = [] audio_features = []
chunked_uris = list(self.chunk(uris, 100)) chunked_uris = list(self.chunk(uris, 100))
for chunk in chunked_uris: for chunk in chunked_uris:
resp = self.make_get_request('getAudioFeatures', resp = self.get_request('getAudioFeatures',
url='audio-features', url='audio-features',
params={'ids': ','.join(i.object_id for i in chunk)}) params={'ids': ','.join(i.object_id for i in chunk)})
if resp: if resp:
if resp.get('audio_features', None): if resp.get('audio_features', None):
@ -1006,10 +1049,10 @@ class PageCollection:
params = {'limit': self.page_limit} params = {'limit': self.page_limit}
if url: if url:
resp = self.net.make_get_request(method=self.name, whole_url=url, params=params) resp = self.net.get_request(method=self.name, whole_url=url, params=params)
else: else:
if self.url: if self.url:
resp = self.net.make_get_request(method=self.name, url=self.url, params=params) resp = self.net.get_request(method=self.name, url=self.url, params=params)
else: else:
raise ValueError('no url to query') raise ValueError('no url to query')

View File

@ -26,6 +26,8 @@ class NetworkUser(User):
self.on_refresh = [] self.on_refresh = []
self.refresh_counter = 0
def __repr__(self): def __repr__(self):
return Color.RED + Color.BOLD + 'NetworkUser' + Color.END + \ return Color.RED + Color.BOLD + 'NetworkUser' + Color.END + \
f': {self.username}, {self.display_name}, {self.uri}' f': {self.username}, {self.display_name}, {self.uri}'
@ -65,15 +67,16 @@ class NetworkUser(User):
retry_after = req.headers.get('Retry-After', None) retry_after = req.headers.get('Retry-After', None)
if retry_after: if retry_after:
logger.warning(f'refresh_token rate limit reached: retrying in {retry_after} seconds') logger.warning(f'rate limit reached: retrying in {retry_after} seconds')
time.sleep(int(retry_after) + 1) time.sleep(int(retry_after) + 1)
return self.refresh_token() return self.refresh_token()
else: else:
logger.error('refresh_token rate limit reached: cannot find Retry-After header') logger.error('rate limit reached: cannot find Retry-After header')
else: else:
error_text = req.json()['error']['message'] error_text = req.json().get('error', 'n/a')
logger.error(f'refresh_token get {req.status_code} {error_text}') error_description = req.json().get('error_description', 'n/a')
logger.error(f'get {req.status_code} {error_text} - {error_description}')
def refresh_info(self) -> None: def refresh_info(self) -> None:
info = self.get_info() info = self.get_info()
@ -109,17 +112,26 @@ class NetworkUser(User):
retry_after = req.headers.get('Retry-After', None) retry_after = req.headers.get('Retry-After', None)
if retry_after: if retry_after:
logger.warning(f'get_info rate limit reached: retrying in {retry_after} seconds') logger.warning(f'rate limit reached: retrying in {retry_after} seconds')
time.sleep(int(retry_after) + 1) time.sleep(int(retry_after) + 1)
return self.get_info() return self.get_info()
else: else:
logger.error('get_info rate limit reached: cannot find Retry-After header') logger.error('rate limit reached: cannot find Retry-After header')
elif req.status_code == 401: elif req.status_code == 401:
logger.warning('access token expired, refreshing') logger.warning('access token expired, refreshing')
self.refresh_token() self.refresh_token()
return self.get_info() if self.refresh_counter < 5:
self.refresh_counter += 1
return self.get_info()
else:
self.refresh_counter = 0
logger.critical('refresh token limit (5) reached')
else: else:
error_text = req.json()['error']['message'] error = req.json().get('error', None)
logger.error(f'get_info get {req.status_code} {error_text}') if error:
message = error.get('message', 'n/a')
logger.error(f'{req.status_code} {message}')
else:
logger.error(f'{req.status_code} no error object found')