Package musicbrainz2 :: Module webservice

[frames] | no frames]

# Source Code for Module musicbrainz2.webservice

       1  """Classes for interacting with the MusicBrainz XML web service.
       2
       3  The L{WebService} class talks to a server implementing the MusicBrainz XML
       4  web service. It mainly handles URL generation and network I/O. Use this
       5  if maximum control is needed.
       6
       7  The L{Query} class provides a convenient interface to the most commonly
       8  used features of the web service. By default it uses L{WebService} to
       9  retrieve data and the L{XML parser <musicbrainz2.wsxml>} to parse the
      10  responses. The results are object trees using the L{MusicBrainz domain
      11  model <musicbrainz2.model>}.
      12
      13  @author: Matthias Friedrich <matt@mafr.de>
      14  """
      15  __revision__ = '$Id: webservice.py 13325 2011-11-03 13:39:40Z luks $'
      16
      17  import urllib
      18  import urllib2
      19  import urlparse
      20  import logging
      21  import musicbrainz2
      22  from musicbrainz2.model import Release
      23  from musicbrainz2.wsxml import MbXmlParser, ParseError
      24  import musicbrainz2.utils as mbutils
      25
      26  __all__ = [
      27          'WebServiceError', 'AuthenticationError', 'ConnectionError',
      28          'RequestError', 'ResourceNotFoundError', 'ResponseError',
      29          'IIncludes', 'ArtistIncludes', 'ReleaseIncludes', 'TrackIncludes',
      30          'LabelIncludes', 'ReleaseGroupIncludes',
      31          'IFilter', 'ArtistFilter', 'ReleaseFilter', 'TrackFilter',
      32          'UserFilter', 'LabelFilter', 'ReleaseGroupFilter',
      33          'IWebService', 'WebService', 'Query',
      34  ]
      35
      36
      37 -class IWebService(object):
      38          """An interface all concrete web service classes have to implement.
      39
      40          All web service classes have to implement this and follow the
      41          method specifications.
      42          """
      43
      44 -        def get(self, entity, id_, include, filter, version):
      45                  """Query the web service.
      46
      47                  Using this method, you can either get a resource by id (using
      48                  the C{id_} parameter, or perform a query on all resources of
      49                  a type.
      50
      51                  The C{filter} and the C{id_} parameter exclude each other. If
      52                  you are using a filter, you may not set C{id_} and vice versa.
      53
      54                  Returns a file-like object containing the result or raises a
      55                  L{WebServiceError} or one of its subclasses in case of an
      56                  error. Which one is used depends on the implementing class.
      57
      58                  @param entity: a string containing the entity's name
      59                  @param id_: a string containing a UUID, or the empty string
      60                  @param include: a tuple containing values for the 'inc' parameter
      61                  @param filter: parameters, depending on the entity
      62                  @param version: a string containing the web service version to use
      63
      64                  @return: a file-like object
      65
      66                  @raise WebServiceError: in case of errors
      67                  """
      68                  raise NotImplementedError()
      69
      70
      71 -        def post(self, entity, id_, data, version):
      72                  """Submit data to the web service.
      73
      74                  @param entity: a string containing the entity's name
      75                  @param id_: a string containing a UUID, or the empty string
      76                  @param data: A string containing the data to post
      77                  @param version: a string containing the web service version to use
      78
      79                  @return: a file-like object
      80
      81                  @raise WebServiceError: in case of errors
      82                  """
      83                  raise NotImplementedError()
      84
      85
      86 -class WebServiceError(Exception):
      87          """A web service error has occurred.
      88
      89          This is the base class for several other web service related
      90          exceptions.
      91          """
      92
      93 -        def __init__(self, msg='Webservice Error', reason=None):
      94                  """Constructor.
      95
      96                  Set C{msg} to an error message which explains why this
      97                  exception was raised. The C{reason} parameter should be the
      98                  original exception which caused this L{WebService} exception
      99                  to be raised. If given, it has to be an instance of
     100                  C{Exception} or one of its child classes.
     101
     102                  @param msg: a string containing an error message
     103                  @param reason: another exception instance, or None
     104                  """
     105                  Exception.__init__(self)
     106                  self.msg = msg
     107                  self.reason = reason
     108
     109 -        def __str__(self):
     110                  """Makes this class printable.
     111
     112                  @return: a string containing an error message
     113                  """
     114                  return self.msg
     115
     116
     117 -class ConnectionError(WebServiceError):
     118          """Getting a server connection failed.
     119
     120          This exception is mostly used if the client couldn't connect to
     121          the server because of an invalid host name or port. It doesn't
     122          make sense if the web service in question doesn't use the network.
     123          """
     124          pass
     125
     126
     127 -class RequestError(WebServiceError):
     128          """An invalid request was made.
     129
     130          This exception is raised if the client made an invalid request.
     131          That could be syntactically invalid identifiers or unknown or
     132          invalid parameter values.
     133          """
     134          pass
     135
     136
     137 -class ResourceNotFoundError(WebServiceError):
     138          """No resource with the given ID exists.
     139
     140          This is usually a wrapper around IOError (which is superclass of
     141          HTTPError).
     142          """
     143          pass
     144
     145
     146 -class AuthenticationError(WebServiceError):
     147          """Authentication failed.
     148
     149          This is thrown if user name, password or realm were invalid while
     150          trying to access a protected resource.
     151          """
     152          pass
     153
     154
     155 -class ResponseError(WebServiceError):
     156          """The returned resource was invalid.
     157
     158          This may be due to a malformed XML document or if the requested
     159          data wasn't part of the response. It can only occur in case of
     160          bugs in the web service itself.
     161          """
     162          pass
     163
     164 -class DigestAuthHandler(urllib2.HTTPDigestAuthHandler):
     165          """Patched DigestAuthHandler to correctly handle Digest Auth according to RFC 2617.
     166
     167          This will allow multiple qop values in the WWW-Authenticate header (e.g. "auth,auth-int").
     168          The only supported qop value is still auth, though.
     169          See http://bugs.python.org/issue9714
     170
     171          @author: Kuno Woudt
     172          """
     173 -        def get_authorization(self, req, chal):
     174                  qop = chal.get('qop')
     175                  if qop and ',' in qop and 'auth' in qop.split(','):
     176                          chal['qop'] = 'auth'
     177
     178                  return urllib2.HTTPDigestAuthHandler.get_authorization(self, req, chal)
     179
     180 -class WebService(IWebService):
     181          """An interface to the MusicBrainz XML web service via HTTP.
     182
     183          By default, this class uses the MusicBrainz server but may be
     184          configured for accessing other servers as well using the
     185          L{constructor <__init__>}. This implements L{IWebService}, so
     186          additional documentation on method parameters can be found there.
     187          """
     188
     189 -        def __init__(self, host='musicbrainz.org', port=80, pathPrefix='/ws',
     190                          username=None, password=None, realm='musicbrainz.org',
     191                          opener=None, userAgent=None):
     192                  """Constructor.
     193
     194                  This can be used without parameters. In this case, the
     195                  MusicBrainz server will be used.
     196
     197                  @param host: a string containing a host name
     198                  @param port: an integer containing a port number
     199                  @param pathPrefix: a string prepended to all URLs
     200                  @param username: a string containing a MusicBrainz user name
     201                  @param password: a string containing the user's password
     202                  @param realm: a string containing the realm used for authentication
     203                  @param opener: an C{urllib2.OpenerDirector} object used for queries
     204                  @param userAgent: a string containing the user agent
     205                  """
     206                  self._host = host
     207                  self._port = port
     208                  self._username = username
     209                  self._password = password
     210                  self._realm = realm
     211                  self._pathPrefix = pathPrefix
     212                  self._log = logging.getLogger(str(self.__class__))
     213
     214                  if opener is None:
     215                          self._opener = urllib2.build_opener()
     216                  else:
     217                          self._opener = opener
     218
     219                  if userAgent is None:
     220                          self._userAgent = "python-musicbrainz/" + musicbrainz2.__version__
     221                  else:
     222                          self._userAgent = userAgent.replace("-", "/") \
     223                                                                  + " python-musicbrainz/" \
     224                                                                  + musicbrainz2.__version__
     225
     226                  passwordMgr = self._RedirectPasswordMgr()
     227                  authHandler = DigestAuthHandler(passwordMgr)
     228                  authHandler.add_password(self._realm, (), # no host set
     229                          self._username, self._password)
     230                  self._opener.add_handler(authHandler)
     231
     232
     233 -        def _makeUrl(self, entity, id_, include=( ), filter={ },
     234                          version='1', type_='xml'):
     235                  params = dict(filter)
     236                  if type_ is not None:
     237                          params['type'] = type_
     238                  if len(include) > 0:
     239                          params['inc'] = ' '.join(include)
     240
     241                  netloc = self._host
     242                  if self._port != 80:
     243                          netloc += ':' + str(self._port)
     244                  path = '/'.join((self._pathPrefix, version, entity, id_))
     245
     246                  query = urllib.urlencode(params)
     247
     248                  url = urlparse.urlunparse(('http', netloc, path, '', query,''))
     249
     250                  return url
     251
     252
     253 -        def _openUrl(self, url, data=None):
     254                  req = urllib2.Request(url)
     255                  req.add_header('User-Agent', self._userAgent)
     256                  return self._opener.open(req, data)
     257
     258
     259 -        def get(self, entity, id_, include=( ), filter={ }, version='1'):
     260                  """Query the web service via HTTP-GET.
     261
     262                  Returns a file-like object containing the result or raises a
     263                  L{WebServiceError}. Conditions leading to errors may be
     264                  invalid entities, IDs, C{include} or C{filter} parameters
     265                  and unsupported version numbers.
     266
     267                  @raise ConnectionError: couldn't connect to server
     268                  @raise RequestError: invalid IDs or parameters
     269                  @raise AuthenticationError: invalid user name and/or password
     270                  @raise ResourceNotFoundError: resource doesn't exist
     271
     272                  @see: L{IWebService.get}
     273                  """
     274                  url = self._makeUrl(entity, id_, include, filter, version)
     275
     276                  self._log.debug('GET ' + url)
     277
     278                  try:
     279                          return self._openUrl(url)
     280                  except urllib2.HTTPError, e:
     281                          self._log.debug("GET failed: " + str(e))
     282                          if e.code == 400:   # in python 2.4: httplib.BAD_REQUEST
     283                                  raise RequestError(str(e), e)
     284                          elif e.code == 401: # httplib.UNAUTHORIZED
     285                                  raise AuthenticationError(str(e), e)
     286                          elif e.code == 404: # httplib.NOT_FOUND
     287                                  raise ResourceNotFoundError(str(e), e)
     288                          else:
     289                                  raise WebServiceError(str(e), e)
     290                  except urllib2.URLError, e:
     291                          self._log.debug("GET failed: " + str(e))
     292                          raise ConnectionError(str(e), e)
     293
     294
     295 -        def post(self, entity, id_, data, version='1'):
     296                  """Send data to the web service via HTTP-POST.
     297
     298                  Note that this may require authentication. You can set
     299                  user name, password and realm in the L{constructor <__init__>}.
     300
     301                  @raise ConnectionError: couldn't connect to server
     302                  @raise RequestError: invalid IDs or parameters
     303                  @raise AuthenticationError: invalid user name and/or password
     304                  @raise ResourceNotFoundError: resource doesn't exist
     305
     306                  @see: L{IWebService.post}
     307                  """
     308                  url = self._makeUrl(entity, id_, version=version, type_=None)
     309
     310                  self._log.debug('POST ' + url)
     311                  self._log.debug('POST-BODY: ' + data)
     312
     313                  try:
     314                          return self._openUrl(url, data)
     315                  except urllib2.HTTPError, e:
     316                          self._log.debug("POST failed: " + str(e))
     317                          if e.code == 400:   # in python 2.4: httplib.BAD_REQUEST
     318                                  raise RequestError(str(e), e)
     319                          elif e.code == 401: # httplib.UNAUTHORIZED
     320                                  raise AuthenticationError(str(e), e)
     321                          elif e.code == 404: # httplib.NOT_FOUND
     322                                  raise ResourceNotFoundError(str(e), e)
     323                          else:
     324                                  raise WebServiceError(str(e), e)
     325                  except urllib2.URLError, e:
     326                          self._log.debug("POST failed: " + str(e))
     327                          raise ConnectionError(str(e), e)
     328
     329
     330          # Special password manager which also works with redirects by simply
     331          # ignoring the URI. As a consequence, only *ONE* (username, password)
     332          # tuple per realm can be used for all URIs.
     333          #
     334 -        class _RedirectPasswordMgr(urllib2.HTTPPasswordMgr):
     335 -                def __init__(self):
     336                          self._realms = { }
     337
     338 -                def find_user_password(self, realm, uri):
     339                          # ignoring the uri parameter intentionally
     340                          try:
     341                                  return self._realms[realm]
     342                          except KeyError:
     343                                  return (None, None)
     344
     345 -                def add_password(self, realm, uri, username, password):
     346                          # ignoring the uri parameter intentionally
     347                          self._realms[realm] = (username, password)
     348
     349
     350 -class IFilter(object):
     351          """A filter for collections.
     352
     353          This is the interface all filters have to implement. Filter classes
     354          are initialized with a set of criteria and are then applied to
     355          collections of items. The criteria are usually strings or integer
     356          values, depending on the filter.
     357
     358          Note that all strings passed to filters should be unicode strings
     359          (python type C{unicode}). Standard strings are converted to unicode
     360          internally, but have a limitation: Only 7 Bit pure ASCII characters
     361          may be used, otherwise a C{UnicodeDecodeError} is raised.
     362          """
     363 -        def createParameters(self):
     364                  """Create a list of query parameters.
     365
     366                  This method creates a list of (C{parameter}, C{value}) tuples,
     367                  based on the contents of the implementing subclass.
     368                  C{parameter} is a string containing a parameter name
     369                  and C{value} an arbitrary string. No escaping of those strings
     370                  is required.
     371
     372                  @return: a sequence of (key, value) pairs
     373                  """
     374                  raise NotImplementedError()
     375
     376
     377 -class ArtistFilter(IFilter):
     378          """A filter for the artist collection."""
     379
     380 -        def __init__(self, name=None, limit=None, offset=None, query=None):
     381                  """Constructor.
     382
     383                  The C{query} parameter may contain a query in U{Lucene syntax
     384                  <http://lucene.apache.org/java/docs/queryparsersyntax.html>}.
     385                  Note that the C{name} and C{query} may not be used together.
     386
     387                  @param name: a unicode string containing the artist's name
     388                  @param limit: the maximum number of artists to return
     389                  @param offset: start results at this zero-based offset
     390                  @param query: a string containing a query in Lucene syntax
     391                  """
     392                  self._params = [
     393                          ('name', name),
     394                          ('limit', limit),
     395                          ('offset', offset),
     396                          ('query', query),
     397                  ]
     398
     399                  if not _paramsValid(self._params):
     400                          raise ValueError('invalid combination of parameters')
     401
     402 -        def createParameters(self):
     403                  return _createParameters(self._params)
     404
     405
     406 -class LabelFilter(IFilter):
     407          """A filter for the label collection."""
     408
     409 -        def __init__(self, name=None, limit=None, offset=None, query=None):
     410                  """Constructor.
     411
     412                  The C{query} parameter may contain a query in U{Lucene syntax
     413                  <http://lucene.apache.org/java/docs/queryparsersyntax.html>}.
     414                  Note that the C{name} and C{query} may not be used together.
     415
     416                  @param name: a unicode string containing the label's name
     417                  @param limit: the maximum number of labels to return
     418                  @param offset: start results at this zero-based offset
     419                  @param query: a string containing a query in Lucene syntax
     420                  """
     421                  self._params = [
     422                          ('name', name),
     423                          ('limit', limit),
     424                          ('offset', offset),
     425                          ('query', query),
     426                  ]
     427
     428                  if not _paramsValid(self._params):
     429                          raise ValueError('invalid combination of parameters')
     430
     431 -        def createParameters(self):
     432                  return _createParameters(self._params)
     433
     434 -class ReleaseGroupFilter(IFilter):
     435          """A filter for the release group collection."""
     436
     437 -        def __init__(self, title=None, releaseTypes=None, artistName=None,
     438                          artistId=None, limit=None, offset=None, query=None):
     439                  """Constructor.
     440
     441                  If C{artistId} is set, only releases matching those IDs are
     442                  returned.  The C{releaseTypes} parameter allows you to limit
     443                  the types of the release groups returned. You can set it to
     444                  C{(Release.TYPE_ALBUM, Release.TYPE_OFFICIAL)}, for example,
     445                  to only get officially released albums. Note that those values
     446                  are connected using the I{AND} operator. MusicBrainz' support
     447                  is currently very limited, so C{Release.TYPE_LIVE} and
     448                  C{Release.TYPE_COMPILATION} exclude each other (see U{the
     449                  documentation on release attributes
     450                  <http://wiki.musicbrainz.org/AlbumAttribute>} for more
     451                  information and all valid values).
     452
     453                  If both the C{artistName} and the C{artistId} parameter are
     454                  given, the server will ignore C{artistName}.
     455
     456                  The C{query} parameter may contain a query in U{Lucene syntax
     457                  <http://lucene.apache.org/java/docs/queryparsersyntax.html>}.
     458                  Note that C{query} may not be used together with the other
     459                  parameters except for C{limit} and C{offset}.
     460
     461                  @param title: a unicode string containing the release group's title
     462                  @param releaseTypes: a sequence of release type URIs
     463                  @param artistName: a unicode string containing the artist's name
     464                  @param artistId: a unicode string containing the artist's ID
     465                  @param limit: the maximum number of release groups to return
     466                  @param offset: start results at this zero-based offset
     467                  @param query: a string containing a query in Lucene syntax
     468
     469                  @see: the constants in L{musicbrainz2.model.Release}
     470                  """
     471                  if releaseTypes is None or len(releaseTypes) == 0:
     472                          releaseTypesStr = None
     473                  else:
     474                          releaseTypesStr = ' '.join(map(mbutils.extractFragment, releaseTypes))
     475
     476                  self._params = [
     477                          ('title', title),
     478                          ('releasetypes', releaseTypesStr),
     479                          ('artist', artistName),
     480                          ('artistid', mbutils.extractUuid(artistId)),
     481                          ('limit', limit),
     482                          ('offset', offset),
     483                          ('query', query),
     484                  ]
     485
     486                  if not _paramsValid(self._params):
     487                          raise ValueError('invalid combination of parameters')
     488
     489 -        def createParameters(self):
     490                  return _createParameters(self._params)
     491
     492
     493 -class ReleaseFilter(IFilter):
     494          """A filter for the release collection."""
     495
     496 -        def __init__(self, title=None, discId=None, releaseTypes=None,
     497                          artistName=None, artistId=None, limit=None,
     498                          offset=None, query=None, trackCount=None):
     499                  """Constructor.
     500
     501                  If C{discId} or C{artistId} are set, only releases matching
     502                  those IDs are returned. The C{releaseTypes} parameter allows
     503                  to limit the types of the releases returned. You can set it to
     504                  C{(Release.TYPE_ALBUM, Release.TYPE_OFFICIAL)}, for example,
     505                  to only get officially released albums. Note that those values
     506                  are connected using the I{AND} operator. MusicBrainz' support
     507                  is currently very limited, so C{Release.TYPE_LIVE} and
     508                  C{Release.TYPE_COMPILATION} exclude each other (see U{the
     509                  documentation on release attributes
     510                  <http://wiki.musicbrainz.org/AlbumAttribute>} for more
     511                  information and all valid values).
     512
     513                  If both the C{artistName} and the C{artistId} parameter are
     514                  given, the server will ignore C{artistName}.
     515
     516                  The C{query} parameter may contain a query in U{Lucene syntax
     517                  <http://lucene.apache.org/java/docs/queryparsersyntax.html>}.
     518                  Note that C{query} may not be used together with the other
     519                  parameters except for C{limit} and C{offset}.
     520
     521                  @param title: a unicode string containing the release's title
     522                  @param discId: a unicode string containing the DiscID
     523                  @param releaseTypes: a sequence of release type URIs
     524                  @param artistName: a unicode string containing the artist's name
     525                  @param artistId: a unicode string containing the artist's ID
     526                  @param limit: the maximum number of releases to return
     527                  @param offset: start results at this zero-based offset
     528                  @param query: a string containing a query in Lucene syntax
     529                  @param trackCount: the number of tracks in the release
     530
     531                  @see: the constants in L{musicbrainz2.model.Release}
     532                  """
     533                  if releaseTypes is None or len(releaseTypes) == 0:
     534                          releaseTypesStr = None
     535                  else:
     536                          tmp = [ mbutils.extractFragment(x) for x in releaseTypes ]
     537                          releaseTypesStr = ' '.join(tmp)
     538
     539                  self._params = [
     540                          ('title', title),
     541                          ('discid', discId),
     542                          ('releasetypes', releaseTypesStr),
     543                          ('artist', artistName),
     544                          ('artistid', mbutils.extractUuid(artistId)),
     545                          ('limit', limit),
     546                          ('offset', offset),
     547                          ('query', query),
     548                          ('count', trackCount),
     549                  ]
     550
     551                  if not _paramsValid(self._params):
     552                          raise ValueError('invalid combination of parameters')
     553
     554 -        def createParameters(self):
     555                  return _createParameters(self._params)
     556
     557
     558 -class TrackFilter(IFilter):
     559          """A filter for the track collection."""
     560
     561 -        def __init__(self, title=None, artistName=None, artistId=None,
     562                          releaseTitle=None, releaseId=None,
     563                          duration=None, puid=None, limit=None, offset=None,
     564                          query=None):
     565                  """Constructor.
     566
     567                  If C{artistId}, C{releaseId} or C{puid} are set, only tracks
     568                  matching those IDs are returned.
     569
     570                  The server will ignore C{artistName} and C{releaseTitle} if
     571                  C{artistId} or ${releaseId} are set respectively.
     572
     573                  The C{query} parameter may contain a query in U{Lucene syntax
     574                  <http://lucene.apache.org/java/docs/queryparsersyntax.html>}.
     575                  Note that C{query} may not be used together with the other
     576                  parameters except for C{limit} and C{offset}.
     577
     578                  @param title: a unicode string containing the track's title
     579                  @param artistName: a unicode string containing the artist's name
     580                  @param artistId: a string containing the artist's ID
     581                  @param releaseTitle: a unicode string containing the release's title
     582                  @param releaseId: a string containing the release's title
     583                  @param duration: the track's length in milliseconds
     584                  @param puid: a string containing a PUID
     585                  @param limit: the maximum number of releases to return
     586                  @param offset: start results at this zero-based offset
     587                  @param query: a string containing a query in Lucene syntax
     588                  """
     589                  self._params = [
     590                          ('title', title),
     591                          ('artist', artistName),
     592                          ('artistid', mbutils.extractUuid(artistId)),
     593                          ('release', releaseTitle),
     594                          ('releaseid', mbutils.extractUuid(releaseId)),
     595                          ('duration', duration),
     596                          ('puid', puid),
     597                          ('limit', limit),
     598                          ('offset', offset),
     599                          ('query', query),
     600                  ]
     601
     602                  if not _paramsValid(self._params):
     603                          raise ValueError('invalid combination of parameters')
     604
     605 -        def createParameters(self):
     606                  return _createParameters(self._params)
     607
     608
     609 -class UserFilter(IFilter):
     610          """A filter for the user collection."""
     611
     612 -        def __init__(self, name=None):
     613                  """Constructor.
     614
     615                  @param name: a unicode string containing a MusicBrainz user name
     616                  """
     617                  self._name = name
     618
     619 -        def createParameters(self):
     620                  if self._name is not None:
     621                          return [ ('name', self._name.encode('utf-8')) ]
     622                  else:
     623                          return [ ]
     624
     625
     626 -class IIncludes(object):
     627          """An interface implemented by include tag generators."""
     628 -        def createIncludeTags(self):
     629                  raise NotImplementedError()
     630
     631
     632 -class ArtistIncludes(IIncludes):
     633          """A specification on how much data to return with an artist.
     634
     635          Example:
     636
     637          >>> from musicbrainz2.model import Release
     638          >>> from musicbrainz2.webservice import ArtistIncludes
     639          >>> inc = ArtistIncludes(artistRelations=True, releaseRelations=True,
     640          ...             releases=(Release.TYPE_ALBUM, Release.TYPE_OFFICIAL))
     641          >>>
     642
     643          The MusicBrainz server only supports some combinations of release
     644          types for the C{releases} and C{vaReleases} include tags. At the
     645          moment, not more than two release types should be selected, while
     646          one of them has to be C{Release.TYPE_OFFICIAL},
     647          C{Release.TYPE_PROMOTION} or C{Release.TYPE_BOOTLEG}.
     648
     649          @note: Only one of C{releases} and C{vaReleases} may be given.
     650          """
     651 -        def __init__(self, aliases=False, releases=(), vaReleases=(),
     652                          artistRelations=False, releaseRelations=False,
     653                          trackRelations=False, urlRelations=False, tags=False,
     654                          ratings=False, releaseGroups=False):
     655
     656                  assert not isinstance(releases, basestring)
     657                  assert not isinstance(vaReleases, basestring)
     658                  assert len(releases) == 0 or len(vaReleases) == 0
     659
     660                  self._includes = {
     661                          'aliases':              aliases,
     662                          'artist-rels':          artistRelations,
     663                          'release-groups':       releaseGroups,
     664                          'release-rels':         releaseRelations,
     665                          'track-rels':           trackRelations,
     666                          'url-rels':             urlRelations,
     667                          'tags':                 tags,
     668                          'ratings':              ratings,
     669                  }
     670
     671                  for elem in releases:
     672                          self._includes['sa-' + mbutils.extractFragment(elem)] = True
     673
     674                  for elem in vaReleases:
     675                          self._includes['va-' + mbutils.extractFragment(elem)] = True
     676
     677 -        def createIncludeTags(self):
     678                  return _createIncludes(self._includes)
     679
     680
     681 -class ReleaseIncludes(IIncludes):
     682          """A specification on how much data to return with a release."""
     683 -        def __init__(self, artist=False, counts=False, releaseEvents=False,
     684                          discs=False, tracks=False,
     685                          artistRelations=False, releaseRelations=False,
     686                          trackRelations=False, urlRelations=False,
     687                          labels=False, tags=False, ratings=False, isrcs=False,
     688                          releaseGroup=False):
     689                  self._includes = {
     690                          'artist':               artist,
     691                          'counts':               counts,
     692                          'labels':               labels,
     693                          'release-groups':       releaseGroup,
     694                          'release-events':       releaseEvents,
     695                          'discs':                discs,
     696                          'tracks':               tracks,
     697                          'artist-rels':          artistRelations,
     698                          'release-rels':         releaseRelations,
     699                          'track-rels':           trackRelations,
     700                          'url-rels':             urlRelations,
     701                          'tags':                 tags,
     702                          'ratings':              ratings,
     703                          'isrcs':                isrcs,
     704                  }
     705
     706                  # Requesting labels without releaseEvents makes no sense,
     707                  # so we pull in releaseEvents, if necessary.
     708                  if labels and not releaseEvents:
     709                          self._includes['release-events'] = True
     710                  # Ditto for isrcs with no tracks
     711                  if isrcs and not tracks:
     712                          self._includes['tracks'] = True
     713
     714 -        def createIncludeTags(self):
     715                  return _createIncludes(self._includes)
     716
     717
     718 -class ReleaseGroupIncludes(IIncludes):
     719          """A specification on how much data to return with a release group."""
     720
     721 -        def __init__(self, artist=False, releases=False, tags=False):
     722                  """Constructor.
     723
     724                  @param artist: Whether to include the release group's main artist info.
     725                  @param releases: Whether to include the release group's releases.
     726                  """
     727                  self._includes = {
     728                          'artist':               artist,
     729                          'releases':             releases,
     730                  }
     731
     732 -        def createIncludeTags(self):
     733                  return _createIncludes(self._includes)
     734
     735
     736 -class TrackIncludes(IIncludes):
     737          """A specification on how much data to return with a track."""
     738 -        def __init__(self, artist=False, releases=False, puids=False,
     739                          artistRelations=False, releaseRelations=False,
     740                          trackRelations=False, urlRelations=False, tags=False,
     741                          ratings=False, isrcs=False):
     742                  self._includes = {
     743                          'artist':               artist,
     744                          'releases':             releases,
     745                          'puids':                puids,
     746                          'artist-rels':          artistRelations,
     747                          'release-rels':         releaseRelations,
     748                          'track-rels':           trackRelations,
     749                          'url-rels':             urlRelations,
     750                          'tags':                 tags,
     751                          'ratings':              ratings,
     752                          'isrcs':                isrcs,
     753                  }
     754
     755 -        def createIncludeTags(self):
     756                  return _createIncludes(self._includes)
     757
     758
     759 -class LabelIncludes(IIncludes):
     760          """A specification on how much data to return with a label."""
     761 -        def __init__(self, aliases=False, tags=False, ratings=False):
     762                  self._includes = {
     763                          'aliases':              aliases,
     764                          'tags':                 tags,
     765                          'ratings':              ratings,
     766                  }
     767
     768 -        def createIncludeTags(self):
     769                  return _createIncludes(self._includes)
     770
     771
     772 -class Query(object):
     773          """A simple interface to the MusicBrainz web service.
     774
     775          This is a facade which provides a simple interface to the MusicBrainz
     776          web service. It hides all the details like fetching data from a server,
     777          parsing the XML and creating an object tree. Using this class, you can
     778          request data by ID or search the I{collection} of all resources
     779          (artists, releases, or tracks) to retrieve those matching given
     780          criteria. This document contains examples to get you started.
     781
     782
     783          Working with Identifiers
     784          ========================
     785
     786          MusicBrainz uses absolute URIs as identifiers. For example, the artist
     787          'Tori Amos' is identified using the following URI::
     788                  http://musicbrainz.org/artist/c0b2500e-0cef-4130-869d-732b23ed9df5
     789
     790          In some situations it is obvious from the context what type of
     791          resource an ID refers to. In these cases, abbreviated identifiers may
     792          be used, which are just the I{UUID} part of the URI. Thus the ID above
     793          may also be written like this::
     794                  c0b2500e-0cef-4130-869d-732b23ed9df5
     795
     796          All methods in this class which require IDs accept both the absolute
     797          URI and the abbreviated form (aka the relative URI).
     798
     799
     800          Creating a Query Object
     801          =======================
     802
     803          In most cases, creating a L{Query} object is as simple as this:
     804
     805          >>> import musicbrainz2.webservice as ws
     806          >>> q = ws.Query()
     807          >>>
     808
     809          The instantiated object uses the standard L{WebService} class to
     810          access the MusicBrainz web service. If you want to use a different
     811          server or you have to pass user name and password because one of
     812          your queries requires authentication, you have to create the
     813          L{WebService} object yourself and configure it appropriately.
     814          This example uses the MusicBrainz test server and also sets
     815          authentication data:
     816
     817          >>> import musicbrainz2.webservice as ws
     818          >>> service = ws.WebService(host='test.musicbrainz.org',
     819          ...                             username='whatever', password='secret')
     820          >>> q = ws.Query(service)
     821          >>>
     822
     823
     824          Querying for Individual Resources
     825          =================================
     826
     827          If the MusicBrainz ID of a resource is known, then the L{getArtistById},
     828          L{getReleaseById}, or L{getTrackById} method can be used to retrieve
     829          it. Example:
     830
     831          >>> import musicbrainz2.webservice as ws
     832          >>> q = ws.Query()
     833          >>> artist = q.getArtistById('c0b2500e-0cef-4130-869d-732b23ed9df5')
     834          >>> artist.name
     835          u'Tori Amos'
     836          >>> artist.sortName
     837          u'Amos, Tori'
     838          >>> print artist.type
     839          http://musicbrainz.org/ns/mmd-1.0#Person
     840          >>>
     841
     842          This returned just the basic artist data, however. To get more detail
     843          about a resource, the C{include} parameters may be used which expect
     844          an L{ArtistIncludes}, L{ReleaseIncludes}, or L{TrackIncludes} object,
     845          depending on the resource type.
     846
     847          To get data about a release which also includes the main artist
     848          and all tracks, for example, the following query can be used:
     849
     850          >>> import musicbrainz2.webservice as ws
     851          >>> q = ws.Query()
     852          >>> releaseId = '33dbcf02-25b9-4a35-bdb7-729455f33ad7'
     853          >>> include = ws.ReleaseIncludes(artist=True, tracks=True)
     854          >>> release = q.getReleaseById(releaseId, include)
     855          >>> release.title
     856          u'Tales of a Librarian'
     857          >>> release.artist.name
     858          u'Tori Amos'
     859          >>> release.tracks[0].title
     860          u'Precious Things'
     861          >>>
     862
     863          Note that the query gets more expensive for the server the more
     864          data you request, so please be nice.
     865
     866
     867          Searching in Collections
     868          ========================
     869
     870          For each resource type (artist, release, and track), there is one
     871          collection which contains all resources of a type. You can search
     872          these collections using the L{getArtists}, L{getReleases}, and
     873          L{getTracks} methods. The collections are huge, so you have to
     874          use filters (L{ArtistFilter}, L{ReleaseFilter}, or L{TrackFilter})
     875          to retrieve only resources matching given criteria.
     876
     877          For example, If you want to search the release collection for
     878          releases with a specified DiscID, you would use L{getReleases}
     879          and a L{ReleaseFilter} object:
     880
     881          >>> import musicbrainz2.webservice as ws
     882          >>> q = ws.Query()
     883          >>> filter = ws.ReleaseFilter(discId='8jJklE258v6GofIqDIrE.c5ejBE-')
     884          >>> results = q.getReleases(filter=filter)
     885          >>> results[0].score
     886          100
     887          >>> results[0].release.title
     888          u'Under the Pink'
     889          >>>
     890
     891          The query returns a list of results (L{wsxml.ReleaseResult} objects
     892          in this case), which are ordered by score, with a higher score
     893          indicating a better match. Note that those results don't contain
     894          all the data about a resource. If you need more detail, you can then
     895          use the L{getArtistById}, L{getReleaseById}, or L{getTrackById}
     896          methods to request the resource.
     897
     898          All filters support the C{limit} argument to limit the number of
     899          results returned. This defaults to 25, but the server won't send
     900          more than 100 results to save bandwidth and processing power. Using
     901          C{limit} and the C{offset} parameter, you can page through the
     902          results.
     903
     904
     905          Error Handling
     906          ==============
     907
     908          All methods in this class raise a L{WebServiceError} exception in case
     909          of errors. Depending on the method, a subclass of L{WebServiceError} may
     910          be raised which allows an application to handle errors more precisely.
     911          The following example handles connection errors (invalid host name
     912          etc.) separately and all other web service errors in a combined
     913          catch clause:
     914
     915          >>> try:
     916          ...     artist = q.getArtistById('c0b2500e-0cef-4130-869d-732b23ed9df5')
     917          ... except ws.ConnectionError, e:
     918          ...     pass # implement your error handling here
     919          ... except ws.WebServiceError, e:
     920          ...     pass # catches all other web service errors
     921          ...
     922          >>>
     923          """
     924
     925 -        def __init__(self, ws=None, wsFactory=WebService, clientId=None):
     926                  """Constructor.
     927
     928                  The C{ws} parameter has to be a subclass of L{IWebService}.
     929                  If it isn't given, the C{wsFactory} parameter is used to
     930                  create an L{IWebService} subclass.
     931
     932                  If the constructor is called without arguments, an instance
     933                  of L{WebService} is used, preconfigured to use the MusicBrainz
     934                  server. This should be enough for most users.
     935
     936                  If you want to use queries which require authentication you
     937                  have to pass a L{WebService} instance where user name and
     938                  password have been set.
     939
     940                  The C{clientId} parameter is required for data submission.
     941                  The format is C{'application-version'}, where C{application}
     942                  is your application's name and C{version} is a version
     943                  number which may not include a '-' character.
     944                  Even if you don't plan to submit data, setting this parameter is
     945                  encouraged because it will set the user agent used to make requests if
     946                  you don't supply the C{ws} parameter.
     947
     948                  @param ws: a subclass instance of L{IWebService}, or None
     949                  @param wsFactory: a callable object which creates an object
     950                  @param clientId: a unicode string containing the application's ID
     951                  """
     952                  if ws is None:
     953                          self._ws = wsFactory(userAgent=clientId)
     954                  else:
     955                          self._ws = ws
     956
     957                  self._clientId = clientId
     958                  self._log = logging.getLogger(str(self.__class__))
     959
     960
     961 -        def getArtistById(self, id_, include=None):
     962                  """Returns an artist.
     963
     964                  If no artist with that ID can be found, C{include} contains
     965                  invalid tags or there's a server problem, an exception is
     966                  raised.
     967
     968                  @param id_: a string containing the artist's ID
     969                  @param include: an L{ArtistIncludes} object, or None
     970
     971                  @return: an L{Artist <musicbrainz2.model.Artist>} object, or None
     972
     973                  @raise ConnectionError: couldn't connect to server
     974                  @raise RequestError: invalid ID or include tags
     975                  @raise ResourceNotFoundError: artist doesn't exist
     976                  @raise ResponseError: server returned invalid data
     977                  """
     978                  uuid = mbutils.extractUuid(id_, 'artist')
     979                  result = self._getFromWebService('artist', uuid, include)
     980                  artist = result.getArtist()
     981                  if artist is not None:
     982                          return artist
     983                  else:
     984                          raise ResponseError("server didn't return artist")
     985
     986
     987 -        def getArtists(self, filter):
     988                  """Returns artists matching given criteria.
     989
     990                  @param filter: an L{ArtistFilter} object
     991
     992                  @return: a list of L{musicbrainz2.wsxml.ArtistResult} objects
     993
     994                  @raise ConnectionError: couldn't connect to server
     995                  @raise RequestError: invalid ID or include tags
     996                  @raise ResponseError: server returned invalid data
     997                  """
     998                  result = self._getFromWebService('artist', '', filter=filter)
     999                  return result.getArtistResults()
    1000
    1001 -        def getLabelById(self, id_, include=None):
    1002                  """Returns a L{model.Label}
    1003
    1004                  If no label with that ID can be found, or there is a server problem,
    1005                  an exception is raised.
    1006
    1007                  @param id_: a string containing the label's ID.
    1008
    1009                  @raise ConnectionError: couldn't connect to server
    1010                  @raise RequestError: invalid ID or include tags
    1011                  @raise ResourceNotFoundError: release doesn't exist
    1012                  @raise ResponseError: server returned invalid data
    1013                  """
    1014                  uuid = mbutils.extractUuid(id_, 'label')
    1015                  result = self._getFromWebService('label', uuid, include)
    1016                  label = result.getLabel()
    1017                  if label is not None:
    1018                          return label
    1019                  else:
    1020                          raise ResponseError("server didn't return a label")
    1021
    1022 -        def getLabels(self, filter):
    1023                  result = self._getFromWebService('label', '', filter=filter)
    1024                  return result.getLabelResults()
    1025
    1026 -        def getReleaseById(self, id_, include=None):
    1027                  """Returns a release.
    1028
    1029                  If no release with that ID can be found, C{include} contains
    1030                  invalid tags or there's a server problem, and exception is
    1031                  raised.
    1032
    1033                  @param id_: a string containing the release's ID
    1034                  @param include: a L{ReleaseIncludes} object, or None
    1035
    1036                  @return: a L{Release <musicbrainz2.model.Release>} object, or None
    1037
    1038                  @raise ConnectionError: couldn't connect to server
    1039                  @raise RequestError: invalid ID or include tags
    1040                  @raise ResourceNotFoundError: release doesn't exist
    1041                  @raise ResponseError: server returned invalid data
    1042                  """
    1043                  uuid = mbutils.extractUuid(id_, 'release')
    1044                  result = self._getFromWebService('release', uuid, include)
    1045                  release = result.getRelease()
    1046                  if release is not None:
    1047                          return release
    1048                  else:
    1049                          raise ResponseError("server didn't return release")
    1050
    1051
    1052 -        def getReleases(self, filter):
    1053                  """Returns releases matching given criteria.
    1054
    1055                  @param filter: a L{ReleaseFilter} object
    1056
    1057                  @return: a list of L{musicbrainz2.wsxml.ReleaseResult} objects
    1058
    1059                  @raise ConnectionError: couldn't connect to server
    1060                  @raise RequestError: invalid ID or include tags
    1061                  @raise ResponseError: server returned invalid data
    1062                  """
    1063                  result = self._getFromWebService('release', '', filter=filter)
    1064                  return result.getReleaseResults()
    1065
    1066 -        def getReleaseGroupById(self, id_, include=None):
    1067                  """Returns a release group.
    1068
    1069                  If no release group with that ID can be found, C{include}
    1070                  contains invalid tags, or there's a server problem, an
    1071                  exception is raised.
    1072
    1073                  @param id_: a string containing the release group's ID
    1074                  @param include: a L{ReleaseGroupIncludes} object, or None
    1075
    1076                  @return: a L{ReleaseGroup <musicbrainz2.model.ReleaseGroup>} object, or None
    1077
    1078                  @raise ConnectionError: couldn't connect to server
    1079                  @raise RequestError: invalid ID or include tags
    1080                  @raise ResourceNotFoundError: release doesn't exist
    1081                  @raise ResponseError: server returned invalid data
    1082                  """
    1083                  uuid = mbutils.extractUuid(id_, 'release-group')
    1084                  result = self._getFromWebService('release-group', uuid, include)
    1085                  releaseGroup = result.getReleaseGroup()
    1086                  if releaseGroup is not None:
    1087                          return releaseGroup
    1088                  else:
    1089                          raise ResponseError("server didn't return releaseGroup")
    1090
    1091 -        def getReleaseGroups(self, filter):
    1092                  """Returns release groups matching the given criteria.
    1093
    1094                  @param filter: a L{ReleaseGroupFilter} object
    1095
    1096                  @return: a list of L{musicbrainz2.wsxml.ReleaseGroupResult} objects
    1097
    1098                  @raise ConnectionError: couldn't connect to server
    1099                  @raise RequestError: invalid ID or include tags
    1100                  @raise ResponseError: server returned invalid data
    1101                  """
    1102                  result = self._getFromWebService('release-group', '', filter=filter)
    1103                  return result.getReleaseGroupResults()
    1104
    1105 -        def getTrackById(self, id_, include=None):
    1106                  """Returns a track.
    1107
    1108                  If no track with that ID can be found, C{include} contains
    1109                  invalid tags or there's a server problem, an exception is
    1110                  raised.
    1111
    1112                  @param id_: a string containing the track's ID
    1113                  @param include: a L{TrackIncludes} object, or None
    1114
    1115                  @return: a L{Track <musicbrainz2.model.Track>} object, or None
    1116
    1117                  @raise ConnectionError: couldn't connect to server
    1118                  @raise RequestError: invalid ID or include tags
    1119                  @raise ResourceNotFoundError: track doesn't exist
    1120                  @raise ResponseError: server returned invalid data
    1121                  """
    1122                  uuid = mbutils.extractUuid(id_, 'track')
    1123                  result = self._getFromWebService('track', uuid, include)
    1124                  track = result.getTrack()
    1125                  if track is not None:
    1126                          return track
    1127                  else:
    1128                          raise ResponseError("server didn't return track")
    1129
    1130
    1131 -        def getTracks(self, filter):
    1132                  """Returns tracks matching given criteria.
    1133
    1134                  @param filter: a L{TrackFilter} object
    1135
    1136                  @return: a list of L{musicbrainz2.wsxml.TrackResult} objects
    1137
    1138                  @raise ConnectionError: couldn't connect to server
    1139                  @raise RequestError: invalid ID or include tags
    1140                  @raise ResponseError: server returned invalid data
    1141                  """
    1142                  result = self._getFromWebService('track', '', filter=filter)
    1143                  return result.getTrackResults()
    1144
    1145
    1146 -        def getUserByName(self, name):
    1147                  """Returns information about a MusicBrainz user.
    1148
    1149                  You can only request user data if you know the user name and
    1150                  password for that account. If username and/or password are
    1151                  incorrect, an L{AuthenticationError} is raised.
    1152
    1153                  See the example in L{Query} on how to supply user name and
    1154                  password.
    1155
    1156                  @param name: a unicode string containing the user's name
    1157
    1158                  @return: a L{User <musicbrainz2.model.User>} object
    1159
    1160                  @raise ConnectionError: couldn't connect to server
    1161                  @raise RequestError: invalid ID or include tags
    1162                  @raise AuthenticationError: invalid user name and/or password
    1163                  @raise ResourceNotFoundError: track doesn't exist
    1164                  @raise ResponseError: server returned invalid data
    1165                  """
    1166                  filter = UserFilter(name=name)
    1167                  result = self._getFromWebService('user', '', None, filter)
    1168
    1169                  if len(result.getUserList()) > 0:
    1170                          return result.getUserList()[0]
    1171                  else:
    1172                          raise ResponseError("response didn't contain user data")
    1173
    1174
    1175 -        def _getFromWebService(self, entity, id_, include=None, filter=None):
    1176                  if filter is None:
    1177                          filterParams = [ ]
    1178                  else:
    1179                          filterParams = filter.createParameters()
    1180
    1181                  if include is None:
    1182                          includeParams = [ ]
    1183                  else:
    1184                          includeParams = include.createIncludeTags()
    1185
    1186                  stream = self._ws.get(entity, id_, includeParams, filterParams)
    1187                  try:
    1188                          parser = MbXmlParser()
    1189                          return parser.parse(stream)
    1190                  except ParseError, e:
    1191                          raise ResponseError(str(e), e)
    1192
    1193
    1194 -        def submitPuids(self, tracks2puids):
    1195                  """Submit track to PUID mappings.
    1196
    1197                  The C{tracks2puids} parameter has to be a dictionary, with the
    1198                  keys being MusicBrainz track IDs (either as absolute URIs or
    1199                  in their 36 character ASCII representation) and the values
    1200                  being PUIDs (ASCII, 36 characters).
    1201
    1202                  Note that this method only works if a valid user name and
    1203                  password have been set. See the example in L{Query} on how
    1204                  to supply authentication data.
    1205
    1206                  @param tracks2puids: a dictionary mapping track IDs to PUIDs
    1207
    1208                  @raise ConnectionError: couldn't connect to server
    1209                  @raise RequestError: invalid track or PUIDs
    1210                  @raise AuthenticationError: invalid user name and/or password
    1211                  """
    1212                  assert self._clientId is not None, 'Please supply a client ID'
    1213                  params = [ ]
    1214                  params.append( ('client', self._clientId.encode('utf-8')) )
    1215
    1216                  for (trackId, puid) in tracks2puids.iteritems():
    1217                          trackId = mbutils.extractUuid(trackId, 'track')
    1218                          params.append( ('puid', trackId + ' ' + puid) )
    1219
    1220                  encodedStr = urllib.urlencode(params, True)
    1221
    1222                  self._ws.post('track', '', encodedStr)
    1223
    1224 -        def submitISRCs(self, tracks2isrcs):
    1225                  """Submit track to ISRC mappings.
    1226
    1227                  The C{tracks2isrcs} parameter has to be a dictionary, with the
    1228                  keys being MusicBrainz track IDs (either as absolute URIs or
    1229                  in their 36 character ASCII representation) and the values
    1230                  being ISRCs (ASCII, 12 characters).
    1231
    1232                  Note that this method only works if a valid user name and
    1233                  password have been set. See the example in L{Query} on how
    1234                  to supply authentication data.
    1235
    1236                  @param tracks2isrcs: a dictionary mapping track IDs to ISRCs
    1237
    1238                  @raise ConnectionError: couldn't connect to server
    1239                  @raise RequestError: invalid track or ISRCs
    1240                  @raise AuthenticationError: invalid user name and/or password
    1241                  """
    1242                  params = [ ]
    1243
    1244                  for (trackId, isrc) in tracks2isrcs.iteritems():
    1245                          trackId = mbutils.extractUuid(trackId, 'track')
    1246                          params.append( ('isrc', trackId + ' ' + isrc) )
    1247
    1248                  encodedStr = urllib.urlencode(params, True)
    1249
    1250                  self._ws.post('track', '', encodedStr)
    1251
    1252 -        def addToUserCollection(self, releases):
    1253                  """Add releases to a user's collection.
    1254
    1255                  The releases parameter must be a list. It can contain either L{Release}
    1256                  objects or a string representing a MusicBrainz release ID (either as
    1257                  absolute URIs or in their 36 character ASCII representation).
    1258
    1259                  Adding a release that is already in the collection has no effect.
    1260
    1261                  @param releases: a list of releases to add to the user collection
    1262
    1263                  @raise ConnectionError: couldn't connect to server
    1264                  @raise AuthenticationError: invalid user name and/or password
    1265                  """
    1266                  rels = []
    1267                  for release in releases:
    1268                          if isinstance(release, Release):
    1269                                  rels.append(mbutils.extractUuid(release.id))
    1270                          else:
    1271                                  rels.append(mbutils.extractUuid(release))
    1272                  encodedStr = urllib.urlencode({'add': ",".join(rels)}, True)
    1273                  self._ws.post('collection', '', encodedStr)
    1274
    1275 -        def removeFromUserCollection(self, releases):
    1276                  """Remove releases from a user's collection.
    1277
    1278                  The releases parameter must be a list. It can contain either L{Release}
    1279                  objects or a string representing a MusicBrainz release ID (either as
    1280                  absolute URIs or in their 36 character ASCII representation).
    1281
    1282                  Removing a release that is not in the collection has no effect.
    1283
    1284                  @param releases: a list of releases to remove from the user collection
    1285
    1286                  @raise ConnectionError: couldn't connect to server
    1287                  @raise AuthenticationError: invalid user name and/or password
    1288                  """
    1289                  rels = []
    1290                  for release in releases:
    1291                          if isinstance(release, Release):
    1292                                  rels.append(mbutils.extractUuid(release.id))
    1293                          else:
    1294                                  rels.append(mbutils.extractUuid(release))
    1295                  encodedStr = urllib.urlencode({'remove': ",".join(rels)}, True)
    1296                  self._ws.post('collection', '', encodedStr)
    1297
    1298 -        def getUserCollection(self, offset=0, maxitems=100):
    1299                  """Get the releases that are in a user's collection
    1300
    1301                  A maximum of 100 items will be returned for any one call
    1302                  to this method. To fetch more than 100 items, use the offset
    1303                  parameter.
    1304
    1305                  @param offset: the offset to start fetching results from
    1306                  @param maxitems: the upper limit on items to return
    1307
    1308                  @return: a list of L{musicbrainz2.wsxml.ReleaseResult} objects
    1309
    1310                  @raise ConnectionError: couldn't connect to server
    1311                  @raise AuthenticationError: invalid user name and/or password
    1312                  """
    1313                  params = { 'offset': offset, 'maxitems': maxitems }
    1314
    1315                  stream = self._ws.get('collection', '', filter=params)
    1316                  try:
    1317                          parser = MbXmlParser()
    1318                          result = parser.parse(stream)
    1319                  except ParseError, e:
    1320                          raise ResponseError(str(e), e)
    1321
    1322                  return result.getReleaseResults()
    1323
    1324 -        def submitUserTags(self, entityUri, tags):
    1325                  """Submit folksonomy tags for an entity.
    1326
    1327                  Note that all previously existing tags from the authenticated
    1328                  user are replaced with the ones given to this method. Other
    1329                  users' tags are not affected.
    1330
    1331                  @param entityUri: a string containing an absolute MB ID
    1332                  @param tags: A list of either L{Tag <musicbrainz2.model.Tag>} objects
    1333                               or strings
    1334
    1335                  @raise ValueError: invalid entityUri
    1336                  @raise ConnectionError: couldn't connect to server
    1337                  @raise RequestError: invalid ID, entity or tags
    1338                  @raise AuthenticationError: invalid user name and/or password
    1339                  """
    1340                  entity = mbutils.extractEntityType(entityUri)
    1341                  uuid = mbutils.extractUuid(entityUri, entity)
    1342                  params = (
    1343                          ('type', 'xml'),
    1344                          ('entity', entity),
    1345                          ('id', uuid),
    1346                          ('tags', ','.join([unicode(tag).encode('utf-8') for tag in tags]))
    1347                  )
    1348
    1349                  encodedStr = urllib.urlencode(params)
    1350
    1351                  self._ws.post('tag', '', encodedStr)
    1352
    1353
    1354 -        def getUserTags(self, entityUri):
    1355                  """Returns a list of folksonomy tags a user has applied to an entity.
    1356
    1357                  The given parameter has to be a fully qualified MusicBrainz ID, as
    1358                  returned by other library functions.
    1359
    1360                  Note that this method only works if a valid user name and
    1361                  password have been set. Only the tags the authenticated user
    1362                  applied to the entity will be returned. If username and/or
    1363                  password are incorrect, an AuthenticationError is raised.
    1364
    1365                  This method will return a list of L{Tag <musicbrainz2.model.Tag>}
    1366                  objects.
    1367
    1368                  @param entityUri: a string containing an absolute MB ID
    1369
    1370                  @raise ValueError: invalid entityUri
    1371                  @raise ConnectionError: couldn't connect to server
    1372                  @raise RequestError: invalid ID or entity
    1373                  @raise AuthenticationError: invalid user name and/or password
    1374                  """
    1375                  entity = mbutils.extractEntityType(entityUri)
    1376                  uuid = mbutils.extractUuid(entityUri, entity)
    1377                  params = { 'entity': entity, 'id': uuid }
    1378
    1379                  stream = self._ws.get('tag', '', filter=params)
    1380                  try:
    1381                          parser = MbXmlParser()
    1382                          result = parser.parse(stream)
    1383                  except ParseError, e:
    1384                          raise ResponseError(str(e), e)
    1385
    1386                  return result.getTagList()
    1387
    1388 -        def submitUserRating(self, entityUri, rating):
    1389                  """Submit rating for an entity.
    1390
    1391                  Note that all previously existing rating from the authenticated
    1392                  user are replaced with the one given to this method. Other
    1393                  users' ratings are not affected.
    1394
    1395                  @param entityUri: a string containing an absolute MB ID
    1396                  @param rating: A L{Rating <musicbrainz2.model.Rating>} object
    1397                               or integer
    1398
    1399                  @raise ValueError: invalid entityUri
    1400                  @raise ConnectionError: couldn't connect to server
    1401                  @raise RequestError: invalid ID, entity or tags
    1402                  @raise AuthenticationError: invalid user name and/or password
    1403                  """
    1404                  entity = mbutils.extractEntityType(entityUri)
    1405                  uuid = mbutils.extractUuid(entityUri, entity)
    1406                  params = (
    1407                          ('type', 'xml'),
    1408                          ('entity', entity),
    1409                          ('id', uuid),
    1410                          ('rating', unicode(rating).encode('utf-8'))
    1411                  )
    1412
    1413                  encodedStr = urllib.urlencode(params)
    1414
    1415                  self._ws.post('rating', '', encodedStr)
    1416
    1417
    1418 -        def getUserRating(self, entityUri):
    1419                  """Return the rating a user has applied to an entity.
    1420
    1421                  The given parameter has to be a fully qualified MusicBrainz
    1422                  ID, as returned by other library functions.
    1423
    1424                  Note that this method only works if a valid user name and
    1425                  password have been set. Only the rating the authenticated user
    1426                  applied to the entity will be returned. If username and/or
    1427                  password are incorrect, an AuthenticationError is raised.
    1428
    1429                  This method will return a L{Rating <musicbrainz2.model.Rating>}
    1430                  object.
    1431
    1432                  @param entityUri: a string containing an absolute MB ID
    1433
    1434                  @raise ValueError: invalid entityUri
    1435                  @raise ConnectionError: couldn't connect to server
    1436                  @raise RequestError: invalid ID or entity
    1437                  @raise AuthenticationError: invalid user name and/or password
    1438                  """
    1439                  entity = mbutils.extractEntityType(entityUri)
    1440                  uuid = mbutils.extractUuid(entityUri, entity)
    1441                  params = { 'entity': entity, 'id': uuid }
    1442
    1443                  stream = self._ws.get('rating', '', filter=params)
    1444                  try:
    1445                          parser = MbXmlParser()
    1446                          result = parser.parse(stream)
    1447                  except ParseError, e:
    1448                          raise ResponseError(str(e), e)
    1449
    1450                  return result.getRating()
    1451
    1452 -        def submitCDStub(self, cdstub):
    1453                  """Submit a CD Stub to the database.
    1454
    1455                  The number of tracks added to the CD Stub must match the TOC and DiscID
    1456                  otherwise the submission wil fail. The submission will also fail if
    1457                  the Disc ID is already in the MusicBrainz database.
    1458
    1459                  This method will only work if no user name and password are set.
    1460
    1461                  @param cdstub: a L{CDStub} object to submit
    1462
    1463                  @raise RequestError: Missmatching TOC/Track information or the
    1464                         the CD Stub already exists or the Disc ID already exists
    1465                  """
    1466                  assert self._clientId is not None, 'Please supply a client ID'
    1467                  disc = cdstub._disc
    1468                  params = [ ]
    1469                  params.append( ('client', self._clientId.encode('utf-8')) )
    1470                  params.append( ('discid', disc.id) )
    1471                  params.append( ('title', cdstub.title) )
    1472                  params.append( ('artist', cdstub.artist) )
    1473                  if cdstub.barcode != "":
    1474                          params.append( ('barcode', cdstub.barcode) )
    1475                  if cdstub.comment != "":
    1476                          params.append( ('comment', cdstub.comment) )
    1477
    1478                  trackind = 0
    1479                  for track,artist in cdstub.tracks:
    1480                          params.append( ('track%d' % trackind, track) )
    1481                          if artist != "":
    1482                                  params.append( ('artist%d' % trackind, artist) )
    1483
    1484                          trackind += 1
    1485
    1486                  toc = "%d %d %d " % (disc.firstTrackNum, disc.lastTrackNum, disc.sectors)
    1487                  toc = toc + ' '.join( map(lambda x: str(x[0]), disc.getTracks()) )
    1488
    1489                  params.append( ('toc', toc) )
    1490
    1491                  encodedStr = urllib.urlencode(params)
    1492                  self._ws.post('release', '', encodedStr)
    1493
    1494 -def _createIncludes(tagMap):
    1495          selected = filter(lambda x: x[1] == True, tagMap.items())
    1496          return map(lambda x: x[0], selected)
    1497
    1498 -def _createParameters(params):
    1499          """Remove (x, None) tuples and encode (x, str/unicode) to utf-8."""
    1500          ret = [ ]
    1501          for p in params:
    1502                  if isinstance(p[1], (str, unicode)):
    1503                          ret.append( (p[0], p[1].encode('utf-8')) )
    1504                  elif p[1] is not None:
    1505                          ret.append(p)
    1506
    1507          return ret
    1508
    1509 -def _paramsValid(params):
    1510          """Check if the query parameter collides with other parameters."""
    1511          tmp = [ ]
    1512          for name, value in params:
    1513                  if value is not None and name not in ('offset', 'limit'):
    1514                          tmp.append(name)
    1515
    1516          if 'query' in tmp and len(tmp) > 1:
    1517                  return False
    1518          else:
    1519                  return True
    1520
    1521  if __name__ == '__main__':
    1522          import doctest
    1523          doctest.testmod()
    1524
    1525  # EOF
    1526