From f0aca3374f418eb43b5597316a2c8a22dac06568 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Sun, 9 Sep 2012 21:39:57 -0500 Subject: [PATCH] upgraded pysimplesoap to revision e054a3903c1d, version 1.06c --- VERSION | 2 +- gluon/contrib/pysimplesoap/__init__.py | 7 +- gluon/contrib/pysimplesoap/client.py | 698 +++++++++++++----------- gluon/contrib/pysimplesoap/server.py | 251 +++++---- gluon/contrib/pysimplesoap/simplexml.py | 347 ++++++++---- gluon/contrib/pysimplesoap/transport.py | 241 ++++++++ 6 files changed, 1039 insertions(+), 507 deletions(-) create mode 100644 gluon/contrib/pysimplesoap/transport.py diff --git a/VERSION b/VERSION index 6f0f9c09..aaea711b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.0.8 (2012-09-09 16:57:02) stable +Version 2.0.8 (2012-09-09 21:39:52) stable diff --git a/gluon/contrib/pysimplesoap/__init__.py b/gluon/contrib/pysimplesoap/__init__.py index 9fbc597e..6043241d 100755 --- a/gluon/contrib/pysimplesoap/__init__.py +++ b/gluon/contrib/pysimplesoap/__init__.py @@ -1,4 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -"Contributed modules" - +"PySimpleSOAP" +import client +import server +import simplexml +import transport \ No newline at end of file diff --git a/gluon/contrib/pysimplesoap/client.py b/gluon/contrib/pysimplesoap/client.py index 9100205e..4b05d3a8 100755 --- a/gluon/contrib/pysimplesoap/client.py +++ b/gluon/contrib/pysimplesoap/client.py @@ -15,26 +15,40 @@ __author__ = "Mariano Reingart (reingart@gmail.com)" __copyright__ = "Copyright (C) 2008 Mariano Reingart" __license__ = "LGPL 3.0" -__version__ = "1.02c" +__version__ = "1.07a" -import urllib -try: - import httplib2 - Http = httplib2.Http -except ImportError: - import urllib2 - class Http(): # wrapper to use when httplib2 not available - def request(self, url, method, body, headers): - f = urllib2.urlopen(urllib2.Request(url, body, headers)) - return f.info(), f.read() +TIMEOUT = 60 +import cPickle as pickle +import hashlib +import logging +import os +import tempfile +import urllib2 +from urlparse import urlsplit +from simplexml import SimpleXMLElement, TYPE_MAP, REVERSE_TYPE_MAP, OrderedDict +from transport import get_http_wrapper, set_http_wrapper, get_Http + +log = logging.getLogger(__name__) +logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.WARNING) -from simplexml import SimpleXMLElement, TYPE_MAP, OrderedDict class SoapFault(RuntimeError): def __init__(self,faultcode,faultstring): self.faultcode = faultcode self.faultstring = faultstring + RuntimeError.__init__(self, faultcode, faultstring) + + def __str__(self): + return self.__unicode__().encode("ascii", "ignore") + + def __unicode__(self): + return u'%s: %s' % (self.faultcode, self.faultstring) + + def __repr__(self): + return u"SoapFault(%s, %s)" % (repr(self.faultcode), + repr(self.faultstring)) + # soap protocol specification & namespace soap_namespaces = dict( @@ -44,45 +58,68 @@ soap_namespaces = dict( soap12="http://www.w3.org/2003/05/soap-env", ) +_USE_GLOBAL_DEFAULT = object() + class SoapClient(object): - "Simple SOAP Client (s�mil PHP)" + "Simple SOAP Client (simil PHP)" def __init__(self, location = None, action = None, namespace = None, - cert = None, trace = False, exceptions = True, proxy = None, ns=False, - soap_ns=None, wsdl = None, cache = False): - self.certssl = cert - self.keyssl = None + cert = None, trace = False, exceptions = True, proxy = None, ns=False, + soap_ns=None, wsdl = None, cache = False, cacert=None, + sessions=False, soap_server=None, timeout=_USE_GLOBAL_DEFAULT, + http_headers={} + ): + """ + :param http_headers: Additional HTTP Headers; example: {'Host': 'ipsec.example.com'} + """ + self.certssl = cert + self.keyssl = None self.location = location # server location (url) self.action = action # SOAP base action - self.namespace = namespace # message + self.namespace = namespace # message self.trace = trace # show debug messages self.exceptions = exceptions # lanzar execpiones? (Soap Faults) self.xml_request = self.xml_response = '' + self.http_headers = http_headers if not soap_ns and not ns: self.__soap_ns = 'soap' # 1.1 elif not soap_ns and ns: self.__soap_ns = 'soapenv' # 1.2 else: self.__soap_ns = soap_ns - - # parse wsdl url - self.services = wsdl and self.wsdl(wsdl, debug=trace, cache=cache) - self.service_port = None # service port for late binding - - if not proxy: - self.http = Http() + + # SOAP Server (special cases like oracle or jbossas6) + self.__soap_server = soap_server + + # SOAP Header support + self.__headers = {} # general headers + self.__call_headers = None # OrderedDict to be marshalled for RPC Call + + # check if the Certification Authority Cert is a string and store it + if cacert and cacert.startswith("-----BEGIN CERTIFICATE-----"): + fd, filename = tempfile.mkstemp() + f = os.fdopen(fd, 'w+b', -1) + if self.trace: log.info(u"Saving CA certificate to %s" % filename) + f.write(cacert) + cacert = filename + f.close() + self.cacert = cacert + + if timeout is _USE_GLOBAL_DEFAULT: + timeout = TIMEOUT else: - import socks - ##httplib2.debuglevel=4 - self.http = httplib2.Http(proxy_info = httplib2.ProxyInfo( - proxy_type=socks.PROXY_TYPE_HTTP, **proxy)) - #if self.certssl: # esto funciona para validar al server? - # self.http.add_certificate(self.keyssl, self.keyssl, self.certssl) + timeout = timeout + + # Create HTTP wrapper + Http = get_Http() + self.http = Http(timeout=timeout, cacert=cacert, proxy=proxy, sessions=sessions) + self.__ns = ns # namespace prefix or False to not use it if not ns: - self.__xml = """ -<%(soap_ns)s:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:xsd="http://www.w3.org/2001/XMLSchema" + self.__xml = """ +<%(soap_ns)s:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:%(soap_ns)s="%(soap_uri)s"> +<%(soap_ns)s:Header/> <%(soap_ns)s:Body> <%(method)s xmlns="%(namespace)s"> @@ -98,20 +135,35 @@ class SoapClient(object): """ + # parse wsdl url + self.services = wsdl and self.wsdl_parse(wsdl, debug=trace, cache=cache) + self.service_port = None # service port for late binding + def __getattr__(self, attr): "Return a pseudo-method that can be called" if not self.services: # not using WSDL? return lambda self=self, *args, **kwargs: self.call(attr,*args,**kwargs) else: # using WSDL: - return lambda self=self, *args, **kwargs: self.wsdl_call(attr,*args,**kwargs) - + return lambda *args, **kwargs: self.wsdl_call(attr,*args,**kwargs) + def call(self, method, *args, **kwargs): - "Prepare xml request and make SOAP call, returning a SimpleXMLElement" + """Prepare xml request and make SOAP call, returning a SimpleXMLElement. + + If a keyword argument called "headers" is passed with a value of a + SimpleXMLElement object, then these headers will be inserted into the + request. + """ #TODO: method != input_message # Basic SOAP request: xml = self.__xml % dict(method=method, namespace=self.namespace, ns=self.__ns, soap_ns=self.__soap_ns, soap_uri=soap_namespaces[self.__soap_ns]) request = SimpleXMLElement(xml,namespace=self.__ns and self.namespace, prefix=self.__ns) + + try: + request_headers = kwargs.pop('headers') + except KeyError: + request_headers = None + # serialize parameters if kwargs: parameters = kwargs.items() @@ -119,48 +171,89 @@ class SoapClient(object): parameters = args if parameters and isinstance(parameters[0], SimpleXMLElement): # merge xmlelement parameter ("raw" - already marshalled) - for param in parameters[0].children(): - getattr(request,method).import_node(param) - else: + if parameters[0].children() is not None: + for param in parameters[0].children(): + getattr(request,method).import_node(param) + elif parameters: # marshall parameters: for k,v in parameters: # dict: tag=valor getattr(request,method).marshall(k,v) + elif not self.__soap_server in ('oracle', ) or self.__soap_server in ('jbossas6',): + # JBossAS-6 requires no empty method parameters! + delattr(request("Body", ns=soap_namespaces.values(),), method) + + # construct header and parameters (if not wsdl given) except wsse + if self.__headers and not self.services: + self.__call_headers = dict([(k, v) for k, v in self.__headers.items() + if not k.startswith("wsse:")]) + # always extract WS Security header and send it + if 'wsse:Security' in self.__headers: + #TODO: namespaces too hardwired, clean-up... + header = request('Header' , ns=soap_namespaces.values(),) + k = 'wsse:Security' + v = self.__headers[k] + header.marshall(k, v, ns=False, add_children_ns=False) + header(k)['xmlns:wsse'] = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' + # + if self.__call_headers: + header = request('Header' , ns=soap_namespaces.values(),) + for k, v in self.__call_headers.items(): + ##if not self.__ns: + ## header['xmlns'] + header.marshall(k, v, ns=self.__ns, add_children_ns=False) + + if request_headers: + header = request('Header' , ns=soap_namespaces.values(),) + for subheader in request_headers.children(): + header.import_node(subheader) + self.xml_request = request.as_xml() self.xml_response = self.send(method, self.xml_request) response = SimpleXMLElement(self.xml_response, namespace=self.namespace) if self.exceptions and response("Fault", ns=soap_namespaces.values(), error=False): raise SoapFault(unicode(response.faultcode), unicode(response.faultstring)) return response - + + def send(self, method, xml): "Send SOAP request using HTTP" if self.location == 'test': return - location = "%s" % self.location #?op=%s" % (self.location, method) + # location = "%s" % self.location #?op=%s" % (self.location, method) + location = self.location + if self.services: - soap_action = self.action + soap_action = self.action else: - soap_action = self.action+method + soap_action = self.action + method + headers={ - 'Content-type': 'text/xml; charset="UTF-8"', - 'Content-length': str(len(xml)), - "SOAPAction": "\"%s\"" % (soap_action) - } + 'Content-type': 'text/xml; charset="UTF-8"', + 'Content-length': str(len(xml)), + "SOAPAction": "\"%s\"" % (soap_action) + } + headers.update(self.http_headers) + log.info("POST %s" % location) + log.info("Headers: %s" % headers) + if self.trace: print "-"*80 print "POST %s" % location print '\n'.join(["%s: %s" % (k,v) for k,v in headers.items()]) print u"\n%s" % xml.decode("utf8","ignore") + response, content = self.http.request( - location,"POST", body=xml, headers=headers ) + location, "POST", body=xml, headers=headers) self.response = response self.content = content - if self.trace: - print + + if self.trace: + print print '\n'.join(["%s: %s" % (k,v) for k,v in response.items()]) print content#.decode("utf8","ignore") print "="*80 return content + def get_operation(self, method): # try to find operation in wsdl file soap_ver = self.__soap_ns == 'soap12' and 'soap12' or 'soap11' @@ -182,7 +275,7 @@ class SoapClient(object): "Service/Port Type: %s" % (method, self.service_port)) return operation - + def wsdl_call(self, method, *args, **kwargs): "Pre and post process SOAP call, input and output parameters using WSDL" soap_uri = soap_namespaces[self.__soap_ns] @@ -190,6 +283,7 @@ class SoapClient(object): # get i/o type declarations: input = operation['input'] output = operation['output'] + header = operation.get('header') if 'action' in operation: self.action = operation['action'] # sort parameters (same order as xsd:sequence) @@ -198,21 +292,34 @@ class SoapClient(object): ret = OrderedDict() for k in od.keys(): v = d.get(k) - if v: + # don't append null tags! + if v is not None: if isinstance(v, dict): v = sort_dict(od[k], v) elif isinstance(v, list): - v = [sort_dict(od[k][0], v1) + v = [sort_dict(od[k][0], v1) for v1 in v] - ret[str(k)] = v + ret[str(k)] = v return ret else: return d + # construct header and parameters + if header: + self.__call_headers = sort_dict(header, self.__headers) + if input and args: + # convert positional parameters to named parameters: + d = [(k, arg) for k, arg in zip(input.values()[0].keys(), args)] + kwargs.update(dict(d)) if input and kwargs: params = sort_dict(input.values()[0], kwargs).items() - method = input.keys()[0] + if self.__soap_server == "axis": + # use the operation name + method = method + else: + # use the message (element) name + method = input.keys()[0] #elif not input: - #TODO: no message! (see wsmtxca.dummy) + #TODO: no message! (see wsmtxca.dummy) else: params = kwargs and kwargs.items() # call remote procedure @@ -224,19 +331,54 @@ class SoapClient(object): def help(self, method): "Return operation documentation and invocation/returned value example" operation = self.get_operation(method) - input = operation['input'].values() - input = input and input[0] - output = operation['output'].values()[0] - return u"%s(%s)\n -> %s:\n\n%s" % ( - method, - input and ", ".join("%s=%s" % (k,repr(v)) for k,v - in input.items()) or "", + input = operation.get('input') + input = input and input.values() and input.values()[0] + if isinstance(input, dict): + input = ", ".join("%s=%s" % (k,repr(v)) for k,v + in input.items()) + elif isinstance(input, list): + input = repr(input) + output = operation.get('output') + if output: + output = operation['output'].values()[0] + headers = operation.get('headers') or None + return u"%s(%s)\n -> %s:\n\n%s\nHeaders: %s" % ( + method, + input or "", output and output or "", operation.get("documentation",""), + headers, ) - def wsdl(self, url, debug=False, cache=False): + def wsdl_parse(self, url, debug=False, cache=False): "Parse Web Service Description v1.1" + + log.debug("wsdl url: %s" % url) + # Try to load a previously parsed wsdl: + force_download = False + if cache: + # make md5 hash of the url for caching... + filename_pkl = "%s.pkl" % hashlib.md5(url).hexdigest() + if isinstance(cache, basestring): + filename_pkl = os.path.join(cache, filename_pkl) + if os.path.exists(filename_pkl): + log.debug("Unpickle file %s" % (filename_pkl, )) + f = open(filename_pkl, "r") + pkl = pickle.load(f) + f.close() + # sanity check: + if pkl['version'][:-1] != __version__.split(" ")[0][:-1] or pkl['url'] != url: + import warnings + warnings.warn('version or url mismatch! discarding cached wsdl', RuntimeWarning) + if debug: + log.debug('Version: %s %s' % (pkl['version'], __version__)) + log.debug('URL: %s %s' % (pkl['url'], url)) + force_download = True + else: + self.namespace = pkl['namespace'] + self.documentation = pkl['documentation'] + return pkl['services'] + soap_ns = { "http://schemas.xmlsoap.org/wsdl/soap/": 'soap11', "http://schemas.xmlsoap.org/wsdl/soap12/": 'soap12', @@ -244,34 +386,57 @@ class SoapClient(object): wsdl_uri="http://schemas.xmlsoap.org/wsdl/" xsd_uri="http://www.w3.org/2001/XMLSchema" xsi_uri="http://www.w3.org/2001/XMLSchema-instance" - - get_local_name = lambda s: str((':' in s) and s.split(':')[1] or s) - - REVERSE_TYPE_MAP = dict([(v,k) for k,v in TYPE_MAP.items()]) + + get_local_name = lambda s: s and str((':' in s) and s.split(':')[1] or s) + get_namespace_prefix = lambda s: s and str((':' in s) and s.split(':')[0] or None) + + # always return an unicode object: + REVERSE_TYPE_MAP[u'string'] = unicode def fetch(url): - "Fetch a document from a URL, save it locally if cache enabled" - import os, hashlib - # make md5 hash of the url for caching... + "Download a document from a URL, save it locally if cache enabled" + + # check / append a valid schema if not given: + url_scheme, netloc, path, query, fragment = urlsplit(url) + if not url_scheme in ('http','https', 'file'): + for scheme in ('http','https', 'file'): + try: + if not url.startswith("/") and scheme in ('http', 'https'): + tmp_url = "%s://%s" % (scheme, url) + else: + tmp_url = "%s:%s" % (scheme, url) + if debug: log.debug("Scheme not found, trying %s" % scheme) + return fetch(tmp_url) + except Exception, e: + log.error(e) + raise RuntimeError("No scheme given for url: %s" % url) + + # make md5 hash of the url for caching... filename = "%s.xml" % hashlib.md5(url).hexdigest() if isinstance(cache, basestring): - filename = os.path.join(cache, filename) - if cache and os.path.exists(filename): - if debug: print "Reading file %s" % (filename, ) + filename = os.path.join(cache, filename) + if cache and os.path.exists(filename) and not force_download: + log.info("Reading file %s" % (filename, )) f = open(filename, "r") xml = f.read() f.close() else: - if debug: print "Fetching url %s" % (url, ) - f = urllib.urlopen(url) - xml = f.read() + if url_scheme == 'file': + log.info("Fetching url %s using urllib2" % (url, )) + f = urllib2.urlopen(url) + xml = f.read() + else: + log.info("GET %s using %s" % (url, self.http._wrapper_version)) + response, xml = self.http.request(url, "GET", None, {}) if cache: - if debug: print "Writing file %s" % (filename, ) + log.info("Writing file %s" % (filename, )) + if not os.path.isdir(cache): + os.makedirs(cache) f = open(filename, "w") f.write(xml) f.close() return xml - + # Open uri and read xml: xml = fetch(url) # Parse WSDL XML: @@ -289,19 +454,19 @@ class SoapClient(object): # Extract useful data: self.namespace = wsdl['targetNamespace'] self.documentation = unicode(wsdl('documentation', error=False) or '') - + services = {} bindings = {} # binding_name: binding operations = {} # operation_name: operation port_type_bindings = {} # port_type_name: binding messages = {} # message: element elements = {} # element: type def - + for service in wsdl.service: service_name=service['name'] if not service_name: continue # empty service? - if debug: print "Processing service", service_name + if debug: log.debug("Processing service %s" % service_name) serv = services.setdefault(service_name, {'ports': {}}) serv['documentation']=service['documentation'] or '' for port in service.port: @@ -315,10 +480,10 @@ class SoapClient(object): 'soap_uri': soap_uri, 'soap_ver': soap_ver, } serv['ports'][port['name']] = bindings[binding_name] - + for binding in wsdl.binding: binding_name = binding['name'] - if debug: print "Processing binding", service_name + if debug: log.debug("Processing binding %s" % service_name) soap_binding = binding('binding', ns=soap_uris.values(), error=False) transport = soap_binding and soap_binding['transport'] or None port_type_name = get_local_name(binding['type']) @@ -334,28 +499,52 @@ class SoapClient(object): d = operations.setdefault(op_name, {}) bindings[binding_name]['operations'][op_name] = d d.update({'name': op_name}) + d['parts'] = {} + # input and/or ouput can be not present! + input = operation('input', error=False) + body = input and input('body', ns=soap_uris.values(), error=False) + d['parts']['input_body'] = body and body['parts'] or None + output = operation('output', error=False) + body = output and output('body', ns=soap_uris.values(), error=False) + d['parts']['output_body'] = body and body['parts'] or None + header = input and input('header', ns=soap_uris.values(), error=False) + d['parts']['input_header'] = header and {'message': header['message'], 'part': header['part']} or None + headers = output and output('header', ns=soap_uris.values(), error=False) + d['parts']['output_header'] = header and {'message': header['message'], 'part': header['part']} or None #if action: #TODO: separe operation_binding from operation if action: d["action"] = action - + + def make_key(element_name, element_type): + "return a suitable key for elements" + # only distinguish 'element' vs other types + if element_type in ('complexType', 'simpleType'): + eltype = 'complexType' + else: + eltype = element_type + if eltype not in ('element', 'complexType', 'simpleType'): + raise RuntimeError("Unknown element type %s = %s" % (unicode(element_name), eltype)) + return (unicode(element_name), eltype) + #TODO: cleanup element/schema/types parsing: - def process_element(element_name, node): + def process_element(element_name, node, element_type): "Parse and define simple element types" - if debug: print "Processing element", element_name + if debug: + log.debug("Processing element %s %s" % (element_name, element_type)) for tag in node: if tag.get_local_name() in ("annotation", "documentation"): continue elif tag.get_local_name() in ('element', 'restriction'): - if debug: print element_name,"has not children!",tag + if debug: log.debug("%s has not children! %s" % (element_name,tag)) children = tag # element "alias"? alias = True elif tag.children(): children = tag.children() alias = False else: - if debug: print element_name,"has not children!",tag + if debug: log.debug("%s has not children! %s" % (element_name,tag)) continue #TODO: abstract? - d = OrderedDict() + d = OrderedDict() for e in children: t = e['type'] if not t: @@ -368,44 +557,47 @@ class SoapClient(object): else: ns, type_name = None, t[0] if element_name == type_name: - continue # prevent infinite recursion + pass ## warning with infinite recursion uri = ns and e.get_namespace_uri(ns) or xsd_uri if uri==xsd_uri: # look for the type, None == any fn = REVERSE_TYPE_MAP.get(unicode(type_name), None) else: - # complex type, postprocess later - fn = elements.setdefault(unicode(type_name), OrderedDict()) + fn = None + if not fn: + # simple / complex type, postprocess later + fn = elements.setdefault(make_key(type_name, "complexType"), OrderedDict()) + if e['name'] is not None and not alias: e_name = unicode(e['name']) d[e_name] = fn else: - if debug: print "complexConent/simpleType/element", element_name, "=", type_name + if debug: log.debug("complexConent/simpleType/element %s = %s" % (element_name, type_name)) d[None] = fn - if e['maxOccurs']=="unbounded": + if e['maxOccurs']=="unbounded" or (ns == 'SOAP-ENC' and type_name == 'Array'): # it's an array... TODO: compound arrays? d.array = True if e is not None and e.get_local_name() == 'extension' and e.children(): # extend base element: - process_element(element_name, e.children()) - elements.setdefault(element_name, OrderedDict()).update(d) + process_element(element_name, e.children(), element_type) + elements.setdefault(make_key(element_name, element_type), OrderedDict()).update(d) # check axis2 namespace at schema types attributes - self.namespace = dict(wsdl.types("schema", ns=xsd_uri)[:]).get('targetNamespace', self.namespace) + self.namespace = dict(wsdl.types("schema", ns=xsd_uri)[:]).get('targetNamespace', self.namespace) imported_schemas = {} def preprocess_schema(schema): "Find schema elements and complex types" - for element in schema.children(): + for element in schema.children() or []: if element.get_local_name() in ('import', ): schema_namespace = element['namespace'] schema_location = element['schemaLocation'] if schema_location is None: - if debug: print "Schema location not provided for %s!" % (schema_namespace, ) + if debug: log.debug("Schema location not provided for %s!" % (schema_namespace, )) continue if schema_location in imported_schemas: - if debug: print "Schema %s already imported!" % (schema_location, ) + if debug: log.debug("Schema %s already imported!" % (schema_location, )) continue imported_schemas[schema_location] = schema_namespace if debug: print "Importing schema %s from %s" % (schema_namespace, schema_location) @@ -415,9 +607,10 @@ class SoapClient(object): imported_schema = SimpleXMLElement(xml, namespace=xsd_uri) preprocess_schema(imported_schema) - if element.get_local_name() in ('element', 'complexType', "simpleType"): + element_type = element.get_local_name() + if element_type in ('element', 'complexType', "simpleType"): element_name = unicode(element['name']) - if debug: print "Parsing Element %s: %s" % (element.get_local_name(),element_name) + if debug: log.debug("Parsing Element %s: %s" % (element_type, element_name)) if element.get_local_name() == 'complexType': children = element.children() elif element.get_local_name() == 'simpleType': @@ -431,7 +624,7 @@ class SoapClient(object): elif element.get_local_name() == 'element': children = element if children: - process_element(element_name, children) + process_element(element_name, children, element_type) def postprocess_element(elements): "Fix unresolved references (elements referenced before its definition, thanks .net)" @@ -445,62 +638,131 @@ class SoapClient(object): if isinstance(v[None], dict): for i, kk in enumerate(v[None]): # extend base -keep orginal order- - elements[k].insert(kk, v[None][kk], i) + if v[None] is not None: + elements[k].insert(kk, v[None][kk], i) del v[None] else: # "alias", just replace - if debug: print "Replacing ", k , " = ", v[None] + if debug: log.debug("Replacing %s = %s" % (k, v[None])) elements[k] = v[None] #break if isinstance(v, list): for n in v: # recurse list postprocess_element(n) - + # process current wsdl schema: - for schema in wsdl.types("schema", ns=xsd_uri): - preprocess_schema(schema) + for schema in wsdl.types("schema", ns=xsd_uri): + preprocess_schema(schema) postprocess_element(elements) for message in wsdl.message: - if debug: print "Processing message", message['name'] - part = message('part', error=False) - element = {} - if part: + if debug: log.debug("Processing message %s" % message['name']) + for part in message('part', error=False) or []: + element = {} element_name = part['element'] if not element_name: - element_name = part['type'] # some uses type instead - element_name = get_local_name(element_name) - element = {element_name: elements.get(element_name)} - messages[message['name']] = element + # some implementations (axis) uses type instead + element_name = part['type'] + type_ns = get_namespace_prefix(element_name) + type_uri = wsdl.get_namespace_uri(type_ns) + if type_uri == xsd_uri: + element_name = get_local_name(element_name) + fn = REVERSE_TYPE_MAP.get(unicode(element_name), None) + element = {part['name']: fn} + # emulate a true Element (complexType) + messages.setdefault((message['name'], None), {message['name']: OrderedDict()}).values()[0].update(element) + else: + element_name = get_local_name(element_name) + fn = elements.get(make_key(element_name, 'element')) + if not fn: + # some axis servers uses complexType for part messages + fn = elements.get(make_key(element_name, 'complexType')) + element = {message['name']: {part['name']: fn}} + else: + element = {element_name: fn} + messages[(message['name'], part['name'])] = element + def get_message(message_name, part_name): + if part_name: + # get the specific part of the message: + return messages.get((message_name, part_name)) + else: + # get the first part for the specified message: + for (message_name_key, part_name_key), message in messages.items(): + if message_name_key == message_name: + return message + for port_type in wsdl.portType: port_type_name = port_type['name'] - if debug: print "Processing port type", port_type_name + if debug: log.debug("Processing port type %s" % port_type_name) binding = port_type_bindings[port_type_name] for operation in port_type.operation: op_name = operation['name'] - op = operations[op_name] + op = operations[op_name] op['documentation'] = unicode(operation('documentation', error=False) or '') - if binding['soap_ver']: + if binding['soap_ver']: #TODO: separe operation_binding from operation (non SOAP?) - input = get_local_name(operation.input['message']) - output = get_local_name(operation.output['message']) - op['input'] = messages[input] - op['output'] = messages[output] + if operation("input", error=False): + input_msg = get_local_name(operation.input['message']) + input_header = op['parts'].get('input_header') + if input_header: + header_msg = get_local_name(input_header.get('message')) + header_part = get_local_name(input_header.get('part')) + # warning: some implementations use a separate message! + header = get_message(header_msg or input_msg, header_part) + else: + header = None # not enought info to search the header message: + op['input'] = get_message(input_msg, op['parts'].get('input_body')) + op['header'] = header + else: + op['input'] = None + op['header'] = None + if operation("output", error=False): + output_msg = get_local_name(operation.output['message']) + op['output'] = get_message(output_msg, op['parts'].get('output_body')) + else: + op['output'] = None if debug: import pprint - pprint.pprint(services) - + log.debug(pprint.pformat(services)) + + # Save parsed wsdl (cache) + if cache: + f = open(filename_pkl, "wb") + pkl = { + 'version': __version__.split(" ")[0], + 'url': url, + 'namespace': self.namespace, + 'documentation': self.documentation, + 'services': services, + } + pickle.dump(pkl, f) + f.close() + return services + def __setitem__(self, item, value): + "Set SOAP Header value - this header will be sent for every request." + self.__headers[item] = value + + def close(self): + "Finish the connection and remove temp files" + self.http.close() + if self.cacert.startswith(tempfile.gettempdir()): + if self.trace: log.info("removing %s" % self.cacert) + os.unlink(self.cacert) + + def parse_proxy(proxy_str): "Parses proxy address user:pass@host:port into a dict suitable for httplib2" + if isinstance(proxy_str, unicode): + proxy_str = proxy_str.encode("utf8") proxy_dict = {} if proxy_str is None: - return + return if "@" in proxy_str: user_pass, host_port = proxy_str.split("@") else: @@ -511,179 +773,7 @@ def parse_proxy(proxy_str): if ":" in user_pass: proxy_dict['proxy_user'], proxy_dict['proxy_pass'] = user_pass.split(":") return proxy_dict - - -if __name__=="__main__": - import sys - - if '--web2py' in sys.argv: - # test local sample webservice exposed by web2py - from client import SoapClient - if not '--wsdl' in sys.argv: - client = SoapClient( - location = "http://127.0.0.1:8000/webservices/sample/call/soap", - action = 'http://127.0.0.1:8000/webservices/sample/call/soap', # SOAPAction - namespace = "http://127.0.0.1:8000/webservices/sample/call/soap", - soap_ns='soap', trace = True, ns = False, exceptions=True) - else: - client = SoapClient(wsdl="http://127.0.0.1:8000/webservices/sample/call/soap?WSDL",trace=True) - response = client.Dummy() - print 'dummy', response - response = client.Echo(value='hola') - print 'echo', repr(response) - response = client.AddIntegers(a=1,b=2) - if not '--wsdl' in sys.argv: - result = response.AddResult # manully convert returned type - print int(result) - else: - result = response['AddResult'] - print result, type(result), "auto-unmarshalled" - - if '--raw' in sys.argv: - # raw (unmarshalled parameter) local sample webservice exposed by web2py - from client import SoapClient - client = SoapClient( - location = "http://127.0.0.1:8000/webservices/sample/call/soap", - action = 'http://127.0.0.1:8000/webservices/sample/call/soap', # SOAPAction - namespace = "http://127.0.0.1:8000/webservices/sample/call/soap", - soap_ns='soap', trace = True, ns = False) - params = SimpleXMLElement("""32""") # manully convert returned type - response = client.call('AddIntegers',params) - result = response.AddResult - print int(result) # manully convert returned type - - if '--ctg' in sys.argv: - # test AFIP Agriculture webservice - client = SoapClient( - location = "https://fwshomo.afip.gov.ar/wsctg/services/CTGService", - action = 'http://impl.service.wsctg.afip.gov.ar/CTGService/', # SOAPAction - namespace = "http://impl.service.wsctg.afip.gov.ar/CTGService/", - trace = True, - ns = True) - response = client.dummy() - result = response.dummyResponse - print str(result.appserver) - print str(result.dbserver) - print str(result.authserver) - - if '--wsfe' in sys.argv: - # Demo & Test (AFIP Electronic Invoice): - ta_file = open("TA.xml") - try: - ta_string = ta_file.read() # read access ticket (wsaa.py) - finally: - ta_file.close() - ta = SimpleXMLElement(ta_string) - token = str(ta.credentials.token) - sign = str(ta.credentials.sign) - cuit = long(20267565393) - id = 1234 - cbte =199 - client = SoapClient( - location = "https://wswhomo.afip.gov.ar/wsfe/service.asmx", - action = 'http://ar.gov.afip.dif.facturaelectronica/', # SOAPAction - namespace = "http://ar.gov.afip.dif.facturaelectronica/", - trace = True) - results = client.FERecuperaQTYRequest( - argAuth= {"Token": token, "Sign": sign, "cuit":long(cuit)} - ) - if int(results.FERecuperaQTYRequestResult.RError.percode) != 0: - print "Percode: %s" % results.FERecuperaQTYRequestResult.RError.percode - print "MSGerror: %s" % results.FERecuperaQTYRequestResult.RError.perrmsg - else: - print int(results.FERecuperaQTYRequestResult.qty.value) - - if '--feriados' in sys.argv: - # Demo & Test: Argentina Holidays (Ministerio del Interior): - # this webservice seems disabled - from datetime import datetime, timedelta - client = SoapClient( - location = "http://webservices.mininterior.gov.ar/Feriados/Service.svc", - action = 'http://tempuri.org/IMyService/', # SOAPAction - namespace = "http://tempuri.org/FeriadoDS.xsd", - trace = True) - dt1 = datetime.today() - timedelta(days=60) - dt2 = datetime.today() + timedelta(days=60) - feriadosXML = client.FeriadosEntreFechasas_xml(dt1=dt1.isoformat(), dt2=dt2.isoformat()); - print feriadosXML - - if '--wsdl-parse' in sys.argv: - client = SoapClient() - # Test PySimpleSOAP WSDL - client.wsdl("file:C:/test.wsdl", debug=True) - # Test Java Axis WSDL: - client.wsdl('https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl',debug=True) - # Test .NET 2.0 WSDL: - client.wsdl('https://wswhomo.afip.gov.ar/wsfe/service.asmx?WSDL',debug=True) - client.wsdl('https://wswhomo.afip.gov.ar/wsfex/service.asmx?WSDL',debug=True) - client.wsdl('https://testdia.afip.gov.ar/Dia/Ws/wDigDepFiel/wDigDepFiel.asmx?WSDL',debug=True) - # Test JBoss WSDL: - client.wsdl('https://fwshomo.afip.gov.ar/wsctg/services/CTGService?wsdl',debug=True) - client.wsdl('https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl',debug=True) - - if '--wsdl-client' in sys.argv: - client = SoapClient(wsdl='https://wswhomo.afip.gov.ar/wsfex/service.asmx?WSDL',trace=True) - results = client.FEXDummy() - print results['FEXDummyResult']['AppServer'] - print results['FEXDummyResult']['DbServer'] - print results['FEXDummyResult']['AuthServer'] - ta_file = open("TA.xml") - try: - ta_string = ta_file.read() # read access ticket (wsaa.py) - finally: - ta_file.close() - ta = SimpleXMLElement(ta_string) - token = str(ta.credentials.token) - sign = str(ta.credentials.sign) - response = client.FEXGetCMP( - Auth={"Token": token, "Sign": sign, "Cuit": 20267565393}, - Cmp={"Tipo_cbte": 19, "Punto_vta": 1, "Cbte_nro": 1}) - result = response['FEXGetCMPResult'] - if False: print result - if 'FEXErr' in result: - print "FEXError:", result['FEXErr']['ErrCode'], result['FEXErr']['ErrCode'] - cbt = result['FEXResultGet'] - print cbt['Cae'] - FEX_event = result['FEXEvents'] - print FEX_event['EventCode'], FEX_event['EventMsg'] - - if '--wsdl-ctg' in sys.argv: - client = SoapClient(wsdl='https://fwshomo.afip.gov.ar/wsctg/services/CTGService?wsdl', - trace=True, ns = "ctg") - results = client.dummy() - print results - print results['DummyResponse']['appserver'] - print results['DummyResponse']['dbserver'] - print results['DummyResponse']['authserver'] - ta_file = open("TA.xml") - try: - ta_string = ta_file.read() # read access ticket (wsaa.py) - finally: - ta_file.close() - ta = SimpleXMLElement(ta_string) - token = str(ta.credentials.token) - sign = str(ta.credentials.sign) - print client.help("obtenerProvincias") - response = client.obtenerProvincias(auth={"token":token, "sign":sign, "cuitRepresentado":20267565393}) - print "response=",response - for ret in response: - print ret['return']['codigoProvincia'], ret['return']['descripcionProvincia'].encode("latin1") - prueba = dict(numeroCartaDePorte=512345678, codigoEspecie=23, - cuitRemitenteComercial=20267565393, cuitDestino=20267565393, cuitDestinatario=20267565393, - codigoLocalidadOrigen=3058, codigoLocalidadDestino=3059, - codigoCosecha='0910', pesoNetoCarga=1000, cantHoras=1, - patenteVehiculo='CZO985', cuitTransportista=20267565393, - numeroCTG="43816783", transaccion='10000001681', observaciones='', - ) - - response = client.solicitarCTG( - auth={"token": token, "sign": sign, "cuitRepresentado": 20267565393}, - solicitarCTGRequest= prueba) - - print response['return']['numeroCTG'] - - ##print parse_proxy(None) - ##print parse_proxy("host:1234") - ##print parse_proxy("user:pass@host:1234") - ##sys.exit(0) - + + +if __name__ == "__main__": + pass diff --git a/gluon/contrib/pysimplesoap/server.py b/gluon/contrib/pysimplesoap/server.py index 5e395680..12c618c4 100755 --- a/gluon/contrib/pysimplesoap/server.py +++ b/gluon/contrib/pysimplesoap/server.py @@ -15,21 +15,67 @@ __author__ = "Mariano Reingart (reingart@gmail.com)" __copyright__ = "Copyright (C) 2010 Mariano Reingart" __license__ = "LGPL 3.0" -__version__ = "1.02c" +__version__ = "1.03c" -from simplexml import SimpleXMLElement, TYPE_MAP, DateTime, Date, Decimal +import logging +import re +import traceback +from simplexml import SimpleXMLElement, TYPE_MAP, Date, Decimal +log = logging.getLogger(__name__) + +# Deprecated DEBUG = False - +NS_RX=re.compile(r'xmlns:(\w+)="(.+?)"') class SoapDispatcher(object): "Simple Dispatcher for SOAP Server" - - def __init__(self, name, documentation='', action='', location='', - namespace=None, prefix=False, - soap_uri="http://schemas.xmlsoap.org/soap/envelope/", + + def __init__(self, name, documentation='', action='', location='', + namespace=None, prefix=False, + soap_uri="http://schemas.xmlsoap.org/soap/envelope/", soap_ns='soap', + namespaces={}, + pretty=False, + debug=False, **kwargs): + """ + :param namespace: Target namespace; xmlns=targetNamespace + :param prefix: Prefix for target namespace; xmlns:prefix=targetNamespace + :param namespaces: Specify additional namespaces; example: {'external': 'http://external.mt.moboperator'} + :param pretty: Prettifies generated xmls + :param debug: Use to add tracebacks in generated xmls. + + Multiple namespaces + =================== + + It is possible to support multiple namespaces. + You need to specify additional namespaces by passing `namespace` parameter. + + >>> dispatcher = SoapDispatcher( + ... name = "MTClientWS", + ... location = "http://localhost:8008/ws/MTClientWS", + ... action = 'http://localhost:8008/ws/MTClientWS', # SOAPAction + ... namespace = "http://external.mt.moboperator", prefix="external", + ... documentation = 'moboperator MTClientWS', + ... namespaces = { + ... 'external': 'http://external.mt.moboperator', + ... 'model': 'http://model.common.mt.moboperator' + ... }, + ... ns = True) + + Now the registered method must return node names with namespaces' prefixes. + + >>> def _multi_ns_func(self, serviceMsisdn): + ... ret = { + ... 'external:activateSubscriptionsReturn': [ + ... {'model:code': '0'}, + ... {'model:description': 'desc'}, + ... ]} + ... return ret + + Our prefixes will be changed to those used by the client. + """ self.methods = {} self.name = name self.documentation = documentation @@ -39,10 +85,28 @@ class SoapDispatcher(object): self.prefix = prefix self.soap_ns = soap_ns self.soap_uri = soap_uri - + self.namespaces = namespaces + self.pretty = pretty + self.debug = debug + + + @staticmethod + def _extra_namespaces(xml, ns): + """Extends xml with extra namespaces. + :param ns: dict with namespaceUrl:prefix pairs + :param xml: XML node to modify + """ + if ns: + _tpl = 'xmlns:%s="%s"' + _ns_str = " ".join([_tpl % (prefix, uri) for uri, prefix in ns.items() if uri not in xml]) + xml = xml.replace('/>', ' '+_ns_str+'/>') + return xml + + def register_function(self, name, fn, returns=None, args=None, doc=None): - self.methods[name] = fn, returns, args, doc or getattr(fn,"__doc__","") - + self.methods[name] = fn, returns, args, doc or getattr(fn, "__doc__", "") + + def dispatch(self, xml, action=None): "Receive and proccess SOAP call" # default values: @@ -50,23 +114,41 @@ class SoapDispatcher(object): ret = fault = None soap_ns, soap_uri = self.soap_ns, self.soap_uri soap_fault_code = 'VersionMismatch' - + name = None + + # namespaces = [('model', 'http://model.common.mt.moboperator'), ('external', 'http://external.mt.moboperator')] + _ns_reversed = dict(((v,k) for k,v in self.namespaces.iteritems())) # Switch keys-values + # _ns_reversed = {'http://external.mt.moboperator': 'external', 'http://model.common.mt.moboperator': 'model'} + try: request = SimpleXMLElement(xml, namespace=self.namespace) - + # detect soap prefix and uri (xmlns attributes of Envelope) for k, v in request[:]: if v in ("http://schemas.xmlsoap.org/soap/envelope/", "http://www.w3.org/2003/05/soap-env",): soap_ns = request.attributes()[k].localName soap_uri = request.attributes()[k].value - + + # If the value from attributes on Envelope is in additional namespaces + elif v in self.namespaces.values(): + _ns = request.attributes()[k].localName + _uri = request.attributes()[k].value + _ns_reversed[_uri] = _ns # update with received alias + # Now we change 'external' and 'model' to the received forms i.e. 'ext' and 'mod' + # After that we know how the client has prefixed additional namespaces + + ns = NS_RX.findall(xml) + for k, v in ns: + if v in self.namespaces.values(): + _ns_reversed[v] = k + soap_fault_code = 'Client' - + # parse request message and get local method method = request('Body', ns=soap_uri).children()(0) if action: - # method name = action + # method name = action name = action[len(self.action)+1:-1] prefix = self.prefix if not action or not name: @@ -74,52 +156,69 @@ class SoapDispatcher(object): name = method.get_local_name() prefix = method.get_prefix() - if DEBUG: print "dispatch method", name + log.debug('dispatch method: %s', name) function, returns_types, args_types, doc = self.methods[name] - + log.debug('returns_types %s', returns_types) + # de-serialize parameters (if type definitions given) if args_types: args = method.children().unmarshall(args_types) elif args_types is None: - args = {'request':method} # send raw request + args = {'request': method} # send raw request else: args = {} # no parameters - + soap_fault_code = 'Server' # execute function ret = function(**args) - if DEBUG: print ret + log.debug('dispathed method returns: %s', ret) - except Exception, e: + except Exception: # This shouldn't be one huge try/except import sys etype, evalue, etb = sys.exc_info() - if DEBUG: - import traceback + log.error(traceback.format_exc()) + if self.debug: detail = ''.join(traceback.format_exception(etype, evalue, etb)) detail += '\n\nXML REQUEST\n\n' + xml else: detail = None - fault = {'faultcode': "%s.%s" % (soap_fault_code, etype.__name__), - 'faultstring': unicode(evalue), + fault = {'faultcode': "%s.%s" % (soap_fault_code, etype.__name__), + 'faultstring': unicode(evalue), 'detail': detail} # build response message if not prefix: - xml = """<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s"/>""" + xml = """<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s"/>""" else: xml = """<%(soap_ns)s:Envelope xmlns:%(soap_ns)s="%(soap_uri)s" - xmlns:%(prefix)s="%(namespace)s"/>""" - - xml = xml % {'namespace': self.namespace, 'prefix': prefix, - 'soap_ns': soap_ns, 'soap_uri': soap_uri} - - response = SimpleXMLElement(xml, namespace=self.namespace, + xmlns:%(prefix)s="%(namespace)s"/>""" + + xml %= { # a %= {} is a shortcut for a = a % {} + 'namespace': self.namespace, + 'prefix': prefix, + 'soap_ns': soap_ns, + 'soap_uri': soap_uri + } + + # Now we add extra namespaces + xml = SoapDispatcher._extra_namespaces(xml, _ns_reversed) + + # Change our namespace alias to that given by the client. + # We put [('model', 'http://model.common.mt.moboperator'), ('external', 'http://external.mt.moboperator')] + # mix it with {'http://external.mt.moboperator': 'ext', 'http://model.common.mt.moboperator': 'mod'} + mapping = dict(((k, _ns_reversed[v]) for k,v in self.namespaces.iteritems())) # Switch keys-values and change value + # and get {'model': u'mod', 'external': u'ext'} + + response = SimpleXMLElement(xml, + namespace=self.namespace, + namespaces_map = mapping, prefix=prefix) - + response['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance" response['xmlns:xsd'] = "http://www.w3.org/2001/XMLSchema" - + body = response.add_child("%s:Body" % soap_ns, ns=False) + if fault: # generate a Soap Fault (with the python exception) body.marshall("%s:Fault" % soap_ns, fault, ns=False) @@ -139,14 +238,16 @@ class SoapDispatcher(object): elif returns_types is None: # merge xmlelement returned res.import_node(ret) + elif returns_types == {}: + log.warning('Given returns_types is an empty dict.') - return response.as_xml() + return response.as_xml(pretty=self.pretty) # Introspection functions: def list_methods(self): "Return a list of aregistered operations" - return [(method, doc) for method, (function, returns, args, doc) in self.methods.items()] + return [(method, doc) for method, (function, returns, args, doc) in self.methods.items()] def help(self, method=None): "Generate sample request and response messages" @@ -185,7 +286,7 @@ class SoapDispatcher(object): def wsdl(self): "Generate Web Service Description v1.1" xml = """ - - - - -

