Skip to content

Commit

Permalink
Python implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
ianloic committed Aug 13, 2011
1 parent 48e6391 commit 9000403
Show file tree
Hide file tree
Showing 7 changed files with 369 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -2,3 +2,5 @@
*.pyo
*.pyc
php/examples/rdio-consumer-credentials.php
python/examples/rdio_consumer_credentials.py
.idea
31 changes: 30 additions & 1 deletion 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/)

48 changes: 48 additions & 0 deletions 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()
4 changes: 4 additions & 0 deletions 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 = ''
128 changes: 128 additions & 0 deletions 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()
91 changes: 91 additions & 0 deletions 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]))
66 changes: 66 additions & 0 deletions 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))

0 comments on commit 9000403

Please sign in to comment.