* added decorator package to setup dependency

* used 'decorator' decorator on the decorators in the decorators module to avoid function signature mangling
* added 'callback' argument and related documentation in the asynchronous methods in api module
master
Abhinav Sarkar 2009-03-24 17:37:42 +00:00
parent 238d88f7e8
commit bbdbfcebb6
3 changed files with 189 additions and 143 deletions

View File

@ -173,7 +173,8 @@ class Api(object):
def get_album(self, def get_album(self,
album = None, album = None,
artist = None, artist = None,
mbid = None): mbid = None,
callback = None):
""" """
Get an album object. Get an album object.
@ -183,6 +184,8 @@ class Api(object):
@type artist: L{str} OR L{Artist} @type artist: L{str} OR L{Artist}
@param mbid: MBID of the album @param mbid: MBID of the album
@type mbid: L{str} @type mbid: L{str}
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: an Album object corresponding the provided album name @return: an Album object corresponding the provided album name
@rtype: L{Album} @rtype: L{Album}
@ -192,13 +195,14 @@ class Api(object):
Otherwise exception is raised. Otherwise exception is raised.
@see: L{Album.get_info} @see: L{Album.get_info}
@see: L{async_callback}
""" """
if isinstance(artist, Artist): if isinstance(artist, Artist):
artist = artist.name artist = artist.name
return Album.get_info(self, artist, album, mbid) return Album.get_info(self, artist, album, mbid)
@async_callback @async_callback
def search_album(self, album, limit = None): def search_album(self, album, limit = None, callback = None):
""" """
Search for an album by name. Search for an album by name.
@ -206,18 +210,19 @@ class Api(object):
@type album: L{str} @type album: L{str}
@param limit: maximum number of results returned (optional) @param limit: maximum number of results returned (optional)
@type limit: L{int} @type limit: L{int}
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: matches sorted by relevance @return: matches sorted by relevance
@rtype: L{lazylist} of L{Album} @rtype: L{lazylist} of L{Album}
@see: L{Album.search} @see: L{Album.search}
@see: L{async_callback}
""" """
return Album.search(self, search_item = album, limit = limit) return Album.search(self, search_item = album, limit = limit)
@async_callback @async_callback
def get_artist(self, def get_artist(self, artist = None, mbid = None, callback = None):
artist = None,
mbid = None):
""" """
Get an artist object. Get an artist object.
@ -225,6 +230,8 @@ class Api(object):
@type artist: L{str} @type artist: L{str}
@param mbid: MBID of the artist @param mbid: MBID of the artist
@type mbid: L{str} @type mbid: L{str}
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: an Artist object corresponding the provided artist name @return: an Artist object corresponding the provided artist name
@rtype: L{Artist} @rtype: L{Artist}
@ -233,13 +240,12 @@ class Api(object):
to be provided. Otherwise exception is raised. to be provided. Otherwise exception is raised.
@see: L{Artist.get_info} @see: L{Artist.get_info}
@see: L{async_callback}
""" """
return Artist.get_info(self, artist, mbid) return Artist.get_info(self, artist, mbid)
@async_callback @async_callback
def search_artist(self, def search_artist(self, artist, limit = None, callback = None):
artist,
limit = None):
""" """
Search for an artist by name. Search for an artist by name.
@ -247,21 +253,26 @@ class Api(object):
@type artist: L{str} @type artist: L{str}
@param limit: maximum number of results returned (optional) @param limit: maximum number of results returned (optional)
@type limit: L{int} @type limit: L{int}
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: matches sorted by relevance @return: matches sorted by relevance
@rtype: L{lazylist} of L{Artist} @rtype: L{lazylist} of L{Artist}
@see: L{Artist.search} @see: L{Artist.search}
@see: L{async_callback}
""" """
return Artist.search(self, search_item = artist, limit = limit) return Artist.search(self, search_item = artist, limit = limit)
@async_callback @async_callback
def get_event(self, event): def get_event(self, event, callback = None):
""" """
Get an event object. Get an event object.
@param event: the event id @param event: the event id
@type event: L{int} @type event: L{int}
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: an event object corresponding to the event id provided @return: an event object corresponding to the event id provided
@rtype: L{Event} @rtype: L{Event}
@ -269,90 +280,113 @@ class Api(object):
@raise InvalidParametersError: Exception is raised if an invalid event id is supplied. @raise InvalidParametersError: Exception is raised if an invalid event id is supplied.
@see: L{Event.get_info} @see: L{Event.get_info}
@see: L{async_callback}
""" """
return Event.get_info(self, event) return Event.get_info(self, event)
@async_callback @async_callback
def get_location(self, city): def get_location(self, city, callback = None):
""" """
Get a location object. Get a location object.
@param city: the city name @param city: the city name
@type city: L{str} @type city: L{str}
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: a location object corresponding to the city name provided @return: a location object corresponding to the city name provided
@rtype: L{Location} @rtype: L{Location}
@see: L{async_callback}
""" """
return Location(self, city = city) return Location(self, city = city)
@async_callback @async_callback
def get_country(self, name): def get_country(self, name, callback = None):
""" """
Get a country object. Get a country object.
@param name: the country name @param name: the country name
@type name: L{str} @type name: L{str}
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: a country object corresponding to the country name provided @return: a country object corresponding to the country name provided
@rtype: L{Country} @rtype: L{Country}
@see: L{async_callback}
""" """
return Country(self, name = name) return Country(self, name = name)
@async_callback @async_callback
def get_group(self, name): def get_group(self, name, callback = None):
""" """
Get a group object. Get a group object.
@param name: the group name @param name: the group name
@type name: L{str} @type name: L{str}
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: a group object corresponding to the group name provided @return: a group object corresponding to the group name provided
@rtype: L{Group} @rtype: L{Group}
@see: L{async_callback}
""" """
return Group(self, name = name) return Group(self, name = name)
@async_callback @async_callback
def get_playlist(self, url): def get_playlist(self, url, callback = None):
""" """
Get a playlist object. Get a playlist object.
@param url: lastfm url of the playlist @param url: lastfm url of the playlist
@type url: L{str} @type url: L{str}
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: a playlist object corresponding to the playlist url provided @return: a playlist object corresponding to the playlist url provided
@rtype: L{Playlist} @rtype: L{Playlist}
@see: L{Playlist.fetch} @see: L{Playlist.fetch}
@see: L{async_callback}
""" """
return Playlist.fetch(self, url) return Playlist.fetch(self, url)
@async_callback @async_callback
def get_tag(self, name): def get_tag(self, name, callback = None):
""" """
Get a tag object. Get a tag object.
@param name: the tag name @param name: the tag name
@type name: L{str} @type name: L{str}
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: a tag object corresponding to the tag name provided @return: a tag object corresponding to the tag name provided
@rtype: L{Tag} @rtype: L{Tag}
@see: L{async_callback}
""" """
return Tag(self, name = name) return Tag(self, name = name)
@async_callback @async_callback
def get_global_top_tags(self): def get_global_top_tags(self, callback = None):
""" """
Get the top global tags on Last.fm, sorted by popularity (number of times used). Get the top global tags on Last.fm, sorted by popularity (number of times used).
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: a list of top global tags @return: a list of top global tags
@rtype: L{list} of L{Tag} @rtype: L{list} of L{Tag}
@see: L{async_callback}
""" """
return Tag.get_top_tags(self) return Tag.get_top_tags(self)
@async_callback @async_callback
def search_tag(self, def search_tag(self, tag, limit = None, callback = None):
tag,
limit = None):
""" """
Search for a tag by name. Search for a tag by name.
@ -360,11 +394,14 @@ class Api(object):
@type tag: L{str} @type tag: L{str}
@param limit: maximum number of results returned (optional) @param limit: maximum number of results returned (optional)
@type limit: L{int} @type limit: L{int}
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: matches sorted by relevance @return: matches sorted by relevance
@rtype: L{lazylist} of L{Tag} @rtype: L{lazylist} of L{Tag}
@see: L{Tag.search} @see: L{Tag.search}
@see: L{async_callback}
""" """
return Tag.search(self, search_item = tag, limit = limit) return Tag.search(self, search_item = tag, limit = limit)
@ -372,7 +409,8 @@ class Api(object):
def compare_taste(self, def compare_taste(self,
type1, type2, type1, type2,
value1, value2, value1, value2,
limit = None): limit = None,
callback = None):
""" """
Get a Tasteometer score from two inputs, along with a list of Get a Tasteometer score from two inputs, along with a list of
shared artists. If the input is a User or a Myspace URL, some shared artists. If the input is a User or a Myspace URL, some
@ -388,16 +426,23 @@ class Api(object):
@type value2: L{str} @type value2: L{str}
@param limit: maximum number of results returned (optional) @param limit: maximum number of results returned (optional)
@type limit: L{int} @type limit: L{int}
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: the taste-o-meter score for the inputs @return: the taste-o-meter score for the inputs
@rtype: L{Tasteometer} @rtype: L{Tasteometer}
@see: L{Tasteometer.compare} @see: L{Tasteometer.compare}
@see: L{async_callback}
""" """
return Tasteometer.compare(self, type1, type2, value1, value2, limit) return Tasteometer.compare(self, type1, type2, value1, value2, limit)
@async_callback @async_callback
def get_track(self, track, artist = None, mbid = None): def get_track(self,
track,
artist = None,
mbid = None,
callback = None):
""" """
Get a track object. Get a track object.
@ -407,6 +452,8 @@ class Api(object):
@type artist: L{str} OR L{Artist} @type artist: L{str} OR L{Artist}
@param mbid: MBID of the track @param mbid: MBID of the track
@type mbid: L{str} @type mbid: L{str}
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: a track object corresponding to the track name provided @return: a track object corresponding to the track name provided
@rtype: L{Track} @rtype: L{Track}
@ -415,13 +462,18 @@ class Api(object):
to be provided. Otherwise exception is raised. to be provided. Otherwise exception is raised.
@see: L{Track.get_info} @see: L{Track.get_info}
@see: L{async_callback}
""" """
if isinstance(artist, Artist): if isinstance(artist, Artist):
artist = artist.name artist = artist.name
return Track.get_info(self, artist, track, mbid) return Track.get_info(self, artist, track, mbid)
@async_callback @async_callback
def search_track(self, track, artist = None, limit = None): def search_track(self,
track,
artist = None,
limit = None,
callback = None):
""" """
Search for a track by name. Search for a track by name.
@ -431,23 +483,28 @@ class Api(object):
@type artist: L{str} OR L{Artist} @type artist: L{str} OR L{Artist}
@param limit: maximum number of results returned (optional) @param limit: maximum number of results returned (optional)
@type limit: L{int} @type limit: L{int}
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: matches sorted by relevance @return: matches sorted by relevance
@rtype: L{lazylist} of L{Track} @rtype: L{lazylist} of L{Track}
@see: L{Track.search} @see: L{Track.search}
@see: L{async_callback}
""" """
if isinstance(artist, Artist): if isinstance(artist, Artist):
artist = artist.name artist = artist.name
return Track.search(self, search_item = track, limit = limit, artist = artist) return Track.search(self, search_item = track, limit = limit, artist = artist)
@async_callback @async_callback
def get_user(self, name): def get_user(self, name, callback = None):
""" """
Get an user object. Get an user object.
@param name: the last.fm user name @param name: the last.fm user name
@type name: L{str} @type name: L{str}
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: an user object corresponding to the user name provided @return: an user object corresponding to the user name provided
@rtype: L{User} @rtype: L{User}
@ -455,18 +512,23 @@ class Api(object):
@raise InvalidParametersError: Exception is raised if an invalid user name is supplied. @raise InvalidParametersError: Exception is raised if an invalid user name is supplied.
@see: L{User.get_info} @see: L{User.get_info}
@see: L{async_callback}
""" """
return User.get_info(self, name = name) return User.get_info(self, name = name)
@async_callback @async_callback
def get_authenticated_user(self): def get_authenticated_user(self, callback = None):
""" """
Get the currently authenticated user. Get the currently authenticated user.
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: The currently authenticated user if the session is authenticated @return: The currently authenticated user if the session is authenticated
@rtype: L{User} @rtype: L{User}
@see: L{User.get_authenticated_user} @see: L{User.get_authenticated_user}
@see: L{async_callback}
""" """
if self.session_key is not None: if self.session_key is not None:
return User.get_authenticated_user(self) return User.get_authenticated_user(self)
@ -474,12 +536,14 @@ class Api(object):
raise AuthenticationFailedError("session key must be present to call this method") raise AuthenticationFailedError("session key must be present to call this method")
@async_callback @async_callback
def get_venue(self, venue): def get_venue(self, venue, callback = None):
""" """
Get a venue object. Get a venue object.
@param venue: the venue name @param venue: the venue name
@type venue: L{str} @type venue: L{str}
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: a venue object corresponding to the venue name provided @return: a venue object corresponding to the venue name provided
@rtype: L{Venue} @rtype: L{Venue}
@ -487,6 +551,7 @@ class Api(object):
@raise InvalidParametersError: Exception is raised if an non-existant venue name is supplied. @raise InvalidParametersError: Exception is raised if an non-existant venue name is supplied.
@see: L{search_venue} @see: L{search_venue}
@see: L{async_callback}
""" """
try: try:
return self.search_venue(venue)[0] return self.search_venue(venue)[0]
@ -494,7 +559,11 @@ class Api(object):
raise InvalidParametersError("No such venue exists") raise InvalidParametersError("No such venue exists")
@async_callback @async_callback
def search_venue(self, venue, limit = None, country = None): def search_venue(self,
venue,
limit = None,
country = None,
callback = None):
""" """
Search for a venue by name. Search for a venue by name.
@ -505,11 +574,14 @@ class Api(object):
@type country: L{str} @type country: L{str}
@param limit: maximum number of results returned (optional) @param limit: maximum number of results returned (optional)
@type limit: L{int} @type limit: L{int}
@param callback: callback function for asynchronous invocation (optional)
@type callback: C{function}
@return: matches sorted by relevance @return: matches sorted by relevance
@rtype: L{lazylist} of L{Venue} @rtype: L{lazylist} of L{Venue}
@see: L{Venue.search} @see: L{Venue.search}
@see: L{async_callback}
""" """
return Venue.search(self, search_item = venue, limit = limit, country = country) return Venue.search(self, search_item = venue, limit = limit, country = country)

