aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2020-06-08 18:20:57 +0000
committerMike Crute <mike@crute.us>2020-06-08 18:20:57 +0000
commit3681a9b616212656462efa0a9c78722f90cca442 (patch)
tree0199712af2f8abc5635d57d7e18ec63fe6d8b58e
parent13b779eafd66a37ad5c50fa9be2bbc7962a73fce (diff)
downloadpydora-3681a9b616212656462efa0a9c78722f90cca442.tar.bz2
pydora-3681a9b616212656462efa0a9c78722f90cca442.tar.xz
pydora-3681a9b616212656462efa0a9c78722f90cca442.zip
Use black for formatting
I don't particularly like the black formatter (does anybody, really?) but it's guaranteed to be consistent so it should prevent nitpicking style.
-rw-r--r--pandora/client.py215
-rw-r--r--pandora/clientbuilder.py42
-rw-r--r--pandora/errors.py9
-rw-r--r--pandora/models/_base.py12
-rw-r--r--pandora/models/ad.py5
-rw-r--r--pandora/models/playlist.py36
-rw-r--r--pandora/models/search.py15
-rw-r--r--pandora/models/station.py9
-rw-r--r--pandora/transport.py35
-rw-r--r--pydora/audio_backend.py7
-rw-r--r--pydora/configure.py15
-rw-r--r--pydora/player.py56
-rw-r--r--pydora/utils.py5
-rwxr-xr-xsetup.py41
-rw-r--r--tests/test_pandora/test_client.py237
-rw-r--r--tests/test_pandora/test_clientbuilder.py112
-rw-r--r--tests/test_pandora/test_errors.py1
-rw-r--r--tests/test_pandora/test_models.py303
-rw-r--r--tests/test_pandora/test_transport.py60
-rw-r--r--tests/test_pydora/test_utils.py27
20 files changed, 708 insertions, 534 deletions
diff --git a/pandora/client.py b/pandora/client.py
index e0d3da9..a98056c 100644
--- a/pandora/client.py
+++ b/pandora/client.py
@@ -29,8 +29,14 @@ class BaseAPIClient:
29 29
30 ALL_QUALITIES = [LOW_AUDIO_QUALITY, MED_AUDIO_QUALITY, HIGH_AUDIO_QUALITY] 30 ALL_QUALITIES = [LOW_AUDIO_QUALITY, MED_AUDIO_QUALITY, HIGH_AUDIO_QUALITY]
31 31
32 def __init__(self, transport, partner_user, partner_password, device, 32 def __init__(
33 default_audio_quality=MED_AUDIO_QUALITY): 33 self,
34 transport,
35 partner_user,
36 partner_password,
37 device,
38 default_audio_quality=MED_AUDIO_QUALITY,
39 ):
34 self.transport = transport 40 self.transport = transport
35 self.partner_user = partner_user 41 self.partner_user = partner_user
36 self.partner_password = partner_password 42 self.partner_password = partner_password
@@ -40,11 +46,13 @@ class BaseAPIClient:
40 self.password = None 46 self.password = None
41 47
42 def _partner_login(self): 48 def _partner_login(self):
43 partner = self.transport("auth.partnerLogin", 49 partner = self.transport(
44 username=self.partner_user, 50 "auth.partnerLogin",
45 password=self.partner_password, 51 username=self.partner_user,
46 deviceModel=self.device, 52 password=self.partner_password,
47 version=self.transport.API_VERSION) 53 deviceModel=self.device,
54 version=self.transport.API_VERSION,
55 )
48 56
49 self.transport.set_partner(partner) 57 self.transport.set_partner(partner)
50 58
@@ -59,16 +67,18 @@ class BaseAPIClient:
59 self._partner_login() 67 self._partner_login()
60 68
61 try: 69 try:
62 user = self.transport("auth.userLogin", 70 user = self.transport(
63 loginType="user", 71 "auth.userLogin",
64 username=self.username, 72 loginType="user",
65 password=self.password, 73 username=self.username,
66 includePandoraOneInfo=True, 74 password=self.password,
67 includeSubscriptionExpiration=True, 75 includePandoraOneInfo=True,
68 returnCapped=True, 76 includeSubscriptionExpiration=True,
69 includeAdAttributes=True, 77 returnCapped=True,
70 includeAdvertiserAttributes=True, 78 includeAdAttributes=True,
71 xplatformAdCapable=True) 79 includeAdvertiserAttributes=True,
80 xplatformAdCapable=True,
81 )
72 except errors.InvalidPartnerLogin: 82 except errors.InvalidPartnerLogin:
73 raise errors.InvalidUserLogin() 83 raise errors.InvalidUserLogin()
74 84
@@ -80,7 +90,7 @@ class BaseAPIClient:
80 def get_qualities(cls, start_at, return_all_if_invalid=True): 90 def get_qualities(cls, start_at, return_all_if_invalid=True):
81 try: 91 try:
82 idx = cls.ALL_QUALITIES.index(start_at) 92 idx = cls.ALL_QUALITIES.index(start_at)
83 return cls.ALL_QUALITIES[:idx + 1] 93 return cls.ALL_QUALITIES[: idx + 1]
84 except ValueError: 94 except ValueError:
85 if return_all_if_invalid: 95 if return_all_if_invalid:
86 return cls.ALL_QUALITIES[:] 96 return cls.ALL_QUALITIES[:]
@@ -105,9 +115,9 @@ class APIClient(BaseAPIClient):
105 def get_station_list(self): 115 def get_station_list(self):
106 from .models.station import StationList 116 from .models.station import StationList
107 117
108 return StationList.from_json(self, 118 return StationList.from_json(
109 self("user.getStationList", 119 self, self("user.getStationList", includeStationArtUrl=True)
110 includeStationArtUrl=True)) 120 )
111 121
112 def get_station_list_checksum(self): 122 def get_station_list_checksum(self):
113 return self("user.getStationListChecksum")["checksum"] 123 return self("user.getStationListChecksum")["checksum"]
@@ -119,19 +129,21 @@ class APIClient(BaseAPIClient):
119 additional_urls = [] 129 additional_urls = []
120 130
121 if isinstance(additional_urls, str): 131 if isinstance(additional_urls, str):
122 raise TypeError('Additional urls should be a list') 132 raise TypeError("Additional urls should be a list")
123 133
124 urls = [getattr(url, "value", url) for url in additional_urls] 134 urls = [getattr(url, "value", url) for url in additional_urls]
125 135
126 resp = self("station.getPlaylist", 136 resp = self(
127 stationToken=station_token, 137 "station.getPlaylist",
128 includeTrackLength=True, 138 stationToken=station_token,
129 xplatformAdCapable=True, 139 includeTrackLength=True,
130 audioAdPodCapable=True, 140 xplatformAdCapable=True,
131 additionalAudioUrl=','.join(urls)) 141 audioAdPodCapable=True,
142 additionalAudioUrl=",".join(urls),
143 )
132 144
133 for item in resp['items']: 145 for item in resp["items"]:
134 item['_paramAdditionalUrls'] = additional_urls 146 item["_paramAdditionalUrls"] = additional_urls
135 147
136 playlist = Playlist.from_json(self, resp) 148 playlist = Playlist.from_json(self, resp)
137 149
@@ -145,58 +157,69 @@ class APIClient(BaseAPIClient):
145 def get_bookmarks(self): 157 def get_bookmarks(self):
146 from .models.bookmark import BookmarkList 158 from .models.bookmark import BookmarkList
147 159
148 return BookmarkList.from_json(self, 160 return BookmarkList.from_json(self, self("user.getBookmarks"))
149 self("user.getBookmarks"))
150 161
151 def get_station(self, station_token): 162 def get_station(self, station_token):
152 from .models.station import Station 163 from .models.station import Station
153 164
154 return Station.from_json(self, 165 return Station.from_json(
155 self("station.getStation", 166 self,
156 stationToken=station_token, 167 self(
157 includeExtendedAttributes=True)) 168 "station.getStation",
169 stationToken=station_token,
170 includeExtendedAttributes=True,
171 ),
172 )
158 173
159 def add_artist_bookmark(self, track_token): 174 def add_artist_bookmark(self, track_token):
160 return self("bookmark.addArtistBookmark", 175 return self("bookmark.addArtistBookmark", trackToken=track_token)
161 trackToken=track_token)
162 176
163 def add_song_bookmark(self, track_token): 177 def add_song_bookmark(self, track_token):
164 return self("bookmark.addSongBookmark", 178 return self("bookmark.addSongBookmark", trackToken=track_token)
165 trackToken=track_token)
166 179
167 def delete_song_bookmark(self, bookmark_token): 180 def delete_song_bookmark(self, bookmark_token):
168 return self("bookmark.deleteSongBookmark", 181 return self(
169 bookmarkToken=bookmark_token) 182 "bookmark.deleteSongBookmark", bookmarkToken=bookmark_token
183 )
170 184
171 def delete_artist_bookmark(self, bookmark_token): 185 def delete_artist_bookmark(self, bookmark_token):
172 return self("bookmark.deleteArtistBookmark", 186 return self(
173 bookmarkToken=bookmark_token) 187 "bookmark.deleteArtistBookmark", bookmarkToken=bookmark_token
188 )
174 189
175 def search(self, search_text, 190 def search(
176 include_near_matches=False, 191 self,
177 include_genre_stations=False): 192 search_text,
193 include_near_matches=False,
194 include_genre_stations=False,
195 ):
178 from .models.search import SearchResult 196 from .models.search import SearchResult
179 197
180 return SearchResult.from_json( 198 return SearchResult.from_json(
181 self, 199 self,
182 self("music.search", 200 self(
183 searchText=search_text, 201 "music.search",
184 includeNearMatches=include_near_matches, 202 searchText=search_text,
185 includeGenreStations=include_genre_stations) 203 includeNearMatches=include_near_matches,
204 includeGenreStations=include_genre_stations,
205 ),
186 ) 206 )
187 207
188 def add_feedback(self, track_token, positive): 208 def add_feedback(self, track_token, positive):
189 return self("station.addFeedback", 209 return self(
190 trackToken=track_token, 210 "station.addFeedback", trackToken=track_token, isPositive=positive
191 isPositive=positive) 211 )
192 212
193 def add_music(self, music_token, station_token): 213 def add_music(self, music_token, station_token):
194 return self("station.addMusic", 214 return self(
195 musicToken=music_token, 215 "station.addMusic",
196 stationToken=station_token) 216 musicToken=music_token,
217 stationToken=station_token,
218 )
197 219
198 def create_station(self, search_token=None, artist_token=None, 220 def create_station(
199 track_token=None): 221 self, search_token=None, artist_token=None, track_token=None
222 ):
200 from .models.station import Station 223 from .models.station import Station
201 224
202 kwargs = {} 225 kwargs = {}
@@ -210,26 +233,23 @@ class APIClient(BaseAPIClient):
210 else: 233 else:
211 raise KeyError("Must pass a type of token") 234 raise KeyError("Must pass a type of token")
212 235
213 return Station.from_json(self, 236 return Station.from_json(self, self("station.createStation", **kwargs))
214 self("station.createStation", **kwargs))
215 237
216 def delete_feedback(self, feedback_id): 238 def delete_feedback(self, feedback_id):
217 return self("station.deleteFeedback", 239 return self("station.deleteFeedback", feedbackId=feedback_id)
218 feedbackId=feedback_id)
219 240
220 def delete_music(self, seed_id): 241 def delete_music(self, seed_id):
221 return self("station.deleteMusic", 242 return self("station.deleteMusic", seedId=seed_id)
222 seedId=seed_id)
223 243
224 def delete_station(self, station_token): 244 def delete_station(self, station_token):
225 return self("station.deleteStation", 245 return self("station.deleteStation", stationToken=station_token)
226 stationToken=station_token)
227 246
228 def get_genre_stations(self): 247 def get_genre_stations(self):
229 from .models.station import GenreStationList 248 from .models.station import GenreStationList
230 249
231 genre_stations = GenreStationList.from_json( 250 genre_stations = GenreStationList.from_json(
232 self, self("station.getGenreStations")) 251 self, self("station.getGenreStations")
252 )
233 genre_stations.checksum = self.get_genre_stations_checksum() 253 genre_stations.checksum = self.get_genre_stations_checksum()
234 254
235 return genre_stations 255 return genre_stations
@@ -238,44 +258,45 @@ class APIClient(BaseAPIClient):
238 return self("station.getGenreStationsChecksum")["checksum"] 258 return self("station.getGenreStationsChecksum")["checksum"]
239 259
240 def rename_station(self, station_token, name): 260 def rename_station(self, station_token, name):
241 return self("station.renameStation", 261 return self(
242 stationToken=station_token, 262 "station.renameStation",
243 stationName=name) 263 stationToken=station_token,
264 stationName=name,
265 )
244 266
245 def explain_track(self, track_token): 267 def explain_track(self, track_token):
246 return self("track.explainTrack", 268 return self("track.explainTrack", trackToken=track_token)
247 trackToken=track_token)
248 269
249 def set_quick_mix(self, *args): 270 def set_quick_mix(self, *args):
250 return self("user.setQuickMix", 271 return self("user.setQuickMix", quickMixStationIds=args)
251 quickMixStationIds=args)
252 272
253 def sleep_song(self, track_token): 273 def sleep_song(self, track_token):
254 return self("user.sleepSong", 274 return self("user.sleepSong", trackToken=track_token)
255 trackToken=track_token)
256 275
257 def share_station(self, station_id, station_token, *emails): 276 def share_station(self, station_id, station_token, *emails):
258 return self("station.shareStation", 277 return self(
259 stationId=station_id, 278 "station.shareStation",
260 stationToken=station_token, 279 stationId=station_id,
261 emails=emails) 280 stationToken=station_token,
281 emails=emails,
282 )
262 283
263 def transform_shared_station(self, station_token): 284 def transform_shared_station(self, station_token):
264 return self("station.transformSharedStation", 285 return self(
265 stationToken=station_token) 286 "station.transformSharedStation", stationToken=station_token
287 )
266 288
267 def share_music(self, music_token, *emails): 289 def share_music(self, music_token, *emails):
268 return self("music.shareMusic", 290 return self(
269 musicToken=music_token, 291 "music.shareMusic", musicToken=music_token, email=emails[0]
270 email=emails[0]) 292 )
271 293
272 def get_ad_item(self, station_id, ad_token): 294 def get_ad_item(self, station_id, ad_token):
273 from .models.ad import AdItem 295 from .models.ad import AdItem
274 296
275 if not station_id: 297 if not station_id:
276 raise errors.ParameterMissing("The 'station_id' param must be " 298 msg = "The 'station_id' param must be defined, got: '{}'"
277 "defined, got: '{}'" 299 raise errors.ParameterMissing(msg.format(station_id))
278 .format(station_id))
279 300
280 ad_item = AdItem.from_json(self, self.get_ad_metadata(ad_token)) 301 ad_item = AdItem.from_json(self, self.get_ad_metadata(ad_token))
281 ad_item.station_id = station_id 302 ad_item.station_id = station_id
@@ -283,12 +304,14 @@ class APIClient(BaseAPIClient):
283 return ad_item 304 return ad_item
284 305
285 def get_ad_metadata(self, ad_token): 306 def get_ad_metadata(self, ad_token):
286 return self("ad.getAdMetadata", 307 return self(
287 adToken=ad_token, 308 "ad.getAdMetadata",
288 returnAdTrackingTokens=True, 309 adToken=ad_token,
289 supportAudioAds=True) 310 returnAdTrackingTokens=True,
311 supportAudioAds=True,
312 )
290 313
291 def register_ad(self, station_id, tokens): 314 def register_ad(self, station_id, tokens):
292 return self("ad.registerAd", 315 return self(
293 stationId=station_id, 316 "ad.registerAd", stationId=station_id, adTrackingTokens=tokens
294 adTrackingTokens=tokens) 317 )
diff --git a/pandora/clientbuilder.py b/pandora/clientbuilder.py
index bc55cec..2b11ffb 100644
--- a/pandora/clientbuilder.py
+++ b/pandora/clientbuilder.py
@@ -70,8 +70,7 @@ class TranslatingDict(dict):
70 70
71 def __setitem__(self, key, value): 71 def __setitem__(self, key, value):
72 key = self.translate_key(key) 72 key = self.translate_key(key)
73 super().__setitem__( 73 super().__setitem__(key, self.translate_value(key, value))
74 key, self.translate_value(key, value))
75 74
76 75
77class APIClientBuilder: 76class APIClientBuilder:
@@ -99,19 +98,25 @@ class APIClientBuilder:
99 self.client_class = client_class or self.DEFAULT_CLIENT_CLASS 98 self.client_class = client_class or self.DEFAULT_CLIENT_CLASS
100 99
101 def build_from_settings_dict(self, settings): 100 def build_from_settings_dict(self, settings):
102 enc = Encryptor(settings["DECRYPTION_KEY"], 101 enc = Encryptor(settings["DECRYPTION_KEY"], settings["ENCRYPTION_KEY"])
103 settings["ENCRYPTION_KEY"])
104 102
105 trans = APITransport(enc, 103 trans = APITransport(
106 settings.get("API_HOST", DEFAULT_API_HOST), 104 enc,
107 settings.get("PROXY", None)) 105 settings.get("API_HOST", DEFAULT_API_HOST),
106 settings.get("PROXY", None),
107 )
108 108
109 quality = settings.get("AUDIO_QUALITY", 109 quality = settings.get(
110 self.client_class.MED_AUDIO_QUALITY) 110 "AUDIO_QUALITY", self.client_class.MED_AUDIO_QUALITY
111 )
111 112
112 return self.client_class(trans, settings["PARTNER_USER"], 113 return self.client_class(
113 settings["PARTNER_PASSWORD"], 114 trans,
114 settings["DEVICE"], quality) 115 settings["PARTNER_USER"],
116 settings["PARTNER_PASSWORD"],
117 settings["DEVICE"],
118 quality,
119 )
115 120
116 121
117class SettingsDict(TranslatingDict): 122class SettingsDict(TranslatingDict):
@@ -185,8 +190,9 @@ class FileBasedClientBuilder(APIClientBuilder):
185 client = self.build_from_settings_dict(config) 190 client = self.build_from_settings_dict(config)
186 191
187 if self.authenticate: 192 if self.authenticate:
188 client.login(config["USER"]["USERNAME"], 193 client.login(
189 config["USER"]["PASSWORD"]) 194 config["USER"]["USERNAME"], config["USER"]["PASSWORD"]
195 )
190 196
191 return client 197 return client
192 198
@@ -201,8 +207,9 @@ class PydoraConfigFileBuilder(FileBasedClientBuilder):
201 207
202 @staticmethod 208 @staticmethod
203 def cfg_to_dict(cfg, key, kind=SettingsDict): 209 def cfg_to_dict(cfg, key, kind=SettingsDict):
204 return kind((k.strip().upper(), v.strip()) 210 return kind(
205 for k, v in cfg.items(key, raw=True)) 211 (k.strip().upper(), v.strip()) for k, v in cfg.items(key, raw=True)
212 )
206 213
207 def parse_config(self): 214 def parse_config(self):
208 cfg = ConfigParser() 215 cfg = ConfigParser()
@@ -212,7 +219,8 @@ class PydoraConfigFileBuilder(FileBasedClientBuilder):
212 219
213 settings = PydoraConfigFileBuilder.cfg_to_dict(cfg, "api") 220 settings = PydoraConfigFileBuilder.cfg_to_dict(cfg, "api")
214 settings["user"] = PydoraConfigFileBuilder.cfg_to_dict( 221 settings["user"] = PydoraConfigFileBuilder.cfg_to_dict(
215 cfg, "user", dict) 222 cfg, "user", dict
223 )
216 224
217 return settings 225 return settings
218 226
diff --git a/pandora/errors.py b/pandora/errors.py
index c978708..3b03db2 100644
--- a/pandora/errors.py
+++ b/pandora/errors.py
@@ -86,10 +86,11 @@ class PandoraException(Exception):
86 for code, api_message in __API_EXCEPTIONS__.items(): 86 for code, api_message in __API_EXCEPTIONS__.items():
87 name = PandoraException._format_name(api_message) 87 name = PandoraException._format_name(api_message)
88 88
89 exception = type(name, (PandoraException,), { 89 exception = type(
90 "code": code, 90 name,
91 "message": api_message, 91 (PandoraException,),
92 }) 92 {"code": code, "message": api_message,},
93 )
93 94
94 export_to[name] = __API_EXCEPTIONS__[code] = exception 95 export_to[name] = __API_EXCEPTIONS__[code] = exception
95 96
diff --git a/pandora/models/_base.py b/pandora/models/_base.py
index 6975d0a..8904be1 100644
--- a/pandora/models/_base.py
+++ b/pandora/models/_base.py
@@ -71,7 +71,6 @@ class DateField(SyntheticField):
71 71
72 72
73class ModelMetaClass(type): 73class ModelMetaClass(type):
74
75 def __new__(cls, name, parents, dct): 74 def __new__(cls, name, parents, dct):
76 dct["_fields"] = fields = {} 75 dct["_fields"] = fields = {}
77 new_dct = dct.copy() 76 new_dct = dct.copy()
@@ -159,7 +158,8 @@ class PandoraModel(metaclass=ModelMetaClass):
159 """ 158 """
160 items = [ 159 items = [
161 "=".join((key, repr(getattr(self, key)))) 160 "=".join((key, repr(getattr(self, key))))
162 for key in sorted(self._fields.keys())] 161 for key in sorted(self._fields.keys())
162 ]
163 163
164 if items: 164 if items:
165 output = ", ".join(items) 165 output = ", ".join(items)
@@ -167,8 +167,9 @@ class PandoraModel(metaclass=ModelMetaClass):
167 output = None 167 output = None
168 168
169 if and_also: 169 if and_also:
170 return "{}({}, {})".format(self.__class__.__name__, 170 return "{}({}, {})".format(
171 output, and_also) 171 self.__class__.__name__, output, and_also
172 )
172 else: 173 else:
173 return "{}({})".format(self.__class__.__name__, output) 174 return "{}({})".format(self.__class__.__name__, output)
174 175
@@ -301,7 +302,8 @@ class PandoraDictListModel(PandoraModel, dict):
301 302
302 for part in item[self.__list_key__]: 303 for part in item[self.__list_key__]:
303 self[key].append( 304 self[key].append(
304 cls.__list_model__.from_json(api_client, part)) 305 cls.__list_model__.from_json(api_client, part)
306 )
305 307
306 return self 308 return self
307 309
diff --git a/pandora/models/ad.py b/pandora/models/ad.py
index ad4b7b0..48cd302 100644
--- a/pandora/models/ad.py
+++ b/pandora/models/ad.py
@@ -24,8 +24,9 @@ class AdItem(PlaylistModel):
24 if self.tracking_tokens: 24 if self.tracking_tokens:
25 self._api_client.register_ad(station_id, self.tracking_tokens) 25 self._api_client.register_ad(station_id, self.tracking_tokens)
26 else: 26 else:
27 raise ParameterMissing('No ad tracking tokens provided for ' 27 raise ParameterMissing(
28 'registration.') 28 "No ad tracking tokens provided for registration."
29 )
29 30
30 def prepare_playback(self): 31 def prepare_playback(self):
31 try: 32 try:
diff --git a/pandora/models/playlist.py b/pandora/models/playlist.py
index 38afb00..8e0d22e 100644
--- a/pandora/models/playlist.py
+++ b/pandora/models/playlist.py
@@ -5,15 +5,15 @@ from ._base import Field, SyntheticField, PandoraModel, PandoraListModel
5 5
6 6
7class AdditionalAudioUrl(Enum): 7class AdditionalAudioUrl(Enum):
8 HTTP_40_AAC_MONO = 'HTTP_40_AAC_MONO' 8 HTTP_40_AAC_MONO = "HTTP_40_AAC_MONO"
9 HTTP_64_AAC = 'HTTP_64_AAC' 9 HTTP_64_AAC = "HTTP_64_AAC"
10 HTTP_32_AACPLUS = 'HTTP_32_AACPLUS' 10 HTTP_32_AACPLUS = "HTTP_32_AACPLUS"
11 HTTP_64_AACPLUS = 'HTTP_64_AACPLUS' 11 HTTP_64_AACPLUS = "HTTP_64_AACPLUS"
12 HTTP_24_AACPLUS_ADTS = 'HTTP_24_AACPLUS_ADTS' 12 HTTP_24_AACPLUS_ADTS = "HTTP_24_AACPLUS_ADTS"
13 HTTP_32_AACPLUS_ADTS = 'HTTP_32_AACPLUS_ADTS' 13 HTTP_32_AACPLUS_ADTS = "HTTP_32_AACPLUS_ADTS"
14 HTTP_64_AACPLUS_ADTS = 'HTTP_64_AACPLUS_ADTS' 14 HTTP_64_AACPLUS_ADTS = "HTTP_64_AACPLUS_ADTS"
15 HTTP_128_MP3 = 'HTTP_128_MP3' 15 HTTP_128_MP3 = "HTTP_128_MP3"
16 HTTP_32_WMA = 'HTTP_32_WMA' 16 HTTP_32_WMA = "HTTP_32_WMA"
17 17
18 18
19class PandoraType(Enum): 19class PandoraType(Enum):
@@ -28,14 +28,14 @@ class PandoraType(Enum):
28 28
29 @staticmethod 29 @staticmethod
30 def from_string(value): 30 def from_string(value):
31 return { 31 types = {
32 "TR": PandoraType.TRACK, 32 "TR": PandoraType.TRACK,
33 "AR": PandoraType.ARTIST, 33 "AR": PandoraType.ARTIST,
34 }.get(value, PandoraType.GENRE) 34 }
35 return types.get(value, PandoraType.GENRE)
35 36
36 37
37class AudioField(SyntheticField): 38class AudioField(SyntheticField):
38
39 def formatter(self, api_client, data, newval): 39 def formatter(self, api_client, data, newval):
40 """Get audio-related fields 40 """Get audio-related fields
41 41
@@ -61,9 +61,11 @@ class AudioField(SyntheticField):
61 elif not url_map: # No audio url available (e.g. ad tokens) 61 elif not url_map: # No audio url available (e.g. ad tokens)
62 return None 62 return None
63 63
64 valid_audio_formats = [BaseAPIClient.HIGH_AUDIO_QUALITY, 64 valid_audio_formats = [
65 BaseAPIClient.MED_AUDIO_QUALITY, 65 BaseAPIClient.HIGH_AUDIO_QUALITY,
66 BaseAPIClient.LOW_AUDIO_QUALITY] 66 BaseAPIClient.MED_AUDIO_QUALITY,
67 BaseAPIClient.LOW_AUDIO_QUALITY,
68 ]
67 69
68 # Only iterate over sublist, starting at preferred audio quality, or 70 # Only iterate over sublist, starting at preferred audio quality, or
69 # from the beginning of the list if nothing is found. Ensures that the 71 # from the beginning of the list if nothing is found. Ensures that the
@@ -84,7 +86,6 @@ class AudioField(SyntheticField):
84 86
85 87
86class AdditionalUrlField(SyntheticField): 88class AdditionalUrlField(SyntheticField):
87
88 def formatter(self, api_client, data, newval): 89 def formatter(self, api_client, data, newval):
89 """Parse additional url fields and map them to inputs 90 """Parse additional url fields and map them to inputs
90 91
@@ -94,7 +95,7 @@ class AdditionalUrlField(SyntheticField):
94 if newval is None: 95 if newval is None:
95 return None 96 return None
96 97
97 user_param = data['_paramAdditionalUrls'] 98 user_param = data["_paramAdditionalUrls"]
98 urls = {} 99 urls = {}
99 if isinstance(newval, str): 100 if isinstance(newval, str):
100 urls[user_param[0]] = newval 101 urls[user_param[0]] = newval
@@ -105,7 +106,6 @@ class AdditionalUrlField(SyntheticField):
105 106
106 107
107class PlaylistModel(PandoraModel): 108class PlaylistModel(PandoraModel):
108
109 def get_is_playable(self): 109 def get_is_playable(self):
110 if not self.audio_url: 110 if not self.audio_url:
111 return False 111 return False
diff --git a/pandora/models/search.py b/pandora/models/search.py
index 94e6ee6..fe31561 100644
--- a/pandora/models/search.py
+++ b/pandora/models/search.py
@@ -12,13 +12,15 @@ class SearchResultItem(PandoraModel):
12 12
13 @property 13 @property
14 def is_artist(self): 14 def is_artist(self):
15 return isinstance(self, ArtistSearchResultItem) and \ 15 return isinstance(
16 self.token.startswith("R") 16 self, ArtistSearchResultItem
17 ) and self.token.startswith("R")
17 18
18 @property 19 @property
19 def is_composer(self): 20 def is_composer(self):
20 return isinstance(self, ArtistSearchResultItem) and \ 21 return isinstance(
21 self.token.startswith("C") 22 self, ArtistSearchResultItem
23 ) and self.token.startswith("C")
22 24
23 @property 25 @property
24 def is_genre_station(self): 26 def is_genre_station(self):
@@ -36,8 +38,9 @@ class SearchResultItem(PandoraModel):
36 elif data["musicToken"].startswith("G"): 38 elif data["musicToken"].startswith("G"):
37 return GenreStationSearchResultItem.from_json(api_client, data) 39 return GenreStationSearchResultItem.from_json(api_client, data)
38 else: 40 else:
39 raise NotImplementedError("Unknown result token type '{}'" 41 raise NotImplementedError(
40 .format(data["musicToken"])) 42 "Unknown result token type '{}'".format(data["musicToken"])
43 )
41 44
42 45
43class ArtistSearchResultItem(SearchResultItem): 46class ArtistSearchResultItem(SearchResultItem):
diff --git a/pandora/models/station.py b/pandora/models/station.py
index a1880ec..d3f6552 100644
--- a/pandora/models/station.py
+++ b/pandora/models/station.py
@@ -80,8 +80,7 @@ class Station(PandoraModel):
80 feedback = Field("feedback", model=StationFeedback) 80 feedback = Field("feedback", model=StationFeedback)
81 81
82 def get_playlist(self, additional_urls=None): 82 def get_playlist(self, additional_urls=None):
83 return iter(self._api_client.get_playlist(self.token, 83 return iter(self._api_client.get_playlist(self.token, additional_urls))
84 additional_urls))
85 84
86 85
87class StationList(PandoraListModel): 86class StationList(PandoraListModel):
@@ -105,8 +104,10 @@ class GenreStation(PandoraModel):
105 category = Field("categoryName") 104 category = Field("categoryName")
106 105
107 def get_playlist(self): 106 def get_playlist(self):
108 raise NotImplementedError("Genre stations do not have playlists. " 107 raise NotImplementedError(
109 "Create a real station using the token.") 108 "Genre stations do not have playlists. "
109 "Create a real station using the token."
110 )
110 111
111 112
112class GenreStationList(PandoraDictListModel): 113class GenreStationList(PandoraDictListModel):
diff --git a/pandora/transport.py b/pandora/transport.py
index edec8a8..84f153b 100644
--- a/pandora/transport.py
+++ b/pandora/transport.py
@@ -40,6 +40,7 @@ def retries(max_tries, exceptions=(Exception,)):
40 function will only be retried if it raises one of the specified 40 function will only be retried if it raises one of the specified
41 exceptions. 41 exceptions.
42 """ 42 """
43
43 def decorator(func): 44 def decorator(func):
44 def function(*args, **kwargs): 45 def function(*args, **kwargs):
45 46
@@ -55,8 +56,9 @@ def retries(max_tries, exceptions=(Exception,)):
55 if isinstance(exc, PandoraException): 56 if isinstance(exc, PandoraException):
56 raise 57 raise
57 if retries_left > 0: 58 if retries_left > 0:
58 time.sleep(delay_exponential( 59 time.sleep(
59 0.5, 2, max_tries - retries_left)) 60 delay_exponential(0.5, 2, max_tries - retries_left)
61 )
60 else: 62 else:
61 raise 63 raise
62 64
@@ -76,11 +78,12 @@ def delay_exponential(base, growth_factor, attempts):
76 Base must be greater than 0, otherwise a ValueError will be 78 Base must be greater than 0, otherwise a ValueError will be
77 raised. 79 raised.
78 """ 80 """
79 if base == 'rand': 81 if base == "rand":
80 base = random.random() 82 base = random.random()
81 elif base <= 0: 83 elif base <= 0:
82 raise ValueError("The 'base' param must be greater than 0, " 84 raise ValueError(
83 "got: {}".format(base)) 85 "The 'base' param must be greater than 0, got: {}".format(base)
86 )
84 time_to_sleep = base * (growth_factor ** (attempts - 1)) 87 time_to_sleep = base * (growth_factor ** (attempts - 1))
85 return time_to_sleep 88 return time_to_sleep
86 89
@@ -95,8 +98,8 @@ class RetryingSession(requests.Session):
95 98
96 def __init__(self): 99 def __init__(self):
97 super().__init__() 100 super().__init__()
98 self.mount('https://', HTTPAdapter(max_retries=3)) 101 self.mount("https://", HTTPAdapter(max_retries=3))
99 self.mount('http://', HTTPAdapter(max_retries=3)) 102 self.mount("http://", HTTPAdapter(max_retries=3))
100 103
101 104
102class APITransport: 105class APITransport:
@@ -109,10 +112,14 @@ class APITransport:
109 112
110 API_VERSION = "5" 113 API_VERSION = "5"
111 114
112 REQUIRE_RESET = ("auth.partnerLogin", ) 115 REQUIRE_RESET = ("auth.partnerLogin",)
113 NO_ENCRYPT = ("auth.partnerLogin", ) 116 NO_ENCRYPT = ("auth.partnerLogin",)
114 REQUIRE_TLS = ("auth.partnerLogin", "auth.userLogin", 117 REQUIRE_TLS = (
115 "station.getPlaylist", "user.createUser") 118 "auth.partnerLogin",
119 "auth.userLogin",
120 "station.getPlaylist",
121 "user.createUser",
122 )
116 123
117 def __init__(self, cryptor, api_host=DEFAULT_API_HOST, proxy=None): 124 def __init__(self, cryptor, api_host=DEFAULT_API_HOST, proxy=None):
118 self.cryptor = cryptor 125 self.cryptor = cryptor
@@ -199,8 +206,8 @@ class APITransport:
199 206
200 def _build_url(self, method): 207 def _build_url(self, method):
201 return "{}://{}".format( 208 return "{}://{}".format(
202 "https" if method in self.REQUIRE_TLS else "http", 209 "https" if method in self.REQUIRE_TLS else "http", self.api_host
203 self.api_host) 210 )
204 211
205 def _build_data(self, method, data): 212 def _build_data(self, method, data):
206 data["userAuthToken"] = self.user_auth_token 213 data["userAuthToken"] = self.user_auth_token
@@ -260,7 +267,7 @@ class BlowfishCryptor:
260 267
261 computed = b"".join([chr(pad_size).encode("ascii")] * pad_size) 268 computed = b"".join([chr(pad_size).encode("ascii")] * pad_size)
262 if not data[-pad_size:] == computed: 269 if not data[-pad_size:] == computed:
263 raise ValueError('Invalid padding') 270 raise ValueError("Invalid padding")
264 271
265 return data[:-pad_size] 272 return data[:-pad_size]
266 273
diff --git a/pydora/audio_backend.py b/pydora/audio_backend.py
index bd778ad..aecd87e 100644
--- a/pydora/audio_backend.py
+++ b/pydora/audio_backend.py
@@ -15,18 +15,21 @@ log = logging.getLogger("pydora.audio_backend")
15class PlayerException(Exception): 15class PlayerException(Exception):
16 """Base class for all player exceptions 16 """Base class for all player exceptions
17 """ 17 """
18
18 pass 19 pass
19 20
20 21
21class UnsupportedEncoding(PlayerException): 22class UnsupportedEncoding(PlayerException):
22 """Song encoding is not supported by player backend 23 """Song encoding is not supported by player backend
23 """ 24 """
25
24 pass 26 pass
25 27
26 28
27class PlayerUnusable(PlayerException): 29class PlayerUnusable(PlayerException):
28 """Player can not be used on this system 30 """Player can not be used on this system
29 """ 31 """
32
30 pass 33 pass
31 34
32 35
@@ -172,7 +175,8 @@ class BasePlayer:
172 self._loop_hook() 175 self._loop_hook()
173 176
174 readers, _, _ = select.select( 177 readers, _, _ = select.select(
175 self._get_select_readers(), [], [], 1) 178 self._get_select_readers(), [], [], 1
179 )
176 180
177 for handle in readers: 181 for handle in readers:
178 if handle.fileno() == self._control_fd: 182 if handle.fileno() == self._control_fd:
@@ -296,7 +300,6 @@ class VLCPlayer(BasePlayer):
296 300
297 301
298class RemoteVLC(VLCPlayer): 302class RemoteVLC(VLCPlayer):
299
300 def __init__(self, host, port, callbacks, control_channel): 303 def __init__(self, host, port, callbacks, control_channel):
301 self._connect_to = (host, int(port)) 304 self._connect_to = (host, int(port))
302 self._control_sock = None 305 self._control_sock = None
diff --git a/pydora/configure.py b/pydora/configure.py
index d31c097..d58d445 100644
--- a/pydora/configure.py
+++ b/pydora/configure.py
@@ -32,11 +32,13 @@ class PandoraKeysConfigParser:
32 the pandora API docs keys source file. 32 the pandora API docs keys source file.
33 """ 33 """
34 34
35 KEYS_URL = ("http://6xq.net/git/lars/pandora-apidoc.git/" 35 KEYS_URL = (
36 "plain/json/partners.rst") 36 "http://6xq.net/git/lars/pandora-apidoc.git/plain/json/partners.rst"
37 )
37 38
38 FIELD_RE = re.compile( 39 FIELD_RE = re.compile(
39 ":(?P<key>[^:]+): (?:`{2})?(?P<value>[^`\n]+)(?:`{2})?$") 40 ":(?P<key>[^:]+): (?:`{2})?(?P<value>[^`\n]+)(?:`{2})?$"
41 )
40 42
41 def _fixup_key(self, key): 43 def _fixup_key(self, key):
42 key = key.lower() 44 key = key.lower()
@@ -90,7 +92,7 @@ class PandoraKeysConfigParser:
90 key = self._clean_device_name(buffer.pop()) 92 key = self._clean_device_name(buffer.pop())
91 current_partner = partners[key] = { 93 current_partner = partners[key] = {
92 "api_host": self._format_api_host(api_host) 94 "api_host": self._format_api_host(api_host)
93 } 95 }
94 96
95 buffer.append(line.strip().lower()) 97 buffer.append(line.strip().lower())
96 98
@@ -165,8 +167,9 @@ class Configurator:
165 self.add_partner_config(self.get_partner_config()) 167 self.add_partner_config(self.get_partner_config())
166 self.get_value("user", "username", "Pandora Username: ") 168 self.get_value("user", "username", "Pandora Username: ")
167 self.get_password("user", "password", "Pandora Password: ") 169 self.get_password("user", "password", "Pandora Password: ")
168 self.set_static_value("api", "default_audio_quality", 170 self.set_static_value(
169 APIClient.HIGH_AUDIO_QUALITY) 171 "api", "default_audio_quality", APIClient.HIGH_AUDIO_QUALITY
172 )
170 173
171 self.write_config() 174 self.write_config()
172 175
diff --git a/pydora/player.py b/pydora/player.py
index fdacc63..c4ce2e0 100644
--- a/pydora/player.py
+++ b/pydora/player.py
@@ -132,8 +132,12 @@ class PlayerApp:
132 if song.is_ad: 132 if song.is_ad:
133 print("{} ".format(Colors.cyan("Advertisement"))) 133 print("{} ".format(Colors.cyan("Advertisement")))
134 else: 134 else:
135 print("{} by {}".format(Colors.cyan(song.song_name), 135 print(
136 Colors.yellow(song.artist_name))) 136 "{} by {}".format(
137 Colors.cyan(song.song_name),
138 Colors.yellow(song.artist_name),
139 )
140 )
137 141
138 def skip_song(self, song): 142 def skip_song(self, song):
139 if song.is_ad: 143 if song.is_ad:
@@ -183,13 +187,15 @@ class PlayerApp:
183 self.screen.print_error("Failed to bookmark artis") 187 self.screen.print_error("Failed to bookmark artis")
184 except NotImplementedError: 188 except NotImplementedError:
185 self.screen.print_error( 189 self.screen.print_error(
186 "Cannot bookmark artist for this type of track") 190 "Cannot bookmark artist for this type of track"
191 )
187 192
188 def sleep_song(self, song): 193 def sleep_song(self, song):
189 try: 194 try:
190 if song.sleep(): 195 if song.sleep():
191 self.screen.print_success( 196 self.screen.print_success(
192 "Song will not be played for 30 days") 197 "Song will not be played for 30 days"
198 )
193 self.player.stop() 199 self.player.stop()
194 else: 200 else:
195 self.screen.print_error("Failed to sleep song") 201 self.screen.print_error("Failed to sleep song")
@@ -214,10 +220,14 @@ class PlayerApp:
214 220
215 def help(self, song): 221 def help(self, song):
216 print("") 222 print("")
217 print("\n".join([ 223 print(
218 "\t{:>2} - {}".format(k, v[0]) 224 "\n".join(
219 for k, v in sorted(self.CMD_MAP.items()) 225 [
220 ])) 226 "\t{:>2} - {}".format(k, v[0])
227 for k, v in sorted(self.CMD_MAP.items())
228 ]
229 )
230 )
221 print("") 231 print("")
222 232
223 def input(self, input, song): 233 def input(self, input, song):
@@ -227,7 +237,8 @@ class PlayerApp:
227 cmd = getattr(self, self.CMD_MAP[input][1]) 237 cmd = getattr(self, self.CMD_MAP[input][1])
228 except (IndexError, KeyError): 238 except (IndexError, KeyError):
229 return self.screen.print_error( 239 return self.screen.print_error(
230 "Invalid command {!r}!".format(input)) 240 "Invalid command {!r}!".format(input)
241 )
231 242
232 cmd(song) 243 cmd(song)
233 244
@@ -240,21 +251,30 @@ class PlayerApp:
240 def pre_flight_checks(self): 251 def pre_flight_checks(self):
241 # See #52, this key no longer passes some server-side check 252 # See #52, this key no longer passes some server-side check
242 if self.client.partner_user == "iphone": 253 if self.client.partner_user == "iphone":
243 self.screen.print_error(( 254 self.screen.print_error(
244 "The `iphone` partner key set is no longer compatible with " 255 (
245 "pydora. Please re-run pydora-configure to re-generate " 256 "The `iphone` partner key set is no longer compatible "
246 "your config file before continuing.")) 257 "with pydora. Please re-run pydora-configure to "
258 "re-generate your config file before continuing."
259 )
260 )
247 sys.exit(1) 261 sys.exit(1)
248 262
249 def _parse_args(self): 263 def _parse_args(self):
250 parser = argparse.ArgumentParser( 264 parser = argparse.ArgumentParser(
251 description="command line Pandora player") 265 description="command line Pandora player"
266 )
252 parser.add_argument( 267 parser.add_argument(
253 "--vlc-net", dest="vlc_net", 268 "--vlc-net",
254 help="connect to VLC over the network (host:port)") 269 dest="vlc_net",
270 help="connect to VLC over the network (host:port)",
271 )
255 parser.add_argument( 272 parser.add_argument(
256 "-v", dest="verbose", action="store_true", 273 "-v",
257 help="enable verbose logging") 274 dest="verbose",
275 action="store_true",
276 help="enable verbose logging",
277 )
258 return parser.parse_args() 278 return parser.parse_args()
259 279
260 def run(self): 280 def run(self):
diff --git a/pydora/utils.py b/pydora/utils.py
index 6675773..5a96ff1 100644
--- a/pydora/utils.py
+++ b/pydora/utils.py
@@ -10,11 +10,11 @@ class TerminalPlatformUnsupported(Exception):
10 Raised by code that can not be used to interact with the terminal on this 10 Raised by code that can not be used to interact with the terminal on this
11 platform. 11 platform.
12 """ 12 """
13
13 pass 14 pass
14 15
15 16
16class Colors: 17class Colors:
17
18 def __wrap_with(raw_code): 18 def __wrap_with(raw_code):
19 @staticmethod 19 @staticmethod
20 def inner(text, bold=False): 20 def inner(text, bold=False):
@@ -22,6 +22,7 @@ class Colors:
22 if bold: 22 if bold:
23 code = "1;{}".format(code) 23 code = "1;{}".format(code)
24 return "\033[{}m{}\033[0m".format(code, text) 24 return "\033[{}m{}\033[0m".format(code, text)
25
25 return inner 26 return inner
26 27
27 red = __wrap_with("31") 28 red = __wrap_with("31")
@@ -44,6 +45,7 @@ class PosixEchoControl:
44 def __init__(self): 45 def __init__(self):
45 try: 46 try:
46 import termios 47 import termios
48
47 self.termios = termios 49 self.termios = termios
48 except ImportError: 50 except ImportError:
49 raise TerminalPlatformUnsupported("POSIX not supported") 51 raise TerminalPlatformUnsupported("POSIX not supported")
@@ -110,7 +112,6 @@ class Win32EchoControl:
110 112
111 113
112class Screen: 114class Screen:
113
114 def __init__(self): 115 def __init__(self):
115 try: 116 try:
116 self._echo_driver = PosixEchoControl() 117 self._echo_driver = PosixEchoControl()
diff --git a/setup.py b/setup.py
index 490bf1d..d7344c5 100755
--- a/setup.py
+++ b/setup.py
@@ -52,23 +52,32 @@ class TestsWithCoverage(test):
52 self.missed_coverage_goals = True 52 self.missed_coverage_goals = True
53 self.announce( 53 self.announce(
54 "Coverage: {!r} coverage is {}%, goal is {}%".format( 54 "Coverage: {!r} coverage is {}%, goal is {}%".format(
55 rel_path, coverage_percent, self.coverage_goal), log.ERROR) 55 rel_path, coverage_percent, self.coverage_goal
56 ),
57 log.ERROR,
58 )
56 59
57 missed_branches = analysis.numbers.n_missing_branches 60 missed_branches = analysis.numbers.n_missing_branches
58 if missed_branches != self.missed_branches_goal: 61 if missed_branches != self.missed_branches_goal:
59 self.missed_coverage_goals = True 62 self.missed_coverage_goals = True
60 self.announce( 63 self.announce(
61 "Coverage: {!r} missed branch count is {}, goal is {}".format( 64 "Coverage: {!r} missed branch count is {}, goal is {}".format(
62 rel_path, missed_branches, self.missed_branches_goal), 65 rel_path, missed_branches, self.missed_branches_goal
63 log.ERROR) 66 ),
67 log.ERROR,
68 )
64 69
65 partially_covered_branches = analysis.numbers.n_partial_branches 70 partially_covered_branches = analysis.numbers.n_partial_branches
66 if partially_covered_branches != self.partial_branches_goal: 71 if partially_covered_branches != self.partial_branches_goal:
67 self.missed_coverage_goals = True 72 self.missed_coverage_goals = True
68 self.announce( 73 self.announce(
69 "Coverage: {!r} partial branch count is {}, goal is {}".format( 74 "Coverage: {!r} partial branch count is {}, goal is {}".format(
70 rel_path, partially_covered_branches, 75 rel_path,
71 self.partial_branches_goal), log.ERROR) 76 partially_covered_branches,
77 self.partial_branches_goal,
78 ),
79 log.ERROR,
80 )
72 81
73 def run(self): 82 def run(self):
74 from coverage import Coverage 83 from coverage import Coverage
@@ -114,13 +123,20 @@ class PyPiReleaseCommand(Command):
114 def configure_environment(self): 123 def configure_environment(self):
115 log.info("Configuring release environment") 124 log.info("Configuring release environment")
116 subprocess.check_call(["python3", "-m", "venv", ".release/py3"]) 125 subprocess.check_call(["python3", "-m", "venv", ".release/py3"])
117 self.venv_run("pip", "install", "-U", 126 self.venv_run(
118 "pip", "setuptools", "virtualenv", "twine") 127 "pip", "install", "-U", "pip", "setuptools", "virtualenv", "twine"
128 )
119 129
120 def build_py3_artifact(self): 130 def build_py3_artifact(self):
121 log.info("Building Python 3 Artifact") 131 log.info("Building Python 3 Artifact")
122 self.venv_run("python", "setup.py", 132 self.venv_run(
123 "release", "bdist_wheel", "--python-tag", "py3") 133 "python",
134 "setup.py",
135 "release",
136 "bdist_wheel",
137 "--python-tag",
138 "py3",
139 )
124 140
125 def build_sdist_artifact(self): 141 def build_sdist_artifact(self):
126 log.info("Building Source Dist Artifact") 142 log.info("Building Source Dist Artifact")
@@ -172,10 +188,7 @@ setup(
172 "setuptools>=36.0.1", 188 "setuptools>=36.0.1",
173 "coverage>=4.1,<5", 189 "coverage>=4.1,<5",
174 ], 190 ],
175 install_requires=[ 191 install_requires=["requests>=2,<3", "blowfish>=0.6.1,<1.0",],
176 "requests>=2,<3",
177 "blowfish>=0.6.1,<1.0",
178 ],
179 python_requires=">=3.5", 192 python_requires=">=3.5",
180 classifiers=[ 193 classifiers=[
181 "Development Status :: 5 - Production/Stable", 194 "Development Status :: 5 - Production/Stable",
@@ -191,5 +204,5 @@ setup(
191 "Programming Language :: Python :: 3 :: Only", 204 "Programming Language :: Python :: 3 :: Only",
192 "Topic :: Internet :: WWW/HTTP", 205 "Topic :: Internet :: WWW/HTTP",
193 "Topic :: Multimedia :: Sound/Audio :: Players", 206 "Topic :: Multimedia :: Sound/Audio :: Players",
194 ] 207 ],
195) 208)
diff --git a/tests/test_pandora/test_client.py b/tests/test_pandora/test_client.py
index 8144f42..1c5a229 100644
--- a/tests/test_pandora/test_client.py
+++ b/tests/test_pandora/test_client.py
@@ -13,7 +13,6 @@ from tests.test_pandora.test_models import TestAdItem
13 13
14 14
15class TestAPIClientLogin(TestCase): 15class TestAPIClientLogin(TestCase):
16
17 class StubTransport: 16 class StubTransport:
18 17
19 API_VERSION = None 18 API_VERSION = None
@@ -56,7 +55,6 @@ class TestAPIClientLogin(TestCase):
56 55
57 56
58class TestCallingAPIClient(TestCase): 57class TestCallingAPIClient(TestCase):
59
60 def test_call_should_retry_on_token_error(self): 58 def test_call_should_retry_on_token_error(self):
61 transport = Mock(side_effect=[errors.InvalidAuthToken(), None]) 59 transport = Mock(side_effect=[errors.InvalidAuthToken(), None])
62 60
@@ -70,37 +68,44 @@ class TestCallingAPIClient(TestCase):
70 transport.assert_has_calls([call("method"), call("method")]) 68 transport.assert_has_calls([call("method"), call("method")])
71 69
72 def test_playlist_fetches_ads(self): 70 def test_playlist_fetches_ads(self):
73 fake_playlist = {"items": [ 71 fake_playlist = {
74 {"songName": "test"}, 72 "items": [
75 {"adToken": "foo"}, 73 {"songName": "test"},
76 {"songName": "test"}, 74 {"adToken": "foo"},
77 ]} 75 {"songName": "test"},
78 with patch.object(APIClient, '__call__', return_value=fake_playlist): 76 ]
77 }
78 with patch.object(APIClient, "__call__", return_value=fake_playlist):
79 client = APIClient(Mock(), None, None, None, None) 79 client = APIClient(Mock(), None, None, None, None)
80 client._authenticate = Mock() 80 client._authenticate = Mock()
81 81
82 items = client.get_playlist('token_mock') 82 items = client.get_playlist("token_mock")
83 self.assertIsInstance(items[1], AdItem) 83 self.assertIsInstance(items[1], AdItem)
84 84
85 def test_ad_support_enabled_parameters(self): 85 def test_ad_support_enabled_parameters(self):
86 with patch.object(APIClient, '__call__') as playlist_mock: 86 with patch.object(APIClient, "__call__") as playlist_mock:
87 transport = Mock(side_effect=[errors.InvalidAuthToken(), None]) 87 transport = Mock(side_effect=[errors.InvalidAuthToken(), None])
88 88
89 client = APIClient(transport, None, None, None, None) 89 client = APIClient(transport, None, None, None, None)
90 client._authenticate = Mock() 90 client._authenticate = Mock()
91 91
92 client.get_playlist('token_mock') 92 client.get_playlist("token_mock")
93 93
94 playlist_mock.assert_has_calls([call("station.getPlaylist", 94 playlist_mock.assert_has_calls(
95 additionalAudioUrl='', 95 [
96 audioAdPodCapable=True, 96 call(
97 includeTrackLength=True, 97 "station.getPlaylist",
98 stationToken='token_mock', 98 additionalAudioUrl="",
99 xplatformAdCapable=True)]) 99 audioAdPodCapable=True,
100 includeTrackLength=True,
101 stationToken="token_mock",
102 xplatformAdCapable=True,
103 )
104 ]
105 )
100 106
101 107
102class TestGettingQualities(TestCase): 108class TestGettingQualities(TestCase):
103
104 def test_with_invalid_quality_returning_all(self): 109 def test_with_invalid_quality_returning_all(self):
105 result = BaseAPIClient.get_qualities("foo", True) 110 result = BaseAPIClient.get_qualities("foo", True)
106 self.assertEqual(BaseAPIClient.ALL_QUALITIES, result) 111 self.assertEqual(BaseAPIClient.ALL_QUALITIES, result)
@@ -111,20 +116,22 @@ class TestGettingQualities(TestCase):
111 116
112 def test_with_valid_quality(self): 117 def test_with_valid_quality(self):
113 result = BaseAPIClient.get_qualities( 118 result = BaseAPIClient.get_qualities(
114 BaseAPIClient.MED_AUDIO_QUALITY, False) 119 BaseAPIClient.MED_AUDIO_QUALITY, False
120 )
115 121
116 expected = [ 122 expected = [
117 BaseAPIClient.LOW_AUDIO_QUALITY, 123 BaseAPIClient.LOW_AUDIO_QUALITY,
118 BaseAPIClient.MED_AUDIO_QUALITY] 124 BaseAPIClient.MED_AUDIO_QUALITY,
125 ]
119 126
120 self.assertEqual(expected, result) 127 self.assertEqual(expected, result)
121 128
122 129
123class TestGettingAds(TestCase): 130class TestGettingAds(TestCase):
124
125 def test_get_ad_item_(self): 131 def test_get_ad_item_(self):
126 metamock = patch.object( 132 metamock = patch.object(
127 APIClient, '__call__', return_value=TestAdItem.JSON_DATA) 133 APIClient, "__call__", return_value=TestAdItem.JSON_DATA
134 )
128 135
129 with metamock as ad_metadata_mock: 136 with metamock as ad_metadata_mock:
130 transport = Mock(side_effect=[errors.InvalidAuthToken(), None]) 137 transport = Mock(side_effect=[errors.InvalidAuthToken(), None])
@@ -132,13 +139,20 @@ class TestGettingAds(TestCase):
132 client = APIClient(transport, None, None, None, None) 139 client = APIClient(transport, None, None, None, None)
133 client._authenticate = Mock() 140 client._authenticate = Mock()
134 141
135 ad_item = client.get_ad_item('id_mock', 'token_mock') 142 ad_item = client.get_ad_item("id_mock", "token_mock")
136 assert ad_item.station_id == 'id_mock' 143 assert ad_item.station_id == "id_mock"
137 assert ad_item.ad_token == 'token_mock' 144 assert ad_item.ad_token == "token_mock"
138 145
139 ad_metadata_mock.assert_has_calls([ 146 ad_metadata_mock.assert_has_calls(
140 call("ad.getAdMetadata", adToken='token_mock', 147 [
141 returnAdTrackingTokens=True, supportAudioAds=True)]) 148 call(
149 "ad.getAdMetadata",
150 adToken="token_mock",
151 returnAdTrackingTokens=True,
152 supportAudioAds=True,
153 )
154 ]
155 )
142 156
143 def test_get_ad_item_with_no_station_id_specified_raises_exception(self): 157 def test_get_ad_item_with_no_station_id_specified_raises_exception(self):
144 transport = Mock(side_effect=[errors.InvalidAuthToken(), None]) 158 transport = Mock(side_effect=[errors.InvalidAuthToken(), None])
@@ -147,28 +161,31 @@ class TestGettingAds(TestCase):
147 client.get_ad_metadata = Mock() 161 client.get_ad_metadata = Mock()
148 162
149 self.assertRaises( 163 self.assertRaises(
150 errors.ParameterMissing, client.get_ad_item, '', 'token_mock') 164 errors.ParameterMissing, client.get_ad_item, "", "token_mock"
165 )
151 166
152 167
153class TestCreatingStation(TestCase): 168class TestCreatingStation(TestCase):
154
155 def test_using_search_token(self): 169 def test_using_search_token(self):
156 client = APIClient(Mock(return_value={}), None, None, None, None) 170 client = APIClient(Mock(return_value={}), None, None, None, None)
157 client.create_station(search_token="foo") 171 client.create_station(search_token="foo")
158 client.transport.assert_called_with( 172 client.transport.assert_called_with(
159 "station.createStation", musicToken="foo") 173 "station.createStation", musicToken="foo"
174 )
160 175
161 def test_using_artist_token(self): 176 def test_using_artist_token(self):
162 client = APIClient(Mock(return_value={}), None, None, None, None) 177 client = APIClient(Mock(return_value={}), None, None, None, None)
163 client.create_station(artist_token="foo") 178 client.create_station(artist_token="foo")
164 client.transport.assert_called_with( 179 client.transport.assert_called_with(
165 "station.createStation", trackToken="foo", musicType="artist") 180 "station.createStation", trackToken="foo", musicType="artist"
181 )
166 182
167 def test_using_track_token(self): 183 def test_using_track_token(self):
168 client = APIClient(Mock(return_value={}), None, None, None, None) 184 client = APIClient(Mock(return_value={}), None, None, None, None)
169 client.create_station(track_token="foo") 185 client.create_station(track_token="foo")
170 client.transport.assert_called_with( 186 client.transport.assert_called_with(
171 "station.createStation", trackToken="foo", musicType="song") 187 "station.createStation", trackToken="foo", musicType="song"
188 )
172 189
173 def test_with_no_token(self): 190 def test_with_no_token(self):
174 with self.assertRaises(KeyError): 191 with self.assertRaises(KeyError):
@@ -177,25 +194,20 @@ class TestCreatingStation(TestCase):
177 194
178 195
179class TestCreatingGenreStation(TestCase): 196class TestCreatingGenreStation(TestCase):
180
181 def test_has_initial_checksum(self): 197 def test_has_initial_checksum(self):
182 fake_data = { 198 fake_data = {
183 "categories": [ 199 "categories": [{"categoryName": "foo", "stations": []},],
184 {"categoryName": "foo", "stations": []},
185 ],
186
187 # Not actually part of the genre station response but is needed to 200 # Not actually part of the genre station response but is needed to
188 # fake out the mock for get_genre_stations_checksum 201 # fake out the mock for get_genre_stations_checksum
189 "checksum": "foo" 202 "checksum": "foo",
190 } 203 }
191 with patch.object(APIClient, '__call__', return_value=fake_data): 204 with patch.object(APIClient, "__call__", return_value=fake_data):
192 client = APIClient(Mock(), None, None, None, None) 205 client = APIClient(Mock(), None, None, None, None)
193 station = client.get_genre_stations() 206 station = client.get_genre_stations()
194 self.assertEqual(station.checksum, "foo") 207 self.assertEqual(station.checksum, "foo")
195 208
196 209
197class TestAdditionalUrls(TestCase): 210class TestAdditionalUrls(TestCase):
198
199 def test_non_iterable_string(self): 211 def test_non_iterable_string(self):
200 with self.assertRaises(TypeError): 212 with self.assertRaises(TypeError):
201 transport = Mock(side_effect=[errors.InvalidAuthToken(), None]) 213 transport = Mock(side_effect=[errors.InvalidAuthToken(), None])
@@ -203,7 +215,7 @@ class TestAdditionalUrls(TestCase):
203 client = APIClient(transport, None, None, None, None) 215 client = APIClient(transport, None, None, None, None)
204 client._authenticate = Mock() 216 client._authenticate = Mock()
205 217
206 client.get_playlist('token_mock', additional_urls='') 218 client.get_playlist("token_mock", additional_urls="")
207 219
208 def test_non_iterable_other(self): 220 def test_non_iterable_other(self):
209 with self.assertRaises(TypeError): 221 with self.assertRaises(TypeError):
@@ -212,50 +224,64 @@ class TestAdditionalUrls(TestCase):
212 client = APIClient(transport, None, None, None, None) 224 client = APIClient(transport, None, None, None, None)
213 client._authenticate = Mock() 225 client._authenticate = Mock()
214 226
215 client.get_playlist('token_mock', 227 client.get_playlist(
216 additional_urls=AdditionalAudioUrl.HTTP_32_WMA) 228 "token_mock", additional_urls=AdditionalAudioUrl.HTTP_32_WMA
229 )
217 230
218 def test_without_enum(self): 231 def test_without_enum(self):
219 with patch.object(APIClient, '__call__') as playlist_mock: 232 with patch.object(APIClient, "__call__") as playlist_mock:
220 transport = Mock(side_effect=[errors.InvalidAuthToken(), None]) 233 transport = Mock(side_effect=[errors.InvalidAuthToken(), None])
221 234
222 client = APIClient(transport, None, None, None, None) 235 client = APIClient(transport, None, None, None, None)
223 client._authenticate = Mock() 236 client._authenticate = Mock()
224 237
225 urls = ['HTTP_128_MP3', 238 urls = ["HTTP_128_MP3", "HTTP_24_AACPLUS_ADTS"]
226 'HTTP_24_AACPLUS_ADTS']
227 239
228 desired = 'HTTP_128_MP3,HTTP_24_AACPLUS_ADTS' 240 desired = "HTTP_128_MP3,HTTP_24_AACPLUS_ADTS"
229 241
230 client.get_playlist('token_mock', additional_urls=urls) 242 client.get_playlist("token_mock", additional_urls=urls)
231 243
232 playlist_mock.assert_has_calls([call("station.getPlaylist", 244 playlist_mock.assert_has_calls(
233 additionalAudioUrl=desired, 245 [
234 audioAdPodCapable=True, 246 call(
235 includeTrackLength=True, 247 "station.getPlaylist",
236 stationToken='token_mock', 248 additionalAudioUrl=desired,
237 xplatformAdCapable=True)]) 249 audioAdPodCapable=True,
250 includeTrackLength=True,
251 stationToken="token_mock",
252 xplatformAdCapable=True,
253 )
254 ]
255 )
238 256
239 def test_with_enum(self): 257 def test_with_enum(self):
240 with patch.object(APIClient, '__call__') as playlist_mock: 258 with patch.object(APIClient, "__call__") as playlist_mock:
241 transport = Mock(side_effect=[errors.InvalidAuthToken(), None]) 259 transport = Mock(side_effect=[errors.InvalidAuthToken(), None])
242 260
243 client = APIClient(transport, None, None, None, None) 261 client = APIClient(transport, None, None, None, None)
244 client._authenticate = Mock() 262 client._authenticate = Mock()
245 263
246 urls = [AdditionalAudioUrl.HTTP_128_MP3, 264 urls = [
247 AdditionalAudioUrl.HTTP_24_AACPLUS_ADTS] 265 AdditionalAudioUrl.HTTP_128_MP3,
266 AdditionalAudioUrl.HTTP_24_AACPLUS_ADTS,
267 ]
248 268
249 desired = 'HTTP_128_MP3,HTTP_24_AACPLUS_ADTS' 269 desired = "HTTP_128_MP3,HTTP_24_AACPLUS_ADTS"
250 270
251 client.get_playlist('token_mock', additional_urls=urls) 271 client.get_playlist("token_mock", additional_urls=urls)
252 272
253 playlist_mock.assert_has_calls([call("station.getPlaylist", 273 playlist_mock.assert_has_calls(
254 additionalAudioUrl=desired, 274 [
255 audioAdPodCapable=True, 275 call(
256 includeTrackLength=True, 276 "station.getPlaylist",
257 stationToken='token_mock', 277 additionalAudioUrl=desired,
258 xplatformAdCapable=True)]) 278 audioAdPodCapable=True,
279 includeTrackLength=True,
280 stationToken="token_mock",
281 xplatformAdCapable=True,
282 )
283 ]
284 )
259 285
260 286
261# On the surface this test class seems dumb because it's mostly just exercising 287# On the surface this test class seems dumb because it's mostly just exercising
@@ -263,7 +289,6 @@ class TestAdditionalUrls(TestCase):
263# introduced to API client methods that will only be spotted at runtime (import 289# introduced to API client methods that will only be spotted at runtime (import
264# errors, etc...) 290# errors, etc...)
265class TestAPIClientExhaustive(TestCase): 291class TestAPIClientExhaustive(TestCase):
266
267 def setUp(self): 292 def setUp(self):
268 self.transport = Mock() 293 self.transport = Mock()
269 self.api = APIClient(self.transport, "puser", "ppass", "device") 294 self.api = APIClient(self.transport, "puser", "ppass", "device")
@@ -271,23 +296,29 @@ class TestAPIClientExhaustive(TestCase):
271 def test_register_ad(self): 296 def test_register_ad(self):
272 self.api.register_ad("sid", "tokens") 297 self.api.register_ad("sid", "tokens")
273 self.transport.assert_called_with( 298 self.transport.assert_called_with(
274 "ad.registerAd", stationId="sid", adTrackingTokens="tokens") 299 "ad.registerAd", stationId="sid", adTrackingTokens="tokens"
300 )
275 301
276 def test_share_music(self): 302 def test_share_music(self):
277 self.api.share_music("token", "foo@example.com") 303 self.api.share_music("token", "foo@example.com")
278 self.transport.assert_called_with( 304 self.transport.assert_called_with(
279 "music.shareMusic", musicToken="token", email="foo@example.com") 305 "music.shareMusic", musicToken="token", email="foo@example.com"
306 )
280 307
281 def test_transform_shared_station(self): 308 def test_transform_shared_station(self):
282 self.api.transform_shared_station("token") 309 self.api.transform_shared_station("token")
283 self.transport.assert_called_with( 310 self.transport.assert_called_with(
284 "station.transformSharedStation", stationToken="token") 311 "station.transformSharedStation", stationToken="token"
312 )
285 313
286 def test_share_station(self): 314 def test_share_station(self):
287 self.api.share_station("sid", "token", "foo@example.com") 315 self.api.share_station("sid", "token", "foo@example.com")
288 self.transport.assert_called_with( 316 self.transport.assert_called_with(
289 "station.shareStation", stationId="sid", stationToken="token", 317 "station.shareStation",
290 emails=("foo@example.com",)) 318 stationId="sid",
319 stationToken="token",
320 emails=("foo@example.com",),
321 )
291 322
292 def test_sleep_song(self): 323 def test_sleep_song(self):
293 self.api.sleep_song("token") 324 self.api.sleep_song("token")
@@ -296,22 +327,26 @@ class TestAPIClientExhaustive(TestCase):
296 def test_set_quick_mix(self): 327 def test_set_quick_mix(self):
297 self.api.set_quick_mix("id") 328 self.api.set_quick_mix("id")
298 self.transport.assert_called_with( 329 self.transport.assert_called_with(
299 "user.setQuickMix", quickMixStationIds=("id",)) 330 "user.setQuickMix", quickMixStationIds=("id",)
331 )
300 332
301 def test_explain_track(self): 333 def test_explain_track(self):
302 self.api.explain_track("token") 334 self.api.explain_track("token")
303 self.transport.assert_called_with( 335 self.transport.assert_called_with(
304 "track.explainTrack", trackToken="token") 336 "track.explainTrack", trackToken="token"
337 )
305 338
306 def test_rename_station(self): 339 def test_rename_station(self):
307 self.api.rename_station("token", "name") 340 self.api.rename_station("token", "name")
308 self.transport.assert_called_with( 341 self.transport.assert_called_with(
309 "station.renameStation", stationToken="token", stationName="name") 342 "station.renameStation", stationToken="token", stationName="name"
343 )
310 344
311 def test_delete_station(self): 345 def test_delete_station(self):
312 self.api.delete_station("token") 346 self.api.delete_station("token")
313 self.transport.assert_called_with( 347 self.transport.assert_called_with(
314 "station.deleteStation", stationToken="token") 348 "station.deleteStation", stationToken="token"
349 )
315 350
316 def test_delete_music(self): 351 def test_delete_music(self):
317 self.api.delete_music("seed") 352 self.api.delete_music("seed")
@@ -320,37 +355,44 @@ class TestAPIClientExhaustive(TestCase):
320 def test_delete_feedback(self): 355 def test_delete_feedback(self):
321 self.api.delete_feedback("id") 356 self.api.delete_feedback("id")
322 self.transport.assert_called_with( 357 self.transport.assert_called_with(
323 "station.deleteFeedback", feedbackId="id") 358 "station.deleteFeedback", feedbackId="id"
359 )
324 360
325 def test_add_music(self): 361 def test_add_music(self):
326 self.api.add_music("mt", "st") 362 self.api.add_music("mt", "st")
327 self.transport.assert_called_with( 363 self.transport.assert_called_with(
328 "station.addMusic", musicToken="mt", stationToken="st") 364 "station.addMusic", musicToken="mt", stationToken="st"
365 )
329 366
330 def test_add_feedback(self): 367 def test_add_feedback(self):
331 self.api.add_feedback("token", False) 368 self.api.add_feedback("token", False)
332 self.transport.assert_called_with( 369 self.transport.assert_called_with(
333 "station.addFeedback", trackToken="token", isPositive=False) 370 "station.addFeedback", trackToken="token", isPositive=False
371 )
334 372
335 def test_add_artist_bookmark(self): 373 def test_add_artist_bookmark(self):
336 self.api.add_artist_bookmark("tt") 374 self.api.add_artist_bookmark("tt")
337 self.transport.assert_called_with( 375 self.transport.assert_called_with(
338 "bookmark.addArtistBookmark", trackToken="tt") 376 "bookmark.addArtistBookmark", trackToken="tt"
377 )
339 378
340 def test_add_song_bookmark(self): 379 def test_add_song_bookmark(self):
341 self.api.add_song_bookmark("tt") 380 self.api.add_song_bookmark("tt")
342 self.transport.assert_called_with( 381 self.transport.assert_called_with(
343 "bookmark.addSongBookmark", trackToken="tt") 382 "bookmark.addSongBookmark", trackToken="tt"
383 )
344 384
345 def test_delete_song_bookmark(self): 385 def test_delete_song_bookmark(self):
346 self.api.delete_song_bookmark("bt") 386 self.api.delete_song_bookmark("bt")
347 self.transport.assert_called_with( 387 self.transport.assert_called_with(
348 "bookmark.deleteSongBookmark", bookmarkToken="bt") 388 "bookmark.deleteSongBookmark", bookmarkToken="bt"
389 )
349 390
350 def test_delete_artist_bookmark(self): 391 def test_delete_artist_bookmark(self):
351 self.api.delete_artist_bookmark("bt") 392 self.api.delete_artist_bookmark("bt")
352 self.transport.assert_called_with( 393 self.transport.assert_called_with(
353 "bookmark.deleteArtistBookmark", bookmarkToken="bt") 394 "bookmark.deleteArtistBookmark", bookmarkToken="bt"
395 )
354 396
355 def test_get_station_list_checksum(self): 397 def test_get_station_list_checksum(self):
356 self.transport.return_value = {"checksum": "foo"} 398 self.transport.return_value = {"checksum": "foo"}
@@ -364,7 +406,8 @@ class TestAPIClientExhaustive(TestCase):
364 self.transport.return_value = {"stations": []} 406 self.transport.return_value = {"stations": []}
365 self.assertIsInstance(self.api.get_station_list(), StationList) 407 self.assertIsInstance(self.api.get_station_list(), StationList)
366 self.transport.assert_called_with( 408 self.transport.assert_called_with(
367 "user.getStationList", includeStationArtUrl=True) 409 "user.getStationList", includeStationArtUrl=True
410 )
368 411
369 def test_get_bookmarks(self): 412 def test_get_bookmarks(self):
370 self.transport.return_value = {} 413 self.transport.return_value = {}
@@ -375,14 +418,22 @@ class TestAPIClientExhaustive(TestCase):
375 self.transport.return_value = {} 418 self.transport.return_value = {}
376 self.assertIsInstance(self.api.get_station("st"), Station) 419 self.assertIsInstance(self.api.get_station("st"), Station)
377 self.transport.assert_called_with( 420 self.transport.assert_called_with(
378 "station.getStation", stationToken="st", 421 "station.getStation",
379 includeExtendedAttributes=True) 422 stationToken="st",
423 includeExtendedAttributes=True,
424 )
380 425
381 def test_search(self): 426 def test_search(self):
382 self.transport.return_value = {} 427 self.transport.return_value = {}
383 self.assertIsInstance(self.api.search( 428 self.assertIsInstance(
384 "text", include_near_matches=True, include_genre_stations=True), 429 self.api.search(
385 SearchResult) 430 "text", include_near_matches=True, include_genre_stations=True
431 ),
432 SearchResult,
433 )
386 self.transport.assert_called_with( 434 self.transport.assert_called_with(
387 "music.search", searchText="text", includeNearMatches=True, 435 "music.search",
388 includeGenreStations=True) 436 searchText="text",
437 includeNearMatches=True,
438 includeGenreStations=True,
439 )
diff --git a/tests/test_pandora/test_clientbuilder.py b/tests/test_pandora/test_clientbuilder.py
index af0e219..17a4036 100644
--- a/tests/test_pandora/test_clientbuilder.py
+++ b/tests/test_pandora/test_clientbuilder.py
@@ -8,7 +8,6 @@ from pandora.transport import DEFAULT_API_HOST
8 8
9 9
10class TestTranslatingDict(TestCase): 10class TestTranslatingDict(TestCase):
11
12 class TestDict(cb.TranslatingDict): 11 class TestDict(cb.TranslatingDict):
13 12
14 KEY_TRANSLATIONS = {"FOO": "BAR"} 13 KEY_TRANSLATIONS = {"FOO": "BAR"}
@@ -62,29 +61,32 @@ class TestTranslatingDict(TestCase):
62 61
63 62
64class TestSettingsDictBuilder(TestCase): 63class TestSettingsDictBuilder(TestCase):
65
66 @classmethod 64 @classmethod
67 def _build_minimal(self): 65 def _build_minimal(self):
68 return cb.SettingsDictBuilder({ 66 return cb.SettingsDictBuilder(
69 "DECRYPTION_KEY": "blowfishkey", 67 {
70 "ENCRYPTION_KEY": "blowfishkey", 68 "DECRYPTION_KEY": "blowfishkey",
71 "PARTNER_USER": "user", 69 "ENCRYPTION_KEY": "blowfishkey",
72 "PARTNER_PASSWORD": "pass", 70 "PARTNER_USER": "user",
73 "DEVICE": "dev", 71 "PARTNER_PASSWORD": "pass",
74 }).build() 72 "DEVICE": "dev",
73 }
74 ).build()
75 75
76 @classmethod 76 @classmethod
77 def _build_maximal(self): 77 def _build_maximal(self):
78 return cb.SettingsDictBuilder({ 78 return cb.SettingsDictBuilder(
79 "DECRYPTION_KEY": "blowfishkey", 79 {
80 "ENCRYPTION_KEY": "blowfishkey", 80 "DECRYPTION_KEY": "blowfishkey",
81 "PARTNER_USER": "user", 81 "ENCRYPTION_KEY": "blowfishkey",
82 "PARTNER_PASSWORD": "pass", 82 "PARTNER_USER": "user",
83 "DEVICE": "dev", 83 "PARTNER_PASSWORD": "pass",
84 "PROXY": "proxy.example.com", 84 "DEVICE": "dev",
85 "AUDIO_QUALITY": "high", 85 "PROXY": "proxy.example.com",
86 "API_HOST": "example.com", 86 "AUDIO_QUALITY": "high",
87 }).build() 87 "API_HOST": "example.com",
88 }
89 ).build()
88 90
89 def test_building(self): 91 def test_building(self):
90 client = TestSettingsDictBuilder._build_minimal() 92 client = TestSettingsDictBuilder._build_minimal()
@@ -97,14 +99,15 @@ class TestSettingsDictBuilder(TestCase):
97 self.assertEqual({}, client.transport._http.proxies) 99 self.assertEqual({}, client.transport._http.proxies)
98 self.assertEqual(DEFAULT_API_HOST, client.transport.api_host) 100 self.assertEqual(DEFAULT_API_HOST, client.transport.api_host)
99 self.assertEqual( 101 self.assertEqual(
100 APIClient.MED_AUDIO_QUALITY, client.default_audio_quality) 102 APIClient.MED_AUDIO_QUALITY, client.default_audio_quality
103 )
101 104
102 def test_validate_client(self): 105 def test_validate_client(self):
103 client = TestSettingsDictBuilder._build_maximal() 106 client = TestSettingsDictBuilder._build_maximal()
104 expected_proxies = { 107 expected_proxies = {
105 "http": "proxy.example.com", 108 "http": "proxy.example.com",
106 "https": "proxy.example.com" 109 "https": "proxy.example.com",
107 } 110 }
108 111
109 self.assertIsNotNone(client.transport.cryptor.bf_in) 112 self.assertIsNotNone(client.transport.cryptor.bf_in)
110 self.assertIsNotNone(client.transport.cryptor.bf_out) 113 self.assertIsNotNone(client.transport.cryptor.bf_out)
@@ -119,7 +122,6 @@ class TestSettingsDictBuilder(TestCase):
119 122
120 123
121class TestFileBasedBuilder(TestCase): 124class TestFileBasedBuilder(TestCase):
122
123 class StubBuilder(cb.FileBasedClientBuilder): 125 class StubBuilder(cb.FileBasedClientBuilder):
124 126
125 DEFAULT_CONFIG_FILE = "foo" 127 DEFAULT_CONFIG_FILE = "foo"
@@ -170,7 +172,6 @@ class TestFileBasedBuilder(TestCase):
170 172
171 173
172class TestPydoraConfigFileBuilder(TestCase): 174class TestPydoraConfigFileBuilder(TestCase):
173
174 def test_cfg_to_dict(self): 175 def test_cfg_to_dict(self):
175 cfg = Mock() 176 cfg = Mock()
176 cfg.items = Mock(return_value=[("a", "b"), ("c", "d")]) 177 cfg.items = Mock(return_value=[("a", "b"), ("c", "d")])
@@ -184,38 +185,43 @@ class TestPydoraConfigFileBuilder(TestCase):
184 path = os.path.join(os.path.dirname(__file__), "pydora.cfg") 185 path = os.path.join(os.path.dirname(__file__), "pydora.cfg")
185 cfg = cb.PydoraConfigFileBuilder(path).parse_config() 186 cfg = cb.PydoraConfigFileBuilder(path).parse_config()
186 187
187 self.assertDictEqual(cfg, { 188 self.assertDictEqual(
188 "AUDIO_QUALITY": "test_quality", 189 cfg,
189 "DECRYPTION_KEY": "test_decryption_key", 190 {
190 "DEVICE": "test_device", 191 "AUDIO_QUALITY": "test_quality",
191 "ENCRYPTION_KEY": "test_encryption_key", 192 "DECRYPTION_KEY": "test_decryption_key",
192 "PARTNER_PASSWORD": "test_partner_password", 193 "DEVICE": "test_device",
193 "PARTNER_USER": "test_partner_username", 194 "ENCRYPTION_KEY": "test_encryption_key",
194 "API_HOST": "test_host", 195 "PARTNER_PASSWORD": "test_partner_password",
195 "USER": { 196 "PARTNER_USER": "test_partner_username",
196 "USERNAME": "test_username", 197 "API_HOST": "test_host",
197 "PASSWORD": "test_password", 198 "USER": {
198 } 199 "USERNAME": "test_username",
199 }) 200 "PASSWORD": "test_password",
201 },
202 },
203 )
200 204
201 205
202class TestPianobarConfigFileBuilder(TestCase): 206class TestPianobarConfigFileBuilder(TestCase):
203
204 def test_integration(self): 207 def test_integration(self):
205 path = os.path.join(os.path.dirname(__file__), "pianobar.cfg") 208 path = os.path.join(os.path.dirname(__file__), "pianobar.cfg")
206 cfg = cb.PianobarConfigFileBuilder(path).parse_config() 209 cfg = cb.PianobarConfigFileBuilder(path).parse_config()
207 210
208 self.assertDictEqual(cfg, { 211 self.assertDictEqual(
209 "AUDIO_QUALITY": "test_qualityQuality", 212 cfg,
210 "DECRYPTION_KEY": "test_decryption_key", 213 {
211 "DEVICE": "test_device", 214 "AUDIO_QUALITY": "test_qualityQuality",
212 "ENCRYPTION_KEY": "test_encryption_key", 215 "DECRYPTION_KEY": "test_decryption_key",
213 "PARTNER_PASSWORD": "test_partner_password", 216 "DEVICE": "test_device",
214 "PARTNER_USER": "test_partner_username", 217 "ENCRYPTION_KEY": "test_encryption_key",
215 "API_HOST": "test_host/services/json/", 218 "PARTNER_PASSWORD": "test_partner_password",
216 "PROXY": "test_proxy", 219 "PARTNER_USER": "test_partner_username",
217 "USER": { 220 "API_HOST": "test_host/services/json/",
218 "USERNAME": "test_username", 221 "PROXY": "test_proxy",
219 "PASSWORD": "test_password", 222 "USER": {
220 } 223 "USERNAME": "test_username",
221 }) 224 "PASSWORD": "test_password",
225 },
226 },
227 )
diff --git a/tests/test_pandora/test_errors.py b/tests/test_pandora/test_errors.py
index 778771a..d9df41f 100644
--- a/tests/test_pandora/test_errors.py
+++ b/tests/test_pandora/test_errors.py
@@ -4,7 +4,6 @@ from pandora.errors import InternalServerError, PandoraException
4 4
5 5
6class TestPandoraExceptionConstructionFromErrorCode(TestCase): 6class TestPandoraExceptionConstructionFromErrorCode(TestCase):
7
8 def test_it_returns_specific_error_class_if_possible(self): 7 def test_it_returns_specific_error_class_if_possible(self):
9 error = PandoraException.from_code(0, "Test Message") 8 error = PandoraException.from_code(0, "Test Message")
10 self.assertIsInstance(error, InternalServerError) 9 self.assertIsInstance(error, InternalServerError)
diff --git a/tests/test_pandora/test_models.py b/tests/test_pandora/test_models.py
index cb650df..16307b4 100644
--- a/tests/test_pandora/test_models.py
+++ b/tests/test_pandora/test_models.py
@@ -14,7 +14,6 @@ import pandora.models.playlist as plm
14 14
15 15
16class TestField(TestCase): 16class TestField(TestCase):
17
18 def test_defaults(self): 17 def test_defaults(self):
19 field = m.Field("name") 18 field = m.Field("name")
20 19
@@ -24,7 +23,6 @@ class TestField(TestCase):
24 23
25 24
26class TestModelMetaClass(TestCase): 25class TestModelMetaClass(TestCase):
27
28 class TestModel(metaclass=m.ModelMetaClass): 26 class TestModel(metaclass=m.ModelMetaClass):
29 27
30 foo = "bar" 28 foo = "bar"
@@ -40,7 +38,6 @@ class TestModelMetaClass(TestCase):
40 38
41 39
42class TestDateField(TestCase): 40class TestDateField(TestCase):
43
44 class SampleModel(m.PandoraModel): 41 class SampleModel(m.PandoraModel):
45 42
46 date_field = m.DateField("foo") 43 date_field = m.DateField("foo")
@@ -57,34 +54,26 @@ class TestDateField(TestCase):
57 54
58 55
59class TestAdditionalUrlField(TestCase): 56class TestAdditionalUrlField(TestCase):
60
61 def test_single_url(self): 57 def test_single_url(self):
62 dummy_data = { 58 dummy_data = {"_paramAdditionalUrls": ["foo"]}
63 '_paramAdditionalUrls': ['foo']
64 }
65 59
66 field = plm.AdditionalUrlField("additionalAudioUrl") 60 field = plm.AdditionalUrlField("additionalAudioUrl")
67 61
68 ret = field.formatter(None, dummy_data, 'test') 62 ret = field.formatter(None, dummy_data, "test")
69 63
70 self.assertEqual(ret, {'foo': 'test'}) 64 self.assertEqual(ret, {"foo": "test"})
71 65
72 def test_multiple_urls(self): 66 def test_multiple_urls(self):
73 dummy_data = { 67 dummy_data = {"_paramAdditionalUrls": ["abc", "def",]}
74 '_paramAdditionalUrls': [
75 'abc',
76 'def',
77 ]
78 }
79 68
80 field = plm.AdditionalUrlField("additionalAudioUrl") 69 field = plm.AdditionalUrlField("additionalAudioUrl")
81 70
82 ret = field.formatter(None, dummy_data, ['foo', 'bar']) 71 ret = field.formatter(None, dummy_data, ["foo", "bar"])
83 72
84 expected = { 73 expected = {
85 'abc': 'foo', 74 "abc": "foo",
86 'def': 'bar', 75 "def": "bar",
87 } 76 }
88 77
89 self.assertEqual(ret, expected) 78 self.assertEqual(ret, expected)
90 79
@@ -99,7 +88,6 @@ class TestPandoraModel(TestCase):
99 } 88 }
100 89
101 class TestModel(m.PandoraModel): 90 class TestModel(m.PandoraModel):
102
103 class SubModel(m.PandoraModel): 91 class SubModel(m.PandoraModel):
104 92
105 field1 = m.Field("field1") 93 field1 = m.Field("field1")
@@ -116,7 +104,6 @@ class TestPandoraModel(TestCase):
116 pass 104 pass
117 105
118 class ExtraReprModel(m.PandoraModel): 106 class ExtraReprModel(m.PandoraModel):
119
120 def __repr__(self): 107 def __repr__(self):
121 return self._base_repr("Foo") 108 return self._base_repr("Foo")
122 109
@@ -153,9 +140,11 @@ class TestPandoraModel(TestCase):
153 self.assertEqual("a string", result[1].field1) 140 self.assertEqual("a string", result[1].field1)
154 141
155 def test_repr(self): 142 def test_repr(self):
156 expected = ("TestModel(field1='a string', field2=['test2'], field3=42," 143 expected = (
157 " field4=SubModel(field1='foo'), " 144 "TestModel(field1='a string', field2=['test2'], field3=42,"
158 "field5=[SubModel(field1='foo'), SubModel(field1='bar')])") 145 " field4=SubModel(field1='foo'), "
146 "field5=[SubModel(field1='foo'), SubModel(field1='bar')])"
147 )
159 result = self.TestModel.from_json(None, self.JSON_DATA) 148 result = self.TestModel.from_json(None, self.JSON_DATA)
160 self.assertEqual(expected, repr(result)) 149 self.assertEqual(expected, repr(result))
161 150
@@ -179,12 +168,12 @@ class TestSubModel(m.PandoraModel):
179class TestPandoraListModel(TestCase): 168class TestPandoraListModel(TestCase):
180 169
181 JSON_DATA = { 170 JSON_DATA = {
182 "field1": 42, 171 "field1": 42,
183 "field2": [ 172 "field2": [
184 {"idx": "foo", "fieldS1": "Foo"}, 173 {"idx": "foo", "fieldS1": "Foo"},
185 {"idx": "bar", "fieldS1": "Bar"}, 174 {"idx": "bar", "fieldS1": "Bar"},
186 ] 175 ],
187 } 176 }
188 177
189 class TestModel(m.PandoraListModel): 178 class TestModel(m.PandoraListModel):
190 179
@@ -204,8 +193,10 @@ class TestPandoraListModel(TestCase):
204 self.assertEqual("Bar", self.result[1].fieldS1) 193 self.assertEqual("Bar", self.result[1].fieldS1)
205 194
206 def test_repr(self): 195 def test_repr(self):
207 expected = ("TestModel(field1=42, [TestSubModel(fieldS1='Foo', " 196 expected = (
208 "idx='foo'), TestSubModel(fieldS1='Bar', idx='bar')])") 197 "TestModel(field1=42, [TestSubModel(fieldS1='Foo', "
198 "idx='foo'), TestSubModel(fieldS1='Bar', idx='bar')])"
199 )
209 self.assertEqual(expected, repr(self.result)) 200 self.assertEqual(expected, repr(self.result))
210 201
211 def test_indexed_model(self): 202 def test_indexed_model(self):
@@ -232,17 +223,17 @@ class TestPandoraListModel(TestCase):
232class TestPandoraDictListModel(TestCase): 223class TestPandoraDictListModel(TestCase):
233 224
234 JSON_DATA = { 225 JSON_DATA = {
235 "field1": 42, 226 "field1": 42,
236 "fieldD1": [ 227 "fieldD1": [
237 { 228 {
238 "dictKey": "Foobear", 229 "dictKey": "Foobear",
239 "listKey": [ 230 "listKey": [
240 {"idx": "foo", "fieldS1": "Foo"}, 231 {"idx": "foo", "fieldS1": "Foo"},
241 {"idx": "bar", "fieldS1": "Bar"}, 232 {"idx": "bar", "fieldS1": "Bar"},
242 ] 233 ],
243 } 234 }
244 ] 235 ],
245 } 236 }
246 237
247 class TestModel(m.PandoraDictListModel): 238 class TestModel(m.PandoraDictListModel):
248 239
@@ -266,9 +257,11 @@ class TestPandoraDictListModel(TestCase):
266 self.assertEqual("bar", self.result["Foobear"][1].idx) 257 self.assertEqual("bar", self.result["Foobear"][1].idx)
267 258
268 def test_repr(self): 259 def test_repr(self):
269 expected = ("TestModel(field1=42, {'Foobear': " 260 expected = (
270 "[TestSubModel(fieldS1='Foo', idx='foo'), " 261 "TestModel(field1=42, {'Foobear': "
271 "TestSubModel(fieldS1='Bar', idx='bar')]})") 262 "[TestSubModel(fieldS1='Foo', idx='foo'), "
263 "TestSubModel(fieldS1='Bar', idx='bar')]})"
264 )
272 self.assertEqual(expected, repr(self.result)) 265 self.assertEqual(expected, repr(self.result))
273 266
274 267
@@ -320,7 +313,6 @@ class TestPlaylistItemModel(TestCase):
320 313
321 314
322class TestPlaylistModel(TestCase): 315class TestPlaylistModel(TestCase):
323
324 def setUp(self): 316 def setUp(self):
325 self.client = Mock() 317 self.client = Mock()
326 self.playlist = plm.PlaylistModel(self.client) 318 self.playlist = plm.PlaylistModel(self.client)
@@ -354,47 +346,47 @@ class TestPlaylistModel(TestCase):
354class TestAdItem(TestCase): 346class TestAdItem(TestCase):
355 347
356 JSON_DATA = { 348 JSON_DATA = {
357 'audioUrlMap': { 349 "audioUrlMap": {
358 'mediumQuality': { 350 "mediumQuality": {
359 'audioUrl': 'med_url_mock', 351 "audioUrl": "med_url_mock",
360 'bitrate': '64', 352 "bitrate": "64",
361 'protocol': 'http', 353 "protocol": "http",
362 'encoding': 'aacplus' 354 "encoding": "aacplus",
363 }, 355 },
364 'highQuality': { 356 "highQuality": {
365 'audioUrl': 'high_url_mock', 357 "audioUrl": "high_url_mock",
366 'bitrate': '64', 358 "bitrate": "64",
367 'protocol': 'http', 359 "protocol": "http",
368 'encoding': 'aacplus' 360 "encoding": "aacplus",
361 },
362 "lowQuality": {
363 "audioUrl": "low_url_mock",
364 "bitrate": "32",
365 "protocol": "http",
366 "encoding": "aacplus",
369 }, 367 },
370 'lowQuality': {
371 'audioUrl': 'low_url_mock',
372 'bitrate': '32',
373 'protocol': 'http',
374 'encoding': 'aacplus'
375 }
376 }, 368 },
377 'clickThroughUrl': 'click_url_mock', 369 "clickThroughUrl": "click_url_mock",
378 'imageUrl': 'img_url_mock', 370 "imageUrl": "img_url_mock",
379 'companyName': '', 371 "companyName": "",
380 'title': '', 372 "title": "",
381 'trackGain': '0.0', 373 "trackGain": "0.0",
382 'adTrackingTokens': ['token_1_mock', 'token_2_mock'] 374 "adTrackingTokens": ["token_1_mock", "token_2_mock"],
383 } 375 }
384 376
385 def setUp(self): 377 def setUp(self):
386 api_client_mock = Mock(spec=APIClient) 378 api_client_mock = Mock(spec=APIClient)
387 api_client_mock.default_audio_quality = APIClient.HIGH_AUDIO_QUALITY 379 api_client_mock.default_audio_quality = APIClient.HIGH_AUDIO_QUALITY
388 self.result = am.AdItem.from_json(api_client_mock, self.JSON_DATA) 380 self.result = am.AdItem.from_json(api_client_mock, self.JSON_DATA)
389 self.result.station_id = 'station_id_mock' 381 self.result.station_id = "station_id_mock"
390 self.result.ad_token = 'token_mock' 382 self.result.ad_token = "token_mock"
391 383
392 def test_is_ad_is_true(self): 384 def test_is_ad_is_true(self):
393 assert self.result.is_ad is True 385 assert self.result.is_ad is True
394 386
395 def test_register_ad(self): 387 def test_register_ad(self):
396 self.result._api_client.register_ad = Mock() 388 self.result._api_client.register_ad = Mock()
397 self.result.register_ad('id_mock') 389 self.result.register_ad("id_mock")
398 390
399 assert self.result._api_client.register_ad.called 391 assert self.result._api_client.register_ad.called
400 392
@@ -403,12 +395,12 @@ class TestAdItem(TestCase):
403 self.result.tracking_tokens = [] 395 self.result.tracking_tokens = []
404 self.result._api_client.register_ad = Mock(spec=am.AdItem) 396 self.result._api_client.register_ad = Mock(spec=am.AdItem)
405 397
406 self.result.register_ad('id_mock') 398 self.result.register_ad("id_mock")
407 399
408 assert self.result._api_client.register_ad.called 400 assert self.result._api_client.register_ad.called
409 401
410 def test_prepare_playback(self): 402 def test_prepare_playback(self):
411 with patch.object(plm.PlaylistModel, 'prepare_playback') as super_mock: 403 with patch.object(plm.PlaylistModel, "prepare_playback") as super_mock:
412 404
413 self.result.register_ad = Mock() 405 self.result.register_ad = Mock()
414 self.result.prepare_playback() 406 self.result.prepare_playback()
@@ -416,21 +408,26 @@ class TestAdItem(TestCase):
416 assert super_mock.called 408 assert super_mock.called
417 409
418 def test_prepare_playback_raises_paramater_missing(self): 410 def test_prepare_playback_raises_paramater_missing(self):
419 with patch.object(plm.PlaylistModel, 'prepare_playback') as super_mock: 411 with patch.object(plm.PlaylistModel, "prepare_playback") as super_mock:
420 412
421 self.result.register_ad = Mock(side_effect=ParameterMissing( 413 self.result.register_ad = Mock(
422 'No ad tracking tokens provided for registration.') 414 side_effect=ParameterMissing(
423 ) 415 "No ad tracking tokens provided for registration."
416 )
417 )
424 self.assertRaises(ParameterMissing, self.result.prepare_playback) 418 self.assertRaises(ParameterMissing, self.result.prepare_playback)
425 assert self.result.register_ad.called 419 assert self.result.register_ad.called
426 assert not super_mock.called 420 assert not super_mock.called
427 421
428 def test_prepare_playback_handles_paramater_missing_if_no_tokens(self): 422 def test_prepare_playback_handles_paramater_missing_if_no_tokens(self):
429 with patch.object(plm.PlaylistModel, 'prepare_playback') as super_mock: 423 with patch.object(plm.PlaylistModel, "prepare_playback") as super_mock:
430 424
431 self.result.tracking_tokens = [] 425 self.result.tracking_tokens = []
432 self.result.register_ad = Mock(side_effect=ParameterMissing( 426 self.result.register_ad = Mock(
433 'No ad tracking tokens provided for registration.')) 427 side_effect=ParameterMissing(
428 "No ad tracking tokens provided for registration."
429 )
430 )
434 self.result.prepare_playback() 431 self.result.prepare_playback()
435 assert self.result.register_ad.called 432 assert self.result.register_ad.called
436 assert super_mock.called 433 assert super_mock.called
@@ -449,43 +446,45 @@ class TestSearchResultItem(TestCase):
449 "artistName": "artist_name_mock", 446 "artistName": "artist_name_mock",
450 "musicToken": "S0000000", 447 "musicToken": "S0000000",
451 "songName": "song_name_mock", 448 "songName": "song_name_mock",
452 "score": 100 449 "score": 100,
453 } 450 }
454 451
455 ARTIST_JSON_DATA = { 452 ARTIST_JSON_DATA = {
456 "artistName": "artist_name_mock", 453 "artistName": "artist_name_mock",
457 "musicToken": "R0000000", 454 "musicToken": "R0000000",
458 "likelyMatch": False, 455 "likelyMatch": False,
459 "score": 100 456 "score": 100,
460 } 457 }
461 458
462 COMPOSER_JSON_DATA = { 459 COMPOSER_JSON_DATA = {
463 "artistName": "composer_name_mock", 460 "artistName": "composer_name_mock",
464 "musicToken": "C0000000", 461 "musicToken": "C0000000",
465 "likelyMatch": False, 462 "likelyMatch": False,
466 "score": 100 463 "score": 100,
467 } 464 }
468 465
469 GENRE_JSON_DATA = { 466 GENRE_JSON_DATA = {
470 "stationName": "station_name_mock", 467 "stationName": "station_name_mock",
471 "musicToken": "G0000000", 468 "musicToken": "G0000000",
472 "score": 100 469 "score": 100,
473 } 470 }
474 471
475 UNKNOWN_JSON_DATA = { 472 UNKNOWN_JSON_DATA = {
476 "stationName": "unknown_name_mock", 473 "stationName": "unknown_name_mock",
477 "musicToken": "U0000000", 474 "musicToken": "U0000000",
478 "score": 100 475 "score": 100,
479 } 476 }
480 477
481 def setUp(self): 478 def setUp(self):
482 self.api_client_mock = Mock(spec=APIClient) 479 self.api_client_mock = Mock(spec=APIClient)
483 self.api_client_mock.default_audio_quality = \ 480 self.api_client_mock.default_audio_quality = (
484 APIClient.HIGH_AUDIO_QUALITY 481 APIClient.HIGH_AUDIO_QUALITY
482 )
485 483
486 def test_is_song(self): 484 def test_is_song(self):
487 result = sm.SearchResultItem.from_json( 485 result = sm.SearchResultItem.from_json(
488 self.api_client_mock, self.SONG_JSON_DATA) 486 self.api_client_mock, self.SONG_JSON_DATA
487 )
489 assert result.is_song 488 assert result.is_song
490 assert not result.is_artist 489 assert not result.is_artist
491 assert not result.is_composer 490 assert not result.is_composer
@@ -493,7 +492,8 @@ class TestSearchResultItem(TestCase):
493 492
494 def test_is_artist(self): 493 def test_is_artist(self):
495 result = sm.SearchResultItem.from_json( 494 result = sm.SearchResultItem.from_json(
496 self.api_client_mock, self.ARTIST_JSON_DATA) 495 self.api_client_mock, self.ARTIST_JSON_DATA
496 )
497 assert not result.is_song 497 assert not result.is_song
498 assert result.is_artist 498 assert result.is_artist
499 assert not result.is_composer 499 assert not result.is_composer
@@ -501,7 +501,8 @@ class TestSearchResultItem(TestCase):
501 501
502 def test_is_composer(self): 502 def test_is_composer(self):
503 result = sm.SearchResultItem.from_json( 503 result = sm.SearchResultItem.from_json(
504 self.api_client_mock, self.COMPOSER_JSON_DATA) 504 self.api_client_mock, self.COMPOSER_JSON_DATA
505 )
505 assert not result.is_song 506 assert not result.is_song
506 assert not result.is_artist 507 assert not result.is_artist
507 assert result.is_composer 508 assert result.is_composer
@@ -509,7 +510,8 @@ class TestSearchResultItem(TestCase):
509 510
510 def test_is_genre_station(self): 511 def test_is_genre_station(self):
511 result = sm.SearchResultItem.from_json( 512 result = sm.SearchResultItem.from_json(
512 self.api_client_mock, self.GENRE_JSON_DATA) 513 self.api_client_mock, self.GENRE_JSON_DATA
514 )
513 assert not result.is_song 515 assert not result.is_song
514 assert not result.is_artist 516 assert not result.is_artist
515 assert not result.is_composer 517 assert not result.is_composer
@@ -518,7 +520,8 @@ class TestSearchResultItem(TestCase):
518 def test_fails_if_unknown(self): 520 def test_fails_if_unknown(self):
519 with self.assertRaises(NotImplementedError): 521 with self.assertRaises(NotImplementedError):
520 sm.SearchResultItem.from_json( 522 sm.SearchResultItem.from_json(
521 self.api_client_mock, self.UNKNOWN_JSON_DATA) 523 self.api_client_mock, self.UNKNOWN_JSON_DATA
524 )
522 525
523 def test_interface(self): 526 def test_interface(self):
524 result = sm.SearchResultItem(self.api_client_mock) 527 result = sm.SearchResultItem(self.api_client_mock)
@@ -533,44 +536,51 @@ class TestArtistSearchResultItem(TestCase):
533 "artistName": "artist_name_mock", 536 "artistName": "artist_name_mock",
534 "musicToken": "R0000000", 537 "musicToken": "R0000000",
535 "likelyMatch": False, 538 "likelyMatch": False,
536 "score": 100 539 "score": 100,
537 } 540 }
538 541
539 COMPOSER_JSON_DATA = { 542 COMPOSER_JSON_DATA = {
540 "artistName": "composer_name_mock", 543 "artistName": "composer_name_mock",
541 "musicToken": "C0000000", 544 "musicToken": "C0000000",
542 "likelyMatch": False, 545 "likelyMatch": False,
543 "score": 100 546 "score": 100,
544 } 547 }
545 548
546 def setUp(self): 549 def setUp(self):
547 self.api_client_mock = Mock(spec=APIClient) 550 self.api_client_mock = Mock(spec=APIClient)
548 self.api_client_mock.default_audio_quality = \ 551 self.api_client_mock.default_audio_quality = (
549 APIClient.HIGH_AUDIO_QUALITY 552 APIClient.HIGH_AUDIO_QUALITY
553 )
550 554
551 def test_repr(self): 555 def test_repr(self):
552 result = sm.SearchResultItem.from_json( 556 result = sm.SearchResultItem.from_json(
553 self.api_client_mock, self.ARTIST_JSON_DATA) 557 self.api_client_mock, self.ARTIST_JSON_DATA
558 )
554 expected = ( 559 expected = (
555 "ArtistSearchResultItem(artist='artist_name_mock', " 560 "ArtistSearchResultItem(artist='artist_name_mock', "
556 "likely_match=False, score=100, token='R0000000')") 561 "likely_match=False, score=100, token='R0000000')"
562 )
557 self.assertEqual(expected, repr(result)) 563 self.assertEqual(expected, repr(result))
558 564
559 result = sm.SearchResultItem.from_json( 565 result = sm.SearchResultItem.from_json(
560 self.api_client_mock, self.COMPOSER_JSON_DATA) 566 self.api_client_mock, self.COMPOSER_JSON_DATA
567 )
561 expected = ( 568 expected = (
562 "ArtistSearchResultItem(artist='composer_name_mock', " 569 "ArtistSearchResultItem(artist='composer_name_mock', "
563 "likely_match=False, score=100, token='C0000000')") 570 "likely_match=False, score=100, token='C0000000')"
571 )
564 self.assertEqual(expected, repr(result)) 572 self.assertEqual(expected, repr(result))
565 573
566 def test_create_station(self): 574 def test_create_station(self):
567 result = sm.SearchResultItem.from_json( 575 result = sm.SearchResultItem.from_json(
568 self.api_client_mock, self.ARTIST_JSON_DATA) 576 self.api_client_mock, self.ARTIST_JSON_DATA
577 )
569 result._api_client.create_station = Mock() 578 result._api_client.create_station = Mock()
570 579
571 result.create_station() 580 result.create_station()
572 result._api_client.create_station.assert_called_with( 581 result._api_client.create_station.assert_called_with(
573 artist_token=result.token) 582 artist_token=result.token
583 )
574 584
575 585
576class TestSongSearchResultItem(TestCase): 586class TestSongSearchResultItem(TestCase):
@@ -579,30 +589,35 @@ class TestSongSearchResultItem(TestCase):
579 "artistName": "artist_name_mock", 589 "artistName": "artist_name_mock",
580 "musicToken": "S0000000", 590 "musicToken": "S0000000",
581 "songName": "song_name_mock", 591 "songName": "song_name_mock",
582 "score": 100 592 "score": 100,
583 } 593 }
584 594
585 def setUp(self): 595 def setUp(self):
586 self.api_client_mock = Mock(spec=APIClient) 596 self.api_client_mock = Mock(spec=APIClient)
587 self.api_client_mock.default_audio_quality = \ 597 self.api_client_mock.default_audio_quality = (
588 APIClient.HIGH_AUDIO_QUALITY 598 APIClient.HIGH_AUDIO_QUALITY
599 )
589 600
590 def test_repr(self): 601 def test_repr(self):
591 result = sm.SearchResultItem.from_json( 602 result = sm.SearchResultItem.from_json(
592 self.api_client_mock, self.SONG_JSON_DATA) 603 self.api_client_mock, self.SONG_JSON_DATA
604 )
593 expected = ( 605 expected = (
594 "SongSearchResultItem(artist='artist_name_mock', score=100, " 606 "SongSearchResultItem(artist='artist_name_mock', score=100, "
595 "song_name='song_name_mock', token='S0000000')") 607 "song_name='song_name_mock', token='S0000000')"
608 )
596 self.assertEqual(expected, repr(result)) 609 self.assertEqual(expected, repr(result))
597 610
598 def test_create_station(self): 611 def test_create_station(self):
599 result = sm.SearchResultItem.from_json( 612 result = sm.SearchResultItem.from_json(
600 self.api_client_mock, self.SONG_JSON_DATA) 613 self.api_client_mock, self.SONG_JSON_DATA
614 )
601 result._api_client.create_station = Mock() 615 result._api_client.create_station = Mock()
602 616
603 result.create_station() 617 result.create_station()
604 result._api_client.create_station.assert_called_with( 618 result._api_client.create_station.assert_called_with(
605 track_token=result.token) 619 track_token=result.token
620 )
606 621
607 622
608class TestGenreStationSearchResultItem(TestCase): 623class TestGenreStationSearchResultItem(TestCase):
@@ -610,61 +625,69 @@ class TestGenreStationSearchResultItem(TestCase):
610 GENRE_JSON_DATA = { 625 GENRE_JSON_DATA = {
611 "stationName": "station_name_mock", 626 "stationName": "station_name_mock",
612 "musicToken": "G0000000", 627 "musicToken": "G0000000",
613 "score": 100 628 "score": 100,
614 } 629 }
615 630
616 def setUp(self): 631 def setUp(self):
617 self.api_client_mock = Mock(spec=APIClient) 632 self.api_client_mock = Mock(spec=APIClient)
618 self.api_client_mock.default_audio_quality = \ 633 self.api_client_mock.default_audio_quality = (
619 APIClient.HIGH_AUDIO_QUALITY 634 APIClient.HIGH_AUDIO_QUALITY
635 )
620 636
621 def test_repr(self): 637 def test_repr(self):
622 result = sm.SearchResultItem.from_json( 638 result = sm.SearchResultItem.from_json(
623 self.api_client_mock, self.GENRE_JSON_DATA) 639 self.api_client_mock, self.GENRE_JSON_DATA
640 )
624 expected = ( 641 expected = (
625 "GenreStationSearchResultItem(score=100, " 642 "GenreStationSearchResultItem(score=100, "
626 "station_name='station_name_mock', token='G0000000')") 643 "station_name='station_name_mock', token='G0000000')"
644 )
627 self.assertEqual(expected, repr(result)) 645 self.assertEqual(expected, repr(result))
628 646
629 def test_create_station(self): 647 def test_create_station(self):
630 result = sm.SearchResultItem.from_json( 648 result = sm.SearchResultItem.from_json(
631 self.api_client_mock, self.GENRE_JSON_DATA) 649 self.api_client_mock, self.GENRE_JSON_DATA
650 )
632 result._api_client.create_station = Mock() 651 result._api_client.create_station = Mock()
633 652
634 result.create_station() 653 result.create_station()
635 result._api_client.create_station.assert_called_with( 654 result._api_client.create_station.assert_called_with(
636 search_token=result.token) 655 search_token=result.token
656 )
637 657
638 658
639class TestSearchResult(TestCase): 659class TestSearchResult(TestCase):
640 660
641 JSON_DATA = { 661 JSON_DATA = {
642 'nearMatchesAvailable': True, 662 "nearMatchesAvailable": True,
643 'explanation': '', 663 "explanation": "",
644 'songs': [{ 664 "songs": [
645 'artistName': 'song_artist_mock', 665 {
646 'musicToken': 'S0000000', 666 "artistName": "song_artist_mock",
647 'songName': 'song_name_mock', 667 "musicToken": "S0000000",
648 'score': 100 668 "songName": "song_name_mock",
649 }], 669 "score": 100,
650 'artists': [{ 670 }
651 'artistName': 'artist_mock', 671 ],
652 'musicToken': 'R000000', 672 "artists": [
653 'likelyMatch': False, 673 {
654 'score': 80 674 "artistName": "artist_mock",
655 }], 675 "musicToken": "R000000",
656 'genreStations': [{ 676 "likelyMatch": False,
657 'musicToken': 'G0000', 677 "score": 80,
658 'stationName': 'station_mock', 678 }
659 'score': 50 679 ],
660 }] 680 "genreStations": [
681 {"musicToken": "G0000", "stationName": "station_mock", "score": 50}
682 ],
661 } 683 }
662 684
663 def setUp(self): 685 def setUp(self):
664 api_client_mock = Mock(spec=APIClient) 686 api_client_mock = Mock(spec=APIClient)
665 api_client_mock.default_audio_quality = APIClient.HIGH_AUDIO_QUALITY 687 api_client_mock.default_audio_quality = APIClient.HIGH_AUDIO_QUALITY
666 self.result = sm.SearchResult.from_json( 688 self.result = sm.SearchResult.from_json(
667 api_client_mock, self.JSON_DATA) 689 api_client_mock, self.JSON_DATA
690 )
668 691
669 def test_repr(self): 692 def test_repr(self):
670 expected = ( 693 expected = (
@@ -676,7 +699,8 @@ class TestSearchResult(TestCase):
676 "nearest_matches_available=True, " 699 "nearest_matches_available=True, "
677 "songs=[SongSearchResultItem(artist='song_artist_mock', " 700 "songs=[SongSearchResultItem(artist='song_artist_mock', "
678 "score=100, song_name='song_name_mock', " 701 "score=100, song_name='song_name_mock', "
679 "token='S0000000')])") 702 "token='S0000000')])"
703 )
680 self.assertEqual(expected, repr(self.result)) 704 self.assertEqual(expected, repr(self.result))
681 705
682 706
@@ -684,9 +708,7 @@ class TestGenreStationList(TestCase):
684 708
685 TEST_DATA = { 709 TEST_DATA = {
686 "checksum": "bar", 710 "checksum": "bar",
687 "categories": [ 711 "categories": [{"categoryName": "foo", "stations": []},],
688 {"categoryName": "foo", "stations": []},
689 ]
690 } 712 }
691 713
692 def test_has_changed(self): 714 def test_has_changed(self):
@@ -706,7 +728,8 @@ class TestGenreStation(TestCase):
706 genre_station = stm.GenreStation.from_json(api_client, self.TEST_DATA) 728 genre_station = stm.GenreStation.from_json(api_client, self.TEST_DATA)
707 729
708 with self.assertRaisesRegex( 730 with self.assertRaisesRegex(
709 NotImplementedError, "Genre stations do not have playlists.*"): 731 NotImplementedError, "Genre stations do not have playlists.*"
732 ):
710 genre_station.get_playlist() 733 genre_station.get_playlist()
711 734
712 735
@@ -750,7 +773,6 @@ class TestBookmark(TestCase):
750 773
751 774
752class TestPandoraType(TestCase): 775class TestPandoraType(TestCase):
753
754 def test_it_can_be_built_from_a_model(self): 776 def test_it_can_be_built_from_a_model(self):
755 pt = plm.PandoraType.from_model(None, "TR") 777 pt = plm.PandoraType.from_model(None, "TR")
756 self.assertIs(plm.PandoraType.TRACK, pt) 778 self.assertIs(plm.PandoraType.TRACK, pt)
@@ -765,7 +787,6 @@ class TestPandoraType(TestCase):
765 787
766 788
767class TestSyntheticField(TestCase): 789class TestSyntheticField(TestCase):
768
769 def test_interface(self): 790 def test_interface(self):
770 sf = m.SyntheticField(field="foo") 791 sf = m.SyntheticField(field="foo")
771 792
diff --git a/tests/test_pandora/test_transport.py b/tests/test_pandora/test_transport.py
index 851fa06..28ca6b2 100644
--- a/tests/test_pandora/test_transport.py
+++ b/tests/test_pandora/test_transport.py
@@ -16,12 +16,12 @@ class SysCallError(Exception):
16 16
17 17
18class TestTransport(TestCase): 18class TestTransport(TestCase):
19
20 def test_test_url_should_return_true_if_request_okay(self): 19 def test_test_url_should_return_true_if_request_okay(self):
21 transport = t.APITransport(Mock()) 20 transport = t.APITransport(Mock())
22 transport._http = Mock() 21 transport._http = Mock()
23 transport._http.head.return_value = Mock( 22 transport._http.head.return_value = Mock(
24 status_code=requests.codes.not_found) 23 status_code=requests.codes.not_found
24 )
25 25
26 self.assertFalse(transport.test_url("foo")) 26 self.assertFalse(transport.test_url("foo"))
27 27
@@ -34,7 +34,8 @@ class TestTransport(TestCase):
34 34
35 time.sleep = Mock() 35 time.sleep = Mock()
36 client.transport._make_http_request = Mock( 36 client.transport._make_http_request = Mock(
37 side_effect=SysCallError("error_mock")) 37 side_effect=SysCallError("error_mock")
38 )
38 client.transport._start_request = Mock() 39 client.transport._start_request = Mock()
39 40
40 client("method") 41 client("method")
@@ -48,7 +49,8 @@ class TestTransport(TestCase):
48 49
49 time.sleep = Mock() 50 time.sleep = Mock()
50 client.transport._make_http_request = Mock( 51 client.transport._make_http_request = Mock(
51 side_effect=PandoraException("error_mock")) 52 side_effect=PandoraException("error_mock")
53 )
52 client.transport._start_request = Mock() 54 client.transport._start_request = Mock()
53 55
54 client("method") 56 client("method")
@@ -62,7 +64,8 @@ class TestTransport(TestCase):
62 64
63 time.sleep = Mock() 65 time.sleep = Mock()
64 client.transport._make_http_request = Mock( 66 client.transport._make_http_request = Mock(
65 side_effect=InvalidAuthToken("error_mock")) 67 side_effect=InvalidAuthToken("error_mock")
68 )
66 client.transport._start_request = Mock() 69 client.transport._start_request = Mock()
67 70
68 client._authenticate = Mock() 71 client._authenticate = Mock()
@@ -82,11 +85,11 @@ class TestTransport(TestCase):
82 transport._http.post.return_value = http_result 85 transport._http.post.return_value = http_result
83 86
84 self.assertEqual( 87 self.assertEqual(
85 "bar", transport(t.APITransport.NO_ENCRYPT[0], foo="bar")) 88 "bar", transport(t.APITransport.NO_ENCRYPT[0], foo="bar")
89 )
86 90
87 91
88class TestTransportSetters(TestCase): 92class TestTransportSetters(TestCase):
89
90 def setUp(self): 93 def setUp(self):
91 self.cryptor = Mock() 94 self.cryptor = Mock()
92 self.transport = t.APITransport(self.cryptor) 95 self.transport = t.APITransport(self.cryptor)
@@ -94,27 +97,29 @@ class TestTransportSetters(TestCase):
94 def test_set_partner(self): 97 def test_set_partner(self):
95 self.cryptor.decrypt_sync_time.return_value = 456 98 self.cryptor.decrypt_sync_time.return_value = 456
96 99
97 self.transport.set_partner({ 100 self.transport.set_partner(
98 "syncTime": "123", 101 {
99 "partnerAuthToken": "partner_auth_token", 102 "syncTime": "123",
100 "partnerId": "partner_id", 103 "partnerAuthToken": "partner_auth_token",
101 }) 104 "partnerId": "partner_id",
105 }
106 )
102 107
103 self.cryptor.decrypt_sync_time.assert_called_with("123") 108 self.cryptor.decrypt_sync_time.assert_called_with("123")
104 self.assertEqual("partner_auth_token", self.transport.auth_token) 109 self.assertEqual("partner_auth_token", self.transport.auth_token)
105 self.assertEqual("partner_id", self.transport.partner_id) 110 self.assertEqual("partner_id", self.transport.partner_id)
106 self.assertEqual( 111 self.assertEqual(
107 "partner_auth_token", self.transport.partner_auth_token) 112 "partner_auth_token", self.transport.partner_auth_token
113 )
108 114
109 self.transport.start_time = 10 115 self.transport.start_time = 10
110 with patch.object(time, "time", return_value=30): 116 with patch.object(time, "time", return_value=30):
111 self.assertEqual(476, self.transport.sync_time) 117 self.assertEqual(476, self.transport.sync_time)
112 118
113 def test_set_user(self): 119 def test_set_user(self):
114 self.transport.set_user({ 120 self.transport.set_user(
115 "userId": "user", 121 {"userId": "user", "userAuthToken": "auth",}
116 "userAuthToken": "auth", 122 )
117 })
118 123
119 self.assertEqual("user", self.transport.user_id) 124 self.assertEqual("user", self.transport.user_id)
120 self.assertEqual("auth", self.transport.user_auth_token) 125 self.assertEqual("auth", self.transport.user_auth_token)
@@ -126,7 +131,6 @@ class TestTransportSetters(TestCase):
126 131
127 132
128class TestDelayExponential(TestCase): 133class TestDelayExponential(TestCase):
129
130 def test_fixed_delay(self): 134 def test_fixed_delay(self):
131 self.assertEqual(8, t.delay_exponential(2, 2, 3)) 135 self.assertEqual(8, t.delay_exponential(2, 2, 3))
132 136
@@ -143,7 +147,6 @@ class TestDelayExponential(TestCase):
143 147
144 148
145class TestRetries(TestCase): 149class TestRetries(TestCase):
146
147 def test_no_retries_returns_none(self): 150 def test_no_retries_returns_none(self):
148 @t.retries(0) 151 @t.retries(0)
149 def foo(): 152 def foo():
@@ -178,7 +181,6 @@ class TestParseResponse(TestCase):
178 181
179 182
180class TestTransportRequestPrep(TestCase): 183class TestTransportRequestPrep(TestCase):
181
182 def setUp(self): 184 def setUp(self):
183 self.cryptor = Mock() 185 self.cryptor = Mock()
184 self.transport = t.APITransport(self.cryptor) 186 self.transport = t.APITransport(self.cryptor)
@@ -207,7 +209,8 @@ class TestTransportRequestPrep(TestCase):
207 209
208 self.transport._http = http 210 self.transport._http = http
209 res = self.transport._make_http_request( 211 res = self.transport._make_http_request(
210 "/url", b"data", {"a": None, "b": "c"}) 212 "/url", b"data", {"a": None, "b": "c"}
213 )
211 214
212 http.post.assert_called_with("/url", data=b"data", params={"b": "c"}) 215 http.post.assert_called_with("/url", data=b"data", params={"b": "c"})
213 retval.raise_for_status.assert_called_with() 216 retval.raise_for_status.assert_called_with()
@@ -237,7 +240,8 @@ class TestTransportRequestPrep(TestCase):
237 240
238 with patch.object(time, "time", return_value=20): 241 with patch.object(time, "time", return_value=20):
239 val = self.transport._build_data( 242 val = self.transport._build_data(
240 t.APITransport.NO_ENCRYPT[0], {"a": "b", "c": None}) 243 t.APITransport.NO_ENCRYPT[0], {"a": "b", "c": None}
244 )
241 245
242 val = json.loads(val) 246 val = json.loads(val)
243 self.assertEqual("b", val["a"]) 247 self.assertEqual("b", val["a"])
@@ -247,7 +251,6 @@ class TestTransportRequestPrep(TestCase):
247 251
248# All Cryptor implementations must pass these test cases unmodified 252# All Cryptor implementations must pass these test cases unmodified
249class CommonCryptorTestCases: 253class CommonCryptorTestCases:
250
251 def test_decrypt_invalid_padding(self): 254 def test_decrypt_invalid_padding(self):
252 with self.assertRaises(ValueError): 255 with self.assertRaises(ValueError):
253 data = b"12345678\x00" 256 data = b"12345678\x00"
@@ -267,7 +270,6 @@ class CommonCryptorTestCases:
267 270
268 271
269class TestPurePythonBlowfishCryptor(TestCase, CommonCryptorTestCases): 272class TestPurePythonBlowfishCryptor(TestCase, CommonCryptorTestCases):
270
271 def setUp(self): 273 def setUp(self):
272 # Ugh... blowfish can't even be *imported* in python2 274 # Ugh... blowfish can't even be *imported* in python2
273 if not t.blowfish: 275 if not t.blowfish:
@@ -288,7 +290,6 @@ class TestEncryptor(TestCase):
288 ENCODED_TIME = "31353037343131313539" 290 ENCODED_TIME = "31353037343131313539"
289 291
290 class NoopCrypto: 292 class NoopCrypto:
291
292 def __init__(self, key): 293 def __init__(self, key):
293 pass 294 pass
294 295
@@ -303,14 +304,17 @@ class TestEncryptor(TestCase):
303 304
304 def test_decrypt(self): 305 def test_decrypt(self):
305 self.assertEqual( 306 self.assertEqual(
306 {"foo": "bar"}, self.cryptor.decrypt(self.ENCODED_JSON)) 307 {"foo": "bar"}, self.cryptor.decrypt(self.ENCODED_JSON)
308 )
307 309
308 def test_encrypt(self): 310 def test_encrypt(self):
309 self.assertEqual( 311 self.assertEqual(
310 self.ENCODED_JSON.encode("ascii"), 312 self.ENCODED_JSON.encode("ascii"),
311 self.cryptor.encrypt(self.UNENCODED_JSON)) 313 self.cryptor.encrypt(self.UNENCODED_JSON),
314 )
312 315
313 def test_decrypt_sync_time(self): 316 def test_decrypt_sync_time(self):
314 self.assertEqual( 317 self.assertEqual(
315 self.EXPECTED_TIME, 318 self.EXPECTED_TIME,
316 self.cryptor.decrypt_sync_time(self.ENCODED_TIME)) 319 self.cryptor.decrypt_sync_time(self.ENCODED_TIME),
320 )
diff --git a/tests/test_pydora/test_utils.py b/tests/test_pydora/test_utils.py
index fc33a40..9d3bc78 100644
--- a/tests/test_pydora/test_utils.py
+++ b/tests/test_pydora/test_utils.py
@@ -10,23 +10,26 @@ from pydora.utils import iterate_forever
10 10
11 11
12class TestIterateForever(TestCase): 12class TestIterateForever(TestCase):
13
14 def setUp(self): 13 def setUp(self):
15 self.transport = Mock(side_effect=[InvalidAuthToken(), None]) 14 self.transport = Mock(side_effect=[InvalidAuthToken(), None])
16 self.client = APIClient(self.transport, None, None, None, None) 15 self.client = APIClient(self.transport, None, None, None, None)
17 self.client._authenticate = Mock() 16 self.client._authenticate = Mock()
18 17
19 def test_handle_missing_params_exception_due_to_missing_ad_tokens(self): 18 def test_handle_missing_params_exception_due_to_missing_ad_tokens(self):
20 with patch.object(APIClient, 'get_playlist') as get_playlist_mock: 19 with patch.object(APIClient, "get_playlist") as get_playlist_mock:
21 admock = patch.object( 20 admock = patch.object(
22 APIClient, 'register_ad', 21 APIClient,
23 side_effect=ParameterMissing("ParameterMissing")) 22 "register_ad",
23 side_effect=ParameterMissing("ParameterMissing"),
24 )
24 25
25 with admock: 26 with admock:
26 station = Station.from_json( 27 station = Station.from_json(
27 self.client, {'stationToken': 'token_mock'}) 28 self.client, {"stationToken": "token_mock"}
29 )
28 ad_mock = AdItem.from_json( 30 ad_mock = AdItem.from_json(
29 self.client, {'station_id': 'id_mock'}) 31 self.client, {"station_id": "id_mock"}
32 )
30 get_playlist_mock.return_value = iter([ad_mock]) 33 get_playlist_mock.return_value = iter([ad_mock])
31 34
32 station_iter = iterate_forever(station.get_playlist) 35 station_iter = iterate_forever(station.get_playlist)
@@ -36,15 +39,19 @@ class TestIterateForever(TestCase):
36 39
37 def test_reraise_missing_params_exception(self): 40 def test_reraise_missing_params_exception(self):
38 plmock = patch.object( 41 plmock = patch.object(
39 APIClient, 'get_playlist', 42 APIClient,
40 side_effect=ParameterMissing("ParameterMissing")) 43 "get_playlist",
44 side_effect=ParameterMissing("ParameterMissing"),
45 )
41 46
42 with plmock as get_playlist_mock: 47 with plmock as get_playlist_mock:
43 with self.assertRaises(ParameterMissing): 48 with self.assertRaises(ParameterMissing):
44 station = Station.from_json( 49 station = Station.from_json(
45 self.client, {'stationToken': 'token_mock'}) 50 self.client, {"stationToken": "token_mock"}
51 )
46 track_mock = PlaylistItem.from_json( 52 track_mock = PlaylistItem.from_json(
47 self.client, {'token': 'token_mock'}) 53 self.client, {"token": "token_mock"}
54 )
48 get_playlist_mock.return_value = iter([track_mock]) 55 get_playlist_mock.return_value = iter([track_mock])
49 56
50 station_iter = iterate_forever(station.get_playlist) 57 station_iter = iterate_forever(station.get_playlist)