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 SERVICE_RESOLUTION_PATH = "/check/"
118
119 - def __init__(self, root, username=None, password=None, token=None):
120 """
121 Constructor
122 ===========
123
124 Construct a connection to a webservice::
125
126 url = "http://www.flymine.org/query/service"
127
128 # An unauthenticated connection - access to all public data
129 service = Service(url)
130
131 # An authenticated connection - access to private and public data
132 service = Service(url, token="ABC123456")
133
134
135 @param root: the root url of the webservice (required)
136 @param username: your login name (optional)
137 @param password: your password (required if a username is given)
138 @param token: your API access token(optional - used in preference to username and password)
139
140 @raise ServiceError: if the version cannot be fetched and parsed
141 @raise ValueError: if a username is supplied, but no password
142
143 There are two alternative authentication systems supported by InterMine
144 webservices. The first is username and password authentication, which
145 is supported by all webservices. Newer webservices (version 6+)
146 also support API access token authentication, which is the recommended
147 system to use. Token access is more secure as you will never have
148 to transmit your username or password, and the token can be easily changed
149 or disabled without changing your webapp login details.
150
151 """
152 o = urlparse(root)
153 if not o.scheme: root = "http://" + root
154 if not root.endswith("/service"): root = root + "/service"
155
156 self.root = root
157 self._templates = None
158 self._model = None
159 self._version = None
160 self._release = None
161 self._list_manager = ListManager(self)
162 self.__missing_method_name = None
163 if token:
164 self.opener = InterMineURLOpener(token=token)
165 elif username:
166 if token:
167 raise ValueError("Both username and token credentials supplied")
168
169 if not password:
170 raise ValueError("Username given, but no password supplied")
171
172 self.opener = InterMineURLOpener((username, password))
173 else:
174 self.opener = InterMineURLOpener()
175
176 try:
177 self.version
178 except WebserviceError, e:
179 raise ServiceError("Could not validate service - is the root url (%s) correct? %s" % (root, e))
180
181 if token and self.version < 6:
182 raise ServiceError("This service does not support API access token authentication")
183
184
185 self.query = self.new_query
186
187
188
189
190 LIST_MANAGER_METHODS = frozenset(["get_list", "get_all_lists", "get_all_list_names",
191 "create_list", "get_list_count", "delete_lists", "l"])
192
195
201
207
208 @property
210 """
211 Returns the webservice version
212 ==============================
213
214 The version specifies what capabilities a
215 specific webservice provides. The most current
216 version is 3
217
218 may raise ServiceError: if the version cannot be fetched
219
220 @rtype: int
221 """
222 if self._version is None:
223 try:
224 url = self.root + self.VERSION_PATH
225 self._version = int(self.opener.open(url).read())
226 except ValueError, e:
227 raise ServiceError("Could not parse a valid webservice version: " + str(e))
228 return self._version
229
234
235 @property
237 """
238 Returns the datawarehouse release
239 =================================
240
241 Service.release S{->} string
242
243 The release is an arbitrary string used to distinguish
244 releases of the datawarehouse. This usually coincides
245 with updates to the data contained within. While a string,
246 releases usually sort in ascending order of recentness
247 (eg: "release-26", "release-27", "release-28"). They can also
248 have less machine readable meanings (eg: "beta")
249
250 @rtype: string
251 """
252 if self._release is None:
253 self._release = urllib.urlopen(self.root + self.RELEASE_PATH).read()
254 return self._release
255
257 """
258 Construct a new Query object for the given webservice
259 =====================================================
260
261 This is the standard method for instantiating new Query
262 objects. Queries require access to the data model, as well
263 as the service itself, so it is easiest to access them through
264 this factory method.
265
266 @return: L{intermine.query.Query}
267 """
268 return Query(self.model, self, root=root)
269
271 """
272 Returns a template of the given name
273 ====================================
274
275 Tries to retrieve a template of the given name
276 from the webservice. If you are trying to fetch
277 a private template (ie. one you made yourself
278 and is not available to others) then you may need to authenticate
279
280 @see: L{intermine.webservice.Service.__init__}
281
282 @param name: the template's name
283 @type name: string
284
285 @raise ServiceError: if the template does not exist
286 @raise QueryParseError: if the template cannot be parsed
287
288 @return: L{intermine.query.Template}
289 """
290 try:
291 t = self.templates[name]
292 except KeyError:
293 raise ServiceError("There is no template called '"
294 + name + "' at this service")
295 if not isinstance(t, Template):
296 t = Template.from_xml(t, self.model, self)
297 self.templates[name] = t
298 return t
299
300 @property
302 """
303 The dictionary of templates from the webservice
304 ===============================================
305
306 Service.templates S{->} dict(intermine.query.Template|string)
307
308 For efficiency's sake, Templates are not parsed until
309 they are required, and until then they are stored as XML
310 strings. It is recommended that in most cases you would want
311 to use L{Service.get_template}.
312
313 You can use this property however to test for template existence though::
314
315 if name in service.templates:
316 template = service.get_template(name)
317
318 @rtype: dict
319
320 """
321 if self._templates is None:
322 sock = self.opener.open(self.root + self.TEMPLATES_PATH)
323 dom = minidom.parse(sock)
324 sock.close()
325 templates = {}
326 for e in dom.getElementsByTagName('template'):
327 name = e.getAttribute('name')
328 if name in templates:
329 raise ServiceError("Two templates with same name: " + name)
330 else:
331 templates[name] = e.toxml()
332 self._templates = templates
333 return self._templates
334
335 @property
337 """
338 The data model for the webservice you are querying
339 ==================================================
340
341 Service.model S{->} L{intermine.model.Model}
342
343 This is used when constructing queries to provide them
344 with information on the structure of the data model
345 they are accessing. You are very unlikely to want to
346 access this object directly.
347
348 raises ModelParseError: if the model cannot be read
349
350 @rtype: L{intermine.model.Model}
351
352 """
353 if self._model is None:
354 model_url = self.root + self.MODEL_PATH
355 self._model = Model(model_url, self)
356 return self._model
357
358 - def get_results(self, path, params, rowformat, view, cld=None):
359 """
360 Return an Iterator over the rows of the results
361 ===============================================
362
363 This method is called internally by the query objects
364 when they are called to get results. You will not
365 normally need to call it directly
366
367 @param path: The resource path (eg: "/query/results")
368 @type path: string
369 @param params: The query parameters for this request as a dictionary
370 @type params: dict
371 @param rowformat: One of "rr", "dict", "list", "tsv", "csv", "jsonrows", "jsonobjects"
372 @type rowformat: string
373 @param view: The output columns
374 @type view: list
375
376 @raise WebserviceError: for failed requests
377
378 @return: L{intermine.webservice.ResultIterator}
379 """
380 return ResultIterator(self.root, path, params, rowformat, view, self.opener, cld)
381
383 """
384 An object used to represent result records as returned in jsonobjects format
385 ============================================================================
386
387 These objects are backed by a row of data and the class descriptor that
388 describes the object. They allow access in standard object style:
389
390 >>> for gene in query.results():
391 ... print gene.symbol
392 ... print map(lambda x: x.name, gene.pathways)
393
394 """
395
397 self._data = data
398 self._cld = cld
399 self._attr_cache = {}
400
402 if name in self._attr_cache:
403 return self._attr_cache[name]
404
405 fld = self._cld.get_field(name)
406 attr = None
407 if isinstance(fld, Attribute):
408 if name in self._data:
409 attr = self._data[name]
410 elif isinstance(fld, Collection):
411 if name in self._data:
412 attr = map(lambda x: ResultObject(x, fld.type_class), self._data[name])
413 else:
414 attr = []
415 elif isinstance(fld, Reference):
416 if name in self._data:
417 attr = ResultObject(self._data[name], fld.type_class)
418 else:
419 raise WebserviceError("Inconsistent model - This should never happen")
420 self._attr_cache[name] = attr
421 return attr
422
425 """
426 An object for representing a row of data received back from the server.
427 =======================================================================
428
429 ResultRows provide access to the fields of the row through index lookup. However,
430 for convenience both list indexes and dictionary keys can be used. So the
431 following all work:
432
433 >>> # view is "Gene.symbol", "Gene.organism.name"
434 >>> row["symbol"]
435 >>> row["Gene.symbol"]
436 >>> row[0]
437 >>> row[:1]
438
439 """
440
442 self.data = data
443 self.views = views
444 self.index_map = None
445
447 """Return the number of cells in this row"""
448 return len(self.data)
449
451 """Return the list view of the row, so each cell can be processed"""
452 return self.to_l()
453
455 if isinstance(key, int):
456 return self.data[key]["value"]
457 elif isinstance(key, slice):
458 vals = map(lambda x: x["value"], self.data[key])
459 return vals
460 else:
461 index = self._get_index_for(key)
462 return self.data[index]["value"]
463
465 if self.index_map is None:
466 self.index_map = {}
467 for i in range(len(self.views)):
468 view = self.views[i]
469 headless_view = re.sub("^[^.]+.", "", view)
470 self.index_map[view] = i
471 self.index_map[headless_view] = i
472
473 return self.index_map[key]
474
476 root = re.sub("\..*$", "", self.views[0])
477 parts = [root + ":"]
478 for view in self.views:
479 short_form = re.sub("^[^.]+.", "", view)
480 value = self[view]
481 parts.append(short_form + "=" + str(value))
482 return " ".join(parts)
483
485 """Return a list view of this row"""
486 return map(lambda x: x["value"], self.data)
487
489 """Return a dictionary view of this row"""
490 d = {}
491 for view in self.views:
492 d[view] = self[view]
493
494 return d
495
497 """
498 A facade over the internal iterator object
499 ==========================================
500
501 These objects handle the iteration over results
502 in the formats requested by the user. They are responsible
503 for generating an appropriate parser,
504 connecting the parser to the results, and delegating
505 iteration appropriately.
506 """
507
508 PARSED_FORMATS = frozenset(["rr", "list", "dict"])
509 STRING_FORMATS = frozenset(["tsv", "csv", "count"])
510 JSON_FORMATS = frozenset(["jsonrows", "jsonobjects"])
511 ROW_FORMATS = PARSED_FORMATS | STRING_FORMATS | JSON_FORMATS
512
513 - def __init__(self, root, path, params, rowformat, view, opener, cld=None):
514 """
515 Constructor
516 ===========
517
518 Services are responsible for getting result iterators. You will
519 not need to create one manually.
520
521 @param root: The root path (eg: "http://www.flymine.org/query/service")
522 @type root: string
523 @param path: The resource path (eg: "/query/results")
524 @type path: string
525 @param params: The query parameters for this request
526 @type params: dict
527 @param rowformat: One of "rr", "object", "count", "dict", "list", "tsv", "csv", "jsonrows", "jsonobjects"
528 @type rowformat: string
529 @param view: The output columns
530 @type view: list
531 @param opener: A url opener (user-agent)
532 @type opener: urllib.URLopener
533
534 @raise ValueError: if the row format is incorrect
535 @raise WebserviceError: if the request is unsuccessful
536 """
537 if rowformat.startswith("object"):
538 rowformat = "jsonobjects"
539 if rowformat not in self.ROW_FORMATS:
540 raise ValueError("'%s' is not one of the valid row formats (%s)" % (rowformat, repr(list(self.ROW_FORMATS))))
541
542 if rowformat in self.PARSED_FORMATS:
543 params.update({"format" : "jsonrows"})
544 else:
545 params.update({"format" : rowformat})
546
547 url = root + path
548 data = urllib.urlencode(params)
549 con = opener.open(url, data)
550 self.reader = {
551 "tsv" : lambda: FlatFileIterator(con, EchoParser()),
552 "csv" : lambda: FlatFileIterator(con, EchoParser()),
553 "count" : lambda: FlatFileIterator(con, EchoParser()),
554 "list" : lambda: JSONIterator(con, ListValueParser()),
555 "rr" : lambda: JSONIterator(con, ResultRowParser(view)),
556 "dict" : lambda: JSONIterator(con, DictValueParser(view)),
557 "jsonobjects" : lambda: JSONIterator(con, ResultObjParser(cld)),
558 "jsonrows" : lambda: JSONIterator(con, EchoParser())
559 }.get(rowformat)()
560
562 """
563 Return an iterator over the results
564 ===================================
565
566 Returns the internal iterator object.
567 """
568 return self.reader
569
571 """
572 Returns the next row, in the appropriate format
573
574 @rtype: whatever the rowformat was determined to be
575 """
576 return self.reader.next()
577
579 """
580 An iterator for handling results returned as a flat file (TSV/CSV).
581 ===================================================================
582
583 This iterator can be used as the sub iterator in a ResultIterator
584 """
585
586 - def __init__(self, connection, parser):
587 """
588 Constructor
589 ===========
590
591 @param connection: The source of data
592 @type connection: socket.socket
593 @param parser: a handler for each row of data
594 @type parser: Parser
595 """
596 self.connection = connection
597 self.parser = parser
598
601
603 """Return a parsed line of data"""
604 line = self.connection.next().strip()
605 if line.startswith("[ERROR]"):
606 raise WebserviceError(line)
607 return self.parser.parse(line)
608
610 """
611 An iterator for handling results returned in the JSONRows format
612 ================================================================
613
614 This iterator can be used as the sub iterator in a ResultIterator
615 """
616
617 - def __init__(self, connection, parser):
618 """
619 Constructor
620 ===========
621
622 @param connection: The source of data
623 @type connection: socket.socket
624 @param parser: a handler for each row of data
625 @type parser: Parser
626 """
627 self.connection = connection
628 self.parser = parser
629 self.header = ""
630 self.footer = ""
631 self.parse_header()
632
635
639
641 """Reads out the header information from the connection"""
642 try:
643 line = self.connection.next().strip()
644 self.header += line
645 if not line.endswith('"results":['):
646 self.parse_header()
647 except StopIteration:
648 raise WebserviceError("The connection returned a bad header" + self.header)
649
651 """
652 Perform status checks
653 =====================
654
655 The footer containts information as to whether the result
656 set was successfully transferred in its entirety. This
657 method makes sure we don't silently accept an
658 incomplete result set.
659
660 @raise WebserviceError: if the footer indicates there was an error
661 """
662 container = self.header + self.footer
663 info = None
664 try:
665 info = json.loads(container)
666 except:
667 raise WebserviceError("Error parsing JSON container: " + container)
668
669 if not info["wasSuccessful"]:
670 raise WebserviceError(info["statusCode"], info["error"])
671
673 """
674 Reads the connection to get the next row, and sends it to the parser
675
676 @raise WebserviceError: if the connection is interrupted
677 """
678 next_row = None
679 try:
680 line = self.connection.next()
681 if line.startswith("]"):
682 self.footer += line;
683 for otherline in self.connection:
684 self.footer += line
685 self.check_return_status()
686 else:
687 line = line.strip().strip(',')
688 if len(line)> 0:
689 try:
690 row = json.loads(line)
691 except json.decoder.JSONDecodeError, e:
692 raise WebserviceError("Error parsing line from results: '" + line + "' - " + str(e))
693 next_row = self.parser.parse(row)
694 except StopIteration:
695 raise WebserviceError("Connection interrupted")
696
697 if next_row is None:
698 raise StopIteration
699 else:
700 return next_row
701
703 """
704 Base class for result line parsers
705 ==================================
706
707 Sub-class this class to gain a default constructor
708
709 """
710
712 """
713 Constructor
714 ===========
715
716 @param view: the list of output columns (default: [])
717 @type view: list
718 """
719 self.view = view
720
722 """
723 Abstract method - implementations must provide behaviour
724
725 @param data: a line of data
726 """
727 raise UnimplementedError
728
730 """
731 A result parser that echoes its input
732 =====================================
733
734 Use for parsing situations when you don't
735 actually want to change the data
736 """
737
739 """
740 Most basic parser - just returns the fed in data structure
741
742 @param data: the data from the result set
743 """
744 return data
745
747 """
748 A result parser that produces lists
749 ===================================
750
751 Parses jsonrow formatted rows into lists
752 of values.
753 """
754
755
757 """
758 Parse a row of JSON results into a list
759
760 @param row: a row of data from a result set
761 @type row: a JSON string
762
763 @rtype: list
764 """
765 return [cell.get("value") for cell in row]
766
768 """
769 A result parser that produces dictionaries
770 ==========================================
771
772 Parses jsonrow formatted rows into dictionaries
773 where the key is the view string for the cell,
774 and the value is the contents of the returned cell.
775 """
776
778 """
779 Parse a row of JSON results into a dictionary
780
781 @param row: a row of data from a result set
782 @type row: a JSON string
783
784 @rtype: dict
785 """
786 pairs = zip(self.view, row)
787 return_dict = {}
788 for view, cell in pairs:
789 return_dict[view] = cell.get("value")
790 return return_dict
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 Parse a row of JSON results into a ResultRow
806
807 @param row: a row of data from a result set
808 @type row: a JSON string
809
810 @rtype: ResultRow
811 """
812 rr = ResultRow(row, self.view)
813 return rr
814
816 """
817 A result parser that produces ResultRow objects, which support both index and key access
818 ========================================================================================
819
820 Parses jsonrow formatted rows into ResultRows,
821 which supports key access by list indices (based on the
822 selected view) as well as lookup by view name (based
823 on the selected view value).
824 """
825
827 """
828 Constructor
829 ===========
830
831 @param cld: the class of object this result object represents
832 @type cld: intermine.model.Class
833 """
834 self.cld = cld
835
837 """
838 Parse a row of JSON results into a ResultRow
839
840 @param row: a row of data from a result set
841 @type row: a JSON string
842
843 @rtype: ResultObject
844 """
845 ro = ResultObject(row, self.cld)
846 return ro
847
849 """
850 Specific implementation of urllib.FancyURLOpener for this client
851 ================================================================
852
853 Provides user agent and authentication headers, and handling of errors
854 """
855 version = "InterMine-Python-Client-0.96.00"
856
857 - def __init__(self, credentials=None, token=None):
858 """
859 Constructor
860 ===========
861
862 InterMineURLOpener((username, password)) S{->} InterMineURLOpener
863
864 Return a new url-opener with the appropriate credentials
865 """
866 urllib.FancyURLopener.__init__(self)
867 self.token = token
868 self.plain_post_header = {
869 "Content-Type": "text/plain; charset=utf-8",
870 "UserAgent": Service.USER_AGENT
871 }
872 if credentials and len(credentials) == 2:
873 base64string = base64.encodestring('%s:%s' % credentials)[:-1]
874 self.addheader("Authorization", base64string)
875 self.plain_post_header["Authorization"] = base64string
876 self.using_authentication = True
877 else:
878 self.using_authentication = False
879
880 - def post_plain_text(self, url, body):
881 url = self.prepare_url(url)
882 o = urlparse(url)
883 con = httplib.HTTPConnection(o.hostname, o.port)
884 con.request('POST', url, body, self.plain_post_header)
885 resp = con.getresponse()
886 content = resp.read()
887 con.close()
888 if resp.status != 200:
889 raise WebserviceError(resp.status, resp.reason, content)
890 return content
891
892 - def open(self, url, data=None):
893 url = self.prepare_url(url)
894 return urllib.FancyURLopener.open(self, url, data)
895
897 if self.token:
898 token_param = "token=" + self.token
899 o = urlparse(url)
900 if o.query:
901 url += "&" + token_param
902 else:
903 url += "?" + token_param
904
905 return url
906
908 url = self.prepare_url(url)
909 o = urlparse(url)
910 con = httplib.HTTPConnection(o.hostname, o.port)
911 con.request('DELETE', url, None, self.plain_post_header)
912 resp = con.getresponse()
913 content = resp.read()
914 con.close()
915 if resp.status != 200:
916 raise WebserviceError(resp.status, resp.reason, content)
917 return content
918
920 """Re-implementation of http_error_default, with content now supplied by default"""
921 content = fp.read()
922 fp.close()
923 raise WebserviceError(errcode, errmsg, content)
924
925 - def http_error_400(self, url, fp, errcode, errmsg, headers, data=None):
926 """
927 Handle 400 HTTP errors, attempting to return informative error messages
928 =======================================================================
929
930 400 errors indicate that something about our request was incorrect
931
932 @raise WebserviceError: in all circumstances
933
934 """
935 content = fp.read()
936 fp.close()
937 raise WebserviceError("There was a problem with our request", errcode, errmsg, content)
938
939 - def http_error_401(self, url, fp, errcode, errmsg, headers, data=None):
940 """
941 Handle 401 HTTP errors, attempting to return informative error messages
942 =======================================================================
943
944 401 errors indicate we don't have sufficient permission for the resource
945 we requested - usually a list or a tempate
946
947 @raise WebserviceError: in all circumstances
948
949 """
950 content = fp.read()
951 fp.close()
952 if self.using_authentication:
953 raise WebserviceError("Insufficient permissions", errcode, errmsg, content)
954 else:
955 raise WebserviceError("No permissions - not logged in", errcode, errmsg, content)
956
957 - def http_error_404(self, url, fp, errcode, errmsg, headers, data=None):
958 """
959 Handle 404 HTTP errors, attempting to return informative error messages
960 =======================================================================
961
962 404 errors indicate that the requested resource does not exist - usually
963 a template that is not longer available.
964
965 @raise WebserviceError: in all circumstances
966
967 """
968 content = fp.read()
969 fp.close()
970 raise WebserviceError("Missing resource", errcode, errmsg, content)
971 - def http_error_500(self, url, fp, errcode, errmsg, headers, data=None):
972 """
973 Handle 500 HTTP errors, attempting to return informative error messages
974 =======================================================================
975
976 500 errors indicate that the server borked during the request - ie: it wasn't
977 our fault.
978
979 @raise WebserviceError: in all circumstances
980
981 """
982 content = fp.read()
983 fp.close()
984 raise WebserviceError("Internal server error", errcode, errmsg, content)
985
988
990 """Errors in the creation and use of the Service object"""
991 pass
993 """Errors from interaction with the webservice"""
994 pass
995