Commits

Anonymous committed f4692f2

First draft of the fork

Comments (0)

Files changed (4)

 from exceptions import *
+from soap import *
 from lxml import etree
 from decimal import Decimal
 from datetime import date, datetime, time
+from urllib2 import urlopen, Request, HTTPError
 
 class XMLType(object):
     """
         #add all children to the current level
         #note that children include also base classes, as they are propagated by
         #the metaclass below
-        for child_name in self._children.keys():
+        for child in self._children:
+            child_name = child["name"]
             #get the value of the argument
             val = getattr(self, child_name, None)
 
             elif val is not None:
                 n = 1
                 val = [val, ]
-            self.check_constraints(n, self._children[child_name]['min'],
-                                      self._children[child_name]['max'])
+            self.check_constraints(n, child['min'], child['max'])
             if n == 0:
                 continue #only nillables can get so far
 
             #conversion
             full_name = ns + child_name #name with namespace
             for single in val:
-                if not(isinstance(single, self._children[child_name]['type'])):
+                if not(isinstance(single, child['type'])):
                     #useful for primitive types:  python int, e.g.,
                     #can be passed directly. If str is used instead
                     #an exception is fired up.
-                    single = self._children[child_name]['type'](single)
+                    single = child['type'](single)
                 single.to_xml(element, full_name)
 
     def from_xml(self, element):
         if bool(element.get('nil')):
             return
 
+        all_children_names = []
+        for child in self._children:
+            all_children_names.append(child["name"])
+
         for subel in element:
             name = get_local_name(subel.tag)
             #check we have such an attribute
-            if name not in self._children.keys():
+            if name not in all_children_names:
                 raise ValueErro('does not have a "%s" member' % name)
 
+            ind = all_children_names.index(name)
             #used for conversion. for primitive types we receive back built-ins
-            inst = self._children[name]['type']()
+            inst = self._children[ind]['type']()
             subvalue = inst.from_xml(subel)
 
             #check conversion
             if subvalue is None:
-                if self._children[name]['min'] != 0:
+                if self._children[ind]['min'] != 0:
                     raise ValueError("Non-nillable %s element is nil." %name)
             else:
                 #unbounded is larger than 1
-                if self._children[name]['max'] > 1:
+                if self._children[ind]['max'] > 1:
                     current_value = getattr(self, name, None)
                     if current_value is None:
                         current_value = []
 
             _children attribute must be present in attributes. It describes
             the arguments to be present in the new type. The he
-            _children argument must be a dictionary of the form:
-            {argnam1:{'min':1, 'max':1, 'type':ClassType}, ...}
+            _children argument must be a list of the form:
+            [{'name':'arg1', 'min':1, 'max':1, 'type':ClassType}, ...]
 
             Parameters
             ----------
             raise ValueError("_children attribute must be present")
 
         #create dictionary for initializing class arguments
-        #children property is passed through
-        clsDict = {"_children":attributes["_children"],}
+        clsDict = {}
         #iterate over children and add arguments to the dictionary
         #all arguments are initially have None value
-        for k in attributes["_children"].keys():
+        for attr in attributes["_children"]:
             #set the argument
-            clsDict[k] = None
+            clsDict[attr['name']] = None
+        #propagate documentation
+        clsDict["__doc__"] = attributes.get("__doc__", None)
 
         #extend children list with that of base classes
+        new = []
         for b in bases:
             base_children = getattr(b, "_children", None)
             if base_children is not None:
-                #copy
-                for k in base_children.keys():
-                    attributes["_children"][k] = base_children[k]
+                #append
+                new.extend(base_children)
+        new.extend(attributes["_children"])
+        attributes["_children"] = new
+
+        #children property is passed through
+        clsDict["_children"] = attributes["_children"]
 
         #add ComplexType to base list
         if XMLType not in bases:
             return Decimal(element.text)
         return None
 
-class XMLDate(XMLType, date):
+class XMLDate(XMLType):
+    def __init__(self, *arg):
+        if len(arg) == 1 and isinstance(arg[0], date):
+            self.value = arg[0]
+        else:
+            self.value = date(2008, 11, 11)
     def to_xml(self, parent, name):
         element = etree.SubElement(parent, name)
-        element.text = value.isoformat()
+        element.text = self.value.isoformat()
 
     def from_xml(self, element):
         """expect ISO formatted dates"""
         return full.date()
 
 
-class XMLDateTime(XMLType, datetime):
+class XMLDateTime(XMLType):
+    def __init__(self, *arg):
+        if len(arg) == 1 and isinstance(arg[0], datetime):
+            self.value = arg[0]
+        else:
+            self.value = datetime(2008, 11, 11)
     def to_xml(self, parent, name):
         element = etree.SubElement(parent, name)
-        element.text = value.isoformat('T')
+        element.text = self.value.isoformat('T')
 
     def from_xml(self, element):
         return datetime.strptime('2011-08-02T17:00:01.000122',
                                         '%Y-%m-%dT%H:%M:%S.%f')
 
+class Message(object):
+    """
+        Message for input and output of service operations.
+
+        Messages perform conversion of Python to xml and backwards
+        of the calls and returns.
+
+        Parameters
+        ----------
+        tag : str
+            Name of the message.
+        namespafe : str
+            Namespace of the message.
+        nsmap : dict
+            Map of namespace prefixes.
+        parts : list
+            List of message parts in the form
+            (part name, part type class).
+        style : str
+            Operation style document/rpc.
+        literal : bool
+            True = literal, False = encoded.
+    """
+    def __init__(self, tag, namespace, nsmap, parts, style, literal):
+        self.tag = tag
+        self.namespace = namespace
+        self.nsmap = nsmap
+        self.parts = parts
+        self.style = style
+        self.literal = literal
+
+    def to_xml(self, *arg, **kw):
+        """
+            Convert from Python into xml message.
+        """
+        if self.style != "document" or not(self.literal):
+            raise RuntimeError(
+                "Only document/literal are supported. Improve Message class!")
+
+        p = self.parts[0][1]() #encoding instance
+
+        #wrapped message is supplied
+        if len(arg) == 1 and isinstance(arg[0], self.parts[0][1]):
+            for child in p._children:
+                setattr(p, child['name'], getattr(arg[0], child['name'], None))
+        else:
+            #reconstruct wrapper from expanded input
+            counter = 0
+            for child in p._children:
+                name = child["name"]
+                #first try keyword
+                val = kw.get(name, None)
+                if val is None: #not keyword
+                    if counter < len(arg):
+                        #assume this is positional argument
+                        val = arg[counter]
+                        counter = counter + 1
+                if val is None: #check if nillable
+                    if child["min"] == 0:
+                        continue
+                    else:
+                        raise ValueError(\
+                                "Non-nillable parameter %s is not present"\
+                                                                    %name)
+                setattr(p, name, val)
+
+        p.to_xml(kw["_body"], "{%s}%s" %(self.namespace, self.tag))
+
+    def from_xml(self, body, header = None):
+        """
+            Convert from xml message to Python.
+        """
+        if self.style != "document" or not(self.literal):
+            raise RuntimeError(
+                "Only document/literal are supported. Improve Message class.")
+
+        p = self.parts[0][1]() #decoding instance
+
+        res = p.from_xml(body)
+
+        #for wrapped doc style (the only one implemented) we now, that
+        #wrapper has only one child, get it
+        if len(p._children) == 1:
+            return getattr(res, p._children[0]["name"], None)
+        else:
+            return res
+
+class Method(object):
+    """
+        Definition of a single SOAP method, including the location, action, name
+        and input and output classes.
+
+        TODO: add a useful repr
+
+        This is a copy from Scio.
+
+        self.input - input message - to convert from Python to xml
+        self.output - output message - to convert from xml to Python
+    """
+    def __init__(self, location, name, action, input, output, doc=None):
+        self.location = location
+        self.name = name
+        self.action = action
+        self.input = input
+        self.output = output
+        self.__doc__ = doc
+
+    def __call__(self, *arg, **kw):
+        """
+            Process rpc-call.
+        """
+        #create soap-wrap around our message
+        env = etree.Element('{%s}Envelope' % SOAPNS['soap-env'], nsmap=SOAPNS)
+        header = etree.SubElement(env, '{%s}Header' % SOAPNS['soap-env'],
+                                                                 nsmap=SOAPNS)
+        body = etree.SubElement(env, '{%s}Body' % SOAPNS['soap-env'],
+                                                                 nsmap=SOAPNS)
+
+        #compose call message - convert all parameters and encode the call
+        kw["_body"] = body
+        self.input.to_xml(*arg, **kw)
+
+        text_msg = etree.tostring(env) #message to send
+        del env
+
+        #http stuff
+        request = Request(self.location, text_msg,
+                                {'Content-Type': 'text/xml',
+                                'SOAPAction': self.action})
+
+        #real rpc
+        try:
+            response = urlopen(request).read()
+        except HTTPError, e:
+            if e.code in (202, 204):#empty returns
+                pass
+                #return self.client.handle_response(self.method, None)
+            else:
+                pass
+                #return self.client.handle_error(self.method, e)
+            raise
+
+        #string to xml
+        xml = etree.fromstring(response)
+        del response
+
+        #find soap body
+        body = xml.find(SOAP_BODY)
+        if body is None:
+            raise NotSOAP("No SOAP body found in response", response)
+        fault = body.find(SOAP_FAULT)
+        if fault is not None:
+            code = fault.find('faultcode')
+            if code is not None:
+                code = code.text
+            string = fault.find('faultstring')
+            if string is not None:
+                string = string.text
+            detail = fault.find('detail')
+            if detail is not None:
+                detail = detail.text
+            raise RuntimeError("SOAP Fault %s:%s <%s> %s%s"\
+                    %(method.location, method.name, code, string, detail))
+        body = body[0] # hacky? get the first real element
+
+        return self.output.from_xml(body)
 
 def get_local_name(full_name):
     """
         Removes namespace part of the name.
     """
-    return full_name[full_name.find('}')+1:]
+    full_name = full_name[full_name.find('}')+1:]
+    full_name = full_name[full_name.find(':')+1:]
+    return full_name

scio/clientnew.py

+from alltypes import *
+import wsdl
+
+class Client(object):
+    """
+        Top level class to talk to soap services.
+
+        Parameters
+        ----------
+        wsdl_url : str
+            Address of wsdl document to consume.
+    """
+    def __init__(self, wsdl_url):
+        parser = wsdl.WSDLParser(wsdl_url)
+        types = parser.get_types()
+        methods = parser.get_methods(types)
+        self.types = type('TypesDispatcher', (), types)()
+        self.service = type('ServiceDispatcher', (), methods)()
+
+
+"""
+    Some common soap constants.
+"""
+
+# soap contstants
+NS_SOAP_ENV = "http://schemas.xmlsoap.org/soap/envelope/"
+NS_SOAP_ENC = "http://schemas.xmlsoap.org/soap/encoding/"
+NS_SOAP = 'http://schemas.xmlsoap.org/wsdl/soap/'
+NS_SOAP12 = 'http://schemas.xmlsoap.org/wsdl/soap12/'
+NS_XSI = "http://www.w3.org/1999/XMLSchema-instance"
+NS_XSD = "http://www.w3.org/1999/XMLSchema"
+NS_WSDL = 'http://schemas.xmlsoap.org/wsdl/'
+SOAP_BODY = '{%s}Body' % NS_SOAP_ENV
+SOAP_FAULT = '{%s}Fault' % NS_SOAP_ENV
+SOAP_HEADER = '{%s}Header' % NS_SOAP_ENV
+
+#namespace mapping
+SOAPNS = {
+             'soap-env'         : NS_SOAP_ENV,
+             'soap-enc'         : NS_SOAP_ENC,
+             'soap'             : NS_SOAP,
+             'soap12'           : NS_SOAP12,
+             'wsdl'             : NS_WSDL,
+             'xsi'              : NS_XSI,
+             'xsd'              : NS_XSD }
 from alltypes import *
+import urllib2
+from lxml import etree
+
+#primitive types mapping xml -> python
+_primmap = { 'anyType'          : XMLAny,
+             'boolean'          : XMLBoolean,
+             'decimal'          : XMLDecimal,
+             'int'              : XMLInteger,
+             'integer'          : XMLInteger,
+             'positiveInteger'  : XMLInteger,
+             'unsignedInt'      : XMLInteger,
+             'short'            : XMLInteger,
+             'byte'             : XMLInteger,
+             'long'             : XMLInteger,
+             'float'            : XMLDouble,
+             'double'           : XMLDouble,
+             'string'           : XMLString,
+             'base64Binary'     : XMLString,
+             'anyURI'           : XMLString,
+             'language'         : XMLString,
+             'token'            : XMLString,
+             'date'             : XMLDate,
+             'dateTime'         : XMLDateTime,
+             # FIXME: probably timedelta, but needs parsing.
+             # It looks like P29DT23H54M58S
+             'duration'         : XMLString}
 
 class WSDLParser(object):
     """
         """
             Initialize parser.
 
-            The WSDL document is loaded.
+            The WSDL document is loaded and is converted into xml.
+            In addition namespace parsing is done.
+
+            Initialized members:
+            self.wsdl_url  - url of wsdl document
+            self.wsdl - xml document read from wsdl_url (etree.Element)
+            self.nsmap - map of namespaces
+            self.tns - target namespace
+            self.xsd - schema namespace prefix used in the current document
+            self.wsdl_ns - wsdl namespace prefix used here
 
             Parameters
             ----------
             wsdl_url : str
                 Address of the WSDL document.
         """
