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 9715 2008-03-01 09:21:28Z matt $' 
  16   
  17  import re 
  18  import urllib 
  19  import urllib2 
  20  import urlparse 
  21  import logging 
  22  import os.path 
  23  from StringIO import StringIO 
  24  import musicbrainz2 
  25  from musicbrainz2.model import Artist, Release, Track 
  26  from musicbrainz2.wsxml import MbXmlParser, ParseError 
  27  import musicbrainz2.utils as mbutils 
  28   
  29  __all__ = [ 
  30          'WebServiceError', 'AuthenticationError', 'ConnectionError', 
  31          'RequestError', 'ResourceNotFoundError', 'ResponseError',  
  32          'IIncludes', 'ArtistIncludes', 'ReleaseIncludes', 'TrackIncludes', 
  33          'LabelIncludes', 
  34          'IFilter', 'ArtistFilter', 'ReleaseFilter', 'TrackFilter', 
  35          'UserFilter', 'LabelFilter', 
  36          'IWebService', 'WebService', 'Query', 
  37  ] 
  38   
  39   
40 -class IWebService(object):
41 """An interface all concrete web service classes have to implement. 42 43 All web service classes have to implement this and follow the 44 method specifications. 45 """ 46
47 - def get(self, entity, id_, include, filter, version):
48 """Query the web service. 49 50 Using this method, you can either get a resource by id (using 51 the C{id_} parameter, or perform a query on all resources of 52 a type. 53 54 The C{filter} and the C{id_} parameter exclude each other. If 55 you are using a filter, you may not set C{id_} and vice versa. 56 57 Returns a file-like object containing the result or raises a 58 L{WebServiceError} or one of its subclasses in case of an 59 error. Which one is used depends on the implementing class. 60 61 @param entity: a string containing the entity's name 62 @param id_: a string containing a UUID, or the empty string 63 @param include: a tuple containing values for the 'inc' parameter 64 @param filter: parameters, depending on the entity 65 @param version: a string containing the web service version to use 66 67 @return: a file-like object 68 69 @raise WebServiceError: in case of errors 70 """ 71 raise NotImplementedError()
72 73
74 - def post(self, entity, id_, data, version):
75 """Submit data to the web service. 76 77 @param entity: a string containing the entity's name 78 @param id_: a string containing a UUID, or the empty string 79 @param data: A string containing the data to post 80 @param version: a string containing the web service version to use 81 82 @return: a file-like object 83 84 @raise WebServiceError: in case of errors 85 """ 86 raise NotImplementedError()
87 88
89 -class WebServiceError(Exception):
90 """A web service error has occurred. 91 92 This is the base class for several other web service related 93 exceptions. 94 """ 95
96 - def __init__(self, msg='Webservice Error', reason=None):
97 """Constructor. 98 99 Set C{msg} to an error message which explains why this 100 exception was raised. The C{reason} parameter should be the 101 original exception which caused this L{WebService} exception 102 to be raised. If given, it has to be an instance of 103 C{Exception} or one of its child classes. 104 105 @param msg: a string containing an error message 106 @param reason: another exception instance, or None 107 """ 108 Exception.__init__(self) 109 self.msg = msg 110 self.reason = reason
111
112 - def __str__(self):
113 """Makes this class printable. 114 115 @return: a string containing an error message 116 """ 117 return self.msg
118 119
120 -class ConnectionError(WebServiceError):
121 """Getting a server connection failed. 122 123 This exception is mostly used if the client couldn't connect to 124 the server because of an invalid host name or port. It doesn't 125 make sense if the web service in question doesn't use the network. 126 """ 127 pass
128 129
130 -class RequestError(WebServiceError):
131 """An invalid request was made. 132 133 This exception is raised if the client made an invalid request. 134 That could be syntactically invalid identifiers or unknown or 135 invalid parameter values. 136 """ 137 pass
138 139
140 -class ResourceNotFoundError(WebServiceError):
141 """No resource with the given ID exists. 142 143 This is usually a wrapper around IOError (which is superclass of 144 HTTPError). 145 """ 146 pass
147 148
149 -class AuthenticationError(WebServiceError):
150 """Authentication failed. 151 152 This is thrown if user name, password or realm were invalid while 153 trying to access a protected resource. 154 """ 155 pass
156 157
158 -class ResponseError(WebServiceError):
159 """The returned resource was invalid. 160 161 This may be due to a malformed XML document or if the requested 162 data wasn't part of the response. It can only occur in case of 163 bugs in the web service itself. 164 """ 165 pass
166 167
168 -class WebService(IWebService):
169 """An interface to the MusicBrainz XML web service via HTTP. 170 171 By default, this class uses the MusicBrainz server but may be 172 configured for accessing other servers as well using the 173 L{constructor <__init__>}. This implements L{IWebService}, so 174 additional documentation on method parameters can be found there. 175 """ 176
177 - def __init__(self, host='musicbrainz.org', port=80, pathPrefix='/ws', 178 username=None, password=None, realm='musicbrainz.org', 179 opener=None):
180 """Constructor. 181 182 This can be used without parameters. In this case, the 183 MusicBrainz server will be used. 184 185 @param host: a string containing a host name 186 @param port: an integer containing a port number 187 @param pathPrefix: a string prepended to all URLs 188 @param username: a string containing a MusicBrainz user name 189 @param password: a string containing the user's password 190 @param realm: a string containing the realm used for authentication 191 @param opener: an C{urllib2.OpenerDirector} object used for queries 192 """ 193 self._host = host 194 self._port = port 195 self._username = username 196 self._password = password 197 self._realm = realm 198 self._pathPrefix = pathPrefix 199 self._log = logging.getLogger(str(self.__class__)) 200 201 if opener is None: 202 self._opener = urllib2.build_opener() 203 else: 204 self._opener = opener 205 206 passwordMgr = self._RedirectPasswordMgr() 207 authHandler = urllib2.HTTPDigestAuthHandler(passwordMgr) 208 authHandler.add_password(self._realm, (), # no host set 209 self._username, self._password) 210 self._opener.add_handler(authHandler)
211 212
213 - def _makeUrl(self, entity, id_, include=( ), filter={ }, 214 version='1', type_='xml'):
215 params = dict(filter) 216 if type_ is not None: 217 params['type'] = type_ 218 if len(include) > 0: 219 params['inc'] = ' '.join(include) 220 221 netloc = self._host 222 if self._port != 80: 223 netloc += ':' + str(self._port) 224 path = '/'.join((self._pathPrefix, version, entity, id_)) 225 226 query = urllib.urlencode(params) 227 228 url = urlparse.urlunparse(('http', netloc, path, '', query,'')) 229 230 return url
231 232
233 - def _openUrl(self, url, data=None):
234 userAgent = 'python-musicbrainz/' + musicbrainz2.__version__ 235 req = urllib2.Request(url) 236 req.add_header('User-Agent', userAgent) 237 return self._opener.open(req, data)
238 239
240 - def get(self, entity, id_, include=( ), filter={ }, version='1'):
241 """Query the web service via HTTP-GET. 242 243 Returns a file-like object containing the result or raises a 244 L{WebServiceError}. Conditions leading to errors may be 245 invalid entities, IDs, C{include} or C{filter} parameters 246 and unsupported version numbers. 247 248 @raise ConnectionError: couldn't connect to server 249 @raise RequestError: invalid IDs or parameters 250 @raise AuthenticationError: invalid user name and/or password 251 @raise ResourceNotFoundError: resource doesn't exist 252 253 @see: L{IWebService.get} 254 """ 255 url = self._makeUrl(entity, id_, include, filter, version) 256 257 self._log.debug('GET ' + url) 258 259 try: 260 return self._openUrl(url) 261 except urllib2.HTTPError, e: 262 self._log.debug("GET failed: " + str(e)) 263 if e.code == 400: # in python 2.4: httplib.BAD_REQUEST 264 raise RequestError(str(e), e) 265 elif e.code == 401: # httplib.UNAUTHORIZED 266 raise AuthenticationError(str(e), e) 267 elif e.code == 404: # httplib.NOT_FOUND 268 raise ResourceNotFoundError(str(e), e) 269 else: 270 raise WebServiceError(str(e), e) 271 except urllib2.URLError, e: 272 self._log.debug("GET failed: " + str(e)) 273 raise ConnectionError(str(e), e)
274 275
276 - def post(self, entity, id_, data, version='1'):
277 """Send data to the web service via HTTP-POST. 278 279 Note that this may require authentication. You can set 280 user name, password and realm in the L{constructor <__init__>}. 281 282 @raise ConnectionError: couldn't connect to server 283 @raise RequestError: invalid IDs or parameters 284 @raise AuthenticationError: invalid user name and/or password 285 @raise ResourceNotFoundError: resource doesn't exist 286 287 @see: L{IWebService.post} 288 """ 289 url = self._makeUrl(entity, id_, version=version, type_=None) 290 291 self._log.debug('POST ' + url) 292 self._log.debug('POST-BODY: ' + data) 293 294 try: 295 return self._openUrl(url, data) 296 except urllib2.HTTPError, e: 297 self._log.debug("POST failed: " + str(e)) 298 if e.code == 400: # in python 2.4: httplib.BAD_REQUEST 299 raise RequestError(str(e), e) 300 elif e.code == 401: # httplib.UNAUTHORIZED 301 raise AuthenticationError(str(e), e) 302 elif e.code == 404: # httplib.NOT_FOUND 303 raise ResourceNotFoundError(str(e), e) 304 else: 305 raise WebServiceError(str(e), e) 306 except urllib2.URLError, e: 307 self._log.debug("POST failed: " + str(e)) 308 raise ConnectionError(str(e), e)
309 310 311 # Special password manager which also works with redirects by simply 312 # ignoring the URI. As a consequence, only *ONE* (username, password) 313 # tuple per realm can be used for all URIs. 314 #
315 - class _RedirectPasswordMgr(urllib2.HTTPPasswordMgr):
316 - def __init__(self):
317 self._realms = { }
318
319 - def find_user_password(self, realm, uri):
320 # ignoring the uri parameter intentionally 321 try: 322 return self._realms[realm] 323 except KeyError: 324 return (None, None)
325
326 - def add_password(self, realm, uri, username, password):
327 # ignoring the uri parameter intentionally 328 self._realms[realm] = (username, password)
329 330
331 -class IFilter(object):
332 """A filter for collections. 333 334 This is the interface all filters have to implement. Filter classes 335 are initialized with a set of criteria and are then applied to 336 collections of items. The criteria are usually strings or integer 337 values, depending on the filter. 338 339 Note that all strings passed to filters should be unicode strings 340 (python type C{unicode}). Standard strings are converted to unicode 341 internally, but have a limitation: Only 7 Bit pure ASCII characters 342 may be used, otherwise a C{UnicodeDecodeError} is raised. 343 """
344 - def createParameters(self):
345 """Create a list of query parameters. 346 347 This method creates a list of (C{parameter}, C{value}) tuples, 348 based on the contents of the implementing subclass. 349 C{parameter} is a string containing a parameter name 350 and C{value} an arbitrary string. No escaping of those strings 351 is required. 352 353 @return: a sequence of (key, value) pairs 354 """ 355 raise NotImplementedError()
356 357
358 -class ArtistFilter(IFilter):
359 """A filter for the artist collection.""" 360
361 - def __init__(self, name=None, limit=None, offset=None, query=None):
362 """Constructor. 363 364 The C{query} parameter may contain a query in U{Lucene syntax 365 <http://lucene.apache.org/java/docs/queryparsersyntax.html>}. 366 Note that the C{name} and C{query} may not be used together. 367 368 @param name: a unicode string containing the artist's name 369 @param limit: the maximum number of artists to return 370 @param offset: start results at this zero-based offset 371 @param query: a string containing a query in Lucene syntax 372 """ 373 self._params = [ 374 ('name', name), 375 ('limit', limit), 376 ('offset', offset), 377 ('query', query), 378 ] 379 380 if not _paramsValid(self._params): 381 raise ValueError('invalid combination of parameters')
382
383 - def createParameters(self):
384 return _createParameters(self._params)
385 386
387 -class LabelFilter(IFilter):
388 """A filter for the label collection.""" 389
390 - def __init__(self, name=None, limit=None, offset=None, query=None):
391 """Constructor. 392 393 The C{query} parameter may contain a query in U{Lucene syntax 394 <http://lucene.apache.org/java/docs/queryparsersyntax.html>}. 395 Note that the C{name} and C{query} may not be used together. 396 397 @param name: a unicode string containing the label's name 398 @param limit: the maximum number of labels to return 399 @param offset: start results at this zero-based offset 400 @param query: a string containing a query in Lucene syntax 401 """ 402 self._params = [ 403 ('name', name), 404 ('limit', limit), 405 ('offset', offset), 406 ('query', query), 407 ] 408 409 if not _paramsValid(self._params): 410 raise ValueError('invalid combination of parameters')
411
412 - def createParameters(self):
413 return _createParameters(self._params)
414 415
416 -class ReleaseFilter(IFilter):
417 """A filter for the release collection.""" 418
419 - def __init__(self, title=None, discId=None, releaseTypes=None, 420 artistName=None, artistId=None, limit=None, 421 offset=None, query=None):
422 """Constructor. 423 424 If C{discId} or C{artistId} are set, only releases matching 425 those IDs are returned. The C{releaseTypes} parameter allows 426 to limit the types of the releases returned. You can set it to 427 C{(Release.TYPE_ALBUM, Release.TYPE_OFFICIAL)}, for example, 428 to only get officially released albums. Note that those values 429 are connected using the I{AND} operator. MusicBrainz' support 430 is currently very limited, so C{Release.TYPE_LIVE} and 431 C{Release.TYPE_COMPILATION} exclude each other (see U{the 432 documentation on release attributes 433 <http://wiki.musicbrainz.org/AlbumAttribute>} for more 434 information and all valid values). 435 436 If both the C{artistName} and the C{artistId} parameter are 437 given, the server will ignore C{artistName}. 438 439 The C{query} parameter may contain a query in U{Lucene syntax 440 <http://lucene.apache.org/java/docs/queryparsersyntax.html>}. 441 Note that C{query} may not be used together with the other 442 parameters except for C{limit} and C{offset}. 443 444 @param title: a unicode string containing the release's title 445 @param discId: a unicode string containing the DiscID 446 @param releaseTypes: a sequence of release type URIs 447 @param artistName: a unicode string containing the artist's name 448 @param artistId: a unicode string containing the artist's ID 449 @param limit: the maximum number of releases to return 450 @param offset: start results at this zero-based offset 451 @param query: a string containing a query in Lucene syntax 452 453 @see: the constants in L{musicbrainz2.model.Release} 454 """ 455 if releaseTypes is None or len(releaseTypes) == 0: 456 releaseTypesStr = None 457 else: 458 tmp = [ mbutils.extractFragment(x) for x in releaseTypes ] 459 releaseTypesStr = ' '.join(tmp) 460 461 self._params = [ 462 ('title', title), 463 ('discid', discId), 464 ('releasetypes', releaseTypesStr), 465 ('artist', artistName), 466 ('artistid', artistId), 467 ('limit', limit), 468 ('offset', offset), 469 ('query', query), 470 ] 471 472 if not _paramsValid(self._params): 473 raise ValueError('invalid combination of parameters')
474
475 - def createParameters(self):
476 return _createParameters(self._params)
477 478
479 -class TrackFilter(IFilter):
480 """A filter for the track collection.""" 481
482 - def __init__(self, title=None, artistName=None, artistId=None, 483 releaseTitle=None, releaseId=None, 484 duration=None, puid=None, limit=None, offset=None, 485 query=None):
486 """Constructor. 487 488 If C{artistId}, C{releaseId} or C{puid} are set, only tracks 489 matching those IDs are returned. 490 491 The server will ignore C{artistName} and C{releaseTitle} if 492 C{artistId} or ${releaseId} are set respectively. 493 494 The C{query} parameter may contain a query in U{Lucene syntax 495 <http://lucene.apache.org/java/docs/queryparsersyntax.html>}. 496 Note that C{query} may not be used together with the other 497 parameters except for C{limit} and C{offset}. 498 499 @param title: a unicode string containing the track's title 500 @param artistName: a unicode string containing the artist's name 501 @param artistId: a string containing the artist's ID 502 @param releaseTitle: a unicode string containing the release's title 503 @param releaseId: a string containing the release's title 504 @param duration: the track's length in milliseconds 505 @param puid: a string containing a PUID 506 @param limit: the maximum number of releases to return 507 @param offset: start results at this zero-based offset 508 @param query: a string containing a query in Lucene syntax 509 """ 510 self._params = [ 511 ('title', title), 512 ('artist', artistName), 513 ('artistid', artistId), 514 ('release', releaseTitle), 515 ('releaseid', releaseId), 516 ('duration', duration), 517 ('puid', puid), 518 ('limit', limit), 519 ('offset', offset), 520 ('query', query), 521 ] 522 523 if not _paramsValid(self._params): 524 raise ValueError('invalid combination of parameters')
525
526 - def createParameters(self):
527 return _createParameters(self._params)
528 529
530 -class UserFilter(IFilter):
531 """A filter for the user collection.""" 532
533 - def __init__(self, name=None):
534 """Constructor. 535 536 @param name: a unicode string containing a MusicBrainz user name 537 """ 538 self._name = name
539
540 - def createParameters(self):
541 if self._name is not None: 542 return [ ('name', self._name.encode('utf-8')) ] 543 else: 544 return [ ]
545 546
547 -class IIncludes(object):
548 """An interface implemented by include tag generators."""
549 - def createIncludeTags(self):
550 raise NotImplementedError()
551 552
553 -class ArtistIncludes(IIncludes):
554 """A specification on how much data to return with an artist. 555 556 Example: 557 558 >>> from musicbrainz2.model import Release 559 >>> from musicbrainz2.webservice import ArtistIncludes 560 >>> inc = ArtistIncludes(artistRelations=True, releaseRelations=True, 561 ... releases=(Release.TYPE_ALBUM, Release.TYPE_OFFICIAL)) 562 >>> 563 564 The MusicBrainz server only supports some combinations of release 565 types for the C{releases} and C{vaReleases} include tags. At the 566 moment, not more than two release types should be selected, while 567 one of them has to be C{Release.TYPE_OFFICIAL}, 568 C{Release.TYPE_PROMOTION} or C{Release.TYPE_BOOTLEG}. 569 570 @note: Only one of C{releases} and C{vaReleases} may be given. 571 """
572 - def __init__(self, aliases=False, releases=(), vaReleases=(), 573 artistRelations=False, releaseRelations=False, 574 trackRelations=False, urlRelations=False, tags=False):
575 576 assert not isinstance(releases, basestring) 577 assert not isinstance(vaReleases, basestring) 578 assert len(releases) == 0 or len(vaReleases) == 0 579 580 self._includes = { 581 'aliases': aliases, 582 'artist-rels': artistRelations, 583 'release-rels': releaseRelations, 584 'track-rels': trackRelations, 585 'url-rels': urlRelations, 586 'tags': tags, 587 } 588 589 for elem in releases: 590 self._includes['sa-' + mbutils.extractFragment(elem)] = True 591 592 for elem in vaReleases: 593 self._includes['va-' + mbutils.extractFragment(elem)] = True
594
595 - def createIncludeTags(self):
596 return _createIncludes(self._includes)
597 598
599 -class ReleaseIncludes(IIncludes):
600 """A specification on how much data to return with a release."""
601 - def __init__(self, artist=False, counts=False, releaseEvents=False, 602 discs=False, tracks=False, 603 artistRelations=False, releaseRelations=False, 604 trackRelations=False, urlRelations=False, 605 labels=False, tags=False):
606 self._includes = { 607 'artist': artist, 608 'counts': counts, 609 'labels': labels, 610 'release-events': releaseEvents, 611 'discs': discs, 612 'tracks': tracks, 613 'artist-rels': artistRelations, 614 'release-rels': releaseRelations, 615 'track-rels': trackRelations, 616 'url-rels': urlRelations, 617 'tags': tags, 618 } 619 620 # Requesting labels without releaseEvents makes no sense, 621 # so we pull in releaseEvents, if necessary. 622 if labels and not releaseEvents: 623 self._includes['release-events'] = True
624
625 - def createIncludeTags(self):
626 return _createIncludes(self._includes)
627 628
629 -class TrackIncludes(IIncludes):
630 """A specification on how much data to return with a track."""
631 - def __init__(self, artist=False, releases=False, puids=False, 632 artistRelations=False, releaseRelations=False, 633 trackRelations=False, urlRelations=False, tags=False):
634 self._includes = { 635 'artist': artist, 636 'releases': releases, 637 'puids': puids, 638 'artist-rels': artistRelations, 639 'release-rels': releaseRelations, 640 'track-rels': trackRelations, 641 'url-rels': urlRelations, 642 'tags': tags, 643 }
644
645 - def createIncludeTags(self):
646 return _createIncludes(self._includes)
647 648
649 -class LabelIncludes(IIncludes):
650 """A specification on how much data to return with a label."""
651 - def __init__(self, aliases=False, tags=False):
652 self._includes = { 653 'aliases': aliases, 654 'tags': tags, 655 }
656
657 - def createIncludeTags(self):
658 return _createIncludes(self._includes)
659 660
661 -class Query(object):
662 """A simple interface to the MusicBrainz web service. 663 664 This is a facade which provides a simple interface to the MusicBrainz 665 web service. It hides all the details like fetching data from a server, 666 parsing the XML and creating an object tree. Using this class, you can 667 request data by ID or search the I{collection} of all resources 668 (artists, releases, or tracks) to retrieve those matching given 669 criteria. This document contains examples to get you started. 670 671 672 Working with Identifiers 673 ======================== 674 675 MusicBrainz uses absolute URIs as identifiers. For example, the artist 676 'Tori Amos' is identified using the following URI:: 677 http://musicbrainz.org/artist/c0b2500e-0cef-4130-869d-732b23ed9df5 678 679 In some situations it is obvious from the context what type of 680 resource an ID refers to. In these cases, abbreviated identifiers may 681 be used, which are just the I{UUID} part of the URI. Thus the ID above 682 may also be written like this:: 683 c0b2500e-0cef-4130-869d-732b23ed9df5 684 685 All methods in this class which require IDs accept both the absolute 686 URI and the abbreviated form (aka the relative URI). 687 688 689 Creating a Query Object 690 ======================= 691 692 In most cases, creating a L{Query} object is as simple as this: 693 694 >>> import musicbrainz2.webservice as ws 695 >>> q = ws.Query() 696 >>> 697 698 The instantiated object uses the standard L{WebService} class to 699 access the MusicBrainz web service. If you want to use a different 700 server or you have to pass user name and password because one of 701 your queries requires authentication, you have to create the 702 L{WebService} object yourself and configure it appropriately. 703 This example uses the MusicBrainz test server and also sets 704 authentication data: 705 706 >>> import musicbrainz2.webservice as ws 707 >>> service = ws.WebService(host='test.musicbrainz.org', 708 ... username='whatever', password='secret') 709 >>> q = ws.Query(service) 710 >>> 711 712 713 Querying for Individual Resources 714 ================================= 715 716 If the MusicBrainz ID of a resource is known, then the L{getArtistById}, 717 L{getReleaseById}, or L{getTrackById} method can be used to retrieve 718 it. Example: 719 720 >>> import musicbrainz2.webservice as ws 721 >>> q = ws.Query() 722 >>> artist = q.getArtistById('c0b2500e-0cef-4130-869d-732b23ed9df5') 723 >>> artist.name 724 u'Tori Amos' 725 >>> artist.sortName 726 u'Amos, Tori' 727 >>> print artist.type 728 http://musicbrainz.org/ns/mmd-1.0#Person 729 >>> 730 731 This returned just the basic artist data, however. To get more detail 732 about a resource, the C{include} parameters may be used which expect 733 an L{ArtistIncludes}, L{ReleaseIncludes}, or L{TrackIncludes} object, 734 depending on the resource type. 735 736 To get data about a release which also includes the main artist 737 and all tracks, for example, the following query can be used: 738 739 >>> import musicbrainz2.webservice as ws 740 >>> q = ws.Query() 741 >>> releaseId = '33dbcf02-25b9-4a35-bdb7-729455f33ad7' 742 >>> include = ws.ReleaseIncludes(artist=True, tracks=True) 743 >>> release = q.getReleaseById(releaseId, include) 744 >>> release.title 745 u'Tales of a Librarian' 746 >>> release.artist.name 747 u'Tori Amos' 748 >>> release.tracks[0].title 749 u'Precious Things' 750 >>> 751 752 Note that the query gets more expensive for the server the more 753 data you request, so please be nice. 754 755 756 Searching in Collections 757 ======================== 758 759 For each resource type (artist, release, and track), there is one 760 collection which contains all resources of a type. You can search 761 these collections using the L{getArtists}, L{getReleases}, and 762 L{getTracks} methods. The collections are huge, so you have to 763 use filters (L{ArtistFilter}, L{ReleaseFilter}, or L{TrackFilter}) 764 to retrieve only resources matching given criteria. 765 766 For example, If you want to search the release collection for 767 releases with a specified DiscID, you would use L{getReleases} 768 and a L{ReleaseFilter} object: 769 770 >>> import musicbrainz2.webservice as ws 771 >>> q = ws.Query() 772 >>> filter = ws.ReleaseFilter(discId='8jJklE258v6GofIqDIrE.c5ejBE-') 773 >>> results = q.getReleases(filter=filter) 774 >>> results[0].score 775 100 776 >>> results[0].release.title 777 u'Under the Pink' 778 >>> 779 780 The query returns a list of results (L{wsxml.ReleaseResult} objects 781 in this case), which are ordered by score, with a higher score 782 indicating a better match. Note that those results don't contain 783 all the data about a resource. If you need more detail, you can then 784 use the L{getArtistById}, L{getReleaseById}, or L{getTrackById} 785 methods to request the resource. 786 787 All filters support the C{limit} argument to limit the number of 788 results returned. This defaults to 25, but the server won't send 789 more than 100 results to save bandwidth and processing power. Using 790 C{limit} and the C{offset} parameter, you can page through the 791 results. 792 793 794 Error Handling 795 ============== 796 797 All methods in this class raise a L{WebServiceError} exception in case 798 of errors. Depending on the method, a subclass of L{WebServiceError} may 799 be raised which allows an application to handle errors more precisely. 800 The following example handles connection errors (invalid host name 801 etc.) separately and all other web service errors in a combined 802 catch clause: 803 804 >>> try: 805 ... artist = q.getArtistById('c0b2500e-0cef-4130-869d-732b23ed9df5') 806 ... except ws.ConnectionError, e: 807 ... pass # implement your error handling here 808 ... except ws.WebServiceError, e: 809 ... pass # catches all other web service errors 810 ... 811 >>> 812 """ 813
814 - def __init__(self, ws=None, wsFactory=WebService, clientId=None):
815 """Constructor. 816 817 The C{ws} parameter has to be a subclass of L{IWebService}. 818 If it isn't given, the C{wsFactory} parameter is used to 819 create an L{IWebService} subclass. 820 821 If the constructor is called without arguments, an instance 822 of L{WebService} is used, preconfigured to use the MusicBrainz 823 server. This should be enough for most users. 824 825 If you want to use queries which require authentication you 826 have to pass a L{WebService} instance where user name and 827 password have been set. 828 829 The C{clientId} parameter is required for data submission. 830 The format is C{'application-version'}, where C{application} 831 is your application's name and C{version} is a version 832 number which may not include a '-' character. 833 834 @param ws: a subclass instance of L{IWebService}, or None 835 @param wsFactory: a callable object which creates an object 836 @param clientId: a unicode string containing the application's ID 837 """ 838 if ws is None: 839 self._ws = wsFactory() 840 else: 841 self._ws = ws 842 843 self._clientId = clientId 844 self._log = logging.getLogger(str(self.__class__))
845 846
847 - def getArtistById(self, id_, include=None):
848 """Returns an artist. 849 850 If no artist with that ID can be found, C{include} contains 851 invalid tags or there's a server problem, an exception is 852 raised. 853 854 @param id_: a string containing the artist's ID 855 @param include: an L{ArtistIncludes} object, or None 856 857 @return: an L{Artist <musicbrainz2.model.Artist>} object, or None 858 859 @raise ConnectionError: couldn't connect to server 860 @raise RequestError: invalid ID or include tags 861 @raise ResourceNotFoundError: artist doesn't exist 862 @raise ResponseError: server returned invalid data 863 """ 864 uuid = mbutils.extractUuid(id_, 'artist') 865 result = self._getFromWebService('artist', uuid, include) 866 artist = result.getArtist() 867 if artist is not None: 868 return artist 869 else: 870 raise ResponseError("server didn't return artist")
871 872
873 - def getArtists(self, filter):
874 """Returns artists matching given criteria. 875 876 @param filter: an L{ArtistFilter} object 877 878 @return: a list of L{musicbrainz2.wsxml.ArtistResult} objects 879 880 @raise ConnectionError: couldn't connect to server 881 @raise RequestError: invalid ID or include tags 882 @raise ResponseError: server returned invalid data 883 """ 884 result = self._getFromWebService('artist', '', filter=filter) 885 return result.getArtistResults()
886
887 - def getLabelById(self, id_, include=None):
888 """Returns a L{model.Label} 889 890 If no label with that ID can be found, or there is a server problem, 891 an exception is raised. 892 893 @param id_: a string containing the label's ID. 894 895 @raise ConnectionError: couldn't connect to server 896 @raise RequestError: invalid ID or include tags 897 @raise ResourceNotFoundError: release doesn't exist 898 @raise ResponseError: server returned invalid data 899 """ 900 uuid = mbutils.extractUuid(id_, 'label') 901 result = self._getFromWebService('label', uuid, include) 902 label = result.getLabel() 903 if label is not None: 904 return label 905 else: 906 raise ResponseError("server didn't return a label")
907
908 - def getLabels(self, filter):
909 result = self._getFromWebService('label', '', filter=filter) 910 return result.getLabelResults()
911
912 - def getReleaseById(self, id_, include=None):
913 """Returns a release. 914 915 If no release with that ID can be found, C{include} contains 916 invalid tags or there's a server problem, and exception is 917 raised. 918 919 @param id_: a string containing the release's ID 920 @param include: a L{ReleaseIncludes} object, or None 921 922 @return: a L{Release <musicbrainz2.model.Release>} object, or None 923 924 @raise ConnectionError: couldn't connect to server 925 @raise RequestError: invalid ID or include tags 926 @raise ResourceNotFoundError: release doesn't exist 927 @raise ResponseError: server returned invalid data 928 """ 929 uuid = mbutils.extractUuid(id_, 'release') 930 result = self._getFromWebService('release', uuid, include) 931 release = result.getRelease() 932 if release is not None: 933 return release 934 else: 935 raise ResponseError("server didn't return release")
936 937
938 - def getReleases(self, filter):
939 """Returns releases matching given criteria. 940 941 @param filter: a L{ReleaseFilter} object 942 943 @return: a list of L{musicbrainz2.wsxml.ReleaseResult} objects 944 945 @raise ConnectionError: couldn't connect to server 946 @raise RequestError: invalid ID or include tags 947 @raise ResponseError: server returned invalid data 948 """ 949 result = self._getFromWebService('release', '', filter=filter) 950 return result.getReleaseResults()
951 952
953 - def getTrackById(self, id_, include=None):
954 """Returns a track. 955 956 If no track with that ID can be found, C{include} contains 957 invalid tags or there's a server problem, and exception is 958 raised. 959 960 @param id_: a string containing the track's ID 961 @param include: a L{TrackIncludes} object, or None 962 963 @return: a L{Track <musicbrainz2.model.Track>} object, or None 964 965 @raise ConnectionError: couldn't connect to server 966 @raise RequestError: invalid ID or include tags 967 @raise ResourceNotFoundError: track doesn't exist 968 @raise ResponseError: server returned invalid data 969 """ 970 uuid = mbutils.extractUuid(id_, 'track') 971 result = self._getFromWebService('track', uuid, include) 972 track = result.getTrack() 973 if track is not None: 974 return track 975 else: 976 raise ResponseError("server didn't return track")
977 978
979 - def getTracks(self, filter):
980 """Returns tracks matching given criteria. 981 982 @param filter: a L{TrackFilter} object 983 984 @return: a list of L{musicbrainz2.wsxml.TrackResult} objects 985 986 @raise ConnectionError: couldn't connect to server 987 @raise RequestError: invalid ID or include tags 988 @raise ResponseError: server returned invalid data 989 """ 990 result = self._getFromWebService('track', '', filter=filter) 991 return result.getTrackResults()
992 993
994 - def getUserByName(self, name):
995 """Returns information about a MusicBrainz user. 996 997 You can only request user data if you know the user name and 998 password for that account. If username and/or password are 999 incorrect, an L{AuthenticationError} is raised. 1000 1001 See the example in L{Query} on how to supply user name and 1002 password. 1003 1004 @param name: a unicode string containing the user's name 1005 1006 @return: a L{User <musicbrainz2.model.User>} object 1007 1008 @raise ConnectionError: couldn't connect to server 1009 @raise RequestError: invalid ID or include tags 1010 @raise AuthenticationError: invalid user name and/or password 1011 @raise ResourceNotFoundError: track doesn't exist 1012 @raise ResponseError: server returned invalid data 1013 """ 1014 filter = UserFilter(name=name) 1015 result = self._getFromWebService('user', '', None, filter) 1016 1017 if len(result.getUserList()) > 0: 1018 return result.getUserList()[0] 1019 else: 1020 raise ResponseError("response didn't contain user data")
1021 1022
1023 - def _getFromWebService(self, entity, id_, include=None, filter=None):
1024 if filter is None: 1025 filterParams = [ ] 1026 else: 1027 filterParams = filter.createParameters() 1028 1029 if include is None: 1030 includeParams = [ ] 1031 else: 1032 includeParams = include.createIncludeTags() 1033 1034 stream = self._ws.get(entity, id_, includeParams, filterParams) 1035 try: 1036 parser = MbXmlParser() 1037 return parser.parse(stream) 1038 except ParseError, e: 1039 raise ResponseError(str(e), e)
1040 1041
1042 - def submitPuids(self, tracks2puids):
1043 """Submit track to PUID mappings. 1044 1045 The C{tracks2puids} parameter has to be a dictionary, with the 1046 keys being MusicBrainz track IDs (either as absolute URIs or 1047 in their 36 character ASCII representation) and the values 1048 being PUIDs (ASCII, 36 characters). 1049 1050 Note that this method only works if a valid user name and 1051 password have been set. See the example in L{Query} on how 1052 to supply authentication data. 1053 1054 @param tracks2puids: a dictionary mapping track IDs to PUIDs 1055 1056 @raise ConnectionError: couldn't connect to server 1057 @raise RequestError: invalid track- or PUIDs 1058 @raise AuthenticationError: invalid user name and/or password 1059 """ 1060 assert self._clientId is not None, 'Please supply a client ID' 1061 params = [ ] 1062 params.append( ('client', self._clientId.encode('utf-8')) ) 1063 1064 for (trackId, puid) in tracks2puids.iteritems(): 1065 trackId = mbutils.extractUuid(trackId, 'track') 1066 params.append( ('puid', trackId + ' ' + puid) ) 1067 1068 encodedStr = urllib.urlencode(params, True) 1069 1070 self._ws.post('track', '', encodedStr)
1071 1072
1073 - def submitUserTags(self, entityUri, tags):
1074 """Submit folksonomy tags for an entity. 1075 1076 Note that all previously existing tags from the authenticated 1077 user are replaced with the ones given to this method. Other 1078 users' tags are not affected. 1079 1080 @param entityUri: a string containing an absolute MB ID 1081 @param tags: A list of either L{Tag <musicbrainz2.model.Tag>} objects 1082 or strings 1083 1084 @raise ValueError: invalid entityUri 1085 @raise ConnectionError: couldn't connect to server 1086 @raise RequestError: invalid ID, entity or tags 1087 @raise AuthenticationError: invalid user name and/or password 1088 """ 1089 entity = mbutils.extractEntityType(entityUri) 1090 uuid = mbutils.extractUuid(entityUri, entity) 1091 params = ( 1092 ('type', 'xml'), 1093 ('entity', entity), 1094 ('id', uuid), 1095 ('tags', ','.join([unicode(tag).encode('utf-8') for tag in tags])) 1096 ) 1097 1098 encodedStr = urllib.urlencode(params) 1099 1100 self._ws.post('tag', '', encodedStr)
1101 1102
1103 - def getUserTags(self, entityUri):
1104 """Returns a list of folksonomy tags a user has applied to an entity. 1105 1106 The given parameter has to be a fully qualified MusicBrainz ID, as 1107 returned by other library functions. 1108 1109 Note that this method only works if a valid user name and 1110 password have been set. Only the tags the authenticated user 1111 applied to the entity will be returned. If username and/or 1112 password are incorrect, an AuthenticationError is raised. 1113 1114 This method will return a list of L{Tag <musicbrainz2.model.Tag>} 1115 objects. 1116 1117 @param entityUri: a string containing an absolute MB ID 1118 1119 @raise ValueError: invalid entityUri 1120 @raise ConnectionError: couldn't connect to server 1121 @raise RequestError: invalid ID or entity 1122 @raise AuthenticationError: invalid user name and/or password 1123 """ 1124 entity = mbutils.extractEntityType(entityUri) 1125 uuid = mbutils.extractUuid(entityUri, entity) 1126 params = { 'entity': entity, 'id': uuid } 1127 1128 stream = self._ws.get('tag', '', filter=params) 1129 try: 1130 parser = MbXmlParser() 1131 result = parser.parse(stream) 1132 except ParseError, e: 1133 raise ResponseError(str(e), e) 1134 1135 return result.getTagList()
1136 1137
1138 -def _createIncludes(tagMap):
1139 selected = filter(lambda x: x[1] == True, tagMap.items()) 1140 return map(lambda x: x[0], selected)
1141
1142 -def _createParameters(params):
1143 """Remove (x, None) tuples and encode (x, str/unicode) to utf-8.""" 1144 ret = [ ] 1145 for p in params: 1146 if isinstance(p[1], (str, unicode)): 1147 ret.append( (p[0], p[1].encode('utf-8')) ) 1148 elif p[1] is not None: 1149 ret.append(p) 1150 1151 return ret
1152
1153 -def _paramsValid(params):
1154 """Check if the query parameter collides with other parameters.""" 1155 tmp = [ ] 1156 for name, value in params: 1157 if value is not None and name not in ('offset', 'limit'): 1158 tmp.append(name) 1159 1160 if 'query' in tmp and len(tmp) > 1: 1161 return False 1162 else: 1163 return True
1164 1165 if __name__ == '__main__': 1166 import doctest 1167 doctest.testmod() 1168 1169 # EOF 1170