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
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
415
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
476 return _createParameters(self._params)
477
478
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
527 return _createParameters(self._params)
528
529
531 """A filter for the user collection."""
532
534 """Constructor.
535
536 @param name: a unicode string containing a MusicBrainz user name
537 """
538 self._name = name
539
541 if self._name is not None:
542 return [ ('name', self._name.encode('utf-8')) ]
543 else:
544 return [ ]
545
546
548 """An interface implemented by include tag generators."""
551
552
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
597
598
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
621
622 if labels and not releaseEvents:
623 self._includes['release-events'] = True
624
627
628
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
647
648
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
659
660
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
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
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
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
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
909 result = self._getFromWebService('label', '', filter=filter)
910 return result.getLabelResults()
911
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
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
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
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
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
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
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
1101
1102
1136
1137
1139 selected = filter(lambda x: x[1] == True, tagMap.items())
1140 return map(lambda x: x[0], selected)
1141
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
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
1170