-        #read wsdl
-        #parse into xml
+        #open wsdl page - get a file like object and
+        # parse it into xml
+        page_handler = urllib2.urlopen(wsdl_url)
+        self.wsdl = etree.parse(page_handler).getroot()
+        page_handler.close()
+        self.wsdl_url = wsdl_url
+
+        #process namespaces
+        self.nsmap = SOAPNS.copy() # copy predifined global map
+        self.nsmap.update(self.wsdl.nsmap) # mapping from the current document
+        #correct default wsdl namespace prefix
+        if None in self.nsmap:
+            del self.nsmap[None]
+        #get target namespace
+        self.tns = self.wsdl.get('targetNamespace', None)
+        #reverse dictionary
+        backmap = dict(zip(self.nsmap.values(), self.nsmap.keys()))
+        self.xsd = backmap['http://www.w3.org/2001/XMLSchema']
+        self.wsdl_ns = backmap['http://schemas.xmlsoap.org/wsdl/']
+
+    def collect_children(self, element, children, types):
+        """
+            Collect information about children (xml sequence, etc.)
+
+            Parameters
+            ----------
+            element : etree.Element
+                XML sequence container.
+            children : list
+                Information is appended to this list.
+            types : dict
+                Known types map.
+        """
+        for subel in element:
+            #iterate over sequence, do not consider in place defs
+            type = get_local_name(subel.get('type', None))
+            if type is None:
+                raise ValueError(
+                        "Do not support this type of complex type: %s"
+                                                         %subsub.tag)
+            ch = types.get(type, None)
+            if ch is None:
+                raise ValueError("Child %s class is not registered yet"\
+                                                                  %type)
+            child_name = subel.get('name', 'unknown')
+            minOccurs = int(subel.get('minOccurs', 1))
+            maxOccurs = subel.get('maxOccurs', 1)
+            if maxOccurs != 'unbounded':
+                maxOccurs = int(maxOccurs)
+            children.append({ "name":child_name,
+                             'type' : ch,
+                             'min' : minOccurs,
+                             'max' : maxOccurs})
+
+    def create_class(self, element, name, types):
+        """
+            Create new type from xml description.
+
+            Parameters
+            ----------
+            element : etree.Element instance
+                XML description of a complex type.
+            name : str
+                Name of the new class.
+            types : dict
+                Map of already known types.
+        """
+        doc = None
+        children = []
+        base = []
+        #iterate over children
+        #handle only children, bases non-primitive classes and docs
+        for subel in element:
+            if subel.tag in ("{%s}sequence" %self.nsmap[self.xsd],
+                             "{%s}all" %self.nsmap[self.xsd],
+                             "{%s}choice" %self.nsmap[self.xsd]):
+                #add children - arguments of new class
+                self.collect_children(subel, children, types)
+
+            elif subel.tag == "{%s}complexContent" %self.nsmap[self.xsd]:
+                #base class
+                subel = subel[0]
+                if subel.tag == "{%s}extension" %self.nsmap[self.xsd]:
+                    base_name = get_local_name(subel.get("base", None))
+                    b = types.get(base_name, None)
+                    if b is None:
+                        raise ValueError("Base %s class is not registered yet"\
+                                                                     %base_name)
+                    base.append(b)
+                for subsub in subel:
+                    if subsub.tag in ("{%s}sequence" %self.nsmap[self.xsd],
+                                      "{%s}all" %self.nsmap[self.xsd],
+                                      "{%s}choice" %self.nsmap[self.xsd]):
+                        self.collect_children(subsub, children, types)
+            elif subel.tag == "{%s}annotation" %self.nsmap[self.xsd]:
+                if len(subel) and\
+                   subel[0].tag == "{%s}documentation" %self.nsmap[self.xsd]:
+                    doc = subel[0].text
+
+        if name not in types:
+            #create new class
+            cls = ComplexTypeMeta(name, base,
+                                      {"_children":children, "__doc__":doc})
+            types[name] = cls
+
+
+    def get_types(self):
+        """
+            Constructs a map of all types defined in the document.
+
+            At the moment simple types are not processed at all!
+            Only complex types are considered. If element, attribute
+            or what so ever are encountered an exception if fired.
+
+            Returns
+            -------
+            out : dict
+                A map of found types {type_name : complex class}
+        """
+        #find all types defined here
+        types = self.wsdl.xpath('//%s:complexType|//%s:simpleType' %
+                                (self.xsd, self.xsd), namespaces=self.nsmap)
+
+        res = _primmap.copy() #types container
+        #iterate over the found types and fill in the container
+        #I take a simplified view that the types are ordered,
+        #i.e. a type can use types that are located before it.
+        for t in types:
+
+            #get name of the type
+            name = t.get('name', None)
+            if name is None:
+                # find name in parent 'element'
+                parent = t.getparent()
+                if parent.tag == "{%s}element" %self.nsmap[self.xsd]:
+                    name = parent.get('name', None)
+            if name is None:
+                continue
+
+            #if type is primitive or was already processed, skip it
+            if (res.get(name, None) is not None):
+                continue
+
+            #if unknown simple type - raise error
+            if t.tag == "{%s}simpleType" %self.nsmap[self.xsd]:
+                raise ValueError("Uknown simple type %s" %name)
+
+            #handle complex type, this also registers new class to result
+            self.create_class(t, name, res)
+
+        return res
+
+    def get_methods(self, types):
+        """
+            Construct a map of all operations defined in the document.
+
+            Parameters
+            ----------
+            types : dict
+                Map of known types as returned by get_types.
+
+            Returns
+            -------
+            out : dict
+                A map of operations: {operation name : method object}
+        """
+        res = {} # future result
+
+        #find all service definitions
+        services = self.wsdl.xpath('//%s:service' % self.wsdl_ns,
+                                                      namespaces=self.nsmap)
+        for service in services:
+            #all ports defined in this service. Port contains
+            #operations
+            for port in service.xpath('//%s:port' % self.wsdl_ns,
+                                                     namespaces=self.nsmap):
+                subel = port[0]
+
+                #check that this is a soap port, since wsdl can
+                #also define other ports
+                if self.nsmap[subel.prefix] not in (NS_SOAP, NS_SOAP12):
+                    continue
+
+                #port location
+                location = subel.get('location')
+
+                #find binding for this port
+                binding_name = get_local_name(port.get('binding'))
+                binding = self.wsdl.xpath("//%s:binding[@name='%s']" %
+                                          (self.wsdl_ns, binding_name),
+                                               namespaces=self.nsmap)[0]
+
+                #find binding style
+                soap_binding = binding.find('{%s}binding' % NS_SOAP)
+                if soap_binding is None:
+                    soap_binding = binding.find('{%s}binding' % NS_SOAP12)
+                if soap_binding is None:
+                    soap_binding = binding.find('binding')
+                if soap_binding is None:
+                    raise SyntaxError("No SOAP binding found in %s" %
+                                                    etree.tostring(binding))
+                style =  soap_binding.get('style')
+
+                #get port type - operation + message links
+                port_type_name = get_local_name(binding.get('type'))
+                port_type = self.wsdl.xpath("//%s:portType[@name='%s']" %
+                                            (self.wsdl_ns, port_type_name),
+                                                   namespaces=self.nsmap)[0]
+
+                #get operations
+                operations = binding.xpath('%s:operation' % self.wsdl_ns,
+                                                        namespaces=self.nsmap)
+                for operation in operations:
+                    #get operation name
+                    name = get_local_name(operation.get("name"))
+
+                    #check we have soap operation
+                    soap_op = operation.find('{%s}operation' % NS_SOAP)
+                    if soap_op is None:
+                        soap_op = operation.find('{%s}operation' % NS_SOAP12)
+                    if soap_op is None:
+                        soap_op = operation.find('operation')
+                    if soap_op is None:
+                        raise SyntaxError("No SOAP operation found in %s" %
+                                                  etree.tostring(operation))
+
+                    #operation action(?), style
+                    action = soap_op.attrib['soapAction']
+                    operation_style = soap_op.get('style', style)
+
+                    # FIXME is it reasonable to assume that input and output
+                    # are the same? Is it ok to ignore the encoding style?
+                    input = operation.find('{%s}input' % NS_WSDL)[0]
+                    literal = input.get('use') == 'literal'
+
+                    #do not support in/out headers, otherwise must be found here
+
+                    #go to port part and find messages.
+                    port_operation = port_type.xpath("%s:operation[@name='%s']" %
+                                                      (self.wsdl_ns, name),
+                                                       namespaces=self.nsmap)[0]
+                    in_msg_name = get_local_name(port_operation.xpath(
+                                            '%s:input/@message' % self.wsdl_ns,
+                                                    namespaces=self.nsmap)[0])
+                    out_msg_name = get_local_name(port_operation.xpath(
+                                            '%s:output/@message' % self.wsdl_ns,
+                                                    namespaces=self.nsmap)[0])
+                    #documentation
+                    doc = port_operation.find('{%s}documentation' %NS_WSDL)
+                    if doc is not None:
+                        doc = doc.text
+
+                    #finally go to message section
+                    in_types = self.wsdl.xpath('//%s:message[@name="%s"]/%s:part' %
+                                            (self.wsdl_ns, in_msg_name, self.wsdl_ns),
+                                                            namespaces=self.nsmap)
+                    out_types = self.wsdl.xpath('//%s:message[@name="%s"]/%s:part' %
+                                        (self.wsdl_ns, out_msg_name, self.wsdl_ns),
+                                                            namespaces=self.nsmap)
+                    #create input and output messages
+                    in_msg = self.create_msg(name, in_types, operation_style,
+                                                                 literal, types)
+                    out_msg = self.create_msg(name, out_types, operation_style,
+                                                                 literal, types)
+                    method = Method(location, name, action, in_msg, out_msg, doc=doc)
+                    res[name] = method
+        return res
+
+    def create_msg(self, name, part_elements, style, literal, types):
+        """
+            Create input or output message.
+
+            Parameters
+            ----------
+            name : str
+                Name of this message.
+            part_elements : list instance
+                List of parts as found in message section.
+            style : str
+                Style of operation: 'document', 'rpc'.
+            literal : bool
+                True = literal, False = encoded.
+            types : dict
+                Map of known types as returned by get_types.
+
+            Returns
+            -------
+            out : Message instance
+                Message for handling calls in/out.
+        """
+        #get all parameters - parts of the message
+        parts = []
+        for t in part_elements:
+            part_name = t.get("name", None)
+            if part_name is None:
+                continue
+            type_name = t.get('element', None)
+            if type_name is None:
+                type_name = t.get('type', None)
+            type_name = get_local_name(type_name)
+            parts.append((part_name, types[type_name]))
+
+        #namespace stuff
+        nsmap = {None: self.tns}
+
+        #create message
+        return Message(name, self.tns, nsmap, parts, style, literal)
+
+
+