From 2645e4565c10c0808e0368e6e1651642ffd57828 Mon Sep 17 00:00:00 2001 From: Abhinav Sarkar Date: Tue, 17 Mar 2009 15:21:40 +0000 Subject: [PATCH] added documentation for error, event, geo and group modules --- lastfm/__init__.py | 8 +- lastfm/album.py | 3 +- lastfm/api.py | 1 + lastfm/artist.py | 1 + lastfm/base.py | 1 + lastfm/error.py | 46 +- lastfm/event.py | 156 +++++-- lastfm/filecache.py | 139 +++--- lastfm/geo.py | 204 ++++++++- lastfm/group.py | 112 ++++- lastfm/mixins/__init__.py | 1 + lastfm/mixins/cacheable.py | 1 + lastfm/mixins/searchable.py | 1 + lastfm/mixins/sharable.py | 2 +- lastfm/mixins/shoutable.py | 1 + lastfm/mixins/taggable.py | 1 + lastfm/objectcache.py | 1 + lastfm/playlist.py | 1 + lastfm/safelist.py | 1 + lastfm/shout.py | 1 + lastfm/stats.py | 1 + lastfm/tag.py | 1 + lastfm/tasteometer.py | 1 + lastfm/track.py | 1 + lastfm/user.py | 1 + lastfm/venue.py | 1 + lastfm/weeklychart.py | 4 +- lastfm/wiki.py | 1 + .../data/828b985a53dc8a323b59dfabcaf548d8.xml | 422 ++++++++++++++++++ test/test_geo.py | 4 +- 30 files changed, 977 insertions(+), 142 deletions(-) create mode 100644 test/data/828b985a53dc8a323b59dfabcaf548d8.xml diff --git a/lastfm/__init__.py b/lastfm/__init__.py index 665ea50..edabba8 100644 --- a/lastfm/__init__.py +++ b/lastfm/__init__.py @@ -1,9 +1,15 @@ #!/usr/bin/env python -"""A python interface to the last.fm web services API""" +""" +A python interface to the last.fm web services API at +U{http://ws.audioscrobbler.com/2.0}. +See U{the official documentation} +of the web service API methods for more information. +""" __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" import sys import os diff --git a/lastfm/album.py b/lastfm/album.py index 3d8e253..daa6fbe 100644 --- a/lastfm/album.py +++ b/lastfm/album.py @@ -4,6 +4,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" from lastfm.base import LastfmBase from lastfm.mixins import Cacheable, Searchable, Taggable @@ -213,7 +214,7 @@ class Album(LastfmBase, Cacheable, Searchable, Taggable): @param mbid: MBID of the album @type mbid: L{str} - @return: an Album object corresponding the provided album name + @return: an Album object corresponding to the provided album name @rtype: L{Album} @raise lastfm.InvalidParametersError: Either album and artist parameters or diff --git a/lastfm/api.py b/lastfm/api.py index c725978..b44b64e 100644 --- a/lastfm/api.py +++ b/lastfm/api.py @@ -4,6 +4,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" from lastfm.base import LastfmBase from lastfm.decorators import cached_property diff --git a/lastfm/artist.py b/lastfm/artist.py index 021b222..39ebda2 100644 --- a/lastfm/artist.py +++ b/lastfm/artist.py @@ -4,6 +4,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" from lastfm.base import LastfmBase from lastfm.mixins import Cacheable, Searchable, Sharable, Shoutable, Taggable diff --git a/lastfm/base.py b/lastfm/base.py index 0d7c156..bac1da4 100644 --- a/lastfm/base.py +++ b/lastfm/base.py @@ -4,6 +4,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" class LastfmBase(object): """Base class for all the classes in this package""" diff --git a/lastfm/error.py b/lastfm/error.py index 1569e40..21da54f 100644 --- a/lastfm/error.py +++ b/lastfm/error.py @@ -1,69 +1,102 @@ #!/usr/bin/env python +"""Mdoule containing the exceptions for this package""" __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" class LastfmError(Exception): - """Base class for Lastfm errors""" - def __init__(self, - message = None, - code = None): + """Base class for Lastfm web service API errors""" + def __init__(self, message = None, code = None): + """ + Initialize the error object. + + @param message: the error message + @type message: L{str} + @param code: the error code + @type code: L{int} + """ super(LastfmError, self).__init__() self._code = code self._message = message @property def code(self): + """ + The error code as returned by last.fm web service API. + @rtype: L{int} + """ return self._code @property def message(self): + """ + The error message as returned by last.fm web service API. + @rtype: L{str} + """ return self._message - + def __str__(self): return "%s" % self.message class InvalidServiceError(LastfmError):#2 + """Invalid service - This service does not exist.""" pass class InvalidMethodError(LastfmError):#3 + """Invalid method - No method with that name in this package.""" pass class AuthenticationFailedError(LastfmError):#4 + """Authentication failed - You do not have permissions to access the service""" pass class InvalidFormatError(LastfmError):#5 + """Invalid format - This service doesn't exist in that format""" pass class InvalidParametersError(LastfmError):#6 + """Invalid parameters - Your request is missing a required parameter""" pass class InvalidResourceError(LastfmError):#7 + """Invalid resource - Invalid resource specified""" pass class OperationFailedError(LastfmError):#8 + """ + Operation failed - There was an error during the requested operation. + lease try again later. + """ pass class InvalidSessionKeyError(LastfmError):#9 + """Invalid session key - Please re-authenticate""" pass class InvalidApiKeyError(LastfmError):#10 + """Invalid API key - You must be granted a valid key by last.fm""" pass class ServiceOfflineError(LastfmError):#11 + """Service offline - This service is temporarily offline. Try again later.""" pass class SubscribersOnlyError(LastfmError):#12 + """Subscribers only - This service is only available to paid last.fm subscribers""" pass class InvalidMethodSignatureError(LastfmError):#13 + """Invalid method signature - the method signature provided is invalid""" pass class TokenNotAuthorizedError(LastfmError):#14 + """Token not authorized - This token has not been authorized""" pass class TokenExpiredError(LastfmError):#15 + """Token expired - This token has expired""" pass error_map = { @@ -82,4 +115,5 @@ error_map = { 13: InvalidMethodSignatureError, 14: TokenNotAuthorizedError, 15: TokenExpiredError, -} \ No newline at end of file +} +"""Map of error codes to the error types""" diff --git a/lastfm/event.py b/lastfm/event.py index 01bfcc8..fc31f3a 100644 --- a/lastfm/event.py +++ b/lastfm/event.py @@ -1,8 +1,10 @@ #!/usr/bin/env python +"""Module for calling Event related last.fm web services API methods""" __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" from lastfm.base import LastfmBase from lastfm.mixins import Cacheable, Sharable, Shoutable @@ -26,7 +28,35 @@ class Event(LastfmBase, Cacheable, Sharable, Shoutable): url = None, stats = None, tag = None, - subject = None): + **kwargs): + """ + Create an Event object by providing all the data related to it. + + @param api: an instance of L{Api} + @type api: L{Api} + @param id: ID of the event + @type id: L{int} + @param title: title of the event + @type title: L{str} + @param artists: artists performing in the event + @type artists: L{list} of L{Artist} + @param headliner: headliner artist of the event + @type headliner: L{Artist} + @param venue: venue of the event + @type venue: L{Venue} + @param start_date: start date and time of the event + @type start_date: C{datetime.datetime} + @param description: description of the event + @type description: L{str} + @param image: poster images of the event in various sizes + @type image: L{dict} + @param url: URL of the event on last.fm + @type url: L{str} + @param stats: the statistics of the event (attendance and no. of reviews) + @type stats: L{Stats} + @param tag: tag for the event + @type tag: L{str} + """ if not isinstance(api, Api): raise InvalidParametersError("api reference must be supplied as an argument") Sharable.init(self, api) @@ -48,64 +78,106 @@ class Event(LastfmBase, Cacheable, Sharable, Shoutable): reviews = stats.reviews ) self._tag = tag - self._subject = subject @property def id(self): - """id of the event""" + """ + id of the event + @rtype: L{int} + """ return self._id @property def title(self): - """title of the event""" + """ + title of the event + @rtype: L{str} + """ return self._title @property def artists(self): - """artists performing in the event""" + """ + artists performing in the event + @rtype: L{list} of L{Artist} + """ return self._artists @property def headliner(self): - """headliner artist of the event""" + """ + headliner artist of the event + @rtype: L{Artist} + """ return self._headliner @property def venue(self): - """venue of the event""" + """ + venue of the event + @rtype: L{Venue} + """ return self._venue @property def start_date(self): - """start date of the event""" + """ + start date of the event + @rtype: C{datetime.datetime} + """ return self._start_date @property def description(self): - """description of the event""" + """ + description of the event + @rtype: L{str} + """ return self._description @property def image(self): - """poster of the event""" + """ + poster of the event + @rtype: L{dict} + """ return self._image @property def url(self): - """url of the event's page""" + """ + url of the event's page + @rtype: L{str} + """ return self._url @property def stats(self): - """stats of the event""" + """ + statistics for the event + @rtype: L{Stats} + """ return self._stats @property def tag(self): - """tags for the event""" + """ + tag for the event + @rtype: L{str} + """ return self._tag def attend(self, status = STATUS_ATTENDING): + """ + Set the attendance status of the authenticated user for this event. + + @param status: attendance status, should be one of: + L{Event.STATUS_ATTENDING} OR L{Event.STATUS_MAYBE} OR L{Event.STATUS_NOT} + @type status: L{int} + + @raise InvalidParametersError: If status parameters is not one of the allowed values + then an exception is raised. + """ if status not in [Event.STATUS_ATTENDING, Event.STATUS_MAYBE, Event.STATUS_NOT]: InvalidParametersError("status has to be 0, 1 or 2") params = self._default_params({'method': 'event.attend', 'status': status}) @@ -113,12 +185,38 @@ class Event(LastfmBase, Cacheable, Sharable, Shoutable): @staticmethod def get_info(api, event): + """ + Get the data for the event. + + @param api: an instance of L{Api} + @type api: L{Api} + @param event: ID of the event + @type event: L{int} + + @return: an Event object corresponding to the provided event id + @rtype: L{Event} + + @note: Use the L{Api.get_event} method instead of using this method directly. + """ params = {'method': 'event.getInfo', 'event': event} data = api._fetch_data(params).find('event') return Event.create_from_data(api, data) @staticmethod def create_from_data(api, data): + """ + Create the Event object from the provided XML element. + + @param api: an instance of L{Api} + @type api: L{Api} + @param data: XML element + @type data: C{xml.etree.ElementTree.Element} + + @return: an Event object corresponding to the provided XML element + @rtype: L{Event} + + @note: Use the L{Api.get_event} method instead of using this method directly. + """ start_date = None if data.findtext('startTime') is not None: @@ -150,7 +248,9 @@ class Event(LastfmBase, Cacheable, Sharable, Shoutable): except ValueError: pass - + latitude = data.findtext('venue/location/{%s}point/{%s}lat' % ((Location.XMLNS,)*2)) + longitude = data.findtext('venue/location/{%s}point/{%s}long' % ((Location.XMLNS,)*2)) + return Event( api, id = int(data.findtext('id')), @@ -161,22 +261,18 @@ class Event(LastfmBase, Cacheable, Sharable, Shoutable): api, name = data.findtext('venue/name'), location = Location( - api, - city = data.findtext('venue/location/city'), - country = Country( - api, - name = data.findtext('venue/location/country') - ), - street = data.findtext('venue/location/street'), - postal_code = data.findtext('venue/location/postalcode'), - latitude = float(data.findtext( - 'venue/location/{%s}point/{%s}lat' % ((Location.XMLNS,)*2) - )), - longitude = float(data.findtext( - 'venue/location/{%s}point/{%s}long' % ((Location.XMLNS,)*2) - )), - #timezone = data.findtext('venue/location/timezone') - ), + api, + city = data.findtext('venue/location/city'), + country = Country( + api, + name = data.findtext('venue/location/country') + ), + street = data.findtext('venue/location/street'), + postal_code = data.findtext('venue/location/postalcode'), + latitude = (latitude.strip()!= '') and float(latitude) or None, + longitude = (longitude.strip()!= '') and float(longitude) or None, + #timezone = data.findtext('venue/location/timezone') + ), url = data.findtext('venue/url') ), start_date = start_date, diff --git a/lastfm/filecache.py b/lastfm/filecache.py index 558c270..6e3c0e8 100644 --- a/lastfm/filecache.py +++ b/lastfm/filecache.py @@ -1,8 +1,10 @@ #!/usr/bin/env python +"""Module for caching the files""" __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" import sys if sys.version < '2.6': @@ -18,84 +20,83 @@ import os import tempfile class _FileCacheError(Exception): - '''Base exception class for FileCache related errors''' + """Base exception class for FileCache related errors""" class FileCache(object): + DEPTH = 3 - DEPTH = 3 + def __init__(self,root_directory=None): + self._InitializeRootDirectory(root_directory) - def __init__(self,root_directory=None): - self._InitializeRootDirectory(root_directory) + def Get(self,key): + path = self._GetPath(key) + if os.path.exists(path): + return open(path).read() + else: + return None - def Get(self,key): - path = self._GetPath(key) - if os.path.exists(path): - return open(path).read() - else: - return None + def Set(self,key,data): + path = self._GetPath(key) + directory = os.path.dirname(path) + if not os.path.exists(directory): + os.makedirs(directory) + if not os.path.isdir(directory): + raise _FileCacheError('%s exists but is not a directory' % directory) + temp_fd, temp_path = tempfile.mkstemp() + temp_fp = os.fdopen(temp_fd, 'w') + temp_fp.write(data) + temp_fp.close() + if not path.startswith(self._root_directory): + raise _FileCacheError('%s does not appear to live under %s' % + (path, self._root_directory)) + if os.path.exists(path): + os.remove(path) + os.rename(temp_path, path) - def Set(self,key,data): - path = self._GetPath(key) - directory = os.path.dirname(path) - if not os.path.exists(directory): - os.makedirs(directory) - if not os.path.isdir(directory): - raise _FileCacheError('%s exists but is not a directory' % directory) - temp_fd, temp_path = tempfile.mkstemp() - temp_fp = os.fdopen(temp_fd, 'w') - temp_fp.write(data) - temp_fp.close() - if not path.startswith(self._root_directory): - raise _FileCacheError('%s does not appear to live under %s' % - (path, self._root_directory)) - if os.path.exists(path): - os.remove(path) - os.rename(temp_path, path) + def Remove(self,key): + path = self._GetPath(key) + if not path.startswith(self._root_directory): + raise _FileCacheError('%s does not appear to live under %s' % + (path, self._root_directory )) + if os.path.exists(path): + os.remove(path) - def Remove(self,key): - path = self._GetPath(key) - if not path.startswith(self._root_directory): - raise _FileCacheError('%s does not appear to live under %s' % - (path, self._root_directory )) - if os.path.exists(path): - os.remove(path) + def GetCachedTime(self,key): + path = self._GetPath(key) + if os.path.exists(path): + return os.path.getmtime(path) + else: + return None - def GetCachedTime(self,key): - path = self._GetPath(key) - if os.path.exists(path): - return os.path.getmtime(path) - else: - return None + def _GetUsername(self): + '''Attempt to find the username in a cross-platform fashion.''' + return os.getenv('USER') or \ + os.getenv('LOGNAME') or \ + os.getenv('USERNAME') or \ + os.getlogin() or \ + 'nobody' - def _GetUsername(self): - '''Attempt to find the username in a cross-platform fashion.''' - return os.getenv('USER') or \ - os.getenv('LOGNAME') or \ - os.getenv('USERNAME') or \ - os.getlogin() or \ - 'nobody' + def _GetTmpCachePath(self): + username = self._GetUsername() + cache_directory = 'python.cache_' + username + return os.path.join(tempfile.gettempdir(), cache_directory) - def _GetTmpCachePath(self): - username = self._GetUsername() - cache_directory = 'python.cache_' + username - return os.path.join(tempfile.gettempdir(), cache_directory) + def _InitializeRootDirectory(self, root_directory): + if not root_directory: + root_directory = self._GetTmpCachePath() + root_directory = os.path.abspath(root_directory) + if not os.path.exists(root_directory): + os.mkdir(root_directory) + if not os.path.isdir(root_directory): + raise _FileCacheError('%s exists but is not a directory' % + root_directory) + self._root_directory = root_directory - def _InitializeRootDirectory(self, root_directory): - if not root_directory: - root_directory = self._GetTmpCachePath() - root_directory = os.path.abspath(root_directory) - if not os.path.exists(root_directory): - os.mkdir(root_directory) - if not os.path.isdir(root_directory): - raise _FileCacheError('%s exists but is not a directory' % - root_directory) - self._root_directory = root_directory + def _GetPath(self,key): + hashed_key = md5hash(key) + return os.path.join(self._root_directory, + self._GetPrefix(hashed_key), + hashed_key) - def _GetPath(self,key): - hashed_key = md5hash(key) - return os.path.join(self._root_directory, - self._GetPrefix(hashed_key), - hashed_key) - - def _GetPrefix(self,hashed_key): - return os.path.sep.join(hashed_key[0:FileCache.DEPTH]) + def _GetPrefix(self,hashed_key): + return os.path.sep.join(hashed_key[0:FileCache.DEPTH]) diff --git a/lastfm/geo.py b/lastfm/geo.py index a82df41..c570ef2 100644 --- a/lastfm/geo.py +++ b/lastfm/geo.py @@ -1,8 +1,10 @@ #!/usr/bin/env python +"""Module for calling Geo related last.fm web services API methods""" __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" from lastfm.base import LastfmBase from lastfm.mixins import Cacheable @@ -10,19 +12,46 @@ from lastfm.lazylist import lazylist from lastfm.decorators import cached_property, top_property class Geo(object): - """A class representing an geographic location.""" + """A class representing an geographic location""" @staticmethod def get_events(api, location, latitude = None, longitude = None, distance = None): + """ + Get the events for a location. + + @param api: an instance of L{Api} + @type api: L{Api} + @param location: location to retrieve events for (optional) + @type location: L{str} + @param latitude: latitude value to retrieve events for (optional) + @type latitude: L{float} + @param longitude: longitude value to retrieve events for (optional) + @type longitude: L{float} + @param distance: find events within a specified distance (optional) + @type distance: L{float} + + @return: events for the location + @rtype: L{lazylist} of L{Event} + + @raise InvalidParametersError: Either location or latitude and longitude + has to be provided. Otherwise exception is + raised. + + @note: Use L{Location.events} instead of using this method directly. + """ + if reduce(lambda x,y: x and y is None, [location, latitude, longitude], True): + raise InvalidParametersError( + "Either location or latitude and longitude has to be provided") + params = {'method': 'geo.getEvents', 'location': location} if distance is not None: params.update({'distance': distance}) if latitude is not None and longitude is not None: - params.update({'latitude': latitude, 'longitude': longitude}) + params.update({'lat': latitude, 'long': longitude}) @lazylist def gen(lst): @@ -46,6 +75,20 @@ class Geo(object): @staticmethod def get_top_artists(api, country): + """ + Get the most popular artists on Last.fm by country + + @param api: an instance of L{Api} + @type api: L{Api} + @param country: a country name, as defined by + the ISO 3166-1 country names standard + @type country: L{str} + + @return: most popular artists of the country + @rtype: L{list} of L{Artist} + + @note: Use L{Country.top_artists} instead of using this method directly. + """ params = {'method': 'geo.getTopArtists', 'country': country} data = api._fetch_data(params).find('topartists') return [ @@ -66,6 +109,23 @@ class Geo(object): @staticmethod def get_top_tracks(api, country, location = None): + """ + Get the most popular tracks on Last.fm by country + + @param api: an instance of L{Api} + @type api: L{Api} + @param country: a country name, as defined by + the ISO 3166-1 country names standard + @type country: L{str} + @param location: a metro name, to fetch the charts for + (must be within the country specified) (optional) + + @return: most popular tracks of the country + @rtype: L{list} of L{Track} + + @note: Use L{Country.top_tracks} and L{Country.get_top_tracks} + instead of using this method directly. + """ params = {'method': 'geo.getTopTracks', 'country': country} if location is not None: params.update({'location': location}) @@ -109,6 +169,29 @@ class Location(LastfmBase, Cacheable): longitude = None, timezone = None, **kwargs): + """ + Create a Location object by providing all the data related to it. + + @param api: an instance of L{Api} + @type api: L{Api} + @param city: city in which the location is situated + @type city: L{str} + @param country: country in which the location is situated + @type country: L{Country} + @param street: street in which the location is situated + @type street: L{str} + @param postal_code: postal code of the location + @type postal_code: L{str} + @param latitude: latitude of the location + @type latitude: L{float} + @param longitude: longitude of the location + @type longitude: L{float} + @param timezone: timezone in which the location is situated + @type timezone: L{str} + + @raise InvalidParametersError: If an instance of L{Api} is not provided as the first + parameter then an Exception is raised. + """ if not isinstance(api, Api): raise InvalidParametersError("api reference must be supplied as an argument") self._api = api @@ -122,53 +205,88 @@ class Location(LastfmBase, Cacheable): @property def city(self): - """city in which the location is situated""" + """ + city in which the location is situated + @rtype: L{str} + """ return self._city @property def country(self): - """country in which the location is situated""" + """ + country in which the location is situated + @rtype: L{Country} + """ return self._country @property def street(self): - """street in which the location is situated""" + """ + street in which the location is situated + @rtype: L{str} + """ return self._street @property def postal_code(self): - """postal code of the location""" + """ + postal code of the location + @rtype: L{str} + """ return self._postal_code @property def latitude(self): - """latitude of the location""" + """ + latitude of the location + @rtype: L{float} + """ return self._latitude @property def longitude(self): - """longitude of the location""" + """ + longitude of the location + @rtype: L{float} + """ return self._longitude @property def timezone(self): - """timezone in which the location is situated""" + """ + timezone in which the location is situated + @rtype: L{str} + """ return self._timezone @cached_property def top_tracks(self): - """top tracks of the location""" + """ + top tracks for the location + @rtype: L{list} of L{Track} + """ if self.country is None or self.city is None: raise InvalidParametersError("country and city of this location are required for calling this method") return Geo.get_top_tracks(self._api, self.country.name, self.city) @top_property("top_tracks") def top_track(self): - """top track of the location""" + """ + top track for the location + @rtype: L{Track} + """ pass - def get_events(self, - distance = None): + def get_events(self, distance = None): + """ + Get the events taking place at the location. + + @param distance: find events within a specified distance (optional) + @type distance: L{float} + + @return: events taking place at the location + @rtype: L{lazylist} of L{Event} + """ return Geo.get_events(self._api, self.city, self.latitude, @@ -177,7 +295,10 @@ class Location(LastfmBase, Cacheable): @cached_property def events(self): - """events taking place at/around the location""" + """ + events taking place at/around the location + @rtype: L{lazylist} of L{Event} + """ return self.get_events() @staticmethod @@ -462,10 +583,18 @@ class Country(LastfmBase, Cacheable): 'ZA': 'South Africa', 'ZM': 'Zambia', 'ZW': 'Zimbabwe'} - def init(self, - api, - name = None, - **kwargs): + """ISO Codes of the countries""" + def init(self, api, name = None, **kwargs): + """ + Create a Country object by providing all the data related to it. + @param api: an instance of L{Api} + @type api: L{Api} + @param name: name of the country + @type name: L{str} + + @raise InvalidParametersError: If an instance of L{Api} is not provided as the first + parameter then an Exception is raised. + """ if not isinstance(api, Api): raise InvalidParametersError("api reference must be supplied as an argument") self._api = api @@ -473,35 +602,62 @@ class Country(LastfmBase, Cacheable): @property def name(self): - """name of the country""" + """ + name of the country + @rtype: L{str} + """ return self._name @cached_property def top_artists(self): - """top artists of the country""" + """ + top artists of the country + @rtype: L{list} of L{Artist} + """ return Geo.get_top_artists(self._api, self.name) @top_property("top_artists") def top_artist(self): - """top artist of the country""" + """ + top artist of the country + @rtype: L{Artist} + """ pass def get_top_tracks(self, location = None): + """ + Get the top tracks for country. + + @param location: a metro name, to fetch the charts for + (must be within the country specified) (optional) + + @return: most popular tracks of the country + @rtype: L{list} of L{Track} + """ return Geo.get_top_tracks(self._api, self.name, location) @cached_property def top_tracks(self): - """top tracks of the country""" + """ + top tracks of the country + @rtype: L{list} of L{Track} + """ return self.get_top_tracks() @top_property("top_tracks") def top_track(self): - """top track of the country""" + """ + top track of the country + @rtype: L{Track} + """ pass @cached_property def events(self): - """events taking place at/around the location""" + """ + events taking place in the country + @rtype: L{lazylist} of L{Event} + """ return Geo.get_events(self._api, self.name) @staticmethod diff --git a/lastfm/group.py b/lastfm/group.py index 2e3e1ec..97d07b8 100644 --- a/lastfm/group.py +++ b/lastfm/group.py @@ -1,8 +1,10 @@ #!/usr/bin/env python +"""Module for calling Group related last.fm web services API methods""" __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" from lastfm.base import LastfmBase from lastfm.mixins import Cacheable @@ -11,10 +13,18 @@ from lastfm.decorators import cached_property, top_property class Group(LastfmBase, Cacheable): """A class representing a group on last.fm.""" - def init(self, - api, - name = None, - **kwargs): + def init(self, api, name = None, **kwargs): + """ + Create a Group object by providing all the data related to it. + + @param api: an instance of L{Api} + @type api: L{Api} + @param name: name of the group on last.fm + @type name: L{str} + + @raise InvalidParametersError: If an instance of L{Api} is not provided as the first + parameter then an Exception is raised. + """ if not isinstance(api, Api): raise InvalidParametersError("api reference must be supplied as an argument") self._api = api @@ -22,10 +32,18 @@ class Group(LastfmBase, Cacheable): @property def name(self): + """ + name of the group + @rtype: L{str} + """ return self._name @cached_property def weekly_chart_list(self): + """ + a list of available weekly charts for this group + @rtype: L{list} of L{WeeklyChart} + """ params = self._default_params({'method': 'group.getWeeklyChartList'}) data = self._api._fetch_data(params).find('weeklychartlist') return [ @@ -33,9 +51,24 @@ class Group(LastfmBase, Cacheable): for c in data.findall('chart') ] - def get_weekly_album_chart(self, - start = None, - end = None): + def get_weekly_album_chart(self, start = None, end = None): + """ + Get an album chart for the group, for a given date range. + If no date range is supplied, it will return the most + recent album chart for the group. + + @param start: the date at which the chart should start from (optional) + @type start: C{datetime.datetime} + @param end: the date at which the chart should end on (optional) + @type end: C{datetime.datetime} + + @return: an album chart for the group + @rtype: L{WeeklyAlbumChart} + + @raise InvalidParametersError: Both start and end parameter have to be either + provided or not provided. Providing only one of + them will raise an exception. + """ params = self._default_params({'method': 'group.getWeeklyAlbumChart'}) params = WeeklyChart._check_weekly_chart_params(params, start, end) data = self._api._fetch_data(params).find('weeklyalbumchart') @@ -43,10 +76,19 @@ class Group(LastfmBase, Cacheable): @cached_property def recent_weekly_album_chart(self): + """ + most recent album chart for the group + @rtype: L{WeeklyAlbumChart} + """ return self.get_weekly_album_chart() @cached_property def weekly_album_chart_list(self): + """ + a list of all album charts for this group in reverse-chronological + order. (that means 0th chart is the most recent chart) + @rtype: L{lazylist} of L{WeeklyAlbumChart} + """ wcl = list(self.weekly_chart_list) wcl.reverse() @lazylist @@ -58,6 +100,23 @@ class Group(LastfmBase, Cacheable): def get_weekly_artist_chart(self, start = None, end = None): + """ + Get an artist chart for the group, for a given date range. + If no date range is supplied, it will return the most + recent artist chart for the group. + + @param start: the date at which the chart should start from (optional) + @type start: C{datetime.datetime} + @param end: the date at which the chart should end on (optional) + @type end: C{datetime.datetime} + + @return: an artist chart for the group + @rtype: L{WeeklyArtistChart} + + @raise InvalidParametersError: Both start and end parameter have to be either + provided or not provided. Providing only one of + them will raise an exception. + """ params = self._default_params({'method': 'group.getWeeklyArtistChart'}) params = WeeklyChart._check_weekly_chart_params(params, start, end) data = self._api._fetch_data(params).find('weeklyartistchart') @@ -65,10 +124,19 @@ class Group(LastfmBase, Cacheable): @cached_property def recent_weekly_artist_chart(self): + """ + most recent artist chart for the group + @rtype: L{WeeklyArtistChart} + """ return self.get_weekly_artist_chart() @cached_property def weekly_artist_chart_list(self): + """ + a list of all artist charts for this group in reverse-chronological + order. (that means 0th chart is the most recent chart) + @rtype: L{lazylist} of L{WeeklyArtistChart} + """ wcl = list(self.weekly_chart_list) wcl.reverse() @lazylist @@ -80,6 +148,23 @@ class Group(LastfmBase, Cacheable): def get_weekly_track_chart(self, start = None, end = None): + """ + Get a track chart for the group, for a given date range. + If no date range is supplied, it will return the most + recent artist chart for the group. + + @param start: the date at which the chart should start from (optional) + @type start: C{datetime.datetime} + @param end: the date at which the chart should end on (optional) + @type end: C{datetime.datetime} + + @return: a track chart for the group + @rtype: L{WeeklyTrackChart} + + @raise InvalidParametersError: Both start and end parameter have to be either + provided or not provided. Providing only one of + them will raise an exception. + """ params = self._default_params({'method': 'group.getWeeklyTrackChart'}) params = WeeklyChart._check_weekly_chart_params(params, start, end) data = self._api._fetch_data(params).find('weeklytrackchart') @@ -87,10 +172,19 @@ class Group(LastfmBase, Cacheable): @cached_property def recent_weekly_track_chart(self): + """ + most recent track chart for the group + @rtype: L{WeeklyTrackChart} + """ return self.get_weekly_track_chart() @cached_property def weekly_track_chart_list(self): + """ + a list of all track charts for this group in reverse-chronological + order. (that means 0th chart is the most recent chart) + @rtype: L{lazylist} of L{WeeklyTrackChart} + """ wcl = list(self.weekly_chart_list) wcl.reverse() @lazylist @@ -101,6 +195,10 @@ class Group(LastfmBase, Cacheable): @cached_property def members(self): + """ + members of the group + @rtype: L{lazylist} of L{User} + """ params = self._default_params({'method': 'group.getMembers'}) @lazylist diff --git a/lastfm/mixins/__init__.py b/lastfm/mixins/__init__.py index 7e04ddf..ddfd1d8 100644 --- a/lastfm/mixins/__init__.py +++ b/lastfm/mixins/__init__.py @@ -3,6 +3,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm.mixins" from lastfm.mixins.cacheable import Cacheable from lastfm.mixins.searchable import Searchable diff --git a/lastfm/mixins/cacheable.py b/lastfm/mixins/cacheable.py index 8041621..3af7233 100644 --- a/lastfm/mixins/cacheable.py +++ b/lastfm/mixins/cacheable.py @@ -3,6 +3,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm.mixins" try: from threading import Lock diff --git a/lastfm/mixins/searchable.py b/lastfm/mixins/searchable.py index e4cca44..cfbc528 100644 --- a/lastfm/mixins/searchable.py +++ b/lastfm/mixins/searchable.py @@ -3,6 +3,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm.mixins" from lastfm.lazylist import lazylist diff --git a/lastfm/mixins/sharable.py b/lastfm/mixins/sharable.py index 6afeab2..5f56627 100644 --- a/lastfm/mixins/sharable.py +++ b/lastfm/mixins/sharable.py @@ -3,6 +3,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm.mixins" from lastfm.decorators import authenticate @@ -10,7 +11,6 @@ class Sharable(object): def init(self, api): self._api = api - @authenticate def share(self, recipient, message = None): from lastfm.user import User params = self._default_params({'method': '%s.share' % self.__class__.__name__.lower()}) diff --git a/lastfm/mixins/shoutable.py b/lastfm/mixins/shoutable.py index fcd96db..1ef03b3 100644 --- a/lastfm/mixins/shoutable.py +++ b/lastfm/mixins/shoutable.py @@ -3,6 +3,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm.mixins" from lastfm.base import LastfmBase from lastfm.decorators import cached_property, top_property diff --git a/lastfm/mixins/taggable.py b/lastfm/mixins/taggable.py index d9fd6d7..ee5f2c3 100644 --- a/lastfm/mixins/taggable.py +++ b/lastfm/mixins/taggable.py @@ -3,6 +3,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm.mixins" from lastfm.base import LastfmBase from lastfm.safelist import SafeList diff --git a/lastfm/objectcache.py b/lastfm/objectcache.py index bd8122c..a2fd0ea 100644 --- a/lastfm/objectcache.py +++ b/lastfm/objectcache.py @@ -3,6 +3,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" from lastfm.album import Album from lastfm.artist import Artist diff --git a/lastfm/playlist.py b/lastfm/playlist.py index 4fc3098..88d3d90 100644 --- a/lastfm/playlist.py +++ b/lastfm/playlist.py @@ -3,6 +3,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" from lastfm.base import LastfmBase from lastfm.mixins import Cacheable diff --git a/lastfm/safelist.py b/lastfm/safelist.py index 67164ef..d9ce597 100644 --- a/lastfm/safelist.py +++ b/lastfm/safelist.py @@ -3,6 +3,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" import sys class SafeList(object): diff --git a/lastfm/shout.py b/lastfm/shout.py index b47c80a..68613fe 100644 --- a/lastfm/shout.py +++ b/lastfm/shout.py @@ -3,6 +3,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" from lastfm.base import LastfmBase from lastfm.mixins import Cacheable diff --git a/lastfm/stats.py b/lastfm/stats.py index 198d657..c476801 100644 --- a/lastfm/stats.py +++ b/lastfm/stats.py @@ -3,6 +3,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" class Stats(object): """A class representing the stats of an artist.""" diff --git a/lastfm/tag.py b/lastfm/tag.py index 1ca8ace..7b7e1e9 100644 --- a/lastfm/tag.py +++ b/lastfm/tag.py @@ -3,6 +3,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" from lastfm.base import LastfmBase from lastfm.mixins import Cacheable, Searchable diff --git a/lastfm/tasteometer.py b/lastfm/tasteometer.py index 6aca50f..b8182ca 100644 --- a/lastfm/tasteometer.py +++ b/lastfm/tasteometer.py @@ -3,6 +3,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" class Tasteometer(object): """A class representing a tasteometer.""" diff --git a/lastfm/track.py b/lastfm/track.py index d9b2135..399975d 100644 --- a/lastfm/track.py +++ b/lastfm/track.py @@ -3,6 +3,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" from lastfm.base import LastfmBase from lastfm.mixins import Cacheable, Searchable, Sharable, Taggable diff --git a/lastfm/user.py b/lastfm/user.py index adea0d3..4b4023d 100644 --- a/lastfm/user.py +++ b/lastfm/user.py @@ -3,6 +3,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" from lastfm.base import LastfmBase from lastfm.mixins import Cacheable, Shoutable diff --git a/lastfm/venue.py b/lastfm/venue.py index a4ea5d3..e81ebb2 100644 --- a/lastfm/venue.py +++ b/lastfm/venue.py @@ -3,6 +3,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" from lastfm.base import LastfmBase from lastfm.mixins import Cacheable, Searchable diff --git a/lastfm/weeklychart.py b/lastfm/weeklychart.py index 9e0c5e9..01a54af 100644 --- a/lastfm/weeklychart.py +++ b/lastfm/weeklychart.py @@ -3,9 +3,11 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" from lastfm.base import LastfmBase from lastfm.mixins import Cacheable +from operator import xor class WeeklyChart(LastfmBase, Cacheable): """A class for representing the weekly charts""" @@ -43,7 +45,7 @@ class WeeklyChart(LastfmBase, Cacheable): @staticmethod def _check_weekly_chart_params(params, start = None, end = None): - if (start is not None and end is None) or (start is None and end is not None): + if xor(start is None, end is None): raise InvalidParametersError("both start and end have to be provided.") if start is not None and end is not None: if isinstance(start, datetime) and isinstance(end, datetime): diff --git a/lastfm/wiki.py b/lastfm/wiki.py index 35170e7..599f54f 100644 --- a/lastfm/wiki.py +++ b/lastfm/wiki.py @@ -3,6 +3,7 @@ __author__ = "Abhinav Sarkar " __version__ = "0.2" __license__ = "GNU Lesser General Public License" +__package__ = "lastfm" class Wiki(object): """A class representing the information from the wiki of the subject.""" diff --git a/test/data/828b985a53dc8a323b59dfabcaf548d8.xml b/test/data/828b985a53dc8a323b59dfabcaf548d8.xml new file mode 100644 index 0000000..218b581 --- /dev/null +++ b/test/data/828b985a53dc8a323b59dfabcaf548d8.xml @@ -0,0 +1,422 @@ + + + + + 920495 + The Birthday + + The Birthday + The Birthday + + + 大分 T.O.P.S + + Japan + Japan + Oita + + + 35.7108378353001 + 139.751586914062 + + JST + + http://www.last.fm/venue/8941482 + + Tue, 17 Mar 2009 + + http://userserve-ak.last.fm/serve/34/557848.jpg + http://userserve-ak.last.fm/serve/64/557848.jpg + http://userserve-ak.last.fm/serve/126/557848.jpg + 1 + 0 + lastfm:event=920495 + http://www.last.fm/event/920495 + + + 929525 + Lewis Furey + + Lewis Furey + Lewis Furey + + + 高田馬場 + + tokyo + Japan + + + + 35.7108378353001 + 139.751586914062 + + JST + + http://www.last.fm/venue/9003318 + + Tue, 17 Mar 2009 + + http://userserve-ak.last.fm/serve/34/23760119.jpg + http://userserve-ak.last.fm/serve/64/23760119.jpg + http://userserve-ak.last.fm/serve/126/23760119.jpg + 0 + 0 + lastfm:event=929525 + http://www.last.fm/event/929525 + + + 828211 + Duffy + + Duffy + Duffy + + + SHIBUYA-AX + + Tōkyō + Japan + 渋谷区神南2-1-1 + 150-0041 + + 35.685 + 139.7513889 + + JST + + http://www.last.fm/venue/8788725 + + Tue, 17 Mar 2009 + 19:00 + + http://userserve-ak.last.fm/serve/34/4893704.jpg + http://userserve-ak.last.fm/serve/64/4893704.jpg + http://userserve-ak.last.fm/serve/126/4893704.jpg + 8 + 0 + lastfm:event=828211 + http://www.last.fm/event/828211 + + + 876654 + Mos Def + + Mos Def + Mos Def + + + Billboard Live + + Tokyo + Japan + Tokyo Midtown Garden Terrace 4F, 7-4 Akasaka 9-chome, Minato-ku + 107-0052 + + 35.7108378353001 + 139.751586914062 + + JST + + http://www.last.fm/venue/8830206 + + Tue, 17 Mar 2009 + 19:00 + + http://userserve-ak.last.fm/serve/34/4114471.jpg + http://userserve-ak.last.fm/serve/64/4114471.jpg + http://userserve-ak.last.fm/serve/126/4114471.jpg + 4 + 0 + lastfm:event=876654 + http://www.last.fm/event/876654 + + + 957549 + No Charge!! Club Cyber Collection 2009 + + Ancestral + GRiST + Diprogram + Ziggrat + Mistes + カルト☆フィクション倶楽部 + Cloud. + ザ☆ビックマウス + Ancestral + + + 池袋CYBER + + Tokyo + Japan + + + + 35.7108378353001 + 139.751586914062 + + JST + + http://www.last.fm/venue/8854834 + + Wed, 18 Mar 2009 + Free live (drink 600 yens)]]> + http://userserve-ak.last.fm/serve/34/17811927.jpg + http://userserve-ak.last.fm/serve/64/17811927.jpg + http://userserve-ak.last.fm/serve/126/17811927.jpg + 2 + 0 + lastfm:event=957549 + http://www.last.fm/event/957549 + + + 888895 + Ryuichi Sakamoto Playing The Piano 2009 + + 坂本龍一 + 坂本龍一 + + + 東京国際フォーラム ホールC + + 東京都 + Japan + 千代田区丸の内3-5-1 + 100-0005 + + 35.7108378353001 + 139.751586914062 + + JST + + http://www.last.fm/venue/8994410 + + Wed, 18 Mar 2009 + 19:00 + + http://userserve-ak.last.fm/serve/34/24067119.jpg + http://userserve-ak.last.fm/serve/64/24067119.jpg + http://userserve-ak.last.fm/serve/126/24067119.jpg + 3 + 0 + lastfm:event=888895 + http://www.last.fm/event/888895 + + + 948701 + Crazy Die Amond Act.3 + + Tecda + Helmet + Asa + Sol + shino + Ein + Hisato + Yudai + starsoldier + koshi fujiyama + digital sato + shikaval + Suoiyo! Masarusan + Tecda + + + Bitters Yokohama + + Yokohama, Kanagawa + Japan + + + + 35.45 + 139.65 + + UTC + + http://www.last.fm/venue/8981623 + + Thu, 19 Mar 2009 + 2009.03.19.THU
+
+Solid De Acid Disco Party
+
+"Crazy Die Amond Act.3″
+
+@Studio Bitters yokohama
+
+door: 2000yen/1D
+
+w/f: 1500yen/1D
+
+Start: 22:00
+
+Live set
+
+Tecda(shizukamura.net)
+
+Djz
+
+Digital Sato(PsycotroBeat)
+
+ASA(IMP, stonetemple)
+
+SOL(MACHiDA CYCHO)
+
+HELMET(dbc17)
+
+Yudai(crazy die amond)
+
+EIN(codekommando/Berlin)
+
+shikaval(closed)
+
+Sound System by 和泉中央
+
+Bar/パラレルラウンジ
+
+Djz
+
+shino(shizukamura.net)
+
+starsoldier(9we9 Sound)
+
+koshi fujiyama(最高変態)
+
+SOL(FirstClass)
+
+hisato
+
+sugoiyo!!masarusan(sexy command)
+
+INCENSE SHOP『幻夢堂』
+
+危険物、法律で禁止されてる物の持ち込みは固くお断りします。
+
+20歳以下の方は入場できません。
+
+尚、すべてのトラブル、事故等は責任を負いかねます。
+
+Studio Bitters yokohama
+
+〒231-0801 神奈川県横浜市中区新山下3-6-14 1F
+
+TEL/FAX 045-623-6099
+
+※お車でお越しの方は最寄のパーキングをお使いください]]>
+ http://userserve-ak.last.fm/serve/34/24268155.jpg + http://userserve-ak.last.fm/serve/64/24268155.jpg + http://userserve-ak.last.fm/serve/126/24268155.jpg + 1 + 0 + lastfm:event=948701 + http://www.last.fm/event/948701 +
+ + 986029 + Byte Size - Panophonic Downturn Special + + skab/t + skab/t + + + Bar Aoyama, Shibuya + + Tokyo + Japan + + + + 35.7108378353001 + 139.751586914062 + + JST + + http://www.last.fm/venue/9015838 + + Thu, 19 Mar 2009 + Map to Venue
+
+
+Byte Size is back! Fighting the spiralling economy with beats and treats at Bar Aoyama in Shibuya. Fun starts at 10pm on Thursday the 19th of March (remember, Friday is a public holiday people!). We will bring you, as always, a rich selection of DJs and live electronic music from around Tokyo. No door/table charge, cheap drinks and a recently redecorated venue.]]>
+ http://userserve-ak.last.fm/serve/34/24739555.jpg + http://userserve-ak.last.fm/serve/64/24739555.jpg + http://userserve-ak.last.fm/serve/126/24739555.jpg + 2 + 0 + lastfm:event=986029 + http://www.last.fm/event/986029 +
+ + 846323 + 『Baggy Bogy Free Festival』 + + バギーボギー + バギーボギー + + + 池袋CYBER + + Tokyo + Japan + + + + 35.7108378353001 + 139.751586914062 + + JST + + http://www.last.fm/venue/8854834 + + Thu, 19 Mar 2009 + OPEN/未定 START/未定
+前売/¥0 当日/¥0
+ドリンク/別]]>
+ http://userserve-ak.last.fm/serve/34/11871967.jpg + http://userserve-ak.last.fm/serve/64/11871967.jpg + http://userserve-ak.last.fm/serve/126/11871967.jpg + 1 + 0 + lastfm:event=846323 + http://www.last.fm/event/846323 +
+ + 951660 + GAN-BAN NIGHT SPECIAL + + 電気グルーヴ + Fumiya Tanaka + Metalmouse + 80KIDZ + 田中フミヤ + DEXPISTOLS + 電気グルーヴ + + + ageHa@STUDIO COAST + + Tokyo + Japan + 2-2-10 Kotou-ku, Shinkiba + 136-0082 + + 35.7108378353001 + 139.751586914062 + + JST + + http://www.last.fm/venue/8962708 + + Thu, 19 Mar 2009 + + http://userserve-ak.last.fm/serve/34/5928937.jpg + http://userserve-ak.last.fm/serve/64/5928937.jpg + http://userserve-ak.last.fm/serve/126/5928937.jpg + 5 + 0 + lastfm:event=951660 + http://www.last.fm/event/951660 + +
diff --git a/test/test_geo.py b/test/test_geo.py index f81ce03..763a328 100644 --- a/test/test_geo.py +++ b/test/test_geo.py @@ -55,8 +55,8 @@ class TestGeo(unittest.TestCase): self.assertEqual((top_track.name, top_track.artist.name), ('Viva la Vida', 'Coldplay')) def testLocationEvents(self): - event_ids = [957543, 871240, 843216, 938214, 910474, - 875468, 863115, 954783, 890885, 843238] + event_ids = [920495, 929525, 828211, 876654, 957549, + 888895, 948701, 986029, 846323, 951660] self.assertEqual([e.id for e in self.location.events[:10]], event_ids) def testCountryName(self):