diff --git a/applications/admin/controllers/debug.py b/applications/admin/controllers/debug.py index a33c0f8b..c5f9bd4f 100644 --- a/applications/admin/controllers/debug.py +++ b/applications/admin/controllers/debug.py @@ -54,9 +54,13 @@ def interact(): filename = web_debugger.filename lineno = web_debugger.lineno if filename: + # prevent IOError 2 on some circuntances (EAFP instead of os.access) + try: + lines = open(filename).readlines() + except: + lines = "" lines = dict([(i + 1, l) for (i, l) in enumerate( - [l.strip("\n").strip("\r") for l - in open(filename).readlines()])]) + [l.strip("\n").strip("\r") for l in lines])]) filename = os.path.basename(filename) else: lines = {} diff --git a/applications/examples/controllers/soap_examples.py b/applications/examples/controllers/soap_examples.py new file mode 100644 index 00000000..e9ab84a3 --- /dev/null +++ b/applications/examples/controllers/soap_examples.py @@ -0,0 +1,53 @@ +# coding: utf8 + +# SOAP webservices (server and client) example and basic test +# (using pysimplesoap contrib included in web2py) +# for more info see: https://code.google.com/p/pysimplesoap/wiki/Web2Py + +from gluon.tools import Service +service = Service(globals()) + +# define the procedures to be exposed: + +@service.soap('AddStrings', returns={'AddResult': str}, args={'a': str, 'b': str}) +@service.soap('AddIntegers', returns={'AddResult': int}, args={'a': int, 'b': int}) +def add(a, b): + "Add two values" + return a+b + +@service.soap('SubIntegers', returns={'SubResult': int}, args={'a': int, 'b': int}) +def sub(a, b): + "Substract two values" + return a-b + +@service.soap('Division', returns={'divisionResult': float}, args={'a': float, 'b': float}) +def division(a, b): + "Divide two values " + return a / b + + +# expose the soap methods + +def call(): + return service() + +# sample function to test the SOAP RPC + +def test_soap_sub(): + from gluon.contrib.pysimplesoap.client import SoapClient, SoapFault + # build the url to the WSDL (web service description) + # like "http://localhost:8000/webservices/sample/call/soap?WSDL" + url = URL(f="call/soap", vars={"WSDL": ""}, scheme=True) + # create a SOAP client + client = SoapClient(wsdl=url) + # call the SOAP remote method + try: + ret = client.SubIntegers(a=3, b=2) + result = ret['SubResult'] + except SoapFault, sf: + result = sf + response.view = "soap_examples/generic.html" + return dict(xml_request=client.xml_request, + xml_response=client.xml_response, + result=result) + diff --git a/applications/examples/views/soap_examples/generic.html b/applications/examples/views/soap_examples/generic.html new file mode 100644 index 00000000..03ea2fb4 --- /dev/null +++ b/applications/examples/views/soap_examples/generic.html @@ -0,0 +1,8 @@ +{{extend 'layout.html'}} +

SOAP Examples

+

Result

+{{=BEAUTIFY(result)}} +

XML Request

+{{=BEAUTIFY(xml_request)}} +

XML Response

