From 80f391761c9f4a8eb4ab17803a3e516c58540c1c Mon Sep 17 00:00:00 2001 From: Jace Browning Date: Tue, 31 Jan 2017 15:00:38 -0500 Subject: [PATCH] Support authentication tokens to construct networks Closes #193 --- .gitignore | 2 ++ pylast/__init__.py | 30 ++++++++++++++++++++---------- tests/test_pylast.py | 14 ++++++++++++++ 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 4535a42..51a71f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # User Credentials test_pylast.yaml +.envrc # Byte-compiled / optimized / DLL files __pycache__/ @@ -12,6 +13,7 @@ __pycache__/ # Distribution / packaging .Python env/ +.venv/ build/ develop-eggs/ dist/ diff --git a/pylast/__init__.py b/pylast/__init__.py index 1a46682..5f96c88 100644 --- a/pylast/__init__.py +++ b/pylast/__init__.py @@ -211,7 +211,8 @@ class _Network(object): def __init__( self, name, homepage, ws_server, api_key, api_secret, session_key, - submission_server, username, password_hash, domain_names, urls): + submission_server, username, password_hash, domain_names, urls, + token=None): """ name: the name of the network homepage: the homepage URL @@ -227,6 +228,7 @@ class _Network(object): domain_names: a dict mapping each DOMAIN_* value to a string domain name urls: a dict mapping types to URLs + token: an authentication token to retrieve a session if username and password_hash were provided and not session_key, session_key will be generated automatically when needed. @@ -257,6 +259,12 @@ class _Network(object): self.last_call_time = 0 self.limit_rate = False + # Load session_key from authentication token if provided + if token and not self.session_key: + sk_gen = SessionKeyGenerator(self) + self.session_key = sk_gen.get_web_auth_session_key( + url=None, token=token) + # Generate a session_key if necessary if ((self.api_key and self.api_secret) and not self.session_key and (self.username and self.password_hash)): @@ -886,7 +894,7 @@ class LastFMNetwork(_Network): def __init__( self, api_key="", api_secret="", session_key="", username="", - password_hash=""): + password_hash="", token=""): _Network.__init__( self, name="Last.fm", @@ -898,6 +906,7 @@ class LastFMNetwork(_Network): submission_server="http://post.audioscrobbler.com:80/", username=username, password_hash=password_hash, + token=token, domain_names={ DOMAIN_ENGLISH: 'www.last.fm', DOMAIN_GERMAN: 'www.lastfm.de', @@ -936,7 +945,7 @@ class LastFMNetwork(_Network): def get_lastfm_network( api_key="", api_secret="", session_key="", username="", - password_hash=""): + password_hash="", token=""): """ Returns a preconfigured _Network object for Last.fm @@ -946,12 +955,13 @@ def get_lastfm_network( username: a username of a valid user password_hash: the output of pylast.md5(password) where password is the user's password + token: an authentication token to retrieve a session if username and password_hash were provided and not session_key, session_key will be generated automatically when needed. - Either a valid session_key or a combination of username and password_hash - must be present for scrobbling. + Either a valid session_key, a combination of username and password_hash, + or token must be present for scrobbling. Most read-only webservices only require an api_key and an api_secret, see about obtaining them from: @@ -961,7 +971,7 @@ def get_lastfm_network( _deprecation_warning("Create a LastFMNetwork object instead") return LastFMNetwork( - api_key, api_secret, session_key, username, password_hash) + api_key, api_secret, session_key, username, password_hash, token) class LibreFMNetwork(_Network): @@ -1304,7 +1314,7 @@ class SessionKeyGenerator(object): return url - def get_web_auth_session_key(self, url): + def get_web_auth_session_key(self, url, token=""): """ Retrieves the session key of a web authorization process by its url. """ @@ -1312,9 +1322,8 @@ class SessionKeyGenerator(object): if url in self.web_auth_tokens.keys(): token = self.web_auth_tokens[url] else: - # That's going to raise a WSError of an unauthorized token when the - # request is executed. - token = "" + # This will raise a WSError if token is blank or unauthorized + token = token request = _Request(self.network, 'auth.getSession', {'token': token}) @@ -1344,6 +1353,7 @@ class SessionKeyGenerator(object): return _extract(doc, "key") + TopItem = collections.namedtuple("TopItem", ["item", "weight"]) SimilarItem = collections.namedtuple("SimilarItem", ["item", "match"]) LibraryItem = collections.namedtuple( diff --git a/tests/test_pylast.py b/tests/test_pylast.py index 868983d..a1b6fa5 100755 --- a/tests/test_pylast.py +++ b/tests/test_pylast.py @@ -2160,6 +2160,20 @@ class TestPyLast(unittest.TestCase): # Assert self.assertEqual(mbid, None) + def test_init_with_token(self): + # Arrange/Act + try: + pylast.LastFMNetwork( + api_key=self.__class__.secrets["api_key"], + api_secret=self.__class__.secrets["api_secret"], + token="invalid", + ) + except pylast.WSError as exc: + msg = str(exc) + + # Assert + self.assertEqual(msg, "Invalid authentication token supplied") + @flaky(max_runs=5, min_passes=1) class TestPyLastWithLibreFm(unittest.TestCase):