Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Python implementation.

  • Loading branch information...
commit 90004037479510777e61c901e3e97694168953a0 1 parent 48e6391
@ianloic ianloic authored
View
2  .gitignore
@@ -2,3 +2,5 @@
*.pyo
*.pyc
php/examples/rdio-consumer-credentials.php
+python/examples/rdio_consumer_credentials.py
+.idea
View
31 python/README
@@ -1 +1,30 @@
-The Python library isn't available yet.
+rdio-simple for Python
+
+An Rdio client including a built-in OAuth implementation.
+
+This has library only depends on libraries included by default in recent
+versions of Python. It has been tested with Python 2.6.
+
+To install the library simple add the om.py and rdio.py files to your source
+directory.
+
+Usage:
+To use the library just load the Rdio class from the rdio module:
+ from rdio import Rdio
+Create an Rdio instance passing in a tuple with your consumer key and secret:
+ rdio = Rdio(array("consumerkey", "consumersecret"))
+Make API calls with the call(methodname, params) method:
+ rdio.call('get', keys='a254895,a104386')
+Authenticate and authorize with the begin_authentication and
+complete_authentication methods.
+
+The current token (either request or access) is stored in rdio.token as a
+tuple with the token and token secret.
+
+Examples:
+Both examples authenticate and then list the user's playlists. They use
+credentials stored in rdio-consumer-credentials.php.
+ examples/command-line.py
+ examples/web-based.py
+NOTE: web-based.py depends on web.py (http://www.webpy.org/)
+
View
48 python/examples/command-line.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+
+# (c) 2011 Rdio Inc
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# include the parent directory in the Python path
+import sys,os.path
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from rdio import Rdio
+from rdio_consumer_credentials import *
+from urllib2 import HTTPError
+
+# create an instance of the Rdio object with our consumer credentials
+rdio = Rdio((RDIO_CONSUMER_KEY, RDIO_CONSUMER_SECRET))
+
+try:
+ # authenticate against the Rdio service
+ url = rdio.begin_authentication('oob')
+ print 'Go to: ' + url
+ verifier = raw_input('Then enter the code: ').strip()
+ rdio.complete_authentication(verifier)
+
+ # find out what playlists you created
+ myPlaylists = rdio.call('getPlaylists')['result']['owned']
+
+ # list them
+ for playlist in myPlaylists:
+ print '%(shortUrl)s\t%(name)s' % playlist
+except HTTPError, e:
+ # if we have a protocol error, print it
+ print e.read()
View
4 python/examples/rdio_consumer_credentials_EXAMPLE.py
@@ -0,0 +1,4 @@
+# you can get these by signing up for a developer account at:
+# http://developer.rdio.com/
+RDIO_CONSUMER_KEY = ''
+RDIO_CONSUMER_SECRET = ''
View
128 python/examples/web-based.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python
+
+# (c) 2011 Rdio Inc
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# include the parent directory in the Python path
+import sys,os.path
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+# import the rdio-simple library
+from rdio import Rdio
+# and our example credentials
+from rdio_consumer_credentials import *
+
+# import web.py
+import web
+
+import urllib2
+
+urls = (
+ '/', 'root',
+ '/login', 'login',
+ '/callback', 'callback',
+ '/logout', 'logout',
+)
+app = web.application(urls, globals())
+
+class root:
+ def GET(self):
+ access_token = web.cookies().get('at')
+ access_token_secret = web.cookies().get('ats')
+ if access_token and access_token_secret:
+ rdio = Rdio((RDIO_CONSUMER_KEY, RDIO_CONSUMER_SECRET),
+ (access_token, access_token_secret))
+ # make sure that we can make an authenticated call
+
+ try:
+ currentUser = rdio.call('currentUser')['result']
+ except urllib2.HTTPError:
+ # this almost certainly means that authentication has been revoked for the app. log out.
+ raise web.seeother('/logout')
+
+ myPlaylists = rdio.call('getPlaylists')['result']['owned']
+
+ response = '''
+ <html><head><title>Rdio-Simple Example</title></head><body>
+ <p>%s's playlists:</p>
+ <ul>
+ ''' % currentUser['firstName']
+ for playlist in myPlaylists:
+ response += '''<li><a href="%(shortUrl)s">%(name)s</a></li>''' % playlist
+ response += '''</ul><a href="/logout">Log out of Rdio</a></body></html>'''
+ return response
+ else:
+ return '''
+ <html><head><title>Rdio-Simple Example</title></head><body>
+ <a href="/login">Log into Rdio</a>
+ </body></html>
+ '''
+
+class login:
+ def GET(self):
+ # clear all of our auth cookies
+ web.setcookie('at', '', expires=-1)
+ web.setcookie('ats', '', expires=-1)
+ web.setcookie('rt', '', expires=-1)
+ web.setcookie('rts', '', expires=-1)
+ # begin the authentication process
+ rdio = Rdio((RDIO_CONSUMER_KEY, RDIO_CONSUMER_SECRET))
+ url = rdio.begin_authentication(callback_url = web.ctx.homedomain+'/callback')
+ # save our request token in cookies
+ web.setcookie('rt', rdio.token[0], expires=60*60*24) # expires in one day
+ web.setcookie('rts', rdio.token[1], expires=60*60*24) # expires in one day
+ # go to Rdio to authenticate the app
+ raise web.seeother(url)
+
+class callback:
+ def GET(self):
+ # get the state from cookies and the query string
+ request_token = web.cookies().get('rt')
+ request_token_secret = web.cookies().get('rts')
+ verifier = web.input()['oauth_verifier']
+ # make sure we have everything we need
+ if request_token and request_token_secret and verifier:
+ # exchange the verifier and request token for an access token
+ rdio = Rdio((RDIO_CONSUMER_KEY, RDIO_CONSUMER_SECRET),
+ (request_token, request_token_secret))
+ rdio.complete_authentication(verifier)
+ # save the access token in cookies (and discard the request token)
+ web.setcookie('at', rdio.token[0], expires=60*60*24*14) # expires in two weeks
+ web.setcookie('ats', rdio.token[1], expires=60*60*24*14) # expires in two weeks
+ web.setcookie('rt', '', expires=-1)
+ web.setcookie('rts', '', expires=-1)
+ # go to the home page
+ raise web.seeother('/')
+ else:
+ # we're missing something important
+ raise web.seeother('/logout')
+
+class logout:
+ def GET(self):
+ # clear all of our auth cookies
+ web.setcookie('at', '', expires=-1)
+ web.setcookie('ats', '', expires=-1)
+ web.setcookie('rt', '', expires=-1)
+ web.setcookie('rts', '', expires=-1)
+ # and go to the homepage
+ raise web.seeother('/')
+
+
+if __name__ == "__main__":
+ app.run()
View
91 python/om.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+
+"""A simple OAuth client implementation. Do less better.
+Here are the restrictions:
+ - only HMAC-SHA1 is supported
+ - only WWW-Authentiate form signatures are generated
+
+To sign a request:
+ auth = om((consumer_key,consumer_secret), url, params)
+ # send Authorization: <auth>
+ # when POSTing <params> to <url>
+Optional additional arguments are:
+ token = (oauth_token, oauth_token_secret)
+ method = "POST"
+ realm = "Realm-for-authorization-header"
+"""
+
+import time, random, hmac, hashlib, urllib, binascii, urlparse
+
+def om(consumer, url, post_params, token=None, method='POST', realm=None):
+ """A one-shot simple OAuth signature generator"""
+
+ # the method must be upper-case
+ method = method.upper()
+
+ # turn the POST params into a list of tuples if it's not already
+ if isinstance(post_params, list):
+ params = list(post_params) # copy the params list since we'll be messing with it
+ else:
+ params = post_params.items()
+
+ # normalize the URL
+ parts = urlparse.urlparse(url)
+ scheme, netloc, path, query = parts[:4]
+ # Exclude default port numbers.
+ if scheme == 'http' and netloc[-3:] == ':80':
+ netloc = netloc[:-3]
+ elif scheme == 'https' and netloc[-4:] == ':443':
+ netloc = netloc[:-4]
+ normalized_url = '%s://%s%s' % (scheme, netloc, path)
+
+ # add query-string params (if any) to the params list
+ params.extend(urlparse.parse_qsl(query))
+
+ # add OAuth params
+ params.extend([
+ ('oauth_version', '1.0'),
+ ('oauth_timestamp', str(int(time.time()))),
+ ('oauth_nonce', str(random.randint(0, 1000000))),
+ ('oauth_signature_method', 'HMAC-SHA1'),
+ ('oauth_consumer_key', consumer[0]),
+ ])
+
+ # the consumer secret is the first half of the HMAC-SHA1 key
+ hmac_key = consumer[1] + '&'
+
+ if token is not None:
+ # include a token in params
+ params.append(('oauth_token', token[0]))
+ # and the token secret in the HMAC-SHA1 key
+ hmac_key += token[1]
+
+ # Sort lexicographically, first after key, then after value.
+ params.sort()
+ # UTF-8 and escape the key/value pairs
+ def escape(s): return urllib.quote(unicode(s).encode('utf-8'), safe='~')
+ params = [(escape(k), escape(v)) for k,v in params]
+ # Combine key value pairs into a string.
+ normalized_params = '&'.join(['%s=%s' % (k, v) for k, v in params])
+
+ # build the signature base string
+ signature_base_string = (escape(method) +
+ '&' + escape(normalized_url) +
+ '&' + escape(normalized_params))
+
+ # HMAC-SHA1
+ hashed = hmac.new(hmac_key, signature_base_string, hashlib.sha1)
+
+ # Calculate the digest base 64.
+ oauth_signature = binascii.b2a_base64(hashed.digest())[:-1]
+
+ # Build the Authorization header
+ authorization_params = [('oauth_signature', oauth_signature)]
+ if realm is not None:
+ authorization_params.insert(0, ('realm', escape(realm)))
+ oauth_params = frozenset(('oauth_version', 'oauth_timestamp', 'oauth_nonce',
+ 'oauth_signature_method', 'oauth_signature',
+ 'oauth_consumer_key', 'oauth_token'))
+ authorization_params.extend([p for p in params if p[0] in oauth_params])
+
+ return 'OAuth ' + (', '.join(['%s="%s"'%p for p in authorization_params]))
View
66 python/rdio.py
@@ -0,0 +1,66 @@
+# (c) 2011 Rdio Inc
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from om import om
+import urllib2, urllib
+from urlparse import parse_qsl
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+class Rdio:
+ def __init__(self, consumer, token=None):
+ self.__consumer = consumer
+ self.token = token
+
+ def __signed_post(self, url, params):
+ auth = om(self.__consumer, url, params, self.token)
+ req = urllib2.Request(url, urllib.urlencode(params), {'Authorization': auth})
+ res = urllib2.urlopen(req)
+ return res.read()
+
+ def begin_authentication(self, callback_url):
+ # request a request token from the server
+ response = self.__signed_post('http://api.rdio.com/oauth/request_token',
+ {'oauth_callback': callback_url})
+ # parse the response
+ parsed = dict(parse_qsl(response))
+ # save the token
+ self.token = (parsed['oauth_token'], parsed['oauth_token_secret'])
+ # return an URL that the user can use to authorize this application
+ return parsed['login_url'] + '?oauth_token=' + parsed['oauth_token']
+
+ def complete_authentication(self, verifier):
+ # request an access token
+ response = self.__signed_post('http://api.rdio.com/oauth/access_token',
+ {'oauth_verifier': verifier})
+ # parse the response
+ parsed = dict(parse_qsl(response))
+ # save the token
+ self.token = (parsed['oauth_token'], parsed['oauth_token_secret'])
+
+ def call(self, method, params=dict()):
+ # make a copy of the dict
+ params = dict(params)
+ # put the method in the dict
+ params['method'] = method
+ # call to the server and parse the response
+ return json.loads(self.__signed_post('http://api.rdio.com/1/', params))
+
Please sign in to comment.
Something went wrong with that request. Please try again.