1 from urlparse import urlunsplit, urljoin
2 from xml.dom import minidom
3 import urllib
4 from urlparse import urlparse
5 import csv
6 import base64
7 import httplib
8 import re
9
10
11 try:
12 import json
13 except ImportError:
14 import simplejson as json
15
16
17 from intermine.query import Query, Template
18 from intermine.model import Model, Attribute, Reference, Collection
19 from intermine.util import ReadableException
20 from intermine.lists.listmanager import ListManager
21
22 """
23 Webservice Interaction Routines for InterMine Webservices
24 =========================================================
25
26 Classes for dealing with communication with an InterMine
27 RESTful webservice.
28
29 """
30
31 __author__ = "Alex Kalderimis"
32 __organization__ = "InterMine"
33 __license__ = "LGPL"
34 __contact__ = "dev@intermine.org"
37 """
38 A class representing connections to different InterMine WebServices
39 ===================================================================
40
41 The intermine.webservice.Service class is the main interface for the user.
42 It will provide access to queries and templates, as well as doing the
43 background task of fetching the data model, and actually requesting
44 the query results.
45
46 SYNOPSIS
47 --------
48
49 example::
50
51 from intermine.webservice import Service
52 service = Service("http://www.flymine.org/query/service")
53
54 template = service.get_template("Gene_Pathways")
55 for row in template.results(A={"value":"zen"}):
56 do_something_with(row)
57 ...
58
59 query = service.new_query()
60 query.add_view("Gene.symbol", "Gene.pathway.name")
61 query.add_constraint("Gene", "LOOKUP", "zen")
62 for row in query.results():
63 do_something_with(row)
64 ...
65
66 new_list = service.create_list("some/file/with.ids", "Gene")
67 list_on_server = service.get_list("On server")
68 in_both = new_list & list_on_server
69 in_both.name = "Intersection of these lists"
70 for row in in_both.to_attribute_query().results():
71 do_something_with(row)
72 ...
73
74 OVERVIEW
75 --------
76 The two methods the user will be most concerned with are:
77 - L{Service.new_query}: constructs a new query to query a service with
78 - L{Service.get_template}: gets a template from the service
79 - L{ListManager.create_list}: creates a new list on the service
80
81 For list management information, see L{ListManager}.
82
83 TERMINOLOGY
84 -----------
85 X{Query} is the term for an arbitrarily complex structured request for
86 data from the webservice. The user is responsible for specifying the
87 structure that determines what records are returned, and what information
88 about each record is provided.
89
90 X{Template} is the term for a predefined "Query", ie: one that has been
91 written and saved on the webservice you will access. The definition
92 of the query is already done, but the user may want to specify the
93 values of the constraints that exist on the template. Templates are accessed
94 by name, and while you can easily introspect templates, it is assumed
95 you know what they do when you use them
96
97 X{List} is a saved result set containing a set of objects previously identified
98 in the database. Lists can be created and managed using this client library.
99
100 @see: L{intermine.query}
101 """
102 USER_AGENT = 'WebserviceInterMinePerlAPIClient'
103 QUERY_PATH = '/query/results'
104 QUERY_LIST_UPLOAD_PATH = '/query/tolist/json'
105 QUERY_LIST_APPEND_PATH = '/query/append/tolist/json'
106 MODEL_PATH = '/model'
107 TEMPLATES_PATH = '/templates/xml'
108 TEMPLATEQUERY_PATH = '/template/results'
109 LIST_PATH = '/lists/json'
110 LIST_CREATION_PATH = '/lists/json'
111 LIST_RENAME_PATH = '/lists/rename/json'
112 LIST_APPENDING_PATH = '/lists/append/json'
113 SAVEDQUERY_PATH = '/savedqueries/xml'
114 VERSION_PATH = '/version/ws'
115 RELEASE_PATH = '/version/release'
116 SCHEME = 'http://'
117
118 - def __init__(self, root, username=None, password=None, token=None):
119 """
120 Constructor
121 ===========
122
123 Construct a connection to a webservice::
124
125 url = "http://www.flymine.org/query/service"
126
127 # An unauthenticated connection - access to all public data
128 service = Service(url)
129
130 # An authenticated connection - access to private and public data
131 service = Service(url, token="ABC123456")
132
133
134 @param root: the root url of the webservice (required)
135 @param username: your login name (optional)
136 @param password: your password (required if a username is given)
137 @param token: your API access token(optional - used in preference to username and password)
138
139 @raise ServiceError: if the version cannot be fetched and parsed
140 @raise ValueError: if a username is supplied, but no password
141
142 There are two alternative authentication systems supported by InterMine
143 webservices. The first is username and password authentication, which
144 is supported by all webservices. Newer webservices (version 6+)
145 also support API access token authentication, which is the recommended
146 system to use. Token access is more secure as you will never have
147 to transmit your username or password, and the token can be easily changed
148 or disabled without changing your webapp login details.
149
150 """
151 o = urlparse(root)
152 if not o.scheme: root = "http://" + root
153 if not root.endswith("/service"): root = root + "/service"
154
155 self.root = root
156 self._templates = None
157 self._model = None
158 self._version = None
159 self._release = None
160 self._list_manager = ListManager(self)
161 self.__missing_method_name = None
162 if token:
163 self.opener = InterMineURLOpener(token=token)
164 elif username:
165 if token:
166 raise ValueError("Both username and token credentials supplied")
167
168 if not password:
169 raise ValueError("Username given, but no password supplied")
170
171 self.opener = InterMineURLOpener((username, password))
172 else:
173 self.opener = InterMineURLOpener()
174
175 try:
176 self.version
177 except WebserviceError, e:
178 raise ServiceError("Could not validate service - is the root url (%s) correct? %s" % (root, e))
179
180 if token and self.version < 6:
181 raise ServiceError("This service does not support API access token authentication")
182
183
184 self.query = self.new_query
185
186
187
188
189 LIST_MANAGER_METHODS = frozenset(["get_list", "get_all_lists", "get_all_list_names",
190 "create_list", "get_list_count", "delete_lists", "l"])
191
194
200
206
207 @property
209 """
210 Returns the webservice version
211 ==============================
212
213 The version specifies what capabilities a
214 specific webservice provides. The most current
215 version is 3
216
217 may raise ServiceError: if the version cannot be fetched
218
219 @rtype: int
220 """
221 if self._version is None:
222 try:
223 url = self.root + self.VERSION_PATH
224 self._version = int(self.opener.open(url).read())
225 except ValueError, e:
226 raise ServiceError("Could not parse a valid webservice version: " + str(e))
227 return self._version
228 @property
230 """
231 Returns the datawarehouse release
232 =================================
233
234 Service.release S{->} string
235
236 The release is an arbitrary string used to distinguish
237 releases of the datawarehouse. This usually coincides
238 with updates to the data contained within. While a string,
239 releases usually sort in ascending order of recentness
240 (eg: "release-26", "release-27", "release-28"). They can also
241 have less machine readable meanings (eg: "beta")
242
243 @rtype: string
244 """
245 if self._release is None:
246 self._release = urllib.urlopen(self.root + self.RELEASE_PATH).read()
247 return self._release
248
250 """
251 Construct a new Query object for the given webservice
252 =====================================================
253
254 This is the standard method for instantiating new Query
255 objects. Queries require access to the data model, as well
256 as the service itself, so it is easiest to access them through
257 this factory method.
258
259 @return: L{intermine.query.Query}
260 """
261 return Query(self.model, self, root=root)
262
264 """
265 Returns a template of the given name
266 ====================================
267
268 Tries to retrieve a template of the given name
269 from the webservice. If you are trying to fetch
270 a private template (ie. one you made yourself
271 and is not available to others) then you may need to authenticate
272
273 @see: L{intermine.webservice.Service.__init__}
274
275 @param name: the template's name
276 @type name: string
277
278 @raise ServiceError: if the template does not exist
279 @raise QueryParseError: if the template cannot be parsed
280
281 @return: L{intermine.query.Template}
282 """
283 try:
284 t = self.templates[name]
285 except KeyError:
286 raise ServiceError("There is no template called '"
287 + name + "' at this service")
288 if not isinstance(t, Template):
289 t = Template.from_xml(t, self.model, self)
290 self.templates[name] = t
291 return t
292
293 @property
295 """
296 The dictionary of templates from the webservice
297 ===============================================
298
299 Service.templates S{->} dict(intermine.query.Template|string)
300
301 For efficiency's sake, Templates are not parsed until
302 they are required, and until then they are stored as XML
303 strings. It is recommended that in most cases you would want
304 to use L{Service.get_template}.
305
306 You can use this property however to test for template existence though::
307
308 if name in service.templates:
309 template = service.get_template(name)
310
311 @rtype: dict
312
313 """
314 if self._templates is None:
315 sock = self.opener.open(self.root + self.TEMPLATES_PATH)
316 dom = minidom.parse(sock)
317 sock.close()
318 templates = {}
319 for e in dom.getElementsByTagName('template'):
320 name = e.getAttribute('name')
321 if name in templates:
322 raise ServiceError("Two templates with same name: " + name)
323 else:
324 templates[name] = e.toxml()
325 self._templates = templates
326 return self._templates
327
328 @property
330 """
331 The data model for the webservice you are querying
332 ==================================================
333
334 Service.model S{->} L{intermine.model.Model}
335
336 This is used when constructing queries to provide them
337 with information on the structure of the data model
338 they are accessing. You are very unlikely to want to
339 access this object directly.
340
341 raises ModelParseError: if the model cannot be read
342
343 @rtype: L{intermine.model.Model}
344
345 """
346 if self._model is None:
347 model_url = self.root + self.MODEL_PATH
348 self._model = Model(model_url, self)
349 return self._model
350
351 - def get_results(self, path, params, rowformat, view, cld=None):
352 """
353 Return an Iterator over the rows of the results
354 ===============================================
355
356 This method is called internally by the query objects
357 when they are called to get results. You will not
358 normally need to call it directly
359
360 @param path: The resource path (eg: "/query/results")
361 @type path: string
362 @param params: The query parameters for this request as a dictionary
363 @type params: dict
364 @param rowformat: One of "rr", "dict", "list", "tsv", "csv", "jsonrows", "jsonobjects"
365 @type rowformat: string
366 @param view: The output columns
367 @type view: list
368
369 @raise WebserviceError: for failed requests
370
371 @return: L{intermine.webservice.ResultIterator}
372 """
373 return ResultIterator(self.root, path, params, rowformat, view, self.opener, cld)
374
376 """
377 An object used to represent result records as returned in jsonobjects format
378 ============================================================================
379
380 These objects are backed by a row of data and the class descriptor that
381 describes the object. They allow access in standard object style:
382
383 >>> for gene in query.results():
384 ... print gene.symbol
385 ... print map(lambda x: x.name, gene.pathways)
386
387 """
388
390 self._data = data
391 self._cld = cld
392 self._attr_cache = {}
393
395 if name in self._attr_cache:
396 return self._attr_cache[name]
397
398 fld = self._cld.get_field(name)
399 attr = None
400 if isinstance(fld, Attribute):
401 if name in self._data:
402 attr = self._data[name]
403 elif isinstance(fld, Collection):
404 if name in self._data:
405 attr = map(lambda x: ResultObject(x, fld.type_class), self._data[name])
406 else:
407 attr = []
408 elif isinstance(fld, Reference):
409 if name in self._data:
410 attr = ResultObject(self._data[name], fld.type_class)
411 else:
412 raise WebserviceError("Inconsistent model - This should never happen")
413 self._attr_cache[name] = attr
414 return attr
415
418 """
419 An object for representing a row of data received back from the server.
420 =======================================================================
421
422 ResultRows provide access to the fields of the row through index lookup. However,
423 for convenience both list indexes and dictionary keys can be used. So the
424 following all work:
425
426 >>> # view is "Gene.symbol", "Gene.organism.name"
427 >>> row["symbol"]
428 >>> row["Gene.symbol"]
429 >>> row[0]
430 >>> row[:1]
431
432 """
433
435 self.data = data
436 self.views = views
437 self.index_map = None
438
440 """Return the number of cells in this row"""
441 return len(self.data)
442
444 """Return the list view of the row, so each cell can be processed"""
445 return self.to_l()
446
448 if isinstance(key, int):
449 return self.data[key]["value"]
450 elif isinstance(key, slice):
451 vals = map(lambda x: x["value"], self.data[key])
452 return vals
453 else:
454 index = self._get_index_for(key)
455 return self.data[index]["value"]
456
458 if self.index_map is None:
459 self.index_map = {}
460 for i in range(len(self.views)):
461 view = self.views[i]
462 headless_view = re.sub("^[^.]+.", "", view)
463 self.index_map[view] = i
464 self.index_map[headless_view] = i
465
466 return self.index_map[key]
467
469 root = re.sub("\..*$", "", self.views[0])
470 parts = [root + ":"]
471 for view in self.views:
472 short_form = re.sub("^[^.]+.", "", view)
473 value = self[view]
474 parts.append(short_form + "=" + str(value))
475 return " ".join(parts)
476
478 """Return a list view of this row"""
479 return map(lambda x: x["value"], self.data)
480
482 """Return a dictionary view of this row"""
483 d = {}
484 for view in self.views:
485 d[view] = self[view]
486
487 return d
488
490
491 PARSED_FORMATS = frozenset(["rr", "list", "dict"])
492 STRING_FORMATS = frozenset(["tsv", "csv", "count"])
493 JSON_FORMATS = frozenset(["jsonrows", "jsonobjects"])
494 ROW_FORMATS = PARSED_FORMATS | STRING_FORMATS | JSON_FORMATS
495
496 - def __init__(self, root, path, params, rowformat, view, opener, cld=None):
497 """
498 Constructor
499 ===========
500
501 Services are responsible for getting result iterators. You will
502 not need to create one manually.
503
504 @param root: The root path (eg: "http://www.flymine.org/query/service")
505 @type root: string
506 @param path: The resource path (eg: "/query/results")
507 @type path: string
508 @param params: The query parameters for this request
509 @type params: dict
510 @param rowformat: One of "rr", "object", "count", "dict", "list", "tsv", "csv", "jsonrows", "jsonobjects"
511 @type rowformat: string
512 @param view: The output columns
513 @type view: list
514 @param opener: A url opener (user-agent)
515 @type opener: urllib.URLopener
516
517 @raise ValueError: if the row format is incorrect
518 @raise WebserviceError: if the request is unsuccessful
519 """
520 if rowformat.startswith("object"):
521 rowformat = "jsonobjects"
522 if rowformat not in self.ROW_FORMATS:
523 raise ValueError("'%s' is not one of the valid row formats (%s)" % (rowformat, repr(list(self.ROW_FORMATS))))
524
525 if rowformat in self.PARSED_FORMATS:
526 params.update({"format" : "jsonrows"})
527 else:
528 params.update({"format" : rowformat})
529
530 url = root + path
531 data = urllib.urlencode(params)
532 con = opener.open(url, data)
533 self.reader = {
534 "tsv" : lambda: FlatFileIterator(con, EchoParser()),
535 "csv" : lambda: FlatFileIterator(con, EchoParser()),
536 "count" : lambda: FlatFileIterator(con, EchoParser()),
537 "list" : lambda: JSONIterator(con, ListValueParser()),
538 "rr" : lambda: JSONIterator(con, ResultRowParser(view)),
539 "dict" : lambda: JSONIterator(con, DictValueParser(view)),
540 "jsonobjects" : lambda: JSONIterator(con, ResultObjParser(cld)),
541 "jsonrows" : lambda: JSONIterator(con, EchoParser())
542 }.get(rowformat)()
543
546
548 """
549 Returns the next row, in the appropriate format
550
551 @rtype: whatever the rowformat was determined to be
552 """
553 return self.reader.next()
554
556 """
557 An iterator for handling results returned as a flat file (TSV/CSV).
558 ===================================================================
559
560 This iterator can be used as the sub iterator in a ResultIterator
561 """
562
563 - def __init__(self, connection, parser):
564 """
565 Constructor
566 ===========
567
568 @param connection: The source of data
569 @type connection: socket.socket
570 @param parser: a handler for each row of data
571 @type parser: Parser
572 """
573 self.connection = connection
574 self.parser = parser
575
578
580 """Return a parsed line of data"""
581 line = self.connection.next().strip()
582 if line.startswith("[ERROR]"):
583 raise WebserviceError(line)
584 return self.parser.parse(line)
585
587 """
588 An iterator for handling results returned in the JSONRows format
589 ================================================================
590
591 This iterator can be used as the sub iterator in a ResultIterator
592 """
593
594 - def __init__(self, connection, parser):
595 """
596 Constructor
597 ===========
598
599 @param connection: The source of data
600 @type connection: socket.socket
601 @param parser: a handler for each row of data
602 @type parser: Parser
603 """
604 self.connection = connection
605 self.parser = parser
606 self.header = ""
607 self.footer = ""
608 self.parse_header()
609
612
616
618 """Reads out the header information from the connection"""
619 try:
620 line = self.connection.next().strip()
621 self.header += line
622 if not line.endswith('"results":['):
623 self.parse_header()
624 except StopIteration:
625 raise WebserviceError("The connection returned a bad header" + self.header)
626
628 """
629 Perform status checks
630 =====================
631
632 The footer containts information as to whether the result
633 set was successfully transferred in its entirety. This
634 method makes sure we don't silently accept an
635 incomplete result set.
636
637 @raise WebserviceError: if the footer indicates there was an error
638 """
639 container = self.header + self.footer
640 info = None
641 try:
642 info = json.loads(container)
643 except:
644 raise WebserviceError("Error parsing JSON container: " + container)
645
646 if not info["wasSuccessful"]:
647 raise WebserviceError(info["statusCode"], info["error"])
648
650 """
651 Reads the connection to get the next row, and sends it to the parser
652
653 @raise WebserviceError: if the connection is interrupted
654 """
655 next_row = None
656 try:
657 line = self.connection.next()
658 if line.startswith("]"):
659 self.footer += line;
660 for otherline in self.connection:
661 self.footer += line
662 self.check_return_status()
663 else:
664 line = line.strip().strip(',')
665 if len(line)> 0:
666 try:
667 row = json.loads(line)
668 except json.decoder.JSONDecodeError, e:
669 raise WebserviceError("Error parsing line from results: '" + line + "' - " + str(e))
670 next_row = self.parser.parse(row)
671 except StopIteration:
672 raise WebserviceError("Connection interrupted")
673
674 if next_row is None:
675 raise StopIteration
676 else:
677 return next_row
678
680 """
681 Base class for result line parsers
682 ==================================
683
684 Sub-class this class to gain a default constructor
685
686 """
687
689 """
690 Constructor
691 ===========
692
693 @param view: the list of output columns (default: [])
694 @type view: list
695 """
696 self.view = view
697
699 """
700 Abstract method - implementations must provide behaviour
701
702 @param data: a line of data
703 """
704 raise UnimplementedError
705
707 """
708 A result parser that echoes its input
709 =====================================
710
711 Use for parsing situations when you don't
712 actually want to change the data
713 """
714
716 """
717 Most basic parser - just returns the fed in data structure
718
719 @param data: the data from the result set
720 """
721 return data
722
724 """
725 A result parser that produces lists
726 ===================================
727
728 Parses jsonrow formatted rows into lists
729 of values.
730 """
731
732
734 """
735 Parse a row of JSON results into a list
736
737 @param row: a row of data from a result set
738 @type row: a JSON string
739
740 @rtype: list
741 """
742 return [cell.get("value") for cell in row]
743
745 """
746 A result parser that produces dictionaries
747 ==========================================
748
749 Parses jsonrow formatted rows into dictionaries
750 where the key is the view string for the cell,
751 and the value is the contents of the returned cell.
752 """
753
755 """
756 Parse a row of JSON results into a dictionary
757
758 @param row: a row of data from a result set
759 @type row: a JSON string
760
761 @rtype: dict
762 """
763 pairs = zip(self.view, row)
764 return_dict = {}
765 for view, cell in pairs:
766 return_dict[view] = cell.get("value")
767 return return_dict
768
770 """
771 A result parser that produces ResultRow objects, which support both index and key access
772 ========================================================================================
773
774 Parses jsonrow formatted rows into ResultRows,
775 which supports key access by list indices (based on the
776 selected view) as well as lookup by view name (based
777 on the selected view value).
778 """
779
781 """
782 Parse a row of JSON results into a ResultRow
783
784 @param row: a row of data from a result set
785 @type row: a JSON string
786
787 @rtype: ResultRow
788 """
789 rr = ResultRow(row, self.view)
790 return rr
791
793 """
794 A result parser that produces ResultRow objects, which support both index and key access
795 ========================================================================================
796
797 Parses jsonrow formatted rows into ResultRows,
798 which supports key access by list indices (based on the
799 selected view) as well as lookup by view name (based
800 on the selected view value).
801 """
802
804 """
805 Constructor
806 ===========
807
808 @param cld: the class of object this result object represents
809 @type cld: intermine.model.Class
810 """
811 self.cld = cld
812
814 """
815 Parse a row of JSON results into a ResultRow
816
817 @param row: a row of data from a result set
818 @type row: a JSON string
819
820 @rtype: ResultObject
821 """
822 ro = ResultObject(row, self.cld)
823 return ro
824
826 """
827 Specific implementation of urllib.FancyURLOpener for this client
828 ================================================================
829
830 Provides user agent and authentication headers, and handling of errors
831 """
832 version = "InterMine-Python-Client-0.96.00"
833
834 - def __init__(self, credentials=None, token=None):
835 """
836 Constructor
837 ===========
838
839 InterMineURLOpener((username, password)) S{->} InterMineURLOpener
840
841 Return a new url-opener with the appropriate credentials
842 """
843 urllib.FancyURLopener.__init__(self)
844 self.token = token
845 self.plain_post_header = {
846 "Content-Type": "text/plain; charset=utf-8",
847 "UserAgent": Service.USER_AGENT
848 }
849 if credentials and len(credentials) == 2:
850 base64string = base64.encodestring('%s:%s' % credentials)[:-1]
851 self.addheader("Authorization", base64string)
852 self.plain_post_header["Authorization"] = base64string
853 self.using_authentication = True
854 else:
855 self.using_authentication = False
856
857 - def post_plain_text(self, url, body):
858 url = self.prepare_url(url)
859 o = urlparse(url)
860 con = httplib.HTTPConnection(o.hostname, o.port)
861 con.request('POST', url, body, self.plain_post_header)
862 resp = con.getresponse()
863 content = resp.read()
864 con.close()
865 if resp.status != 200:
866 raise WebserviceError(resp.status, resp.reason, content)
867 return content
868
869 - def open(self, url, data=None):
870 url = self.prepare_url(url)
871 return urllib.FancyURLopener.open(self, url, data)
872
874 if self.token:
875 token_param = "token=" + self.token
876 o = urlparse(url)
877 if o.query:
878 url += "&" + token_param
879 else:
880 url += "?" + token_param
881
882 return url
883
885 url = self.prepare_url(url)
886 o = urlparse(url)
887 con = httplib.HTTPConnection(o.hostname, o.port)
888 con.request('DELETE', url, None, self.plain_post_header)
889 resp = con.getresponse()
890 content = resp.read()
891 con.close()
892 if resp.status != 200:
893 raise WebserviceError(resp.status, resp.reason, content)
894 return content
895
897 """Re-implementation of http_error_default, with content now supplied by default"""
898 content = fp.read()
899 fp.close()
900 raise WebserviceError(errcode, errmsg, content)
901
902 - def http_error_400(self, url, fp, errcode, errmsg, headers, data=None):
903 """
904 Handle 400 HTTP errors, attempting to return informative error messages
905 =======================================================================
906
907 400 errors indicate that something about our request was incorrect
908
909 @raise WebserviceError: in all circumstances
910
911 """
912 content = fp.read()
913 fp.close()
914 raise WebserviceError("There was a problem with our request", errcode, errmsg, content)
915
916 - def http_error_401(self, url, fp, errcode, errmsg, headers, data=None):
917 """
918 Handle 401 HTTP errors, attempting to return informative error messages
919 =======================================================================
920
921 401 errors indicate we don't have sufficient permission for the resource
922 we requested - usually a list or a tempate
923
924 @raise WebserviceError: in all circumstances
925
926 """
927 content = fp.read()
928 fp.close()
929 if self.using_authentication:
930 raise WebserviceError("Insufficient permissions", errcode, errmsg, content)
931 else:
932 raise WebserviceError("No permissions - not logged in", errcode, errmsg, content)
933
934 - def http_error_404(self, url, fp, errcode, errmsg, headers, data=None):
935 """
936 Handle 404 HTTP errors, attempting to return informative error messages
937 =======================================================================
938
939 404 errors indicate that the requested resource does not exist - usually
940 a template that is not longer available.
941
942 @raise WebserviceError: in all circumstances
943
944 """
945 content = fp.read()
946 fp.close()
947 raise WebserviceError("Missing resource", errcode, errmsg, content)
948 - def http_error_500(self, url, fp, errcode, errmsg, headers, data=None):
949 """
950 Handle 500 HTTP errors, attempting to return informative error messages
951 =======================================================================
952
953 500 errors indicate that the server borked during the request - ie: it wasn't
954 our fault.
955
956 @raise WebserviceError: in all circumstances
957
958 """
959 content = fp.read()
960 fp.close()
961 raise WebserviceError("Internal server error", errcode, errmsg, content)
962
965
967 """Errors in the creation and use of the Service object"""
968 pass
970 """Errors from interaction with the webservice"""
971 pass
972