+{{=BEAUTIFY(xml_response)}} diff --git a/gluon/contrib/pysimplesoap/__init__.py b/gluon/contrib/pysimplesoap/__init__.py index 6043241d..28bfee12 100755 --- a/gluon/contrib/pysimplesoap/__init__.py +++ b/gluon/contrib/pysimplesoap/__init__.py @@ -1,7 +1,16 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -"PySimpleSOAP" -import client -import server -import simplexml -import transport \ No newline at end of file + +"""PySimpleSOAP""" + + +__author__ = "Mariano Reingart" +__author_email__ = "reingart@gmail.com" +__copyright__ = "Copyright (C) 2013 Mariano Reingart" +__license__ = "LGPL 3.0" +__version__ = "1.11" + +TIMEOUT = 60 + + +from . import client, server, simplexml, transport diff --git a/gluon/contrib/pysimplesoap/client.py b/gluon/contrib/pysimplesoap/client.py index 75bca191..88300eba 100755 --- a/gluon/contrib/pysimplesoap/client.py +++ b/gluon/contrib/pysimplesoap/client.py @@ -1,5 +1,5 @@ -#!/usr/bin/env python -# -*- coding: latin-1 -*- +#!/usr/bin/python +# -*- coding: utf-8 -*- # 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 @@ -10,114 +10,134 @@ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. -"Pythonic simple SOAP Client implementation" +"""Pythonic simple SOAP Client implementation""" -__author__ = "Mariano Reingart (reingart@gmail.com)" -__copyright__ = "Copyright (C) 2008 Mariano Reingart" -__license__ = "LGPL 3.0" -__version__ = "1.07a" +from __future__ import unicode_literals +import sys +if sys.version > '3': + unicode = str -TIMEOUT = 60 - -import cPickle as pickle +try: + import cPickle as pickle +except ImportError: + import 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 + +from . import __author__, __copyright__, __license__, __version__, TIMEOUT +from .simplexml import SimpleXMLElement, TYPE_MAP, REVERSE_TYPE_MAP, OrderedDict +from .transport import get_http_wrapper, set_http_wrapper, get_Http +# Utility functions used throughout wsdl_parse, moved aside for readability +from .helpers import fetch, sort_dict, make_key, process_element, \ + postprocess_element, get_message, preprocess_schema, \ + get_local_name, get_namespace_prefix, TYPE_MAP, urlsplit + log = logging.getLogger(__name__) -logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.WARNING) class SoapFault(RuntimeError): - def __init__(self,faultcode,faultstring): + 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) + return '%s: %s' % (self.faultcode, self.faultstring) + + if sys.version > '3': + __str__ = __unicode__ + else: + def __str__(self): + return self.__unicode__().encode('ascii', 'ignore') def __repr__(self): - return u"SoapFault(%s, %s)" % (repr(self.faultcode), - repr(self.faultstring)) + return "SoapFault(%s, %s)" % (repr(self.faultcode), + repr(self.faultstring)) # soap protocol specification & namespace soap_namespaces = dict( - soap11="http://schemas.xmlsoap.org/soap/envelope/", - soap="http://schemas.xmlsoap.org/soap/envelope/", - soapenv="http://schemas.xmlsoap.org/soap/envelope/", - soap12="http://www.w3.org/2003/05/soap-env", + soap11='http://schemas.xmlsoap.org/soap/envelope/', + soap='http://schemas.xmlsoap.org/soap/envelope/', + soapenv='http://schemas.xmlsoap.org/soap/envelope/', + soap12='http://www.w3.org/2003/05/soap-env', + soap12env="http://www.w3.org/2003/05/soap-envelope", ) -_USE_GLOBAL_DEFAULT = object() class SoapClient(object): - "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, cacert=None, - sessions=False, soap_server=None, timeout=_USE_GLOBAL_DEFAULT, - http_headers={} + """Simple SOAP Client (simil PHP)""" + def __init__(self, location=None, action=None, namespace=None, + cert=None, exceptions=True, proxy=None, ns=None, + soap_ns=None, wsdl=None, wsdl_basedir='', cache=False, cacert=None, + sessions=False, soap_server=None, timeout=TIMEOUT, + http_headers=None, trace=False, + username=None, password=None, ): """ :param http_headers: Additional HTTP Headers; example: {'Host': 'ipsec.example.com'} """ - self.certssl = cert - self.keyssl = None + self.certssl = cert + self.keyssl = None self.location = location # server location (url) self.action = action # SOAP base action - self.namespace = namespace # message - self.trace = trace # show debug messages + self.namespace = namespace # message self.exceptions = exceptions # lanzar execpiones? (Soap Faults) self.xml_request = self.xml_response = '' - self.http_headers = http_headers + self.http_headers = http_headers or {} + # extract the base directory / url for wsdl relative imports: + if wsdl and wsdl_basedir == '': + # parse the wsdl url, strip the scheme and filename + url_scheme, netloc, path, query, fragment = urlsplit(wsdl) + wsdl_basedir = os.path.dirname(netloc + path) + + self.wsdl_basedir = wsdl_basedir + + # shortcut to print all debugging info and sent / received xml messages + if trace: + logging.basicConfig(level=logging.DEBUG) + if not soap_ns and not ns: - self.__soap_ns = 'soap' # 1.1 + self.__soap_ns = 'soap' # 1.1 elif not soap_ns and ns: - self.__soap_ns = 'soapenv' # 1.2 + self.__soap_ns = 'soapenv' # 1.2 else: self.__soap_ns = soap_ns - - # SOAP Server (special cases like oracle or jbossas6) + + # SOAP Server (special cases like oracle, jbossas6 or jetty) 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-----"): + 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) + log.debug("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: - 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 username and password: + if hasattr(self.http, 'add_credentials'): + self.http.add_credentials(username, password) + + + # namespace prefix, None to use xmlns attribute or False to not use it: + self.__ns = ns 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> @@ -136,127 +156,127 @@ class SoapClient(object): """ # parse wsdl url - self.services = wsdl and self.wsdl_parse(wsdl, debug=trace, cache=cache) + self.services = wsdl and self.wsdl_parse(wsdl, 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 *args, **kwargs: self.wsdl_call(attr,*args,**kwargs) - + """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 *args, **kwargs: self.wsdl_call(attr, *args, **kwargs) + def call(self, method, *args, **kwargs): """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 - + xml = self.__xml % dict(method=method, # method tag name + namespace=self.namespace, # method ns uri + ns=self.__ns, # method ns prefix + soap_ns=self.__soap_ns, # soap prefix & uri + soap_uri=soap_namespaces[self.__soap_ns]) + request = SimpleXMLElement(xml, namespace=self.__ns and self.namespace, + prefix=self.__ns) + + request_headers = kwargs.pop('headers', None) + # serialize parameters if kwargs: - parameters = kwargs.items() + parameters = list(kwargs.items()) else: parameters = args if parameters and isinstance(parameters[0], SimpleXMLElement): # merge xmlelement parameter ("raw" - already marshalled) if parameters[0].children() is not None: for param in parameters[0].children(): - getattr(request,method).import_node(param) + getattr(request, method).import_node(param) + for k,v in parameters[0].attributes().items(): + getattr(request, method)[k] = v 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',): + use_ns = None if (self.__soap_server == "jetty" or self.qualified is False) else True + for k, v in parameters: # dict: tag=valor + getattr(request, method).marshall(k, v, ns=use_ns) + 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) - + delattr(request("Body", ns=list(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:")]) + 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(),) + header = request('Header', ns=list(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(),) + header = request('Header', ns=list(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 isinstance(v, SimpleXMLElement): + # allows a SimpleXMLElement to be constructed and inserted + # rather than a dictionary. marshall doesn't allow ns: prefixes + # in dict key names + header.import_node(v) + else: + header.marshall(k, v, ns=self.__ns, add_children_ns=False) if request_headers: - header = request('Header' , ns=soap_namespaces.values(),) + header = request('Header', ns=list(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): + response = SimpleXMLElement(self.xml_response, namespace=self.namespace, + jetty=self.__soap_server in ('jetty',)) + if self.exceptions and response("Fault", ns=list(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" + """Send SOAP request using HTTP""" if self.location == 'test': return - # location = "%s" % self.location #?op=%s" % (self.location, method) - location = self.location - + # location = '%s' % self.location #?op=%s" % (self.location, method) + location = str(self.location) + if self.services: - soap_action = self.action + soap_action = str(self.action) else: - soap_action = self.action + method - - headers={ + soap_action = str(self.action) + method + + headers = { 'Content-type': 'text/xml; charset="UTF-8"', 'Content-length': str(len(xml)), - "SOAPAction": "\"%s\"" % (soap_action) + '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") - + log.debug('\n'.join(["%s: %s" % (k, v) for k, v in headers.items()])) + log.debug(xml) + 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 - print '\n'.join(["%s: %s" % (k,v) for k,v in response.items()]) - print content#.decode("utf8","ignore") - print "="*80 - return content + log.debug('\n'.join(["%s: %s" % (k, v) for k, v in response.items()])) + log.debug(content) + return content def get_operation(self, method): # try to find operation in wsdl file - soap_ver = self.__soap_ns == 'soap12' and 'soap12' or 'soap11' + soap_ver = self.__soap_ns.startswith('soap12') and 'soap12' or 'soap11' if not self.service_port: for service_name, service in self.services.items(): for port_name, port in [port for port in service['ports'].items()]: @@ -264,400 +284,347 @@ class SoapClient(object): self.service_port = service_name, port_name break else: - raise RuntimeError("Cannot determine service in WSDL: " - "SOAP version: %s" % soap_ver) + raise RuntimeError('Cannot determine service in WSDL: ' + 'SOAP version: %s' % soap_ver) else: port = self.services[self.service_port[0]]['ports'][self.service_port[1]] - self.location = port['location'] - operation = port['operations'].get(unicode(method)) + if not self.location: + self.location = port['location'] + operation = port['operations'].get(method) if not operation: - raise RuntimeError("Operation %s not found in WSDL: " - "Service/Port Type: %s" % + raise RuntimeError('Operation %s not found in WSDL: ' + '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" + """Pre and post process SOAP call, input and output parameters using WSDL""" soap_uri = soap_namespaces[self.__soap_ns] operation = self.get_operation(method) + # 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) - def sort_dict(od, d): - if isinstance(od, dict): - ret = OrderedDict() - for k in od.keys(): - v = d.get(k) - # 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) - for v1 in v] - ret[str(k)] = v - return ret - else: - return d + + if 'namespace' in operation: + self.namespace = operation['namespace'] or '' + self.qualified = operation['qualified'] + # construct header and parameters if header: self.__call_headers = sort_dict(header, self.__headers) + method, params = self.wsdl_call_get_params(method, input, *args, **kwargs) + + # call remote procedure + response = self.call(method, *params) + # parse results: + resp = response('Body', ns=soap_uri).children().unmarshall(output) + return resp and list(resp.values())[0] # pass Response tag children + + def wsdl_call_get_params(self, method, input, *args, **kwargs): + """Build params from input and args/kwargs""" + params = inputname = inputargs = None + all_args = {} + if input: + inputname = list(input.keys())[0] + inputargs = input[inputname] + 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() - if self.__soap_server == "axis": + d = {} + for idx, arg in enumerate(args): + key = list(inputargs.keys())[idx] + if isinstance(arg, dict): + if key in arg: + d[key] = arg[key] + else: + raise KeyError('Unhandled key %s. use client.help(method)') + else: + d[key] = arg + all_args.update({inputname: d}) + + if input and (kwargs or all_args): + if kwargs: + all_args.update({inputname: kwargs}) + valid, errors, warnings = self.wsdl_validate_params(input, all_args) + if not valid: + raise ValueError('Invalid Args Structure. Errors: %s' % errors) + params = list(sort_dict(input, all_args).values())[0].items() + # TODO: check style and document attributes + if self.__soap_server in ('axis', ): # use the operation name method = method else: # use the message (element) name - method = input.keys()[0] + method = inputname #elif not input: - #TODO: no message! (see wsmtxca.dummy) + #TODO: no message! (see wsmtxca.dummy) else: params = kwargs and kwargs.items() - # call remote procedure - response = self.call(method, *params) - # parse results: - resp = response('Body',ns=soap_uri).children().unmarshall(output) - return resp and resp.values()[0] # pass Response tag children + + return (method, params) + + def wsdl_validate_params(self, struct, value): + """Validate the arguments (actual values) for the parameters structure. + Fail for any invalid arguments or type mismatches.""" + errors = [] + warnings = [] + valid = True + + # Determine parameter type + if type(struct) == type(value): + typematch = True + if not isinstance(struct, dict) and isinstance(value, dict): + typematch = True # struct can be an OrderedDict + else: + typematch = False + + if struct == str: + struct = unicode # fix for py2 vs py3 string handling + + if not isinstance(struct, (list, dict, tuple)) and struct in TYPE_MAP.keys(): + if not type(value) == struct: + try: + struct(value) # attempt to cast input to parameter type + except: + valid = False + errors.append('Type mismatch for argument value. parameter(%s): %s, value(%s): %s' % (type(struct), struct, type(value), value)) + + elif isinstance(struct, list) and len(struct) == 1 and not isinstance(value, list): + # parameter can have a dict in a list: [{}] indicating a list is allowed, but not needed if only one argument. + next_valid, next_errors, next_warnings = self.wsdl_validate_params(struct[0], value) + if not next_valid: + valid = False + errors.extend(next_errors) + warnings.extend(next_warnings) + + # traverse tree + elif isinstance(struct, dict): + if struct and value: + for key in value: + if key not in struct: + valid = False + errors.append('Argument key %s not in parameter. parameter: %s, args: %s' % (key, struct, value)) + else: + next_valid, next_errors, next_warnings = self.wsdl_validate_params(struct[key], value[key]) + if not next_valid: + valid = False + errors.extend(next_errors) + warnings.extend(next_warnings) + for key in struct: + if key not in value: + warnings.append('Parameter key %s not in args. parameter: %s, value: %s' % (key, struct, value)) + elif struct and not value: + warnings.append('parameter keys not in args. parameter: %s, args: %s' % (struct, value)) + elif not struct and value: + valid = False + errors.append('Args keys not in parameter. parameter: %s, args: %s' % (struct, value)) + else: + pass + elif isinstance(struct, list): + struct_list_value = struct[0] + for item in value: + next_valid, next_errors, next_warnings = self.wsdl_validate_params(struct_list_value, item) + if not next_valid: + valid = False + errors.extend(next_errors) + warnings.extend(next_warnings) + elif not typematch: + valid = False + errors.append('Type mismatch. parameter(%s): %s, value(%s): %s' % (type(struct), struct, type(value), value)) + + return (valid, errors, warnings) def help(self, method): - "Return operation documentation and invocation/returned value example" + """Return operation documentation and invocation/returned value example""" operation = self.get_operation(method) input = operation.get('input') - input = input and input.values() and input.values()[0] + input = input and input.values() and list(input.values())[0] if isinstance(input, dict): - input = ", ".join("%s=%s" % (k,repr(v)) for k,v - in input.items()) + 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] + output = list(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",""), + return "%s(%s)\n -> %s:\n\n%s\nHeaders: %s" % ( + method, + input or '', + output and output or '', + operation.get('documentation', ''), headers, - ) + ) - def wsdl_parse(self, url, debug=False, cache=False): - "Parse Web Service Description v1.1" + def wsdl_parse(self, url, cache=False): + """Parse Web Service Description v1.1""" - log.debug("wsdl url: %s" % url) + log.debug('Parsing 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() + # 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) + 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") + 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: + 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)) + warnings.warn('version or url mismatch! discarding cached wsdl', RuntimeWarning) + 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', - } - 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: 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) - + 'http://schemas.xmlsoap.org/wsdl/soap/': 'soap11', + 'http://schemas.xmlsoap.org/wsdl/soap12/': 'soap12', + } + wsdl_uri = 'http://schemas.xmlsoap.org/wsdl/' + xsd_uri = 'http://www.w3.org/2001/XMLSchema' + xsi_uri = 'http://www.w3.org/2001/XMLSchema-instance' + # always return an unicode object: - REVERSE_TYPE_MAP[u'string'] = unicode + REVERSE_TYPE_MAP['string'] = str - def fetch(url): - "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) and not force_download: - log.info("Reading file %s" % (filename, )) - f = open(filename, "r") - xml = f.read() - f.close() - else: - 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: - 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) + xml = fetch(url, self.http, cache, force_download, self.wsdl_basedir) # Parse WSDL XML: wsdl = SimpleXMLElement(xml, namespace=wsdl_uri) + # Extract useful data: + self.namespace = "" + self.documentation = unicode(wsdl('documentation', error=False)) or '' + + # some wsdl are splitted down in several files, join them: + imported_wsdls = {} + for element in wsdl.children() or []: + if element.get_local_name() in ('import'): + wsdl_namespace = element['namespace'] + wsdl_location = element['location'] + if wsdl_location is None: + log.warning('WSDL location not provided for %s!' % wsdl_namespace) + continue + if wsdl_location in imported_wsdls: + log.warning('WSDL %s already imported!' % wsdl_location) + continue + imported_wsdls[wsdl_location] = wsdl_namespace + log.debug('Importing wsdl %s from %s' % (wsdl_namespace, wsdl_location)) + # Open uri and read xml: + xml = fetch(wsdl_location, self.http, cache, force_download, self.wsdl_basedir) + # Parse imported XML schema (recursively): + imported_wsdl = SimpleXMLElement(xml, namespace=xsd_uri) + # merge the imported wsdl into the main document: + wsdl.import_node(imported_wsdl) + # warning: do not process schemas to avoid infinite recursion! + + # detect soap prefix and uri (xmlns attributes of ) xsd_ns = None soap_uris = {} for k, v in wsdl[:]: - if v in soap_ns and k.startswith("xmlns:"): + if v in soap_ns and k.startswith('xmlns:'): soap_uris[get_local_name(k)] = v - if v== xsd_uri and k.startswith("xmlns:"): + if v == xsd_uri and k.startswith('xmlns:'): xsd_ns = get_local_name(k) - # 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 - + 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'] + service_name = service['name'] if not service_name: - continue # empty service? - if debug: log.debug("Processing service %s" % service_name) + continue # empty service? serv = services.setdefault(service_name, {'ports': {}}) - serv['documentation']=service['documentation'] or '' + serv['documentation'] = service['documentation'] or '' for port in service.port: binding_name = get_local_name(port['binding']) - address = port('address', ns=soap_uris.values(), error=False) + operations[binding_name] = {} + address = port('address', ns=list(soap_uris.values()), error=False) location = address and address['location'] or None soap_uri = address and soap_uris.get(address.get_prefix()) soap_ver = soap_uri and soap_ns.get(soap_uri) - bindings[binding_name] = {'service_name': service_name, - 'location': location, - 'soap_uri': soap_uri, 'soap_ver': soap_ver, - } + bindings[binding_name] = {'name': binding_name, + 'service_name': service_name, + 'location': location, + '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: log.debug("Processing binding %s" % service_name) - soap_binding = binding('binding', ns=soap_uris.values(), error=False) + soap_binding = binding('binding', ns=list(soap_uris.values()), error=False) transport = soap_binding and soap_binding['transport'] or None port_type_name = get_local_name(binding['type']) bindings[binding_name].update({ 'port_type_name': port_type_name, 'transport': transport, 'operations': {}, - }) - port_type_bindings[port_type_name] = bindings[binding_name] + }) + if port_type_name not in port_type_bindings: + port_type_bindings[port_type_name] = [] + port_type_bindings[port_type_name].append(bindings[binding_name]) for operation in binding.operation: op_name = operation['name'] - op = operation('operation',ns=soap_uris.values(), error=False) + op = operation('operation', ns=list(soap_uris.values()), error=False) action = op and op['soapAction'] - d = operations.setdefault(op_name, {}) + d = operations[binding_name].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) + body = input and input('body', ns=list(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) + body = output and output('body', ns=list(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) + header = input and input('header', ns=list(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) + header = output and output('header', ns=list(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, element_type): - "Parse and define simple element types" - 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: 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: log.debug("%s has not children! %s" % (element_name,tag)) - continue #TODO: abstract? - d = OrderedDict() - for e in children: - t = e['type'] - if not t: - t = e['base'] # complexContent (extension)! - if not t: - t = 'anyType' # no type given! - t = t.split(":") - if len(t)>1: - ns, type_name = t - else: - ns, type_name = None, t[0] - if element_name == type_name: - 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: - 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: log.debug("complexConent/simpleType/element %s = %s" % (element_name, type_name)) - d[None] = fn - 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(), 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) + d['action'] = action + # check axis2 namespace at schema types attributes (europa.eu checkVat) + if "http://xml.apache.org/xml-soap" in dict(wsdl[:]).values(): + # get the sub-namespace in the first schema element (see issue 8) + if wsdl('types', error=False): + schema = wsdl.types('schema', ns=xsd_uri) + attrs = dict(schema[:]) + self.namespace = attrs.get('targetNamespace', self.namespace) + if not self.namespace or self.namespace == "urn:DefaultNamespace": + self.namespace = wsdl['targetNamespace'] or self.namespace + imported_schemas = {} + global_namespaces = {None: self.namespace} - def preprocess_schema(schema): - "Find schema elements and complex types" - 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: log.debug("Schema location not provided for %s!" % (schema_namespace, )) - continue - if schema_location in imported_schemas: - 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) - # Open uri and read xml: - xml = fetch(schema_location) - # Parse imported XML schema (recursively): - imported_schema = SimpleXMLElement(xml, namespace=xsd_uri) - preprocess_schema(imported_schema) + # process current wsdl schema (if any): + if wsdl('types', error=False): + for schema in wsdl.types('schema', ns=xsd_uri): + preprocess_schema(schema, imported_schemas, elements, xsd_uri, + self.__soap_server, self.http, cache, + force_download, self.wsdl_basedir, + global_namespaces=global_namespaces) - element_type = element.get_local_name() - if element_type in ('element', 'complexType', "simpleType"): - element_name = unicode(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': - children = element("restriction", ns=xsd_uri) - elif element.get_local_name() == 'element' and element['type']: - children = element - else: - children = element.children() - if children: - children = children.children() - elif element.get_local_name() == 'element': - children = element - if children: - process_element(element_name, children, element_type) - - def postprocess_element(elements): - "Fix unresolved references (elements referenced before its definition, thanks .net)" - for k,v in elements.items(): - if isinstance(v, OrderedDict): - if v.array: - elements[k] = [v] # convert arrays to python lists - if v!=elements: #TODO: fix recursive elements - postprocess_element(v) - if None in v and v[None]: # extension base? - if isinstance(v[None], dict): - for i, kk in enumerate(v[None]): - # extend base -keep orginal order- - if v[None] is not None: - elements[k].insert(kk, v[None][kk], i) - del v[None] - else: # "alias", just replace - 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) - - postprocess_element(elements) + # 2nd phase: alias, postdefined elements, extend bases, convert lists + postprocess_element(elements, []) for message in wsdl.message: - if debug: log.debug("Processing message %s" % message['name']) for part in message('part', error=False) or []: element = {} element_name = part['element'] @@ -668,112 +635,110 @@ class SoapClient(object): 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) + fn = REVERSE_TYPE_MAP.get(element_name, None) element = {part['name']: fn} # emulate a true Element (complexType) - messages.setdefault((message['name'], None), {message['name']: OrderedDict()}).values()[0].update(element) + list(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')) + fn = elements.get(make_key(element_name, 'element', type_uri)) if not fn: # some axis servers uses complexType for part messages - fn = elements.get(make_key(element_name, 'complexType')) + fn = elements.get(make_key(element_name, 'complexType', type_uri)) 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: 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['documentation'] = unicode(operation('documentation', error=False) or '') - if binding['soap_ver']: - #TODO: separe operation_binding from operation (non SOAP?) - 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) + for binding in port_type_bindings.get(port_type_name, []): + for operation in port_type.operation: + op_name = operation['name'] + op = operations[binding['name']][op_name] + op['documentation'] = unicode(operation('documentation', error=False)) or '' + if binding['soap_ver']: + #TODO: separe operation_binding from operation (non SOAP?) + 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(messages, header_msg or input_msg, header_part) + else: + header = None # not enought info to search the header message: + op['input'] = get_message(messages, input_msg, op['parts'].get('input_body')) + op['header'] = header + try: + element = list(op['input'].values())[0] + ns_uri = element.namespace + qualified = element.qualified + except AttributeError: + # TODO: fix if no parameters parsed or "variants" + ns = get_namespace_prefix(operation.input['message']) + ns_uri = operation.get_namespace_uri(ns) + qualified = None + if ns_uri: + op['namespace'] = ns_uri + op['qualified'] = qualified 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 + op['input'] = None + op['header'] = None + if operation('output', error=False): + output_msg = get_local_name(operation.output['message']) + op['output'] = get_message(messages, output_msg, op['parts'].get('output_body')) + else: + op['output'] = None + + # dump the full service/port/operation map + #log.debug(pprint.pformat(services)) - if debug: - import pprint - 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, + '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." + """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" + """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) + log.debug('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") + """Parses proxy address user:pass@host:port into a dict suitable for httplib2""" proxy_dict = {} if proxy_str is None: - return - if "@" in proxy_str: - user_pass, host_port = proxy_str.split("@") + return + if '@' in proxy_str: + user_pass, host_port = proxy_str.split('@') else: - user_pass, host_port = "", proxy_str - if ":" in host_port: - host, port = host_port.split(":") + user_pass, host_port = '', proxy_str + if ':' in host_port: + host, port = host_port.split(':') proxy_dict['proxy_host'], proxy_dict['proxy_port'] = host, int(port) - if ":" in user_pass: - proxy_dict['proxy_user'], proxy_dict['proxy_pass'] = user_pass.split(":") + if ':' in user_pass: + proxy_dict['proxy_user'], proxy_dict['proxy_pass'] = user_pass.split(':') return proxy_dict - - -if __name__ == "__main__": + + +if __name__ == '__main__': pass diff --git a/gluon/contrib/pysimplesoap/helpers.py b/gluon/contrib/pysimplesoap/helpers.py new file mode 100644 index 00000000..b0e1af7d --- /dev/null +++ b/gluon/contrib/pysimplesoap/helpers.py @@ -0,0 +1,489 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# 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 helpers""" + + +from __future__ import unicode_literals +import sys +if sys.version > '3': + basestring = unicode = str + +import datetime +from decimal import Decimal +import os +import logging +import hashlib +import warnings + +try: + import urllib2 + from urlparse import urlsplit +except ImportError: + from urllib import request as urllib2 + from urllib.parse import urlsplit + +from . import __author__, __copyright__, __license__, __version__ + + +log = logging.getLogger(__name__) + + +def fetch(url, http, cache=False, force_download=False, wsdl_basedir=''): + """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, os.path.join(wsdl_basedir, url)) + else: + tmp_url = "%s:%s" % (scheme, os.path.join(wsdl_basedir, url)) + log.debug('Scheme not found, trying %s' % scheme) + return fetch(tmp_url, http, cache, force_download, wsdl_basedir) + except Exception as 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.encode('utf8')).hexdigest() + if isinstance(cache, basestring): + 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 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, http._wrapper_version)) + response, xml = http.request(url, 'GET', None, {}) + if cache: + 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 + + +def sort_dict(od, d): + """Sort parameters (same order as xsd:sequence)""" + if isinstance(od, dict): + ret = OrderedDict() + for k in od.keys(): + v = d.get(k) + # 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) for v1 in v] + ret[k] = v + if hasattr(od, 'namespace'): + ret.namespace = od.namespace + ret.qualified = od.qualified + return ret + else: + return d + + +def make_key(element_name, element_type, namespace): + """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" % (element_name, eltype)) + return (element_name, eltype, namespace) + + +def process_element(elements, element_name, node, element_type, xsd_uri, dialect, namespace, qualified=None, + soapenc_uri = 'http://schemas.xmlsoap.org/soap/encoding/'): + """Parse and define simple element types""" + + 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'): + log.debug('%s has no children! %s' % (element_name, tag)) + children = tag # element "alias"? + alias = True + elif tag.children(): + children = tag.children() + alias = False + else: + log.debug('%s has no children! %s' % (element_name, tag)) + continue # TODO: abstract? + d = OrderedDict() + d.namespace = namespace + d.qualified = qualified + for e in children: + t = e['type'] + if not t: + t = e['base'] # complexContent (extension)! + if not t: + t = e['ref'] # reference to another element + if not t: + # "anonymous" elements had no type attribute but children + if e['name'] and e.children(): + # create a type name to process the children + t = "%s_%s" % (element_name, e['name']) + c = e.children() + et = c.get_local_name() + c = c.children() + process_element(elements, t, c, et, xsd_uri, dialect, namespace, qualified) + else: + t = 'anyType' # no type given! + t = t.split(":") + if len(t) > 1: + ns, type_name = t + else: + ns, type_name = None, t[0] + if element_name == type_name and not alias and len(children) > 1: + continue # abort to prevent infinite recursion + uri = ns and e.get_namespace_uri(ns) or xsd_uri + if uri in (xsd_uri, soapenc_uri) and type_name != 'Array': + # look for the type, None == any + fn = REVERSE_TYPE_MAP.get(type_name, None) + elif uri == soapenc_uri and type_name == 'Array': + # arrays of simple types (look at the attribute tags): + fn = [] + for a in e.children(): + for k, v in a[:]: + if k.endswith(":arrayType"): + type_name = v + if ":" in type_name: + type_name = type_name[type_name.index(":")+1:] + if "[]" in type_name: + type_name = type_name[:type_name.index("[]")] + fn.append(REVERSE_TYPE_MAP.get(type_name, None)) + else: + fn = None + + if not fn: + # simple / complex type, postprocess later + if ns: + fn_namespace = uri # use the specified namespace + else: + fn_namespace = namespace # use parent namespace (default) + for k, v in e[:]: + if k.startswith("xmlns:"): + # get the namespace uri from the element + fn_namespace = v + fn = elements.setdefault(make_key(type_name, 'complexType', fn_namespace), OrderedDict()) + + if e['maxOccurs'] == 'unbounded' or (uri == soapenc_uri and type_name == 'Array'): + # it's an array... TODO: compound arrays? and check ns uri! + if isinstance(fn, OrderedDict): + if len(children) > 1 and dialect in ('jetty',): + # Jetty style support + # {'ClassName': [{'attr1': val1, 'attr2': val2}] + fn.array = True + else: + # .NET style support (backward compatibility) + # [{'ClassName': {'attr1': val1, 'attr2': val2}] + d.array = True + else: + if dialect in ('jetty',): + # scalar support [{'attr1': [val1]}] + fn = [fn] + else: + d.array = True + + if (e['name'] is not None and not alias) or e['ref']: + e_name = e['name'] or type_name # for refs, use the type name + d[e_name] = fn + else: + log.debug('complexContent/simpleType/element %s = %s' % (element_name, type_name)) + d[None] = fn + if e is not None and e.get_local_name() == 'extension' and e.children(): + # extend base element: + process_element(elements, element_name, e.children(), element_type, xsd_uri, dialect, namespace, qualified) + elements.setdefault(make_key(element_name, element_type, namespace), OrderedDict()).update(d) + + +def postprocess_element(elements, processed): + """Fix unresolved references (elements referenced before its definition, thanks .net)""" + + # avoid already processed elements: + if elements in processed: + return + processed.append(elements) + + for k, v in elements.items(): + if isinstance(v, OrderedDict): + if v != elements: # TODO: fix recursive elements + postprocess_element(v, processed) + if None in v and v[None]: # extension base? + if isinstance(v[None], dict): + for i, kk in enumerate(v[None]): + # extend base -keep orginal order- + if v[None] is not None: + elements[k].insert(kk, v[None][kk], i) + del v[None] + else: # "alias", just replace + log.debug('Replacing %s = %s' % (k, v[None])) + elements[k] = v[None] + #break + if v.array: + elements[k] = [v] # convert arrays to python lists + if isinstance(v, list): + for n in v: # recurse list + if isinstance(n, (OrderedDict, list)): + #if n != elements: # TODO: fix recursive elements + postprocess_element(n, processed) + + +def get_message(messages, 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 + + +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) + + +def preprocess_schema(schema, imported_schemas, elements, xsd_uri, dialect, http, cache, force_download, wsdl_basedir, global_namespaces=None, qualified=False): + """Find schema elements and complex types""" + + from .simplexml import SimpleXMLElement # here to avoid recursive imports + + # analyze the namespaces used in this schema + local_namespaces = {} + for k, v in schema[:]: + if k.startswith("xmlns"): + local_namespaces[get_local_name(k)] = v + if k == 'targetNamespace': + # URI namespace reference for this schema + if v == "urn:DefaultNamespace": + v = global_namespaces[None] + local_namespaces[None] = v + if k == 'elementFormDefault': + qualified = (v == "qualified") + # add schema namespaces to the global namespace dict = {URI: ns prefix} + for ns in local_namespaces.values(): + if ns not in global_namespaces: + global_namespaces[ns] = 'ns%s' % len(global_namespaces) + + for element in schema.children() or []: + if element.get_local_name() in ('import', 'include',): + schema_namespace = element['namespace'] + schema_location = element['schemaLocation'] + if schema_location is None: + log.debug('Schema location not provided for %s!' % schema_namespace) + continue + if schema_location in imported_schemas: + log.debug('Schema %s already imported!' % schema_location) + continue + imported_schemas[schema_location] = schema_namespace + log.debug('Importing schema %s from %s' % (schema_namespace, schema_location)) + # Open uri and read xml: + xml = fetch(schema_location, http, cache, force_download, wsdl_basedir) + + # Parse imported XML schema (recursively): + imported_schema = SimpleXMLElement(xml, namespace=xsd_uri) + preprocess_schema(imported_schema, imported_schemas, elements, xsd_uri, dialect, http, cache, force_download, wsdl_basedir, global_namespaces, qualified) + + element_type = element.get_local_name() + if element_type in ('element', 'complexType', "simpleType"): + namespace = local_namespaces[None] # get targetNamespace + element_ns = global_namespaces[ns] # get the prefix + element_name = element['name'] + 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': + children = element('restriction', ns=xsd_uri) + elif element.get_local_name() == 'element' and element['type']: + children = element + else: + children = element.children() + if children: + children = children.children() + elif element.get_local_name() == 'element': + children = element + if children: + process_element(elements, element_name, children, element_type, xsd_uri, dialect, namespace, qualified) + + +# simplexml utilities: + +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() +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: _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(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) + +if sys.version > '3': + long = Alias(int, 'long') +byte = Alias(str, 'byte') +short = Alias(int, 'short') +double = Alias(float, 'double') +integer = Alias(long, 'integer') +DateTime = datetime.datetime +Date = datetime.date +Time = datetime.time + +# Define convertion function (python type): xml schema type +TYPE_MAP = { + 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()]) + +REVERSE_TYPE_MAP.update({ + 'base64Binary': str, +}) + +# insert str here to avoid collision in REVERSE_TYPE_MAP (i.e. decoding errors) +if str not in TYPE_MAP: + TYPE_MAP[str] = 'string' + + +class OrderedDict(dict): + """Minimal ordered dictionary for xsd:sequences""" + def __init__(self): + self.__keys = [] + self.array = False + self.namespace = None + self.qualified = None + + def __setitem__(self, key, value): + if key not in self.__keys: + self.__keys.append(key) + dict.__setitem__(self, key, value) + + def insert(self, key, value, index=0): + if key not in self.__keys: + self.__keys.insert(index, key) + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + if key in self.__keys: + self.__keys.remove(key) + dict.__delitem__(self, key) + + def __iter__(self): + return iter(self.__keys) + + def keys(self): + return self.__keys + + def items(self): + return [(key, self[key]) for key in self.__keys] + + def update(self, other): + for k, v in other.items(): + self[k] = v + # do not change if we are an array but the other is not: + if isinstance(other, OrderedDict) and not self.array: + self.array = other.array + if isinstance(other, OrderedDict) and not self.namespace: + self.namespace = other.namespace + self.qualified = other.qualified + + def copy(self): + "Make a duplicate" + new = OrderedDict() + new.update(self) + return new + + def __str__(self): + return "%s" % dict.__str__(self) + + def __repr__(self): + s = "{%s}" % ", ".join(['%s: %s' % (repr(k), repr(v)) for k, v in self.items()]) + if self.array and False: + s = "[%s]" % s + return s diff --git a/gluon/contrib/pysimplesoap/server.py b/gluon/contrib/pysimplesoap/server.py index 3888b49c..fb971494 100755 --- a/gluon/contrib/pysimplesoap/server.py +++ b/gluon/contrib/pysimplesoap/server.py @@ -1,5 +1,5 @@ -#!/usr/bin/env python -# -*- coding: latin-1 -*- +#!/usr/bin/python +# -*- coding: utf-8 -*- # 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 @@ -10,30 +10,41 @@ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. -"Simple SOAP Server implementation" +"""Pythonic simple SOAP Server implementation""" -__author__ = "Mariano Reingart (reingart@gmail.com)" -__copyright__ = "Copyright (C) 2010 Mariano Reingart" -__license__ = "LGPL 3.0" -__version__ = "1.03c" +from __future__ import unicode_literals +import sys +if sys.version > '3': + unicode = str + + +import datetime +import sys import logging +import warnings import re import traceback -from simplexml import SimpleXMLElement, TYPE_MAP, Date, Decimal +try: + from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer +except ImportError: + from http.server import BaseHTTPRequestHandler, HTTPServer + +from . import __author__, __copyright__, __license__, __version__ +from .simplexml import SimpleXMLElement, TYPE_MAP, Date, Decimal log = logging.getLogger(__name__) -# Deprecated -DEBUG = False -NS_RX=re.compile(r'xmlns:(\w+)="(.+?)"') +# Deprecated? +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/", + """Simple Dispatcher for SOAP Server""" + + 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, @@ -45,13 +56,13 @@ class SoapDispatcher(object): :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", @@ -59,13 +70,13 @@ class SoapDispatcher(object): ... namespace = "http://external.mt.moboperator", prefix="external", ... documentation = 'moboperator MTClientWS', ... namespaces = { - ... 'external': 'http://external.mt.moboperator', + ... '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': [ @@ -73,23 +84,22 @@ class SoapDispatcher(object): ... {'model:description': 'desc'}, ... ]} ... return ret - + Our prefixes will be changed to those used by the client. """ self.methods = {} self.name = name self.documentation = documentation - self.action = action # base SoapAction + self.action = action # base SoapAction self.location = location - self.namespace = namespace # targetNamespace + self.namespace = namespace # targetNamespace 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. @@ -99,56 +109,57 @@ class SoapDispatcher(object): 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+'/>') + 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__", "") - - - def dispatch(self, xml, action=None): - "Receive and proccess SOAP call" + + def dispatch(self, xml, action=None, fault=None): + """Receive and process SOAP call, returns the xml""" + # a dict can be sent in fault to expose it to the caller # default values: prefix = self.prefix - ret = fault = None + ret = None + if fault is None: + fault = {} 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 = dict(((v, k) for k, v in self.namespaces.items())) # 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",): + "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 + _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: @@ -159,21 +170,21 @@ class SoapDispatcher(object): 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 - + args = {} # no parameters + soap_fault_code = 'Server' # execute function ret = function(**args) log.debug('dispathed method returns: %s', ret) - except Exception: # This shouldn't be one huge try/except + except Exception: # This shouldn't be one huge try/except import sys etype, evalue, etb = sys.exc_info() log.error(traceback.format_exc()) @@ -182,43 +193,43 @@ class SoapDispatcher(object): detail += '\n\nXML REQUEST\n\n' + xml else: detail = None - fault = {'faultcode': "%s.%s" % (soap_fault_code, etype.__name__), - 'faultstring': unicode(evalue), - 'detail': detail} + fault.update({'faultcode': "%s.%s" % (soap_fault_code, etype.__name__), + 'faultstring': 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"/>""" - + xmlns:%(prefix)s="%(namespace)s"/>""" + xml %= { # a %= {} is a shortcut for a = a % {} - 'namespace': self.namespace, + 'namespace': self.namespace, 'prefix': prefix, - 'soap_ns': soap_ns, + '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 + mapping = dict(((k, _ns_reversed[v]) for k, v in self.namespaces.items())) # Switch keys-values and change value # and get {'model': u'mod', 'external': u'ext'} - - response = SimpleXMLElement(xml, + + response = SimpleXMLElement(xml, namespace=self.namespace, - namespaces_map = mapping, + 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) @@ -226,14 +237,24 @@ class SoapDispatcher(object): # return normal value res = body.add_child("%sResponse" % name, ns=prefix) if not prefix: - res['xmlns'] = self.namespace # add target namespace + res['xmlns'] = self.namespace # add target namespace # serialize returned values (response) if type definition available if returns_types: - if not isinstance(ret, dict): + # TODO: full sanity check of type structure (recursive) + complex_type = isinstance(ret, dict) + if complex_type: + # check if type mapping correlates with return value + types_ok = all([k in returns_types for k in ret.keys()]) + if not types_ok: + warnings.warn("Return value doesn't match type structure: " + "%s vs %s" % (str(returns_types), str(ret))) + if not complex_type or not types_ok: + # backward compatibility for scalar and simple types res.marshall(returns_types.keys()[0], ret, ) else: - for k,v in ret.items(): + # new style for complex classes + for k, v in ret.items(): res.marshall(k, v) elif returns_types is None: # merge xmlelement returned @@ -246,16 +267,16 @@ class SoapDispatcher(object): # 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 a list of aregistered operations""" + return [(method, doc) for method, (function, returns, args, doc) in self.methods.items()] def help(self, method=None): - "Generate sample request and response messages" + """Generate sample request and response messages""" (function, returns, args, doc) = self.methods[method] xml = """ <%(method)s xmlns="%(namespace)s"/> -""" % {'method':method, 'namespace':self.namespace} +""" % {'method': method, 'namespace': self.namespace} request = SimpleXMLElement(xml, namespace=self.namespace, prefix=self.prefix) if args: items = args.items() @@ -263,13 +284,13 @@ class SoapDispatcher(object): items = [('value', None)] else: items = [] - for k,v in items: + for k, v in items: request(method).marshall(k, v, add_comments=True, ns=False) xml = """ <%(method)sResponse xmlns="%(namespace)s"/> -""" % {'method':method, 'namespace':self.namespace} +""" % {'method': method, 'namespace': self.namespace} response = SimpleXMLElement(xml, namespace=self.namespace, prefix=self.prefix) if returns: items = returns.items() @@ -277,16 +298,15 @@ class SoapDispatcher(object): items = [('value', None)] else: items = [] - for k,v in items: - response('%sResponse'%method).marshall(k, v, add_comments=True, ns=False) + for k, v in items: + response('%sResponse' % method).marshall(k, v, add_comments=True, ns=False) return request.as_xml(pretty=True), response.as_xml(pretty=True), doc - def wsdl(self): - "Generate Web Service Description v1.1" + """Generate Web Service Description v1.1""" xml = """ -response (generic, any type)" + """Copy request->response (generic, any type)""" return request.value - dispatcher.register_function('Adder', adder, - returns={'AddResult': {'ab': int, 'dd': str } }, - args={'p': {'a': int,'b': int}, 'dt': Date, 'c': [{'d': Decimal}]}) + dispatcher.register_function( + 'Adder', adder, + returns={'AddResult': {'ab': int, 'dd': unicode, 'dt': datetime.date}}, + args={'p': {'a': int, 'b': int}, 'dt': Date, 'c': [{'d': Decimal}]} + ) - dispatcher.register_function('Dummy', dummy, - returns={'out0': str}, - args={'in0': str}) + dispatcher.register_function( + 'Dummy', dummy, + returns={'out0': str}, + args={'in0': str} + ) dispatcher.register_function('Echo', echo) if '--local' in sys.argv: - wsdl=dispatcher.wsdl() - print wsdl - - # Commented because path is platform dependent - # Looks that it doesnt matter. - # open("C:/test.wsdl","w").write(wsdl) + wsdl = dispatcher.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..." + log.info("Starting server...") httpd = HTTPServer(("", 8008), SOAPHandler) httpd.dispatcher = dispatcher httpd.serve_forever() + if '--wsgi-serve' in sys.argv: + log.info("Starting wsgi server...") + from wsgiref.simple_server import make_server + application = WSGISOAPHandler(dispatcher) + wsgid = make_server('', 8008, application) + wsgid.serve_forever() + if '--consume' in sys.argv: - from client import SoapClient + from .client import SoapClient client = SoapClient( - location = "http://localhost:8008/", - action = 'http://localhost:8008/', # SOAPAction - namespace = "http://example.com/sample.wsdl", + location="http://localhost:8008/", + action='http://localhost:8008/', # SOAPAction + namespace="http://example.com/sample.wsdl", soap_ns='soap', - trace = True, - ns = False) - response = client.Adder(p={'a':1,'b':2},dt='20100724',c=[{'d':'1.20'},{'d':'2.01'}]) + trace=True, + ns=False + ) + p = {'a': 1, 'b': 2} + c = [{'d': '1.20'}, {'d': '2.01'}] + response = client.Adder(p=p, dt='2010-07-24', c=c) result = response.AddResult - print int(result.ab) - print str(result.dd) + log.info(int(result.ab)) + log.info(str(result.dd)) + + if '--consume-wsdl' in sys.argv: + from .client import SoapClient + client = SoapClient( + wsdl="http://localhost:8008/", + ) + p = {'a': 1, 'b': 2} + c = [{'d': '1.20'}, {'d': '2.01'}] + dt = datetime.date.today() + response = client.Adder(p=p, dt=dt, c=c) + result = response['AddResult'] + log.info(int(result['ab'])) + log.info(str(result['dd'])) + diff --git a/gluon/contrib/pysimplesoap/simplexml.py b/gluon/contrib/pysimplesoap/simplexml.py index 2a57c902..9a23497a 100755 --- a/gluon/contrib/pysimplesoap/simplexml.py +++ b/gluon/contrib/pysimplesoap/simplexml.py @@ -1,5 +1,5 @@ -#!/usr/bin/env python -# -*- coding: latin-1 -*- +#!/usr/bin/python +# -*- coding: utf-8 -*- # 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 @@ -10,164 +10,47 @@ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. -"Simple XML manipulation" +"""Simple XML manipulation""" -__author__ = "Mariano Reingart (reingart@gmail.com)" -__copyright__ = "Copyright (C) 2008/009 Mariano Reingart" -__license__ = "LGPL 3.0" -__version__ = "1.03a" -import datetime +from __future__ import unicode_literals +import sys +if sys.version > '3': + basestring = str + unicode = str + import logging import re import time -import warnings import xml.dom.minidom -from decimal import Decimal + +from . import __author__, __copyright__, __license__, __version__ + +# Utility functions used for marshalling, moved aside for readability +from .helpers import TYPE_MAP, TYPE_MARSHAL_FN, TYPE_UNMARSHAL_FN, \ + REVERSE_TYPE_MAP, OrderedDict, Date, Decimal log = logging.getLogger(__name__) -logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.WARNING) - -DEBUG = False - -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: _strptime(s[0:10], "%Y-%m-%d").date() -date_m = lambda d: d.strftime("%Y-%m-%d") -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(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') -integer = Alias(long,'integer') -DateTime = datetime.datetime -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, - 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" - def __init__(self): - self.__keys = [] - self.array = False - def __setitem__(self, key, value): - if key not in self.__keys: - self.__keys.append(key) - dict.__setitem__(self, key, value) - def insert(self, key, value, index=0): - if key not in self.__keys: - self.__keys.insert(index, key) - dict.__setitem__(self, key, value) - def __delitem__(self, key): - if key in self.__keys: - self.__keys.remove(key) - dict.__delitem__(self, key) - def __iter__(self): - return iter(self.__keys) - def keys(self): - return self.__keys - def items(self): - return [(key, self[key]) for key in self.__keys] - def update(self, other): - for k,v in other.items(): - self[k] = v - if isinstance(other, OrderedDict): - self.array = other.array - def __str__(self): - return "*%s*" % dict.__str__(self) - def __repr__(self): - s= "*{%s}*" % ", ".join(['%s: %s' % (repr(k),repr(v)) for k,v in self.items()]) - if self.array and False: - s = "[%s]" % s - return s class SimpleXMLElement(object): - "Simple XML manipulation (simil PHP)" - - def __init__(self, text = None, elements = None, document = None, - namespace = None, prefix=None, namespaces_map={}): + """Simple XML manipulation (simil PHP)""" + + def __init__(self, text=None, elements=None, document=None, + namespace=None, prefix=None, namespaces_map={}, jetty=False): """ :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 + _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 - + self.__jetty = jetty # special list support + if text is not None: try: self.__document = xml.dom.minidom.parseString(text) @@ -178,78 +61,81 @@ class SimpleXMLElement(object): else: self.__elements = elements self.__document = document - + def add_child(self, name, text=None, ns=True): - "Adding a child tag to a node" - if not ns or not self.__ns: - log.debug('adding %s', name) + """Adding a child tag to a node""" + if not ns or self.__ns is False: + ##log.debug('adding %s without namespace', name) element = self.__document.createElement(name) else: - log.debug('adding %s ns "%s" %s', name, self.__ns, ns) - if self.__prefix: + ##log.debug('adding %s ns "%s" %s', name, self.__ns, ns) + if isinstance(ns, basestring): + element = self.__document.createElement(name) + if ns: + element.setAttribute("xmlns", ns) + elif self.__prefix: element = self.__document.createElementNS(self.__ns, "%s:%s" % (self.__prefix, name)) else: element = self.__document.createElementNS(self.__ns, name) # don't append null tags! if text is not None: - if isinstance(text, unicode): - element.appendChild(self.__document.createTextNode(text)) - else: - element.appendChild(self.__document.createTextNode(str(text))) + element.appendChild(self.__document.createTextNode(text)) self._element.appendChild(element) return SimpleXMLElement( - elements=[element], - document=self.__document, - namespace=self.__ns, - prefix=self.__prefix, - namespaces_map=self.__namespaces_map) - + elements=[element], + document=self.__document, + namespace=self.__ns, + prefix=self.__prefix, + jetty=self.__jetty, + namespaces_map=self.__namespaces_map + ) + def __setattr__(self, tag, text): - "Add text child tag node (short form)" + """Add text child tag node (short form)""" if tag.startswith("_"): object.__setattr__(self, tag, text) else: - log.debug('__setattr__(%s, %s)', 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 - ] + """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" + """Add an xml comment to this child""" comment = self.__document.createComment(data) self._element.appendChild(comment) def as_xml(self, filename=None, pretty=False): - "Return the XML representation of the document" + """Return the XML representation of the document""" if not pretty: return self.__document.toxml('UTF-8') else: return self.__document.toprettyxml(encoding='UTF-8') def __repr__(self): - "Return the XML representation of this tag" + """Return the XML representation of this tag""" + # NOTE: do not use self.as_xml('UTF-8') as it returns the whole xml doc return self._element.toxml('UTF-8') def get_name(self): - "Return the tag name of this node" + """Return the tag name of this node""" return self._element.tagName def get_local_name(self): - "Return the tag loca name (prefix:name) of this node" + """Return the tag local name (prefix:name) of this node""" return self._element.localName def get_prefix(self): - "Return the namespace prefix of this node" + """Return the namespace prefix of this node""" return self._element.prefix def get_namespace_uri(self, ns): - "Return the namespace uri for a prefix" + """Return the namespace uri for a prefix""" element = self._element while element is not None and element.attributes is not None: try: @@ -257,38 +143,39 @@ class SimpleXMLElement(object): except KeyError: element = element.parentNode - def attributes(self): - "Return a dict of attributes for this tag" + """Return a dict of attributes for this tag""" #TODO: use slice syntax [:]? return self._element.attributes def __getitem__(self, item): - "Return xml tag attribute value or a slice of attributes (iter)" - log.debug('__getitem__(%s)', item) + """Return xml tag attribute value or a slice of attributes (iter)""" + ##log.debug('__getitem__(%s)', item) if isinstance(item, basestring): if self._element.hasAttribute(item): return self._element.attributes[item].value elif isinstance(item, slice): # return a list with name:values - return self._element.attributes.items()[item] + return list(self._element.attributes.items())[item] else: # return element by index (position) element = self.__elements[item] return SimpleXMLElement( - elements=[element], - document=self.__document, - namespace=self.__ns, - prefix=self.__prefix, - namespaces_map=self.__namespaces_map) - + elements=[element], + document=self.__document, + namespace=self.__ns, + prefix=self.__prefix, + jetty=self.__jetty, + namespaces_map=self.__namespaces_map + ) + def add_attribute(self, name, value): - "Set an attribute value from a string" + """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): + """Set an attribute value""" + if isinstance(item, basestring): self.add_attribute(item, value) elif isinstance(item, slice): # set multiple attributes at once @@ -297,7 +184,7 @@ class SimpleXMLElement(object): 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" + """Search (even in child nodes) and return a child tag by name""" try: if root: # return entire document @@ -306,6 +193,7 @@ class SimpleXMLElement(object): document=self.__document, namespace=self.__ns, prefix=self.__prefix, + jetty=self.__jetty, namespaces_map=self.__namespaces_map ) if tag is None: @@ -317,23 +205,23 @@ class SimpleXMLElement(object): elements = None if isinstance(tag, int): # return tag by index - elements=[self.__elements[tag]] + elements = [self.__elements[tag]] if ns and not elements: for ns_uri in isinstance(ns, (tuple, list)) and ns or (ns, ): - log.debug('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: - log.debug('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: - log.debug('searching %s', tag) + ##log.debug('searching %s', tag) elements = self._element.getElementsByTagName(tag) if not elements: - #log.debug(self._element.toxml()) + ##log.debug(self._element.toxml()) if error: - raise AttributeError(u"No elements found") + raise AttributeError("No elements found") else: return return SimpleXMLElement( @@ -341,16 +229,17 @@ class SimpleXMLElement(object): document=self.__document, namespace=self.__ns, prefix=self.__prefix, + jetty=self.__jetty, namespaces_map=self.__namespaces_map) - except AttributeError, e: - raise AttributeError(u"Tag not found: %s (%s)" % (tag, unicode(e))) + except AttributeError as e: + raise AttributeError("Tag not found: %s (%s)" % (tag, e)) def __getattr__(self, tag): - "Shortcut for __call__" + """Shortcut for __call__""" return self.__call__(tag) - + def __iter__(self): - "Iterate over xml tags at this level" + """Iterate over xml tags at this level""" try: for __element in self.__elements: yield SimpleXMLElement( @@ -358,67 +247,72 @@ class SimpleXMLElement(object): document=self.__document, namespace=self.__ns, prefix=self.__prefix, + jetty=self.__jetty, namespaces_map=self.__namespaces_map) except: raise def __dir__(self): - "List xml children tags names" - return [node.tagName for node + """List xml children tags names""" + return [node.tagName for node in self._element.childNodes if node.nodeType != node.TEXT_NODE] def children(self): - "Return xml children tags element" - elements=[__element for __element in self._element.childNodes - if __element.nodeType == __element.ELEMENT_NODE] + """Return xml children tags element""" + elements = [__element for __element in self._element.childNodes + if __element.nodeType == __element.ELEMENT_NODE] if not elements: return None #raise IndexError("Tag %s has no children" % self._element.tagName) return SimpleXMLElement( - elements=elements, - document=self.__document, - namespace=self.__ns, - prefix=self.__prefix, - namespaces_map=self.__namespaces_map) + elements=elements, + document=self.__document, + namespace=self.__ns, + prefix=self.__prefix, + jetty=self.__jetty, + namespaces_map=self.__namespaces_map + ) def __len__(self): - "Return elements count" + """Return element count""" return len(self.__elements) - - def __contains__( self, item): - "Search for a tag name in this element or child nodes" + + 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" + """Returns the unicode text nodes of the current element""" if self._element.childNodes: - rc = u"" + rc = "" for node in self._element.childNodes: if node.nodeType == node.TEXT_NODE: 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") + """Returns the str text nodes of the current element""" + return self.__unicode__() def __int__(self): - "Returns the integer value of the current element" + """Returns the integer value of the current element""" return int(self.__str__()) def __float__(self): - "Returns the float value of the current element" + """Returns the float value of the current element""" 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, strict=True): - "Convert to python values the current serialized xml element" + #import pdb; pdb.set_trace() + + """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}]} @@ -435,37 +329,62 @@ class SimpleXMLElement(object): if ref_node['id'] == href: node = ref_node ref_name_type = ref_node['xsi:type'].split(":")[1] - break + break + try: - fn = types[name] - except (KeyError, ), e: - if node.get_namespace_uri("soapenc"): - fn = None # ignore multirefs! - elif 'xsi:type' in node.attributes().keys(): + if isinstance(types, dict): + fn = types[name] + # custom array only in the response (not defined in the WSDL): + # 1): + # Jetty array style support [{k, v}] + for parent in node: + tmp_dict = {} # unmarshall each value & mix + for child in (node.children() or []): + tmp_dict.update(child.unmarshall(fn[0], strict)) + value.append(tmp_dict) + else: # .Net / Java + for child in (children or []): + value.append(child.unmarshall(fn[0], strict)) + elif isinstance(fn, tuple): value = [] _d = {} children = node.children() as_dict = len(fn) == 1 and isinstance(fn[0], dict) - for child in (children and children() or []): # Readability counts + for child in (children and children() or []): # Readability counts if as_dict: - _d.update(child.unmarshall(fn[0], strict)) # Merging pairs + _d.update(child.unmarshall(fn[0], strict)) # Merging pairs else: value.append(child.unmarshall(fn[0], strict)) if as_dict: @@ -477,32 +396,32 @@ class SimpleXMLElement(object): 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, strict) else: - if fn is None: # xsd:anyType not unmarshalled + if fn is None: # xsd:anyType not unmarshalled value = node - elif str(node) or fn == str: + elif unicode(node) or (fn == str and unicode(node) != ''): try: # get special deserialization function (if any) - fn = TYPE_UNMARSHAL_FN.get(fn,fn) + fn = TYPE_UNMARSHAL_FN.get(fn, fn) if fn == str: # always return an unicode object: + # (avoid encoding errors in py<3!) value = unicode(node) else: value = fn(unicode(node)) - except (ValueError, TypeError), e: - raise ValueError(u"Tag: %s: %s" % (name, unicode(e))) + except (ValueError, TypeError) as e: + raise ValueError("Tag: %s: %s" % (name, e)) else: value = None d[name] = value return d - - + def _update_ns(self, name): """Replace the defined namespace alias with tohse used by the client.""" pref = self.__ns_rx.search(name) @@ -513,46 +432,50 @@ class SimpleXMLElement(object): except KeyError: log.warning('Unknown namespace alias %s' % name) return name - - - def marshall(self, name, value, add_child=True, add_comments=False, + + 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" + """Analyze 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) + # for the first parent node, use the document target namespace + # (ns==True) or use the namespace string uri if passed (elements) child = add_child and self.add_child(name, ns=ns) or self - for k,v in value.items(): + for k, v in value.items(): if not add_children_ns: ns = False + else: + # for children, use the wsdl element target namespace: + ns = getattr(value, 'namespace', None) 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 if not add_children_ns: ns = False - for k,v in value: + for k, v in value: 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) + elif isinstance(value, list): # serialize lists + 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) - elif isinstance(value, basestring): # do not convert strings or unicodes - self.add_child(name, value,ns=ns) - elif value is None: # sent a empty tag? + elif isinstance(value, basestring): # do not convert strings or unicodes + self.add_child(name, value, ns=ns) + elif value is None: # sent a empty tag? 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) + self.add_child(name, fn(value), ns=ns) def import_node(self, other): x = self.__document.importNode(other._element, True) # deep copy diff --git a/gluon/contrib/pysimplesoap/transport.py b/gluon/contrib/pysimplesoap/transport.py index 5441e342..87806c06 100644 --- a/gluon/contrib/pysimplesoap/transport.py +++ b/gluon/contrib/pysimplesoap/transport.py @@ -1,5 +1,5 @@ -#!/usr/bin/env python -# -*- coding: latin-1 -*- +#!/usr/bin/python +# -*- coding: utf-8 -*- # 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 @@ -10,30 +10,46 @@ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. -"Pythonic simple SOAP Client implementation" +"""Pythonic simple SOAP Client transport""" -__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 +import sys +try: + import urllib2 + from cookielib import CookieJar +except ImportError: + from urllib import request as urllib2 + from http.cookiejar import CookieJar + +from . import __author__, __copyright__, __license__, __version__, TIMEOUT +from .simplexml import SimpleXMLElement, TYPE_MAP, OrderedDict log = logging.getLogger(__name__) -logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.WARNING) + +# +# Socket wrapper to enable socket.TCP_NODELAY - this greatly speeds up transactions in Linux +# WARNING: this will modify the standard library socket module, use with care! +# TODO: implement this as a transport faciliy +# (to pass options directly to httplib2 or pycurl) +# be aware of metaclasses and socks.py (SocksiPy) used by httplib2 + +if False: + import socket + realsocket = socket.socket + def socketwrap(family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0): + sockobj = realsocket(family, type, proto) + if type == socket.SOCK_STREAM: + sockobj.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + return sockobj + socket.socket = socketwrap # # We store metadata about what available transport mechanisms we have available. # -_http_connectors = {} # libname: classimpl mapping -_http_facilities = {} # functionalitylabel: [sequence of libname] mapping +_http_connectors = {} # libname: classimpl mapping +_http_facilities = {} # functionalitylabel: [sequence of libname] mapping + class TransportBase: @classmethod @@ -45,27 +61,41 @@ class TransportBase: # try: import httplib2 + if sys.version > '3' and httplib2.__version__ <= "0.7.7": + import http.client + # httplib2 workaround: check_hostname needs a SSL context with either + # CERT_OPTIONAL or CERT_REQUIRED + # see https://code.google.com/p/httplib2/issues/detail?id=173 + orig__init__ = http.client.HTTPSConnection.__init__ + def fixer(self, host, port, key_file, cert_file, timeout, context, + check_hostname, *args, **kwargs): + chk = kwargs.get('disable_ssl_certificate_validation', True) ^ True + orig__init__(self, host, port=port, key_file=key_file, + cert_file=cert_file, timeout=timeout, context=context, + check_hostname=chk) + http.client.HTTPSConnection.__init__ = fixer except ImportError: - TIMEOUT = None # timeout not supported by urllib2 + 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 + log.info("using proxy %s" % 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 + kwargs['ca_certs'] = cacert httplib2.Http.__init__(self, **kwargs) _http_connectors['httplib2'] = Httplib2Transport @@ -76,15 +106,15 @@ else: 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' + _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: @@ -94,26 +124,26 @@ class urllib2Transport(TransportBase): 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: + return f.info(), f.read() + except urllib2.HTTPError as f: if f.code != 500: raise - return f.info(), f.read() + return f.info(), f.read() _http_connectors['urllib2'] = urllib2Transport _http_facilities.setdefault('sessions', []).append('urllib2') import sys -if sys.version_info >= (2,6): +if sys.version_info >= (2, 6): _http_facilities.setdefault('timeout', []).append('urllib2') del sys @@ -129,19 +159,23 @@ else: try: from cStringIO import StringIO except ImportError: - from StringIO import StringIO + try: + from StringIO import StringIO + except ImportError: + from io 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.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)) + c.setopt(pycurl.URL, url) if 'proxy_host' in self.proxy: c.setopt(pycurl.PROXY, self.proxy['proxy_host']) if 'proxy_port' in self.proxy: @@ -154,20 +188,19 @@ else: #self.body = StringIO(body) #c.setopt(pycurl.HEADERFUNCTION, self.header) if self.cacert: - c.setopt(c.CAINFO, str(self.cacert)) + c.setopt(c.CAINFO, 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.CONNECTTIMEOUT, self.timeout / 6) c.setopt(pycurl.TIMEOUT, self.timeout) - if method=='POST': + if method == 'POST': c.setopt(pycurl.POST, 1) - c.setopt(pycurl.POSTFIELDS, body) + c.setopt(pycurl.POSTFIELDS, body) if headers: - hdrs = ['%s: %s' % (str(k), str(v)) for k, v in headers.items()] - ##print hdrs + hdrs = ['%s: %s' % (k, v) for k, v in headers.items()] + log.debug(hdrs) c.setopt(pycurl.HTTPHEADER, hdrs) c.perform() - ##print "pycurl perform..." c.close() return {}, self.buf.getvalue() @@ -178,15 +211,15 @@ else: class DummyTransport: - "Testing class to load a xml response" - + """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 + log.debug("%s %s", method, location) + log.debug(headers) + log.debug(body) return {}, self.xml_response @@ -222,20 +255,19 @@ def get_http_wrapper(library=None, features=[]): else: return _http_connectors[candidate_name] + def set_http_wrapper(library=None, features=[]): - "Set a suitable HTTP connection wrapper." + """Set a suitable HTTP connection wrapper.""" global Http Http = get_http_wrapper(library, features) return Http def get_Http(): - "Return current transport class" + """Return current transport class""" global Http return Http - + # define the default HTTP connection class (it can be changed at runtime!): set_http_wrapper() - - diff --git a/gluon/contrib/webclient.py b/gluon/contrib/webclient.py index c2f18fb5..602c79e6 100644 --- a/gluon/contrib/webclient.py +++ b/gluon/contrib/webclient.py @@ -1,3 +1,4 @@ + """ Developed by Massimo Di Pierro Released under the web2py license (LGPL) @@ -105,9 +106,9 @@ class WebClient(object): # assume everything is ok and make http request error = None try: - if isinstance(data,str): + if isinstance(data, str): self.method = 'POST' if method=='auto' else method - if isinstance(data, dict): + elif isinstance(data, dict): self.method = 'POST' if method=='auto' else method # if there is only one form, set _formname automatically if not '_formname' in data and len(self.forms) == 1: diff --git a/gluon/tests/test_web.py b/gluon/tests/test_web.py index a241e5ae..54b4059b 100644 --- a/gluon/tests/test_web.py +++ b/gluon/tests/test_web.py @@ -40,6 +40,7 @@ def fix_sys_path(): fix_sys_path() from contrib.webclient import WebClient +from urllib2 import HTTPError webserverprocess = None @@ -140,6 +141,37 @@ class TestWeb(LiveTest): assert('expires' in s.headers) assert(s.headers['cache-control'].startswith('max-age')) + def testSoap(self): + # test soap server implementation + from gluon.contrib.pysimplesoap.client import SoapClient, SoapFault + url = 'http://127.0.0.1:8000/examples/soap_examples/call/soap?WSDL' + client = SoapClient(wsdl=url) + ret = client.SubIntegers(a=3, b=2) + # check that the value returned is ok + assert('SubResult' in ret) + assert(ret['SubResult'] == 1) + + try: + ret = client.Division(a=3, b=0) + except SoapFault, sf: + # verify the exception value is ok + assert(sf.faultstring == "float division by zero") + assert(sf.faultcode == "Server.ZeroDivisionError") + + # store sent and received xml for low level test + xml_request = client.xml_request + xml_response = client.xml_response + + # do a low level raw soap request (using + s = WebClient('http://127.0.0.1:8000/') + try: + s.post('examples/soap_examples/call/soap', data=xml_request, method="POST") + except HTTPError, e: + assert(e.msg=='INTERNAL SERVER ERROR') + # check internal server error returned (issue 153) + assert(s.status == 500) + assert(s.text == xml_response) + if __name__ == '__main__': unittest.main() diff --git a/gluon/tools.py b/gluon/tools.py index 9e893be4..b5b9aa1d 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -4830,9 +4830,15 @@ class Service(object): for method, (function, returns, args, doc) in procedures.iteritems(): dispatcher.register_function(method, function, returns, args, doc) if request.env.request_method == 'POST': + fault = {} # Process normal Soap Operation response.headers['Content-Type'] = 'text/xml' - return dispatcher.dispatch(request.body.read()) + xml = dispatcher.dispatch(request.body.read(), fault=fault) + if fault: + # May want to consider populating a ticket here... + response.status = 500 + # return the soap response + return xml elif 'WSDL' in request.vars: # Return Web Service Description response.headers['Content-Type'] = 'text/xml'