View File

@ -6,6 +6,8 @@ __version__ = "0.2"
__license__ = "GNU Lesser General Public License" __license__ = "GNU Lesser General Public License"
__package__ = "lastfm" __package__ = "lastfm"
from decorator import decorator
def top_property(list_property_name): def top_property(list_property_name):
""" """
A decorator to return a property that returns the first value of list A decorator to return a property that returns the first value of list
@ -57,7 +59,8 @@ def cached_property(func):
return property(fget = wrapper, doc = func.__doc__) return property(fget = wrapper, doc = func.__doc__)
def authenticate(func): @decorator
def authenticate(func, *args, **kwargs):
""" """
A decorator to check if the current user is authenticated or not. Used only A decorator to check if the current user is authenticated or not. Used only
on the functions that need authentication. If not authenticated then an on the functions that need authentication. If not authenticated then an
@ -72,34 +75,33 @@ def authenticate(func):
@raise AuthenticationFailedError: If the user is not authenticated, then an @raise AuthenticationFailedError: If the user is not authenticated, then an
exception is raised. exception is raised.
""" """
def wrapper(self, *args, **kwargs): self = args[0]
from lastfm.user import User, Api from lastfm.user import User, Api
username = None username = None
if isinstance(self, User): if isinstance(self, User):
username = self.name username = self.name
if self.authenticated: if self.authenticated:
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
elif hasattr(self, 'user'): elif hasattr(self, 'user'):
username = self.user.name username = self.user.name
if self.user.authenticated: if self.user.authenticated:
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
elif hasattr(self, '_subject') and isinstance(self._subject, User): elif hasattr(self, '_subject') and isinstance(self._subject, User):
username = self._subject.name username = self._subject.name
if self._subject.authenticated: if self._subject.authenticated:
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
elif hasattr(self, '_api') and isinstance(self._api, Api): elif hasattr(self, '_api') and isinstance(self._api, Api):
try: try:
user = self._api.get_authenticated_user() user = self._api.get_authenticated_user()
username = user.name username = user.name
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
except AuthenticationFailedError: except AuthenticationFailedError:
pass pass
raise AuthenticationFailedError( raise AuthenticationFailedError(
"user '%s' does not have permissions to access the service" % username) "user '%s' does not have permissions to access the service" % username)
wrapper.__doc__ = func.__doc__
return wrapper
def depaginate(func): @decorator
def depaginate(func, *args, **kwargs):
""" """
A decorator to depaginate the search results. A decorator to depaginate the search results.
@ -111,24 +113,22 @@ def depaginate(func):
@rtype: C{function} @rtype: C{function}
""" """
from lastfm.lazylist import lazylist from lastfm.lazylist import lazylist
def wrapper(*args, **kwargs): @lazylist
@lazylist def generator(lst):
def generator(lst): gen = func(*args, **kwargs)
total_pages = gen.next()
for e in gen:
yield e
for page in xrange(2, total_pages+1):
kwargs['page'] = page
gen = func(*args, **kwargs) gen = func(*args, **kwargs)
total_pages = gen.next() gen.next()
for e in gen: for e in gen:
yield e yield e
for page in xrange(2, total_pages+1): return generator()
kwargs['page'] = page
gen = func(*args, **kwargs) @decorator
gen.next() def async_callback(func, *args, **kwargs):
for e in gen:
yield e
return generator()
wrapper.__doc__ = func.__doc__
return wrapper
def async_callback(func):
""" """
A decorator to convert a synchronous (blocking) function into A decorator to convert a synchronous (blocking) function into
an asynchronous (non-blocking) function. an asynchronous (non-blocking) function.
@ -154,56 +154,30 @@ def async_callback(func):
@rtype: C{function} @rtype: C{function}
""" """
from threading import Thread from threading import Thread
def wrapper(self, *args, **kwargs): callback = None
callback = None for a in args:
for a in args: if callable(a):
if callable(a): callback = a
callback = a args = list(args)
args = list(args) args.remove(a)
args.remove(a) args = tuple(args)
args = tuple(args) break
break if 'callback' in kwargs:
if 'callback' in kwargs: callback = kwargs['callback']
callback = kwargs['callback'] del kwargs['callback']
del kwargs['callback']
if (callback is not None and callable(callback)):
def async_call():
result = None
try:
result = func(self, *args, **kwargs)
except Exception, e:
result = e
callback(result)
thread = Thread(target = async_call)
thread.start()
return
return func(self, *args, **kwargs)
wrapper.__doc__ = "%s\n @see: L{async_callback}" % func.__doc__
return wrapper
def _get_arg_string(argspecs):
arg_str = ""
args = argspecs.args
args.remove('self')
defaults = argspecs.defaults
if defaults is not None: if callback is not None and callable(callback):
defargs = args[-len(defaults):] def async_call():
nondefargs = args[:-len(defaults)] result = None
nondefargs_str = ", ".join(nondefargs) try:
defargs_str = ", ".join(["%s = %s" % (defargs[i], defaults[i]) for i in xrange(len(defargs))]) result = func(*args, **kwargs)
if nondefargs_str != '' and defargs_str != '': except Exception, e:
arg_str = "%s, %s" % (nondefargs_str, defargs_str) result = e
elif nondefargs_str != '': callback(result)
arg_str = nondefargs_str thread = Thread(target = async_call)
elif defargs_str != '': thread.start()
arg_str = defargs_str return
else: return func(*args, **kwargs)
arg_str = ", ".join(args)
print arg_str
return arg_str
import copy import copy
from lastfm.error import LastfmError, AuthenticationFailedError from lastfm.error import LastfmError, AuthenticationFailedError

