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 .query import Query, Template
18 from .model import Model, Attribute, Reference, Collection
19 from .util import ReadableException
20 from .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 QUERY_PATH = '/query/results'
103 QUERY_LIST_UPLOAD_PATH = '/query/tolist/json'
104 QUERY_LIST_APPEND_PATH = '/query/append/tolist/json'
105 MODEL_PATH = '/model'
106 TEMPLATES_PATH = '/templates/xml'
107 TEMPLATEQUERY_PATH = '/template/results'
108 VERSION_PATH = '/version'
109 USER_AGENT = 'WebserviceInterMinePerlAPIClient'
110 LIST_PATH = '/lists/json'
111 LIST_CREATION_PATH = '/lists/json'
112 LIST_RENAME_PATH = '/lists/rename/json'
113 LIST_APPENDING_PATH = '/lists/append/json'
114 SAVEDQUERY_PATH = '/savedqueries/xml'
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 correct? " + str(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 "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 not in self.ROW_FORMATS:
521 raise ValueError("'" + rowformat + "' is not a valid row format:" + self.ROW_FORMATS)
522
523 if rowformat in self.PARSED_FORMATS:
524 params.update({"format" : "jsonrows"})
525 else:
526 params.update({"format" : rowformat})
527
528 url = root + path
529 data = urllib.urlencode(params)
530 con = opener.open(url, data)
531 self.reader = {
532 "tsv" : lambda: FlatFileIterator(con, EchoParser()),
533 "csv" : lambda: FlatFileIterator(con, EchoParser()),
534 "count" : lambda: FlatFileIterator(con, EchoParser()),
535 "list" : lambda: JSONIterator(con, ListValueParser()),
536 "rr" : lambda: JSONIterator(con, ResultRowParser(view)),
537 "dict" : lambda: JSONIterator(con, DictValueParser(view)),
538 "jsonobjects" : lambda: JSONIterator(con, ResultObjParser(cld)),
539 "jsonrows" : lambda: JSONIterator(con, EchoParser())
540 }.get(rowformat)()
541
544
546 """
547 Returns the next row, in the appropriate format
548
549 @rtype: whatever the rowformat was determined to be
550 """
551 return self.reader.next()
552
554 """
555 An iterator for handling results returned as a flat file (TSV/CSV).
556 ===================================================================
557
558 This iterator can be used as the sub iterator in a ResultIterator
559 """
560
561 - def __init__(self, connection, parser):
562 """
563 Constructor
564 ===========
565
566 @param connection: The source of data
567 @type connection: socket.socket
568 @param parser: a handler for each row of data
569 @type parser: Parser
570 """
571 self.connection = connection
572 self.parser = parser
573
576
578 """Return a parsed line of data"""
579 line = self.connection.next().strip()
580 if line.startswith("[ERROR]"):
581 raise WebserviceError(line)
582 return self.parser.parse(line)
583
585 """
586 An iterator for handling results returned in the JSONRows format
587 ================================================================
588
589 This iterator can be used as the sub iterator in a ResultIterator
590 """
591
592 - def __init__(self, connection, parser):
593 """
594 Constructor
595 ===========
596
597 @param connection: The source of data
598 @type connection: socket.socket
599 @param parser: a handler for each row of data
600 @type parser: Parser
601 """
602 self.connection = connection
603 self.parser = parser
604 self.header = ""
605 self.footer = ""
606 self.parse_header()
607
610
614
616 """Reads out the header information from the connection"""
617 try:
618 line = self.connection.next().strip()
619 self.header += line
620 if not line.endswith('"results":['):
621 self.parse_header()
622 except StopIteration:
623 raise WebserviceError("The connection returned a bad header" + self.header)
624
626 """
627 Perform status checks
628 =====================
629
630 The footer containts information as to whether the result
631 set was successfully transferred in its entirety. This
632 method makes sure we don't silently accept an
633 incomplete result set.
634
635 @raise WebserviceError: if the footer indicates there was an error
636 """
637 container = self.header + self.footer
638 info = None
639 try:
640 info = json.loads(container)
641 except:
642 raise WebserviceError("Error parsing JSON container: " + container)
643
644 if not info["wasSuccessful"]:
645 raise WebserviceError(info["statusCode"], info["error"])
646
648 """
649 Reads the connection to get the next row, and sends it to the parser
650
651 @raise WebserviceError: if the connection is interrupted
652 """
653 next_row = None
654 try:
655 line = self.connection.next()
656 if line.startswith("]"):
657 self.footer += line;
658 for otherline in self.connection:
659 self.footer += line
660 self.check_return_status()
661 else:
662 line = line.strip().strip(',')
663 if len(line)> 0:
664 try:
665 row = json.loads(line)
666 except json.decoder.JSONDecodeError, e:
667 raise WebserviceError("Error parsing line from results: '" + line + "' - " + str(e))
668 next_row = self.parser.parse(row)
669 except StopIteration:
670 raise WebserviceError("Connection interrupted")
671
672 if next_row is None:
673 raise StopIteration
674 else:
675 return next_row
676
678 """
679 Base class for result line parsers
680 ==================================
681
682 Sub-class this class to gain a default constructor
683
684 """
685
687 """
688 Constructor
689 ===========
690
691 @param view: the list of output columns (default: [])
692 @type view: list
693 """
694 self.view = view
695
697 """
698 Abstract method - implementations must provide behaviour
699
700 @param data: a line of data
701 """
702 raise UnimplementedError
703
705 """
706 A result parser that echoes its input
707 =====================================
708
709 Use for parsing situations when you don't
710 actually want to change the data
711 """
712
714 """
715 Most basic parser - just returns the fed in data structure
716
717 @param data: the data from the result set
718 """
719 return data
720
722 """
723 A result parser that produces lists
724 ===================================
725
726 Parses jsonrow formatted rows into lists
727 of values.
728 """
729
730
732 """
733 Parse a row of JSON results into a list
734
735 @param row: a row of data from a result set
736 @type row: a JSON string
737
738 @rtype: list
739 """
740 return [cell.get("value") for cell in row]
741
743 """
744 A result parser that produces dictionaries
745 ==========================================
746
747 Parses jsonrow formatted rows into dictionaries
748 where the key is the view string for the cell,
749 and the value is the contents of the returned cell.
750 """
751
753 """
754 Parse a row of JSON results into a dictionary
755
756 @param row: a row of data from a result set
757 @type row: a JSON string
758
759 @rtype: dict
760 """
761 pairs = zip(self.view, row)
762 return_dict = {}
763 for view, cell in pairs:
764 return_dict[view] = cell.get("value")
765 return return_dict
766
768 """
769 A result parser that produces ResultRow objects, which support both index and key access
770 ========================================================================================
771
772 Parses jsonrow formatted rows into ResultRows,
773 which supports key access by list indices (based on the
774 selected view) as well as lookup by view name (based
775 on the selected view value).
776 """
777
779 """
780 Parse a row of JSON results into a ResultRow
781
782 @param row: a row of data from a result set
783 @type row: a JSON string
784
785 @rtype: ResultRow
786 """
787 rr = ResultRow(row, self.view)
788 return rr
789
791 """
792 A result parser that produces ResultRow objects, which support both index and key access
793 ========================================================================================
794
795 Parses jsonrow formatted rows into ResultRows,
796 which supports key access by list indices (based on the
797 selected view) as well as lookup by view name (based
798 on the selected view value).
799 """
800
802 """
803 Constructor
804 ===========
805
806 @param cld: the class of object this result object represents
807 @type cld: intermine.model.Class
808 """
809 self.cld = cld
810
812 """
813 Parse a row of JSON results into a ResultRow
814
815 @param row: a row of data from a result set
816 @type row: a JSON string
817
818 @rtype: ResultObject
819 """
820 ro = ResultObject(row, self.cld)
821 return ro
822
824 """
825 Specific implementation of urllib.FancyURLOpener for this client
826 ================================================================
827
828 Provides user agent and authentication headers, and handling of errors
829 """
830 version = "InterMine-Python-Client-0.96.00"
831
832 - def __init__(self, credentials=None, token=None):
833 """
834 Constructor
835 ===========
836
837 InterMineURLOpener((username, password)) S{->} InterMineURLOpener
838
839 Return a new url-opener with the appropriate credentials
840 """
841 urllib.FancyURLopener.__init__(self)
842 self.token = token
843 self.plain_post_header = {
844 "Content-Type": "text/plain; charset=utf-8",
845 "UserAgent": Service.USER_AGENT
846 }
847 if credentials and len(credentials) == 2:
848 base64string = base64.encodestring('%s:%s' % credentials)[:-1]
849 self.addheader("Authorization", base64string)
850 self.plain_post_header["Authorization"] = base64string
851 self.using_authentication = True
852 else:
853 self.using_authentication = False
854
855 - def post_plain_text(self, url, body):
856 url = self.prepare_url(url)
857 o = urlparse(url)
858 con = httplib.HTTPConnection(o.hostname, o.port)
859 con.request('POST', url, body, self.plain_post_header)
860 resp = con.getresponse()
861 content = resp.read()
862 con.close()
863 if resp.status != 200:
864 raise WebserviceError(resp.status, resp.reason, content)
865 return content
866
867 - def open(self, url, data=None):
868 url = self.prepare_url(url)
869 return urllib.FancyURLopener.open(self, url, data)
870
872 if self.token:
873 token_param = "token=" + self.token
874 o = urlparse(url)
875 if o.query:
876 url += "&" + token_param
877 else:
878 url += "?" + token_param
879
880 return url
881
883 url = self.prepare_url(url)
884 o = urlparse(url)
885 con = httplib.HTTPConnection(o.hostname, o.port)
886 con.request('DELETE', url, None, self.plain_post_header)
887 resp = con.getresponse()
888 content = resp.read()
889 con.close()
890 if resp.status != 200:
891 raise WebserviceError(resp.status, resp.reason, content)
892 return content
893
895 """Re-implementation of http_error_default, with content now supplied by default"""
896 content = fp.read()
897 fp.close()
898 raise WebserviceError(errcode, errmsg, content)
899
900 - def http_error_400(self, url, fp, errcode, errmsg, headers, data=None):
901 """
902 Handle 400 HTTP errors, attempting to return informative error messages
903 =======================================================================
904
905 400 errors indicate that something about our request was incorrect
906
907 @raise WebserviceError: in all circumstances
908
909 """
910 content = fp.read()
911 fp.close()
912 raise WebserviceError("There was a problem with our request", errcode, errmsg, content)
913
914 - def http_error_401(self, url, fp, errcode, errmsg, headers, data=None):
915 """
916 Handle 401 HTTP errors, attempting to return informative error messages
917 =======================================================================
918
919 401 errors indicate we don't have sufficient permission for the resource
920 we requested - usually a list or a tempate
921
922 @raise WebserviceError: in all circumstances
923
924 """
925 content = fp.read()
926 fp.close()
927 if self.using_authentication:
928 raise WebserviceError("Insufficient permissions", errcode, errmsg, content)
929 else:
930 raise WebserviceError("No permissions - not logged in", errcode, errmsg, content)
931
932 - def http_error_404(self, url, fp, errcode, errmsg, headers, data=None):
933 """
934 Handle 404 HTTP errors, attempting to return informative error messages
935 =======================================================================
936
937 404 errors indicate that the requested resource does not exist - usually
938 a template that is not longer available.
939
940 @raise WebserviceError: in all circumstances
941
942 """
943 content = fp.read()
944 fp.close()
945 raise WebserviceError("Missing resource", errcode, errmsg, content)
946 - def http_error_500(self, url, fp, errcode, errmsg, headers, data=None):
947 """
948 Handle 500 HTTP errors, attempting to return informative error messages
949 =======================================================================
950
951 500 errors indicate that the server borked during the request - ie: it wasn't
952 our fault.
953
954 @raise WebserviceError: in all circumstances
955
956 """
957 content = fp.read()
958 fp.close()
959 raise WebserviceError("Internal server error", errcode, errmsg, content)
960
963
965 """Errors in the creation and use of the Service object"""
966 pass
968 """Errors from interaction with the webservice"""
969 pass
970