Package intermine :: Module webservice
[hide private]
[frames] | no frames]

Source Code for Module intermine.webservice

  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  # Use core json for 2.6+, simplejson for <=2.5 
 11  try: 
 12      import json 
 13  except ImportError: # pragma: no cover 
 14      import simplejson as json 
 15   
 16  # Local intermine imports 
 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" 
35 36 -class Service(object):
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 # Set up sugary aliases 185 self.query = self.new_query
186 187 188 # Delegated list methods 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
193 - def __getattribute__(self, name):
194 return object.__getattribute__(self, name)
195
196 - def __getattr__(self, name):
197 if name in self.LIST_MANAGER_METHODS: 198 method = getattr(self._list_manager, name) 199 return method 200 raise AttributeError("Could not find " + name)
201
202 - def __del__(self):
203 try: 204 self._list_manager.delete_temporary_lists() 205 except ReferenceError: 206 pass
207 208 @property
209 - def version(self):
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
230 - def resolve_service_path(self, variant):
231 """Resolve the path to optional services""" 232 url = self.root + self.SERVICE_RESOLUTION_PATH + variant 233 return self.opener.open(url).read()
234 235 @property
236 - def release(self):
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
256 - def new_query(self, root=None):
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
270 - def get_template(self, name):
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
301 - def templates(self):
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
336 - def model(self):
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
382 -class ResultObject(object):
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
396 - def __init__(self, data, cld):
397 self._data = data 398 self._cld = cld 399 self._attr_cache = {}
400
401 - def __getattr__(self, name):
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
423 424 -class ResultRow(object):
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
441 - def __init__(self, data, views):
442 self.data = data 443 self.views = views 444 self.index_map = None
445
446 - def __len__(self):
447 """Return the number of cells in this row""" 448 return len(self.data)
449
450 - def __iter__(self):
451 """Return the list view of the row, so each cell can be processed""" 452 return self.to_l()
453
454 - def __getitem__(self, key):
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
464 - def _get_index_for(self, key):
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
475 - def __str__(self):
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
484 - def to_l(self):
485 """Return a list view of this row""" 486 return map(lambda x: x["value"], self.data)
487
488 - def to_d(self):
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
496 -class ResultIterator(object):
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"): # Accept "object", "objects", "objectformat", etc... 538 rowformat = "jsonobjects" # these are synonymous 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
561 - def __iter__(self):
562 """ 563 Return an iterator over the results 564 =================================== 565 566 Returns the internal iterator object. 567 """ 568 return self.reader
569
570 - def next(self):
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
578 -class FlatFileIterator(object):
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
599 - def __iter__(self):
600 return self
601
602 - def next(self):
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
609 -class JSONIterator(object):
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
633 - def __iter__(self):
634 return self
635
636 - def next(self):
637 """Returns a parsed row of data""" 638 return self.get_next_row_from_connection()
639
640 - def parse_header(self):
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
650 - def check_return_status(self):
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
702 -class Parser(object):
703 """ 704 Base class for result line parsers 705 ================================== 706 707 Sub-class this class to gain a default constructor 708 709 """ 710
711 - def __init__(self, view=[]):
712 """ 713 Constructor 714 =========== 715 716 @param view: the list of output columns (default: []) 717 @type view: list 718 """ 719 self.view = view
720
721 - def parse(self, data):
722 """ 723 Abstract method - implementations must provide behaviour 724 725 @param data: a line of data 726 """ 727 raise UnimplementedError
728
729 -class EchoParser(Parser):
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
738 - def parse(self, data):
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
746 -class ListValueParser(Parser):
747 """ 748 A result parser that produces lists 749 =================================== 750 751 Parses jsonrow formatted rows into lists 752 of values. 753 """ 754 755
756 - def parse(self, row):
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
767 -class DictValueParser(Parser):
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
777 - def parse(self, row):
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
792 -class ResultRowParser(Parser):
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
803 - def parse(self, row):
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
815 -class ResultObjParser(Parser):
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
826 - def __init__(self, cld):
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
836 - def parse(self, row):
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
848 -class InterMineURLOpener(urllib.FancyURLopener):
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
896 - def prepare_url(self, url):
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
907 - def delete(self, url):
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
919 - def http_error_default(self, url, fp, errcode, errmsg, headers):
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
986 -class UnimplementedError(Exception):
987 pass
988
989 -class ServiceError(ReadableException):
990 """Errors in the creation and use of the Service object""" 991 pass
992 -class WebserviceError(IOError):
993 """Errors from interaction with the webservice""" 994 pass
995