View File

@ -22,7 +22,7 @@ located at http://ws.audioscrobbler.com/2.0/ .""",
) )
SETUPTOOLS_METADATA = dict( SETUPTOOLS_METADATA = dict(
install_requires = ['setuptools'], install_requires = ['setuptools', 'decorator'],
include_package_data = True, include_package_data = True,
tests_require = ['wsgi_intercept'], tests_require = ['wsgi_intercept'],
classifiers = [ classifiers = [
@ -40,18 +40,18 @@ SETUPTOOLS_METADATA = dict(
import sys import sys
if sys.version < '2.5': if sys.version < '2.5':
SETUPTOOLS_METADATA['install_requires'].append('ElementTree') SETUPTOOLS_METADATA['install_requires'].append('ElementTree')
SETUPTOOLS_METADATA['install_requires'].append('cElementTree') SETUPTOOLS_METADATA['install_requires'].append('cElementTree')
def Main(): def Main():
# Use setuptools if available, otherwise fallback and use distutils # Use setuptools if available, otherwise fallback and use distutils
try: try:
import setuptools import setuptools
METADATA.update(SETUPTOOLS_METADATA) METADATA.update(SETUPTOOLS_METADATA)
setuptools.setup(**METADATA) setuptools.setup(**METADATA)
except ImportError: except ImportError:
import distutils.core import distutils.core
distutils.core.setup(**METADATA) distutils.core.setup(**METADATA)
if __name__ == '__main__': if __name__ == '__main__':
Main() Main()