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 12708 2010-03-15 17:45:25Z 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', 'ReleaseGroupIncludes',
34 'IFilter', 'ArtistFilter', 'ReleaseFilter', 'TrackFilter',
35 'UserFilter', 'LabelFilter', 'ReleaseGroupFilter',
36 'IWebService', 'WebService', 'Query',
37 ]
38
39
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
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
113 """Makes this class printable.
114
115 @return: a string containing an error message
116 """
117 return self.msg
118
119
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
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
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
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
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
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, (),
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
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:
264 raise RequestError(str(e), e)
265 elif e.code == 401:
266 raise AuthenticationError(str(e), e)
267 elif e.code == 404:
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:
299 raise RequestError(str(e), e)
300 elif e.code == 401:
301 raise AuthenticationError(str(e), e)
302 elif e.code == 404:
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
312
313
314
318
320
321 try:
322 return self._realms[realm]
323 except KeyError:
324 return (None, None)
325
327
328 self._realms[realm] = (username, password)
329
330
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 """
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
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
384 return _createParameters(self._params)
385
386
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
413 return _createParameters(self._params)
414
416 """A filter for the release group collection."""
417
418 - def __init__(self, title=None, releaseTypes=None, artistName=None,
419 artistId=None, limit=None, offset=None, query=None):
420 """Constructor.
421
422 If C{artistId} is set, only releases matching those IDs are
423 returned. The C{releaseTypes} parameter allows you to limit
424 the types of the release groups returned. You can set it to
425 C{(Release.TYPE_ALBUM, Release.TYPE_OFFICIAL)}, for example,
426 to only get officially released albums. Note that those values
427 are connected using the I{AND} operator. MusicBrainz' support
428 is currently very limited, so C{Release.TYPE_LIVE} and
429 C{Release.TYPE_COMPILATION} exclude each other (see U{the
430 documentation on release attributes
431 <http://wiki.musicbrainz.org/AlbumAttribute>} for more
432 information and all valid values).
433
434 If both the C{artistName} and the C{artistId} parameter are
435 given, the server will ignore C{artistName}.
436
437 The C{query} parameter may contain a query in U{Lucene syntax
438 <http://lucene.apache.org/java/docs/queryparsersyntax.html>}.
439 Note that C{query} may not be used together with the other
440 parameters except for C{limit} and C{offset}.
441
442 @param title: a unicode string containing the release group's title
443 @param releaseTypes: a sequence of release type URIs
444 @param artistName: a unicode string containing the artist's name
445 @param artistId: a unicode string containing the artist's ID
446 @param limit: the maximum number of release groups to return
447 @param offset: start results at this zero-based offset
448 @param query: a string containing a query in Lucene syntax
449
450 @see: the constants in L{musicbrainz2.model.Release}
451 """
452 if releaseTypes is None or len(releaseTypes) == 0:
453 releaseTypesStr = None
454 else:
455 releaseTypesStr = ' '.join(map(mbutils.extractFragment, releaseTypes))
456
457 self._params = [
458 ('title', title),
459 ('releasetypes', releaseTypesStr),
460 ('artist', artistName),
461 ('artistid', mbutils.extractUuid(artistId)),
462 ('limit', limit),
463 ('offset', offset),
464 ('query', query),
465 ]
466
467 if not _paramsValid(self._params):
468 raise ValueError('invalid combination of parameters')
469
471 return _createParameters(self._params)
472
473
475 """A filter for the release collection."""
476
477 - def __init__(self, title=None, discId=None, releaseTypes=None,
478 artistName=None, artistId=None, limit=None,
479 offset=None, query=None, trackCount=None):
480 """Constructor.
481
482 If C{discId} or C{artistId} are set, only releases matching
483 those IDs are returned. The C{releaseTypes} parameter allows
484 to limit the types of the releases returned. You can set it to
485 C{(Release.TYPE_ALBUM, Release.TYPE_OFFICIAL)}, for example,
486 to only get officially released albums. Note that those values
487 are connected using the I{AND} operator. MusicBrainz' support
488 is currently very limited, so C{Release.TYPE_LIVE} and
489 C{Release.TYPE_COMPILATION} exclude each other (see U{the
490 documentation on release attributes
491 <http://wiki.musicbrainz.org/AlbumAttribute>} for more
492 information and all valid values).
493
494 If both the C{artistName} and the C{artistId} parameter are
495 given, the server will ignore C{artistName}.
496
497 The C{query} parameter may contain a query in U{Lucene syntax
498 <http://lucene.apache.org/java/docs/queryparsersyntax.html>}.
499 Note that C{query} may not be used together with the other
500 parameters except for C{limit} and C{offset}.
501
502 @param title: a unicode string containing the release's title
503 @param discId: a unicode string containing the DiscID
504 @param releaseTypes: a sequence of release type URIs
505 @param artistName: a unicode string containing the artist's name
506 @param artistId: a unicode string containing the artist's ID
507 @param limit: the maximum number of releases to return
508 @param offset: start results at this zero-based offset
509 @param query: a string containing a query in Lucene syntax
510 @param trackCount: the number of tracks in the release
511
512 @see: the constants in L{musicbrainz2.model.Release}
513 """
514 if releaseTypes is None or len(releaseTypes) == 0:
515 releaseTypesStr = None
516 else:
517 tmp = [ mbutils.extractFragment(x) for x in releaseTypes ]
518 releaseTypesStr = ' '.join(tmp)
519
520 self._params = [
521 ('title', title),
522 ('discid', discId),
523 ('releasetypes', releaseTypesStr),
524 ('artist', artistName),
525 ('artistid', mbutils.extractUuid(artistId)),
526 ('limit', limit),
527 ('offset', offset),
528 ('query', query),
529 ('count', trackCount),
530 ]
531
532 if not _paramsValid(self._params):
533 raise ValueError('invalid combination of parameters')
534
536 return _createParameters(self._params)
537
538
540 """A filter for the track collection."""
541
542 - def __init__(self, title=None, artistName=None, artistId=None,
543 releaseTitle=None, releaseId=None,
544 duration=None, puid=None, limit=None, offset=None,
545 query=None):
546 """Constructor.
547
548 If C{artistId}, C{releaseId} or C{puid} are set, only tracks
549 matching those IDs are returned.
550
551 The server will ignore C{artistName} and C{releaseTitle} if
552 C{artistId} or ${releaseId} are set respectively.
553
554 The C{query} parameter may contain a query in U{Lucene syntax
555 <http://lucene.apache.org/java/docs/queryparsersyntax.html>}.
556 Note that C{query} may not be used together with the other
557 parameters except for C{limit} and C{offset}.
558
559 @param title: a unicode string containing the track's title
560 @param artistName: a unicode string containing the artist's name
561 @param artistId: a string containing the artist's ID
562 @param releaseTitle: a unicode string containing the release's title
563 @param releaseId: a string containing the release's title
564 @param duration: the track's length in milliseconds
565 @param puid: a string containing a PUID
566 @param limit: the maximum number of releases to return
567 @param offset: start results at this zero-based offset
568 @param query: a string containing a query in Lucene syntax
569 """
570 self._params = [
571 ('title', title),
572 ('artist', artistName),
573 ('artistid', mbutils.extractUuid(artistId)),
574 ('release', releaseTitle),
575 ('releaseid', mbutils.extractUuid(releaseId)),
576 ('duration', duration),
577 ('puid', puid),
578 ('limit', limit),
579 ('offset', offset),
580 ('query', query),
581 ]
582
583 if not _paramsValid(self._params):
584 raise ValueError('invalid combination of parameters')
585
587 return _createParameters(self._params)
588
589
591 """A filter for the user collection."""
592
594 """Constructor.
595
596 @param name: a unicode string containing a MusicBrainz user name
597 """
598 self._name = name
599
601 if self._name is not None:
602 return [ ('name', self._name.encode('utf-8')) ]
603 else:
604 return [ ]
605
606
608 """An interface implemented by include tag generators."""
611
612
614 """A specification on how much data to return with an artist.
615
616 Example:
617
618 >>> from musicbrainz2.model import Release
619 >>> from musicbrainz2.webservice import ArtistIncludes
620 >>> inc = ArtistIncludes(artistRelations=True, releaseRelations=True,
621 ... releases=(Release.TYPE_ALBUM, Release.TYPE_OFFICIAL))
622 >>>
623
624 The MusicBrainz server only supports some combinations of release
625 types for the C{releases} and C{vaReleases} include tags. At the
626 moment, not more than two release types should be selected, while
627 one of them has to be C{Release.TYPE_OFFICIAL},
628 C{Release.TYPE_PROMOTION} or C{Release.TYPE_BOOTLEG}.
629
630 @note: Only one of C{releases} and C{vaReleases} may be given.
631 """
632 - def __init__(self, aliases=False, releases=(), vaReleases=(),
633 artistRelations=False, releaseRelations=False,
634 trackRelations=False, urlRelations=False, tags=False,
635 ratings=False, releaseGroups=False):
636
637 assert not isinstance(releases, basestring)
638 assert not isinstance(vaReleases, basestring)
639 assert len(releases) == 0 or len(vaReleases) == 0
640
641 self._includes = {
642 'aliases': aliases,
643 'artist-rels': artistRelations,
644 'release-groups': releaseGroups,
645 'release-rels': releaseRelations,
646 'track-rels': trackRelations,
647 'url-rels': urlRelations,
648 'tags': tags,
649 'ratings': ratings,
650 }
651
652 for elem in releases:
653 self._includes['sa-' + mbutils.extractFragment(elem)] = True
654
655 for elem in vaReleases:
656 self._includes['va-' + mbutils.extractFragment(elem)] = True
657
660
661
663 """A specification on how much data to return with a release."""
664 - def __init__(self, artist=False, counts=False, releaseEvents=False,
665 discs=False, tracks=False,
666 artistRelations=False, releaseRelations=False,
667 trackRelations=False, urlRelations=False,
668 labels=False, tags=False, ratings=False, isrcs=False,
669 releaseGroup=False):
670 self._includes = {
671 'artist': artist,
672 'counts': counts,
673 'labels': labels,
674 'release-groups': releaseGroup,
675 'release-events': releaseEvents,
676 'discs': discs,
677 'tracks': tracks,
678 'artist-rels': artistRelations,
679 'release-rels': releaseRelations,
680 'track-rels': trackRelations,
681 'url-rels': urlRelations,
682 'tags': tags,
683 'ratings': ratings,
684 'isrcs': isrcs,
685 }
686
687
688
689 if labels and not releaseEvents:
690 self._includes['release-events'] = True
691
692 if isrcs and not tracks:
693 self._includes['tracks'] = True
694
697
698
700 """A specification on how much data to return with a release group."""
701
702 - def __init__(self, artist=False, releases=False, tags=False):
703 """Constructor.
704
705 @param artist: Whether to include the release group's main artist info.
706 @param releases: Whether to include the release group's releases.
707 """
708 self._includes = {
709 'artist': artist,
710 'releases': releases,
711 }
712
715
716
718 """A specification on how much data to return with a track."""
719 - def __init__(self, artist=False, releases=False, puids=False,
720 artistRelations=False, releaseRelations=False,
721 trackRelations=False, urlRelations=False, tags=False,
722 ratings=False, isrcs=False):
723 self._includes = {
724 'artist': artist,
725 'releases': releases,
726 'puids': puids,
727 'artist-rels': artistRelations,
728 'release-rels': releaseRelations,
729 'track-rels': trackRelations,
730 'url-rels': urlRelations,
731 'tags': tags,
732 'ratings': ratings,
733 'isrcs': isrcs,
734 }
735
738
739
741 """A specification on how much data to return with a label."""
742 - def __init__(self, aliases=False, tags=False, ratings=False):
743 self._includes = {
744 'aliases': aliases,
745 'tags': tags,
746 'ratings': ratings,
747 }
748
751
752
754 """A simple interface to the MusicBrainz web service.
755
756 This is a facade which provides a simple interface to the MusicBrainz
757 web service. It hides all the details like fetching data from a server,
758 parsing the XML and creating an object tree. Using this class, you can
759 request data by ID or search the I{collection} of all resources
760 (artists, releases, or tracks) to retrieve those matching given
761 criteria. This document contains examples to get you started.
762
763
764 Working with Identifiers
765 ========================
766
767 MusicBrainz uses absolute URIs as identifiers. For example, the artist
768 'Tori Amos' is identified using the following URI::
769 http://musicbrainz.org/artist/c0b2500e-0cef-4130-869d-732b23ed9df5
770
771 In some situations it is obvious from the context what type of
772 resource an ID refers to. In these cases, abbreviated identifiers may
773 be used, which are just the I{UUID} part of the URI. Thus the ID above
774 may also be written like this::
775 c0b2500e-0cef-4130-869d-732b23ed9df5
776
777 All methods in this class which require IDs accept both the absolute
778 URI and the abbreviated form (aka the relative URI).
779
780
781 Creating a Query Object
782 =======================
783
784 In most cases, creating a L{Query} object is as simple as this:
785
786 >>> import musicbrainz2.webservice as ws
787 >>> q = ws.Query()
788 >>>
789
790 The instantiated object uses the standard L{WebService} class to
791 access the MusicBrainz web service. If you want to use a different
792 server or you have to pass user name and password because one of
793 your queries requires authentication, you have to create the
794 L{WebService} object yourself and configure it appropriately.
795 This example uses the MusicBrainz test server and also sets
796 authentication data:
797
798 >>> import musicbrainz2.webservice as ws
799 >>> service = ws.WebService(host='test.musicbrainz.org',
800 ... username='whatever', password='secret')
801 >>> q = ws.Query(service)
802 >>>
803
804
805 Querying for Individual Resources
806 =================================
807
808 If the MusicBrainz ID of a resource is known, then the L{getArtistById},
809 L{getReleaseById}, or L{getTrackById} method can be used to retrieve
810 it. Example:
811
812 >>> import musicbrainz2.webservice as ws
813 >>> q = ws.Query()
814 >>> artist = q.getArtistById('c0b2500e-0cef-4130-869d-732b23ed9df5')
815 >>> artist.name
816 u'Tori Amos'
817 >>> artist.sortName
818 u'Amos, Tori'
819 >>> print artist.type
820 http://musicbrainz.org/ns/mmd-1.0#Person
821 >>>
822
823 This returned just the basic artist data, however. To get more detail
824 about a resource, the C{include} parameters may be used which expect
825 an L{ArtistIncludes}, L{ReleaseIncludes}, or L{TrackIncludes} object,
826 depending on the resource type.
827
828 To get data about a release which also includes the main artist
829 and all tracks, for example, the following query can be used:
830
831 >>> import musicbrainz2.webservice as ws
832 >>> q = ws.Query()
833 >>> releaseId = '33dbcf02-25b9-4a35-bdb7-729455f33ad7'
834 >>> include = ws.ReleaseIncludes(artist=True, tracks=True)
835 >>> release = q.getReleaseById(releaseId, include)
836 >>> release.title
837 u'Tales of a Librarian'
838 >>> release.artist.name
839 u'Tori Amos'
840 >>> release.tracks[0].title
841 u'Precious Things'
842 >>>
843
844 Note that the query gets more expensive for the server the more
845 data you request, so please be nice.
846
847
848 Searching in Collections
849 ========================
850
851 For each resource type (artist, release, and track), there is one
852 collection which contains all resources of a type. You can search
853 these collections using the L{getArtists}, L{getReleases}, and
854 L{getTracks} methods. The collections are huge, so you have to
855 use filters (L{ArtistFilter}, L{ReleaseFilter}, or L{TrackFilter})
856 to retrieve only resources matching given criteria.
857
858 For example, If you want to search the release collection for
859 releases with a specified DiscID, you would use L{getReleases}
860 and a L{ReleaseFilter} object:
861
862 >>> import musicbrainz2.webservice as ws
863 >>> q = ws.Query()
864 >>> filter = ws.ReleaseFilter(discId='8jJklE258v6GofIqDIrE.c5ejBE-')
865 >>> results = q.getReleases(filter=filter)
866 >>> results[0].score
867 100
868 >>> results[0].release.title
869 u'Under the Pink'
870 >>>
871
872 The query returns a list of results (L{wsxml.ReleaseResult} objects
873 in this case), which are ordered by score, with a higher score
874 indicating a better match. Note that those results don't contain
875 all the data about a resource. If you need more detail, you can then
876 use the L{getArtistById}, L{getReleaseById}, or L{getTrackById}
877 methods to request the resource.
878
879 All filters support the C{limit} argument to limit the number of
880 results returned. This defaults to 25, but the server won't send
881 more than 100 results to save bandwidth and processing power. Using
882 C{limit} and the C{offset} parameter, you can page through the
883 results.
884
885
886 Error Handling
887 ==============
888
889 All methods in this class raise a L{WebServiceError} exception in case
890 of errors. Depending on the method, a subclass of L{WebServiceError} may
891 be raised which allows an application to handle errors more precisely.
892 The following example handles connection errors (invalid host name
893 etc.) separately and all other web service errors in a combined
894 catch clause:
895
896 >>> try:
897 ... artist = q.getArtistById('c0b2500e-0cef-4130-869d-732b23ed9df5')
898 ... except ws.ConnectionError, e:
899 ... pass # implement your error handling here
900 ... except ws.WebServiceError, e:
901 ... pass # catches all other web service errors
902 ...
903 >>>
904 """
905
907 """Constructor.
908
909 The C{ws} parameter has to be a subclass of L{IWebService}.
910 If it isn't given, the C{wsFactory} parameter is used to
911 create an L{IWebService} subclass.
912
913 If the constructor is called without arguments, an instance
914 of L{WebService} is used, preconfigured to use the MusicBrainz
915 server. This should be enough for most users.
916
917 If you want to use queries which require authentication you
918 have to pass a L{WebService} instance where user name and
919 password have been set.
920
921 The C{clientId} parameter is required for data submission.
922 The format is C{'application-version'}, where C{application}
923 is your application's name and C{version} is a version
924 number which may not include a '-' character.
925
926 @param ws: a subclass instance of L{IWebService}, or None
927 @param wsFactory: a callable object which creates an object
928 @param clientId: a unicode string containing the application's ID
929 """
930 if ws is None:
931 self._ws = wsFactory()
932 else:
933 self._ws = ws
934
935 self._clientId = clientId
936 self._log = logging.getLogger(str(self.__class__))
937
938
940 """Returns an artist.
941
942 If no artist with that ID can be found, C{include} contains
943 invalid tags or there's a server problem, an exception is
944 raised.
945
946 @param id_: a string containing the artist's ID
947 @param include: an L{ArtistIncludes} object, or None
948
949 @return: an L{Artist <musicbrainz2.model.Artist>} object, or None
950
951 @raise ConnectionError: couldn't connect to server
952 @raise RequestError: invalid ID or include tags
953 @raise ResourceNotFoundError: artist doesn't exist
954 @raise ResponseError: server returned invalid data
955 """
956 uuid = mbutils.extractUuid(id_, 'artist')
957 result = self._getFromWebService('artist', uuid, include)
958 artist = result.getArtist()
959 if artist is not None:
960 return artist
961 else:
962 raise ResponseError("server didn't return artist")
963
964
966 """Returns artists matching given criteria.
967
968 @param filter: an L{ArtistFilter} object
969
970 @return: a list of L{musicbrainz2.wsxml.ArtistResult} objects
971
972 @raise ConnectionError: couldn't connect to server
973 @raise RequestError: invalid ID or include tags
974 @raise ResponseError: server returned invalid data
975 """
976 result = self._getFromWebService('artist', '', filter=filter)
977 return result.getArtistResults()
978
980 """Returns a L{model.Label}
981
982 If no label with that ID can be found, or there is a server problem,
983 an exception is raised.
984
985 @param id_: a string containing the label's ID.
986
987 @raise ConnectionError: couldn't connect to server
988 @raise RequestError: invalid ID or include tags
989 @raise ResourceNotFoundError: release doesn't exist
990 @raise ResponseError: server returned invalid data
991 """
992 uuid = mbutils.extractUuid(id_, 'label')
993 result = self._getFromWebService('label', uuid, include)
994 label = result.getLabel()
995 if label is not None:
996 return label
997 else:
998 raise ResponseError("server didn't return a label")
999
1001 result = self._getFromWebService('label', '', filter=filter)
1002 return result.getLabelResults()
1003
1005 """Returns a release.
1006
1007 If no release with that ID can be found, C{include} contains
1008 invalid tags or there's a server problem, and exception is
1009 raised.
1010
1011 @param id_: a string containing the release's ID
1012 @param include: a L{ReleaseIncludes} object, or None
1013
1014 @return: a L{Release <musicbrainz2.model.Release>} object, or None
1015
1016 @raise ConnectionError: couldn't connect to server
1017 @raise RequestError: invalid ID or include tags
1018 @raise ResourceNotFoundError: release doesn't exist
1019 @raise ResponseError: server returned invalid data
1020 """
1021 uuid = mbutils.extractUuid(id_, 'release')
1022 result = self._getFromWebService('release', uuid, include)
1023 release = result.getRelease()
1024 if release is not None:
1025 return release
1026 else:
1027 raise ResponseError("server didn't return release")
1028
1029
1031 """Returns releases matching given criteria.
1032
1033 @param filter: a L{ReleaseFilter} object
1034
1035 @return: a list of L{musicbrainz2.wsxml.ReleaseResult} objects
1036
1037 @raise ConnectionError: couldn't connect to server
1038 @raise RequestError: invalid ID or include tags
1039 @raise ResponseError: server returned invalid data
1040 """
1041 result = self._getFromWebService('release', '', filter=filter)
1042 return result.getReleaseResults()
1043
1045 """Returns a release group.
1046
1047 If no release group with that ID can be found, C{include}
1048 contains invalid tags, or there's a server problem, an
1049 exception is raised.
1050
1051 @param id_: a string containing the release group's ID
1052 @param include: a L{ReleaseGroupIncludes} object, or None
1053
1054 @return: a L{ReleaseGroup <musicbrainz2.model.ReleaseGroup>} object, or None
1055
1056 @raise ConnectionError: couldn't connect to server
1057 @raise RequestError: invalid ID or include tags
1058 @raise ResourceNotFoundError: release doesn't exist
1059 @raise ResponseError: server returned invalid data
1060 """
1061 uuid = mbutils.extractUuid(id_, 'release-group')
1062 result = self._getFromWebService('release-group', uuid, include)
1063 releaseGroup = result.getReleaseGroup()
1064 if releaseGroup is not None:
1065 return releaseGroup
1066 else:
1067 raise ResponseError("server didn't return releaseGroup")
1068
1070 """Returns release groups matching the given criteria.
1071
1072 @param filter: a L{ReleaseGroupFilter} object
1073
1074 @return: a list of L{musicbrainz2.wsxml.ReleaseGroupResult} objects
1075
1076 @raise ConnectionError: couldn't connect to server
1077 @raise RequestError: invalid ID or include tags
1078 @raise ResponseError: server returned invalid data
1079 """
1080 result = self._getFromWebService('release-group', '', filter=filter)
1081 return result.getReleaseGroupResults()
1082
1084 """Returns a track.
1085
1086 If no track with that ID can be found, C{include} contains
1087 invalid tags or there's a server problem, an exception is
1088 raised.
1089
1090 @param id_: a string containing the track's ID
1091 @param include: a L{TrackIncludes} object, or None
1092
1093 @return: a L{Track <musicbrainz2.model.Track>} object, or None
1094
1095 @raise ConnectionError: couldn't connect to server
1096 @raise RequestError: invalid ID or include tags
1097 @raise ResourceNotFoundError: track doesn't exist
1098 @raise ResponseError: server returned invalid data
1099 """
1100 uuid = mbutils.extractUuid(id_, 'track')
1101 result = self._getFromWebService('track', uuid, include)
1102 track = result.getTrack()
1103 if track is not None:
1104 return track
1105 else:
1106 raise ResponseError("server didn't return track")
1107
1108
1110 """Returns tracks matching given criteria.
1111
1112 @param filter: a L{TrackFilter} object
1113
1114 @return: a list of L{musicbrainz2.wsxml.TrackResult} objects
1115
1116 @raise ConnectionError: couldn't connect to server
1117 @raise RequestError: invalid ID or include tags
1118 @raise ResponseError: server returned invalid data
1119 """
1120 result = self._getFromWebService('track', '', filter=filter)
1121 return result.getTrackResults()
1122
1123
1125 """Returns information about a MusicBrainz user.
1126
1127 You can only request user data if you know the user name and
1128 password for that account. If username and/or password are
1129 incorrect, an L{AuthenticationError} is raised.
1130
1131 See the example in L{Query} on how to supply user name and
1132 password.
1133
1134 @param name: a unicode string containing the user's name
1135
1136 @return: a L{User <musicbrainz2.model.User>} object
1137
1138 @raise ConnectionError: couldn't connect to server
1139 @raise RequestError: invalid ID or include tags
1140 @raise AuthenticationError: invalid user name and/or password
1141 @raise ResourceNotFoundError: track doesn't exist
1142 @raise ResponseError: server returned invalid data
1143 """
1144 filter = UserFilter(name=name)
1145 result = self._getFromWebService('user', '', None, filter)
1146
1147 if len(result.getUserList()) > 0:
1148 return result.getUserList()[0]
1149 else:
1150 raise ResponseError("response didn't contain user data")
1151
1152
1154 if filter is None:
1155 filterParams = [ ]
1156 else:
1157 filterParams = filter.createParameters()
1158
1159 if include is None:
1160 includeParams = [ ]
1161 else:
1162 includeParams = include.createIncludeTags()
1163
1164 stream = self._ws.get(entity, id_, includeParams, filterParams)
1165 try:
1166 parser = MbXmlParser()
1167 return parser.parse(stream)
1168 except ParseError, e:
1169 raise ResponseError(str(e), e)
1170
1171
1173 """Submit track to PUID mappings.
1174
1175 The C{tracks2puids} parameter has to be a dictionary, with the
1176 keys being MusicBrainz track IDs (either as absolute URIs or
1177 in their 36 character ASCII representation) and the values
1178 being PUIDs (ASCII, 36 characters).
1179
1180 Note that this method only works if a valid user name and
1181 password have been set. See the example in L{Query} on how
1182 to supply authentication data.
1183
1184 @param tracks2puids: a dictionary mapping track IDs to PUIDs
1185
1186 @raise ConnectionError: couldn't connect to server
1187 @raise RequestError: invalid track or PUIDs
1188 @raise AuthenticationError: invalid user name and/or password
1189 """
1190 assert self._clientId is not None, 'Please supply a client ID'
1191 params = [ ]
1192 params.append( ('client', self._clientId.encode('utf-8')) )
1193
1194 for (trackId, puid) in tracks2puids.iteritems():
1195 trackId = mbutils.extractUuid(trackId, 'track')
1196 params.append( ('puid', trackId + ' ' + puid) )
1197
1198 encodedStr = urllib.urlencode(params, True)
1199
1200 self._ws.post('track', '', encodedStr)
1201
1203 """Submit track to ISRC mappings.
1204
1205 The C{tracks2isrcs} parameter has to be a dictionary, with the
1206 keys being MusicBrainz track IDs (either as absolute URIs or
1207 in their 36 character ASCII representation) and the values
1208 being ISRCs (ASCII, 12 characters).
1209
1210 Note that this method only works if a valid user name and
1211 password have been set. See the example in L{Query} on how
1212 to supply authentication data.
1213
1214 @param tracks2isrcs: a dictionary mapping track IDs to ISRCs
1215
1216 @raise ConnectionError: couldn't connect to server
1217 @raise RequestError: invalid track or ISRCs
1218 @raise AuthenticationError: invalid user name and/or password
1219 """
1220 params = [ ]
1221
1222 for (trackId, isrc) in tracks2isrcs.iteritems():
1223 trackId = mbutils.extractUuid(trackId, 'track')
1224 params.append( ('isrc', trackId + ' ' + isrc) )
1225
1226 encodedStr = urllib.urlencode(params, True)
1227
1228 self._ws.post('track', '', encodedStr)
1229
1231 """Add releases to a user's collection.
1232
1233 The releases parameter must be a list. It can contain either L{Release}
1234 objects or a string representing a MusicBrainz release ID (either as
1235 absolute URIs or in their 36 character ASCII representation).
1236
1237 Adding a release that is already in the collection has no effect.
1238
1239 @param releases: a list of releases to add to the user collection
1240
1241 @raise ConnectionError: couldn't connect to server
1242 @raise AuthenticationError: invalid user name and/or password
1243 """
1244 rels = []
1245 for release in releases:
1246 if isinstance(release, Release):
1247 rels.append(mbutils.extractUuid(release.id))
1248 else:
1249 rels.append(mbutils.extractUuid(release))
1250 encodedStr = urllib.urlencode({'add': ",".join(rels)}, True)
1251 self._ws.post('collection', '', encodedStr)
1252
1254 """Remove releases from a user's collection.
1255
1256 The releases parameter must be a list. It can contain either L{Release}
1257 objects or a string representing a MusicBrainz release ID (either as
1258 absolute URIs or in their 36 character ASCII representation).
1259
1260 Removing a release that is not in the collection has no effect.
1261
1262 @param releases: a list of releases to remove from the user collection
1263
1264 @raise ConnectionError: couldn't connect to server
1265 @raise AuthenticationError: invalid user name and/or password
1266 """
1267 rels = []
1268 for release in releases:
1269 if isinstance(release, Release):
1270 rels.append(mbutils.extractUuid(release.id))
1271 else:
1272 rels.append(mbutils.extractUuid(release))
1273 encodedStr = urllib.urlencode({'remove': ",".join(rels)}, True)
1274 self._ws.post('collection', '', encodedStr)
1275
1277 """Get the releases that are in a user's collection
1278
1279 A maximum of 100 items will be returned for any one call
1280 to this method. To fetch more than 100 items, use the offset
1281 parameter.
1282
1283 @param offset: the offset to start fetching results from
1284 @param maxitems: the upper limit on items to return
1285
1286 @return: a list of L{musicbrainz2.wsxml.ReleaseResult} objects
1287
1288 @raise ConnectionError: couldn't connect to server
1289 @raise AuthenticationError: invalid user name and/or password
1290 """
1291 params = { 'offset': offset, 'maxitems': maxitems }
1292
1293 stream = self._ws.get('collection', '', filter=params)
1294 try:
1295 parser = MbXmlParser()
1296 result = parser.parse(stream)
1297 except ParseError, e:
1298 raise ResponseError(str(e), e)
1299
1300 return result.getReleaseResults()
1301
1330
1331
1365
1367 """Submit rating for an entity.
1368
1369 Note that all previously existing rating from the authenticated
1370 user are replaced with the one given to this method. Other
1371 users' ratings are not affected.
1372
1373 @param entityUri: a string containing an absolute MB ID
1374 @param rating: A L{Rating <musicbrainz2.model.Rating>} object
1375 or integer
1376
1377 @raise ValueError: invalid entityUri
1378 @raise ConnectionError: couldn't connect to server
1379 @raise RequestError: invalid ID, entity or tags
1380 @raise AuthenticationError: invalid user name and/or password
1381 """
1382 entity = mbutils.extractEntityType(entityUri)
1383 uuid = mbutils.extractUuid(entityUri, entity)
1384 params = (
1385 ('type', 'xml'),
1386 ('entity', entity),
1387 ('id', uuid),
1388 ('rating', unicode(rating).encode('utf-8'))
1389 )
1390
1391 encodedStr = urllib.urlencode(params)
1392
1393 self._ws.post('rating', '', encodedStr)
1394
1395
1397 """Return the rating a user has applied to an entity.
1398
1399 The given parameter has to be a fully qualified MusicBrainz
1400 ID, as returned by other library functions.
1401
1402 Note that this method only works if a valid user name and
1403 password have been set. Only the rating the authenticated user
1404 applied to the entity will be returned. If username and/or
1405 password are incorrect, an AuthenticationError is raised.
1406
1407 This method will return a L{Rating <musicbrainz2.model.Rating>}
1408 object.
1409
1410 @param entityUri: a string containing an absolute MB ID
1411
1412 @raise ValueError: invalid entityUri
1413 @raise ConnectionError: couldn't connect to server
1414 @raise RequestError: invalid ID or entity
1415 @raise AuthenticationError: invalid user name and/or password
1416 """
1417 entity = mbutils.extractEntityType(entityUri)
1418 uuid = mbutils.extractUuid(entityUri, entity)
1419 params = { 'entity': entity, 'id': uuid }
1420
1421 stream = self._ws.get('rating', '', filter=params)
1422 try:
1423 parser = MbXmlParser()
1424 result = parser.parse(stream)
1425 except ParseError, e:
1426 raise ResponseError(str(e), e)
1427
1428 return result.getRating()
1429
1431 """Submit a CD Stub to the database.
1432
1433 The number of tracks added to the CD Stub must match the TOC and DiscID
1434 otherwise the submission wil fail. The submission will also fail if
1435 the Disc ID is already in the MusicBrainz database.
1436
1437 This method will only work if no user name and password are set.
1438
1439 @param cdstub: a L{CDStub} object to submit
1440
1441 @raise RequestError: Missmatching TOC/Track information or the
1442 the CD Stub already exists or the Disc ID already exists
1443 """
1444 assert self._clientId is not None, 'Please supply a client ID'
1445 disc = cdstub._disc
1446 params = [ ]
1447 params.append( ('client', self._clientId.encode('utf-8')) )
1448 params.append( ('discid', disc.id) )
1449 params.append( ('title', cdstub.title) )
1450 params.append( ('artist', cdstub.artist) )
1451 if cdstub.barcode != "":
1452 params.append( ('barcode', cdstub.barcode) )
1453 if cdstub.comment != "":
1454 params.append( ('comment', cdstub.comment) )
1455
1456 trackind = 0
1457 for track,artist in cdstub.tracks:
1458 params.append( ('track%d' % trackind, track) )
1459 if artist != "":
1460 params.append( ('artist%d' % trackind, artist) )
1461
1462 trackind += 1
1463
1464 toc = "%d %d %d " % (disc.firstTrackNum, disc.lastTrackNum, disc.sectors)
1465 toc = toc + ' '.join( map(lambda x: str(x[0]), disc.getTracks()) )
1466
1467 params.append( ('toc', toc) )
1468
1469 encodedStr = urllib.urlencode(params)
1470 self._ws.post('release', '', encodedStr)
1471
1473 selected = filter(lambda x: x[1] == True, tagMap.items())
1474 return map(lambda x: x[0], selected)
1475
1477 """Remove (x, None) tuples and encode (x, str/unicode) to utf-8."""
1478 ret = [ ]
1479 for p in params:
1480 if isinstance(p[1], (str, unicode)):
1481 ret.append( (p[0], p[1].encode('utf-8')) )
1482 elif p[1] is not None:
1483 ret.append(p)
1484
1485 return ret
1486
1488 """Check if the query parameter collides with other parameters."""
1489 tmp = [ ]
1490 for name, value in params:
1491 if value is not None and name not in ('offset', 'limit'):
1492 tmp.append(name)
1493
1494 if 'query' in tmp and len(tmp) > 1:
1495 return False
1496 else:
1497 return True
1498
1499 if __name__ == '__main__':
1500 import doctest
1501 doctest.testmod()
1502
1503
1504