12

5000000.1.2
20100724
-
-
-
""" - - print dispatcher.dispatch(xml) - - # dummy local test (modern soap dialect, SoapUI) - xml = """ - - - - - 93 - 19690720 - 10.0015.02 - - - - """ - print dispatcher.dispatch(xml) - - # echo local test (generic soap service) - xml = """ - - - - Hello world - - - """ - - print dispatcher.dispatch(xml) - + + # Commented because path is platform dependent + # Looks that it doesnt matter. + # open("C:/test.wsdl","w").write(wsdl) for method, doc in dispatcher.list_methods(): request, response, doc = dispatcher.help(method) ##print request ##print response - + if '--serve' in sys.argv: print "Starting server..." httpd = HTTPServer(("", 8008), SOAPHandler) @@ -442,7 +500,7 @@ if __name__=="__main__": client = SoapClient( location = "http://localhost:8008/", action = 'http://localhost:8008/', # SOAPAction - namespace = "http://example.com/sample.wsdl", + namespace = "http://example.com/sample.wsdl", soap_ns='soap', trace = True, ns = False) @@ -450,6 +508,3 @@ if __name__=="__main__": result = response.AddResult print int(result.ab) print str(result.dd) - - - diff --git a/gluon/contrib/pysimplesoap/simplexml.py b/gluon/contrib/pysimplesoap/simplexml.py index ea790563..6b0cea5f 100755 --- a/gluon/contrib/pysimplesoap/simplexml.py +++ b/gluon/contrib/pysimplesoap/simplexml.py @@ -15,33 +15,67 @@ __author__ = "Mariano Reingart (reingart@gmail.com)" __copyright__ = "Copyright (C) 2008/009 Mariano Reingart" __license__ = "LGPL 3.0" -__version__ = "1.02c" +__version__ = "1.03a" +import datetime +import logging +import re +import time +import warnings import xml.dom.minidom from decimal import Decimal -import datetime -import time + +log = logging.getLogger(__name__) +logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.WARNING) DEBUG = False -# Functions to serialize/unserialize special immutable types: -datetime_u = lambda s: datetime.datetime.strptime(s, "%Y-%m-%dT%H:%M:%S") +try: + _strptime = datetime.datetime.strptime +except AttributeError: # python2.4 + _strptime = lambda s, fmt: datetime.datetime(*(time.strptime(s, fmt)[:6])) + + +# Functions to serialize/deserialize special immutable types: +def datetime_u(s): + fmt = "%Y-%m-%dT%H:%M:%S" + try: + return _strptime(s, fmt) + except ValueError: + try: + # strip utc offset + if s[-3] == ":" and s[-6] in (' ', '-', '+'): + warnings.warn('removing unsupported UTC offset', RuntimeWarning) + s = s[:-6] + # parse microseconds + try: + return _strptime(s, fmt + ".%f") + except: + return _strptime(s, fmt) + except ValueError: + # strip microseconds (not supported in this platform) + if "." in s: + warnings.warn('removing unsuppported microseconds', RuntimeWarning) + s = s[:s.index(".")] + return _strptime(s, fmt) + datetime_m = lambda dt: dt.isoformat('T') -date_u = lambda s: datetime.datetime.strptime(s[0:10], "%Y-%m-%d").date() +date_u = lambda s: _strptime(s[0:10], "%Y-%m-%d").date() date_m = lambda d: d.strftime("%Y-%m-%d") -time_u = lambda s: datetime.datetime.strptime(s, "%H:%M:%S").time() +time_u = lambda s: _strptime(s, "%H:%M:%S").time() time_m = lambda d: d.strftime("%H%M%S") bool_u = lambda s: {'0':False, 'false': False, '1': True, 'true': True}[s] +bool_m = lambda s: {False: 'false', True: 'true'}[s] # aliases: -class Alias(): +class Alias(object): def __init__(self, py_type, xml_type): self.py_type, self.xml_type = py_type, xml_type def __call__(self, value): return self.py_type(value) def __repr__(self): return "" % (self.xml_type, self.py_type) - + byte = Alias(str,'byte') short = Alias(int,'short') double = Alias(float,'double') @@ -51,18 +85,34 @@ Date = datetime.date Time = datetime.time # Define convertion function (python type): xml schema type -TYPE_MAP = {str:'string',unicode:'string', - bool:'boolean', short:'short', byte:'byte', - int:'int', long:'long', integer:'integer', - float:'float', double:'double', - Decimal:'decimal', - datetime.datetime:'dateTime', datetime.date:'date', - } -TYPE_MARSHAL_FN = {datetime.datetime:datetime_m, datetime.date:date_m,} -TYPE_UNMARSHAL_FN = {datetime.datetime:datetime_u, datetime.date:date_u, - bool:bool_u, - } +TYPE_MAP = { + str:'string', + unicode:'string', + bool:'boolean', + short:'short', + byte:'byte', + int:'int', + long:'long', + integer:'integer', + float:'float', + double:'double', + Decimal:'decimal', + datetime.datetime:'dateTime', + datetime.date:'date', +} +TYPE_MARSHAL_FN = { + datetime.datetime:datetime_m, + datetime.date:date_m, + bool:bool_m +} +TYPE_UNMARSHAL_FN = { + datetime.datetime:datetime_u, + datetime.date:date_u, + bool:bool_u, + str:unicode, +} +REVERSE_TYPE_MAP = dict([(v,k) for k,v in TYPE_MAP.items()]) class OrderedDict(dict): "Minimal ordered dictionary for xsd:sequences" @@ -103,33 +153,45 @@ class OrderedDict(dict): class SimpleXMLElement(object): "Simple XML manipulation (simil PHP)" - - def __init__(self, text = None, elements = None, document = None, namespace = None, prefix=None): + + def __init__(self, text = None, elements = None, document = None, + namespace = None, prefix=None, namespaces_map={}): + """ + :param namespaces_map: How to map our namespace prefix to that given by the client; + {prefix: received_prefix} + """ + self.__namespaces_map = namespaces_map + _rx = "|".join(namespaces_map.keys()) # {'external': 'ext', 'model': 'mod'} -> 'external|model' + self.__ns_rx = re.compile(r"^(%s):.*$" % _rx) # And now we build an expression ^(external|model):.*$ + # to find prefixes in all xml nodes i.e.: 1 + # and later change that to 1 self.__ns = namespace self.__prefix = prefix - if text: + + if text is not None: try: self.__document = xml.dom.minidom.parseString(text) except: - if DEBUG: print text + log.error(text) raise self.__elements = [self.__document.documentElement] else: self.__elements = elements self.__document = document - - def add_child(self,name,text=None,ns=True): + + def add_child(self, name, text=None, ns=True): "Adding a child tag to a node" if not ns or not self.__ns: - if DEBUG: print "adding %s" % (name) + log.debug('adding %s', name) element = self.__document.createElement(name) else: - if DEBUG: print "adding %s ns %s %s" % (name, self.__ns,ns) + log.debug('adding %s ns "%s" %s', name, self.__ns, ns) if self.__prefix: element = self.__document.createElementNS(self.__ns, "%s:%s" % (self.__prefix, name)) else: element = self.__document.createElementNS(self.__ns, name) - if text: + # don't append null tags! + if text is not None: if isinstance(text, unicode): element.appendChild(self.__document.createTextNode(text)) else: @@ -139,22 +201,31 @@ class SimpleXMLElement(object): elements=[element], document=self.__document, namespace=self.__ns, - prefix=self.__prefix) - + prefix=self.__prefix, + namespaces_map=self.__namespaces_map) + def __setattr__(self, tag, text): "Add text child tag node (short form)" if tag.startswith("_"): object.__setattr__(self, tag, text) else: - if DEBUG: print "__setattr__(%s,%s)" % (tag, text) - self.add_child(tag,text) + log.debug('__setattr__(%s, %s)', tag, text) + self.add_child(tag, text) + + def __delattr__(self, tag): + "Remove a child tag (non recursive!)" + elements=[__element for __element in self._element.childNodes + if __element.nodeType == __element.ELEMENT_NODE + ] + for element in elements: + self._element.removeChild(element) def add_comment(self, data): "Add an xml comment to this child" comment = self.__document.createComment(data) self._element.appendChild(comment) - def as_xml(self,filename=None,pretty=False): + def as_xml(self, filename=None, pretty=False): "Return the XML representation of the document" if not pretty: return self.__document.toxml('UTF-8') @@ -179,8 +250,13 @@ class SimpleXMLElement(object): def get_namespace_uri(self, ns): "Return the namespace uri for a prefix" - v = self.__document.documentElement.attributes['xmlns:%s' % ns] - return v.value + element = self._element + while element is not None and element.attributes is not None: + try: + return element.attributes['xmlns:%s' % ns].value + except KeyError: + element = element.parentNode + def attributes(self): "Return a dict of attributes for this tag" @@ -189,8 +265,8 @@ class SimpleXMLElement(object): def __getitem__(self, item): "Return xml tag attribute value or a slice of attributes (iter)" - if DEBUG: print "__getitem__(%s)" % item - if isinstance(item,basestring): + log.debug('__getitem__(%s)', item) + if isinstance(item, basestring): if self._element.hasAttribute(item): return self._element.attributes[item].value elif isinstance(item, slice): @@ -203,12 +279,13 @@ class SimpleXMLElement(object): elements=[element], document=self.__document, namespace=self.__ns, - prefix=self.__prefix) - + prefix=self.__prefix, + namespaces_map=self.__namespaces_map) + def add_attribute(self, name, value): "Set an attribute value from a string" self._element.setAttribute(name, value) - + def __setitem__(self, item, value): "Set an attribute value" if isinstance(item,basestring): @@ -218,9 +295,19 @@ class SimpleXMLElement(object): for k, v in value.items(): self.add_attribute(k, v) - def __call__(self, tag=None, ns=None, children=False, error=True): + def __call__(self, tag=None, ns=None, children=False, root=False, + error=True, ): "Search (even in child nodes) and return a child tag by name" try: + if root: + # return entire document + return SimpleXMLElement( + elements=[self.__document.documentElement], + document=self.__document, + namespace=self.__ns, + prefix=self.__prefix, + namespaces_map=self.__namespaces_map + ) if tag is None: # if no name given, iterate over siblings (same level) return self.__iter__() @@ -233,34 +320,35 @@ class SimpleXMLElement(object): elements=[self.__elements[tag]] if ns and not elements: for ns_uri in isinstance(ns, (tuple, list)) and ns or (ns, ): - if DEBUG: print "searching %s by ns=%s" % (tag,ns_uri) + log.debug('searching %s by ns=%s', tag, ns_uri) elements = self._element.getElementsByTagNameNS(ns_uri, tag) - if elements: + if elements: break if self.__ns and not elements: - if DEBUG: print "searching %s by ns=%s" % (tag, self.__ns) + log.debug('searching %s by ns=%s', tag, self.__ns) elements = self._element.getElementsByTagNameNS(self.__ns, tag) if not elements: - if DEBUG: print "searching %s " % (tag) + log.debug('searching %s', tag) elements = self._element.getElementsByTagName(tag) if not elements: - if DEBUG: print self._element.toxml() + #log.debug(self._element.toxml()) if error: - raise AttributeError("No elements found") + raise AttributeError(u"No elements found") else: return return SimpleXMLElement( elements=elements, document=self.__document, namespace=self.__ns, - prefix=self.__prefix) + prefix=self.__prefix, + namespaces_map=self.__namespaces_map) except AttributeError, e: - raise AttributeError("Tag not found: %s (%s)" % (tag, str(e))) + raise AttributeError(u"Tag not found: %s (%s)" % (tag, unicode(e))) def __getattr__(self, tag): "Shortcut for __call__" return self.__call__(tag) - + def __iter__(self): "Iterate over xml tags at this level" try: @@ -269,13 +357,14 @@ class SimpleXMLElement(object): elements=[__element], document=self.__document, namespace=self.__ns, - prefix=self.__prefix) + prefix=self.__prefix, + namespaces_map=self.__namespaces_map) except: raise def __dir__(self): "List xml children tags names" - return [node.tagName for node + return [node.tagName for node in self._element.childNodes if node.nodeType != node.TEXT_NODE] @@ -290,16 +379,17 @@ class SimpleXMLElement(object): elements=elements, document=self.__document, namespace=self.__ns, - prefix=self.__prefix) + prefix=self.__prefix, + namespaces_map=self.__namespaces_map) def __len__(self): "Return elements count" return len(self.__elements) - + def __contains__( self, item): "Search for a tag name in this element or child nodes" return self._element.getElementsByTagName(item) - + def __unicode__(self): "Returns the unicode text nodes of the current element" if self._element.childNodes: @@ -309,7 +399,7 @@ class SimpleXMLElement(object): rc = rc + node.data return rc return '' - + def __str__(self): "Returns the str text nodes of the current element" return unicode(self).encode("utf8","ignore") @@ -323,94 +413,147 @@ class SimpleXMLElement(object): try: return float(self.__str__()) except: - raise IndexError(self._element.toxml()) - + raise IndexError(self._element.toxml()) + _element = property(lambda self: self.__elements[0]) - def unmarshall(self, types): + def unmarshall(self, types, strict=True): "Convert to python values the current serialized xml element" # types is a dict of {tag name: convertion function} + # strict=False to use default type conversion if not specified # example: types={'p': {'a': int,'b': int}, 'c': [{'d':str}]} # expected xml:

12

holachau # returnde value: {'p': {'a':1,'b':2}, `'c':[{'d':'hola'},{'d':'chau'}]} d = {} for node in self(): name = str(node.get_local_name()) + ref_name_type = None + # handle multirefs: href="#id0" + if 'href' in node.attributes().keys(): + href = node['href'][1:] + for ref_node in self(root=True)("multiRef"): + if ref_node['id'] == href: + node = ref_node + ref_name_type = ref_node['xsi:type'].split(":")[1] + break try: fn = types[name] except (KeyError, ), e: - raise TypeError("Tag: %s invalid" % (name,)) - if isinstance(fn,list): + if node.get_namespace_uri("soapenc"): + fn = None # ignore multirefs! + elif 'xsi:type' in node.attributes().keys(): + xsd_type = node['xsi:type'].split(":")[1] + fn = REVERSE_TYPE_MAP[xsd_type] + elif strict: + raise TypeError(u"Tag: %s invalid (type not found)" % (name,)) + else: + # if not strict, use default type conversion + fn = unicode + + if isinstance(fn, list): + # append to existing list (if any) - unnested dict arrays - + value = d.setdefault(name, []) + children = node.children() + for child in (children and children() or []): # Readability counts + value.append(child.unmarshall(fn[0], strict)) + + elif isinstance(fn, tuple): value = [] + _d = {} children = node.children() - for child in children and children() or []: - value.append(child.unmarshall(fn[0])) - elif isinstance(fn,dict): + as_dict = len(fn) == 1 and isinstance(fn[0], dict) + + for child in (children and children() or []): # Readability counts + if as_dict: + _d.update(child.unmarshall(fn[0], strict)) # Merging pairs + else: + value.append(child.unmarshall(fn[0], strict)) + if as_dict: + value.append(_d) + + if name in d: + _tmp = list(d[name]) + _tmp.extend(value) + value = tuple(_tmp) + else: + value = tuple(value) + + elif isinstance(fn, dict): + ##if ref_name_type is not None: + ## fn = fn[ref_name_type] children = node.children() - value = children and children.unmarshall(fn) + value = children and children.unmarshall(fn, strict) else: if fn is None: # xsd:anyType not unmarshalled value = node elif str(node) or fn == str: try: - # get special desserialization function (if any) - fn = TYPE_UNMARSHAL_FN.get(fn,fn) - value = fn(unicode(node)) + # get special deserialization function (if any) + fn = TYPE_UNMARSHAL_FN.get(fn,fn) + if fn == str: + # always return an unicode object: + value = unicode(node) + else: + value = fn(unicode(node)) except (ValueError, TypeError), e: - raise ValueError("Tag: %s: %s" % (name, unicode(e))) + raise ValueError(u"Tag: %s: %s" % (name, unicode(e))) else: value = None d[name] = value return d - - def marshall(self, name, value, add_child=True, add_comments=False, ns=False): + + + def _update_ns(self, name): + """Replace the defined namespace alias with tohse used by the client.""" + pref = self.__ns_rx.search(name) + if pref: + pref = pref.groups()[0] + try: + name = name.replace(pref, self.__namespaces_map[pref]) + except KeyError: + log.warning('Unknown namespace alias %s' % name) + return name + + + def marshall(self, name, value, add_child=True, add_comments=False, + ns=False, add_children_ns=True): "Analize python value and add the serialized XML element using tag name" + # Change node name to that used by a client + name = self._update_ns(name) + if isinstance(value, dict): # serialize dict (value) - child = add_child and self.add_child(name,ns=ns) or self + child = add_child and self.add_child(name, ns=ns) or self for k,v in value.items(): + if not add_children_ns: + ns = False child.marshall(k, v, add_comments=add_comments, ns=ns) elif isinstance(value, tuple): # serialize tuple (value) - child = add_child and self.add_child(name,ns=ns) or self + child = add_child and self.add_child(name, ns=ns) or self + if not add_children_ns: + ns = False for k,v in value: - getattr(self,name).marshall(k, v, add_comments=add_comments, ns=ns) + getattr(self, name).marshall(k, v, add_comments=add_comments, ns=ns) elif isinstance(value, list): # serialize lists - child=self.add_child(name,ns=ns) + child=self.add_child(name, ns=ns) + if not add_children_ns: + ns = False if add_comments: child.add_comment("Repetitive array of:") for t in value: - child.marshall(name,t, False, add_comments=add_comments, ns=ns) + child.marshall(name, t, False, add_comments=add_comments, ns=ns) elif isinstance(value, basestring): # do not convert strings or unicodes - self.add_child(name,value,ns=ns) + self.add_child(name, value,ns=ns) elif value is None: # sent a empty tag? - self.add_child(name,ns=ns) + self.add_child(name, ns=ns) elif value in TYPE_MAP.keys(): # add commented placeholders for simple tipes (for examples/help only) - child = self.add_child(name,ns=ns) + child = self.add_child(name, ns=ns) child.add_comment(TYPE_MAP[value]) - else: # the rest of object types are converted to string + else: # the rest of object types are converted to string # get special serialization function (if any) - fn = TYPE_MARSHAL_FN.get(type(value),str) - self.add_child(name,fn(value),ns=ns) + fn = TYPE_MARSHAL_FN.get(type(value), str) + self.add_child(name, fn(value), ns=ns) def import_node(self, other): x = self.__document.importNode(other._element, True) # deep copy self._element.appendChild(x) - - -if __name__ == "__main__": - span = SimpleXMLElement('pyar11.5') - assert str(span.a)==str(span('a'))==str(span.a(0))=="pyar" - assert span.a['href']=="python.org.ar" - assert int(span.prueba.i)==1 and float(span.prueba.float)==1.5 - span1 = SimpleXMLElement('googleyahoohotmail') - assert [str(a) for a in span1.a()] == ['google', 'yahoo', 'hotmail'] - span1.add_child('a','altavista') - span1.b = "ex msn" - d = {'href':'http://www.bing.com/', 'alt': 'Bing'} - span1.b[:] = d - assert sorted([(k,v) for k,v in span1.b[:]]) == sorted(d.items()) - print span1.as_xml() - assert 'b' in span1 - span.import_node(span1) - print span.as_xml() - diff --git a/gluon/contrib/pysimplesoap/transport.py b/gluon/contrib/pysimplesoap/transport.py new file mode 100644 index 00000000..a1491334 --- /dev/null +++ b/gluon/contrib/pysimplesoap/transport.py @@ -0,0 +1,241 @@ +#!/usr/bin/python +# -*- coding: latin-1 -*- +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation; either version 3, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. + +"Pythonic simple SOAP Client implementation" + +__author__ = "Mariano Reingart (reingart@gmail.com)" +__copyright__ = "Copyright (C) 2008 Mariano Reingart" +__license__ = "LGPL 3.0" + +TIMEOUT = 60 + +import os +import cPickle as pickle +import urllib2 +from urlparse import urlparse +import tempfile +from simplexml import SimpleXMLElement, TYPE_MAP, OrderedDict +import logging + +log = logging.getLogger(__name__) +logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.WARNING) + +# +# We store metadata about what available transport mechanisms we have available. +# +_http_connectors = {} # libname: classimpl mapping +_http_facilities = {} # functionalitylabel: [sequence of libname] mapping + +class TransportBase: + @classmethod + def supports_feature(cls, feature_name): + return cls._wrapper_name in _http_facilities[feature_name] + +# +# httplib2 support. +# +try: + import httplib2 +except ImportError: + TIMEOUT = None # timeout not supported by urllib2 + pass +else: + class Httplib2Transport(httplib2.Http, TransportBase): + _wrapper_version = "httplib2 %s" % httplib2.__version__ + _wrapper_name = 'httplib2' + def __init__(self, timeout, proxy=None, cacert=None, sessions=False): + ##httplib2.debuglevel=4 + kwargs = {} + if proxy: + import socks + kwargs['proxy_info'] = httplib2.ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, **proxy) + print "using proxy", proxy + + # set optional parameters according supported httplib2 version + if httplib2.__version__ >= '0.3.0': + kwargs['timeout'] = timeout + if httplib2.__version__ >= '0.7.0': + kwargs['disable_ssl_certificate_validation'] = cacert is None + kwargs['ca_certs'] = cacert + httplib2.Http.__init__(self, **kwargs) + + _http_connectors['httplib2'] = Httplib2Transport + _http_facilities.setdefault('proxy', []).append('httplib2') + _http_facilities.setdefault('cacert', []).append('httplib2') + + import inspect + if 'timeout' in inspect.getargspec(httplib2.Http.__init__)[0]: + _http_facilities.setdefault('timeout', []).append('httplib2') + +# +# urllib2 support. +# +import urllib2 +class urllib2Transport(TransportBase): + _wrapper_version = "urllib2 %s" % urllib2.__version__ + _wrapper_name = 'urllib2' + def __init__(self, timeout=None, proxy=None, cacert=None, sessions=False): + import sys + if (timeout is not None) and not self.supports_feature('timeout'): + raise RuntimeError('timeout is not supported with urllib2 transport') + if proxy: + raise RuntimeError('proxy is not supported with urllib2 transport') + if cacert: + raise RuntimeError('cacert is not support with urllib2 transport') + + self.request_opener = urllib2.urlopen + if sessions: + from cookielib import CookieJar + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(CookieJar())) + self.request_opener = opener.open + + self._timeout = timeout + + def request(self, url, method="GET", body=None, headers={}): + req = urllib2.Request(url, body, headers) + try: + f = self.request_opener(req, timeout=self._timeout) + except urllib2.HTTPError, f: + if f.code != 500: + raise + return f.info(), f.read() + +_http_connectors['urllib2'] = urllib2Transport +_http_facilities.setdefault('sessions', []).append('urllib2') + +import sys +if sys.version_info >= (2,6): + _http_facilities.setdefault('timeout', []).append('urllib2') +del sys + +# +# pycurl support. +# experimental: pycurl seems faster + better proxy support (NTLM) + ssl features +# +try: + import pycurl +except ImportError: + pass +else: + try: + from cStringIO import StringIO + except ImportError: + from StringIO import StringIO + + class pycurlTransport(TransportBase): + _wrapper_version = pycurl.version + _wrapper_name = 'pycurl' + def __init__(self, timeout, proxy=None, cacert=None, sessions=False): + self.timeout = timeout + self.proxy = proxy or {} + self.cacert = cacert + + def request(self, url, method, body, headers): + c = pycurl.Curl() + c.setopt(pycurl.URL, str(url)) + if 'proxy_host' in self.proxy: + c.setopt(pycurl.PROXY, self.proxy['proxy_host']) + if 'proxy_port' in self.proxy: + c.setopt(pycurl.PROXYPORT, self.proxy['proxy_port']) + if 'proxy_user' in self.proxy: + c.setopt(pycurl.PROXYUSERPWD, "%(proxy_user)s:%(proxy_pass)s" % self.proxy) + self.buf = StringIO() + c.setopt(pycurl.WRITEFUNCTION, self.buf.write) + #c.setopt(pycurl.READFUNCTION, self.read) + #self.body = StringIO(body) + #c.setopt(pycurl.HEADERFUNCTION, self.header) + if self.cacert: + c.setopt(c.CAINFO, str(self.cacert)) + c.setopt(pycurl.SSL_VERIFYPEER, self.cacert and 1 or 0) + c.setopt(pycurl.SSL_VERIFYHOST, self.cacert and 2 or 0) + c.setopt(pycurl.CONNECTTIMEOUT, self.timeout/6) + c.setopt(pycurl.TIMEOUT, self.timeout) + if method=='POST': + c.setopt(pycurl.POST, 1) + c.setopt(pycurl.POSTFIELDS, body) + if headers: + hdrs = ['%s: %s' % (str(k), str(v)) for k, v in headers.items()] + ##print hdrs + c.setopt(pycurl.HTTPHEADER, hdrs) + c.perform() + ##print "pycurl perform..." + c.close() + return {}, self.buf.getvalue() + + _http_connectors['pycurl'] = pycurlTransport + _http_facilities.setdefault('proxy', []).append('pycurl') + _http_facilities.setdefault('cacert', []).append('pycurl') + _http_facilities.setdefault('timeout', []).append('pycurl') + + +class DummyTransport: + "Testing class to load a xml response" + + def __init__(self, xml_response): + self.xml_response = xml_response + + def request(self, location, method, body, headers): + print method, location + print headers + print body + return {}, self.xml_response + + +def get_http_wrapper(library=None, features=[]): + # If we are asked for a specific library, return it. + if library is not None: + try: + return _http_connectors[library] + except KeyError: + raise RuntimeError('%s transport is not available' % (library,)) + + # If we haven't been asked for a specific feature either, then just return our favourite + # implementation. + if not features: + return _http_connectors.get('httplib2', _http_connectors['urllib2']) + + # If we are asked for a connector which supports the given features, then we will + # try that. + current_candidates = _http_connectors.keys() + new_candidates = [] + for feature in features: + for candidate in current_candidates: + if candidate in _http_facilities.get(feature, []): + new_candidates.append(candidate) + current_candidates = new_candidates + new_candidates = [] + + # Return the first candidate in the list. + try: + candidate_name = current_candidates[0] + except IndexError: + raise RuntimeError("no transport available which supports these features: %s" % (features,)) + else: + return _http_connectors[candidate_name] + +def set_http_wrapper(library=None, features=[]): + "Set a suitable HTTP connection wrapper." + global Http + Http = get_http_wrapper(library, features) + return Http + + +def get_Http(): + "Return current transport class" + global Http + return Http + + +# define the default HTTP connection class (it can be changed at runtime!): +set_http_wrapper() + +